/* * @(#)AbstractApplication.java * * Copyright (c) 1996-2010 The authors and contributors of JHotDraw. * You may not use, copy or modify this file, except in compliance with the * accompanying license terms. */ package org.jhotdraw.app; import org.jhotdraw.beans.AbstractBean; import javax.annotation.Nullable; import java.awt.Container; import java.awt.Window; import java.beans.PropertyChangeEvent; import java.beans.PropertyChangeListener; import java.io.File; import java.net.URISyntaxException; import org.jhotdraw.util.*; import java.util.prefs.*; import javax.swing.*; import java.net.URI; import java.util.Collection; import java.util.Collections; import java.util.LinkedList; import java.util.List; import org.jhotdraw.app.action.file.ClearRecentFilesMenuAction; import org.jhotdraw.app.action.file.LoadDirectoryAction; import org.jhotdraw.app.action.file.LoadFileAction; import org.jhotdraw.app.action.file.LoadRecentFileAction; import org.jhotdraw.app.action.file.OpenRecentFileAction; import org.jhotdraw.gui.BackgroundTask; import org.jhotdraw.gui.URIChooser; import org.jhotdraw.util.prefs.PreferencesUtil; /** * This abstract class can be extended to implement an {@link Application}. * <p> * {@code AbstractApplication} supports the command line parameter * {@code -open filename} to open views for specific URI's upon launch of * the application. * * <hr> * <b>Features</b> * * <p><em>Open last URI on launch</em><br> * When the application is started, the last opened URI is opened in a view.<br> * The following methods participate in this feature:<br> * Data suppliers {@link #addRecentURI}, {@link #getRecentURIs}.<br> * Behavior: {@link #start}.<br> * See {@link org.jhotdraw.app} for a list of participating classes. * * <p><em>Allow multiple views for URI</em><br> * Allows opening the same URI in multiple views. * When the feature is disabled, opening multiple views is prevented, and saving * to a file for which a view is currently open is prevented.<br> * The following methods participate in this feature:<br> * Data suppliers {@link #getViews}. * See {@link org.jhotdraw.app} for a list of participating classes. * * @author Werner Randelshofer * @version $Id$ */ public abstract class AbstractApplication extends AbstractBean implements Application { private static final long serialVersionUID = 1L; private LinkedList<View> views = new LinkedList<>(); private Collection<View> unmodifiableViews; private boolean isEnabled = true; protected ResourceBundleUtil labels; protected ApplicationModel model; private Preferences prefs; @Nullable private View activeView; public static final String VIEW_COUNT_PROPERTY = "viewCount"; private LinkedList<URI> recentURIs = new LinkedList<>(); private static final int maxRecentFilesCount = 10; private ActionMap actionMap; private URIChooser openChooser; private URIChooser saveChooser; private URIChooser importChooser; private URIChooser exportChooser; /** Creates a new instance. */ public AbstractApplication() { } /** * Initializes the application after it has been configured. */ @Override public void init() { prefs = PreferencesUtil.userNodeForPackage((getModel() == null) ? getClass() : getModel().getClass()); int count = prefs.getInt("recentFileCount", 0); for (int i = 0; i < count; i++) { String path = prefs.get("recentFile." + i, null); if (path != null) { try { recentURIs.add(new URI(path)); } catch (URISyntaxException ex) { // Silently don't add this URI } } } } /** * {@inheritDoc} */ @Override public void start(List<URI> uris) { if (uris.isEmpty()) { final View v = createView(); add(v); v.setEnabled(false); show(v); // Set the start view immediately active, so that // ApplicationOpenFileAction picks it up on Mac OS X. setActiveView(v); v.execute(new BackgroundTask() { @Override public void construct() { v.clear(); } @Override public void finished() { v.setEnabled(true); } }); } else { for (final URI uri : uris) { final View v = createView(); add(v); v.setEnabled(false); show(v); // Set the start view immediately active, so that // ApplicationOpenFileAction picks it up on Mac OS X. setActiveView(v); v.execute(new BackgroundTask() { @Override public void construct() throws Exception { v.read(uri, null); } @Override protected void done() { v.setURI(uri); } @Override protected void failed(Throwable error) { error.printStackTrace(); v.clear(); } @Override public void finished() { v.setEnabled(true); } }); } } } @Override public final View createView() { View v = basicCreateView(); v.setActionMap(createViewActionMap(v)); return v; } @Override public void setModel(ApplicationModel newValue) { ApplicationModel oldValue = model; model = newValue; firePropertyChange("model", oldValue, newValue); } @Override public ApplicationModel getModel() { return model; } protected View basicCreateView() { return model.createView(); } /** * Sets the active view. Calls deactivate on the previously * active view, and then calls activate on the given view. * * @param newValue Active view, can be null. */ public void setActiveView(@Nullable View newValue) { View oldValue = activeView; if (activeView != null) { activeView.deactivate(); } activeView = newValue; if (activeView != null) { activeView.activate(); } firePropertyChange(ACTIVE_VIEW_PROPERTY, oldValue, newValue); } /** * Gets the active view. * * @return The active view can be null. */ @Override @Nullable public View getActiveView() { return activeView; } @Override public String getName() { return model.getName(); } @Override public String getVersion() { return model.getVersion(); } @Override public String getCopyright() { return model.getCopyright(); } @Override public void stop() { for (View p : new LinkedList<>(views())) { dispose(p); } } @Override public void destroy() { stop(); model.destroyApplication(this); System.exit(0); } @Override public void remove(View v) { hide(v); if (v == getActiveView()) { setActiveView(null); } int oldCount = views.size(); views.remove(v); v.setApplication(null); firePropertyChange(VIEW_COUNT_PROPERTY, oldCount, views.size()); } @Override public void add(View v) { if (v.getApplication() != this) { int oldCount = views.size(); views.add(v); v.setApplication(this); v.init(); model.initView(this, v); firePropertyChange(VIEW_COUNT_PROPERTY, oldCount, views.size()); } } @Override public List<View> getViews() { return Collections.unmodifiableList(views); } protected abstract ActionMap createViewActionMap(View p); @Override public void dispose(View view) { remove(view); model.destroyView(this, view); view.dispose(); } @Override public Collection<View> views() { if (unmodifiableViews == null) { unmodifiableViews = Collections.unmodifiableCollection(views); } return unmodifiableViews; } @Override public boolean isEnabled() { return isEnabled; } @Override public void setEnabled(boolean newValue) { boolean oldValue = isEnabled; isEnabled = newValue; firePropertyChange("enabled", oldValue, newValue); } public Container createContainer() { return new JFrame(); } /** Launches the application. * * @param args This implementation supports the command-line parameter "-open" * which can be followed by one or more filenames or URI's. */ @Override public void launch(String[] args) { configure(args); // Get URI's from command line final List<URI> uris = getOpenURIsFromMainArgs(args); SwingUtilities.invokeLater(new Runnable() { @Override public void run() { init(); // Call this right after init. model.initApplication(AbstractApplication.this); // Get start URIs final LinkedList<URI> startUris; if (uris.isEmpty()) { startUris = new LinkedList<>(); if (model.isOpenLastURIOnLaunch() && !recentURIs.isEmpty()) { startUris.add(recentURIs.getFirst()); } } else { startUris = new LinkedList<>(uris); } // Start with start URIs start(startUris); } }); } /** Parses the arguments to the main method and returns a list of URI's * for which views need to be opened upon launch of the application. * <p> * This implementation supports the command-line parameter "-open" * which can be followed by one or more filenames or URI's. * <p> * This method is invoked from the {@code Application.launch} method. * * @param args Arguments to the main method. * @return A list of URI's parsed from the arguments. Returns an empty list * if no URI's shall be opened. */ protected List<URI> getOpenURIsFromMainArgs(String[] args) { LinkedList<URI> uris = new LinkedList<>(); for (int i = 0; i < args.length; ++i) { if ("-open".equals(args[i])) { for (++i; i < args.length; ++i) { if (args[i].startsWith("-")) { break; } URI uri; uri = new File(args[i]).toURI(); uris.add(uri); } } } return uris; } protected void initLabels() { labels = ResourceBundleUtil.getBundle("org.jhotdraw.app.Labels"); } /** * Configures the application using the provided arguments array. */ @Override public void configure(String[] args) { } @Override public void removePalette(Window palette) { } @Override public void addPalette(Window palette) { } @Override public void removeWindow(Window window) { } @Override public void addWindow(Window window, @Nullable View p) { } protected Action getAction(@Nullable View view, String actionID) { return getActionMap(view).get(actionID); } /** Adds the specified action as a menu item to the supplied menu. * @param m the menu * @param view the view * @param actionID the action id */ protected void addAction(JMenu m, @Nullable View view, String actionID) { addAction(m, getAction(view, actionID)); } /** Adds the specified action as a menu item to the supplied menu. * @param m the menu * @param a the action */ protected void addAction(JMenu m, Action a) { if (a != null) { if (m.getClientProperty("needsSeparator") == Boolean.TRUE) { m.addSeparator(); m.putClientProperty("needsSeparator", null); } JMenuItem mi; mi = m.add(a); mi.setIcon(null); mi.setToolTipText(null); } } /** Adds the specified action as a menu item to the supplied menu. * @param m the menu * @param mi the menu item */ protected void addMenuItem(JMenu m, JMenuItem mi) { if (mi != null) { if (m.getClientProperty("needsSeparator") == Boolean.TRUE) { m.addSeparator(); m.putClientProperty("needsSeparator", null); } m.add(mi); } } /** Adds a separator to the supplied menu. The separator will only * be added, if the previous item is not a separator. * @param m the menu */ protected void maybeAddSeparator(JMenu m) { JPopupMenu pm = m.getPopupMenu(); if (pm.getComponentCount() > 0 // && !(pm.getComponent(pm.getComponentCount() - 1) instanceof JSeparator)) { m.addSeparator(); } } protected void removeTrailingSeparators(JMenu m) { JPopupMenu pm = m.getPopupMenu(); for (int i = pm.getComponentCount() - 1; i > 0 && (pm.getComponent(i) instanceof JSeparator); i--) { pm.remove(i); } } @Override public java.util.List<URI> getRecentURIs() { return Collections.unmodifiableList(recentURIs); } @Override public void clearRecentURIs() { @SuppressWarnings("unchecked") java.util.List<URI> oldValue = (java.util.List<URI>) recentURIs.clone(); recentURIs.clear(); prefs.putInt("recentFileCount", recentURIs.size()); firePropertyChange(RECENT_URIS_PROPERTY, Collections.unmodifiableList(oldValue), Collections.unmodifiableList(recentURIs)); } @Override public void addRecentURI(URI uri) { @SuppressWarnings("unchecked") java.util.List<URI> oldValue = (java.util.List<URI>) recentURIs.clone(); if (recentURIs.contains(uri)) { recentURIs.remove(uri); } recentURIs.addFirst(uri); if (recentURIs.size() > maxRecentFilesCount) { recentURIs.removeLast(); } prefs.putInt("recentFileCount", recentURIs.size()); int i = 0; for (URI f : recentURIs) { prefs.put("recentFile." + i, f.toString()); i++; } firePropertyChange(RECENT_URIS_PROPERTY, oldValue, 0); firePropertyChange(RECENT_URIS_PROPERTY, Collections.unmodifiableList(oldValue), Collections.unmodifiableList(recentURIs)); } protected JMenu createOpenRecentFileMenu(@Nullable View view) { JMenuItem mi; JMenu m; m = new JMenu(); labels.configureMenu(m, // (getAction(view, LoadFileAction.ID) != null || // getAction(view, LoadDirectoryAction.ID) != null) ?// "file.loadRecent" :// "file.openRecent"// ); m.setIcon(null); m.add(getAction(view, ClearRecentFilesMenuAction.ID)); new OpenRecentMenuHandler(m, view); return m; } /** Updates the menu items in the "Open Recent" file menu. */ private class OpenRecentMenuHandler implements PropertyChangeListener, Disposable { private JMenu openRecentMenu; private LinkedList<Action> openRecentActions = new LinkedList<>(); @Nullable private View view; public OpenRecentMenuHandler(JMenu openRecentMenu, @Nullable View view) { this.openRecentMenu = openRecentMenu; this.view = view; if (view != null) { view.addDisposable(this); } updateOpenRecentMenu(); addPropertyChangeListener(this); } @Override public void propertyChange(PropertyChangeEvent evt) { String name = evt.getPropertyName(); if (name == RECENT_URIS_PROPERTY) { updateOpenRecentMenu(); } } /** * Updates the "File > Open Recent" menu. */ protected void updateOpenRecentMenu() { if (openRecentMenu.getItemCount() > 0) { JMenuItem clearRecentFilesItem = openRecentMenu.getItem( openRecentMenu.getItemCount() - 1); openRecentMenu.remove(openRecentMenu.getItemCount() - 1); // Dispose the actions and the menu items that are currently in the menu for (Action action : openRecentActions) { if (action instanceof Disposable) { ((Disposable) action).dispose(); } } openRecentActions.clear(); openRecentMenu.removeAll(); // Create new actions and add them to the menu if (getAction(view, LoadFileAction.ID) != null || // getAction(view, LoadDirectoryAction.ID) != null) { for (URI f : getRecentURIs()) { LoadRecentFileAction action = new LoadRecentFileAction(AbstractApplication.this, view, f); openRecentMenu.add(action); openRecentActions.add(action); } } else { for (URI f : getRecentURIs()) { OpenRecentFileAction action = new OpenRecentFileAction(AbstractApplication.this, f); openRecentMenu.add(action); openRecentActions.add(action); } } if (getRecentURIs().size() > 0) { openRecentMenu.addSeparator(); } // Add a separator and the clear recent files item. openRecentMenu.add(clearRecentFilesItem); } } @Override public void dispose() { removePropertyChangeListener(this); // Dispose the actions and the menu items that are currently in the menu for (Action action : openRecentActions) { if (action instanceof Disposable) { ((Disposable) action).dispose(); } } openRecentActions.clear(); } } /** Gets an open chooser for the specified view or for the application. * <p> * If the chooser has an accessory panel, it can access the view using * the client property "view" on the component of the chooser. It can * access the application using the client property "application" on the * chooser. * </p> * * @param v The view. Specify null to get a chooser for the application. */ @Override public URIChooser getOpenChooser(View v) { if (v == null) { if (openChooser == null) { openChooser = model.createOpenChooser(this, null); openChooser.getComponent().putClientProperty("application", this); List<URI> ruris = getRecentURIs(); if (ruris.size() > 0) { try { openChooser.setSelectedURI(ruris.get(0)); } catch (IllegalArgumentException e) { // Ignore illegal values in recent URI list. } } } return openChooser; } else { URIChooser chooser = (URIChooser) v.getComponent().getClientProperty("openChooser"); if (chooser == null) { chooser = model.createOpenChooser(this, v); v.getComponent().putClientProperty("openChooser", chooser); chooser.getComponent().putClientProperty("view", v); chooser.getComponent().putClientProperty("application", this); List<URI> ruris = getRecentURIs(); if (ruris.size() > 0) { try { chooser.setSelectedURI(ruris.get(0)); } catch (IllegalArgumentException e) { // Ignore illegal values in recent URI list. } } } return chooser; } } /** Gets a save chooser for the specified view or for the application. * <p> * If the chooser has an accessory panel, it can access the view using * the client property "view" on the component of the chooser. It can * access the application using the client property "application" on the * chooser. * </p> * * @param v The view. Specify null to get a chooser for the application. */ @Override public URIChooser getSaveChooser(View v) { if (v == null) { if (saveChooser == null) { saveChooser = model.createSaveChooser(this, null); saveChooser.getComponent().putClientProperty("application", this); } return saveChooser; } else { URIChooser chooser = (URIChooser) v.getComponent().getClientProperty("saveChooser"); if (chooser == null) { chooser = model.createSaveChooser(this, v); v.getComponent().putClientProperty("saveChooser", chooser); chooser.getComponent().putClientProperty("view", v); chooser.getComponent().putClientProperty("application", this); try { chooser.setSelectedURI(v.getURI()); } catch (IllegalArgumentException e) { // ignore illegal values } } return chooser; } } /** Gets an import chooser for the specified view or for the application. * <p> * If the chooser has an accessory panel, it can access the view using * the client property "view" on the component of the chooser. It can * access the application using the client property "application" on the * chooser. * </p> * * @param v The view. Specify null to get a chooser for the application. */ @Override public URIChooser getImportChooser(View v) { if (v == null) { if (importChooser == null) { importChooser = model.createImportChooser(this, null); importChooser.getComponent().putClientProperty("application", this); } return importChooser; } else { URIChooser chooser = (URIChooser) v.getComponent().getClientProperty("importChooser"); if (chooser == null) { chooser = model.createImportChooser(this, v); v.getComponent().putClientProperty("importChooser", chooser); chooser.getComponent().putClientProperty("view", v); chooser.getComponent().putClientProperty("application", this); } return chooser; } } /** Gets an export chooser for the specified view or for the application. * <p> * If the chooser has an accessory panel, it can access the view using * the client property "view" on the component of the chooser. It can * access the application using the client property "application" on the * chooser. * </p> * * @param v The view. Specify null to get a chooser for the application. */ @Override public URIChooser getExportChooser(View v) { if (v == null) { if (exportChooser == null) { exportChooser = model.createExportChooser(this, null); exportChooser.getComponent().putClientProperty("application", this); } return exportChooser; } else { URIChooser chooser = (URIChooser) v.getComponent().getClientProperty("exportChooser"); if (chooser == null) { chooser = model.createExportChooser(this, v); v.getComponent().putClientProperty("exportChooser", chooser); chooser.getComponent().putClientProperty("view", v); chooser.getComponent().putClientProperty("application", this); } return chooser; } } /** * Sets the application-wide action map. * @param m the map */ public void setActionMap(ActionMap m) { actionMap = m; } /** * Gets the action map. * @return the map */ @Override public ActionMap getActionMap(@Nullable View v) { return (v == null) ? actionMap : v.getActionMap(); } }