/* * Copyright 2003-2010 Tufts University Licensed under the * Educational Community License, Version 2.0 (the "License"); you may * not use this file except in compliance with the License. You may * obtain a copy of the License at * * http://www.osedu.org/licenses/ECL-2.0 * * Unless required by applicable law or agreed to in writing, * software distributed under the License is distributed on an "AS IS" * BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express * or implied. See the License for the specific language governing * permissions and limitations under the License. */ package tufts.vue; import tufts.Util; import java.util.List; import java.util.Iterator; import java.util.ArrayList; import java.util.Collection; import java.util.Collections; import java.util.EventObject; import java.awt.event.InputEvent; import java.awt.event.ActionEvent; import javax.swing.Action; import javax.swing.AbstractButton; import javax.swing.KeyStroke; import javax.swing.Icon; /** * Base class for VueActions that don't use the selection. * @see Actions.LWCAction for actions that use the selection * * @version $Revision: 1.52 $ / $Date: 2010-02-03 19:17:41 $ / $Author: mike $ */ public class VueAction extends javax.swing.AbstractAction { protected static final org.apache.log4j.Logger Log = org.apache.log4j.Logger.getLogger(VueAction.class); public static final boolean EnableSmallIconsForMenus = false; public static final String LARGE_ICON = "vue.largeIcon"; private static final String CHECKBOX_LIST = "vue.checkBoxList"; private static List<VueAction> AllActionList = new ArrayList(); private static boolean allIgnored = false; private final String permanentName; protected static long currentActionTime; public final KeyStroke keyStroke; public static List<VueAction> getAllActions() { return Collections.unmodifiableList(AllActionList); } static class DeniedException extends RuntimeException { DeniedException(String msg) { super(msg); } } /** Set's all action events to be temporarily ignored. E.g., used while a TextBox edit is active */ // todo: may want to allow NewItem actions as they automatically // activate an edit, thus preventing a quick series of NewItem // actions to be done. static void setAllActionsIgnored(final boolean disabled) { if (DEBUG.Enabled) { Log.debug("allIgnored=" + disabled); if (DEBUG.META) tufts.Util.printStackTrace("ALL ACTIONS IGNORED: " + disabled); } allIgnored = disabled; for (VueAction a : AllActionList) { final boolean enabled = a.isUserEnabled(); if (DEBUG.EVENTS && DEBUG.META && disabled && enabled) Log.debug("setAllActionsIgnored override; always enabled: " + a); a.setEnabled(enabled); } } /** for debug only */ private static java.util.Map<KeyStroke,VueAction> AllStrokes; /** for debug only */ private static java.util.Set<Action> DupeStrokeActions; /** for debug only */ public static boolean isDupeStrokeAction(Action a) { if (DupeStrokeActions != null) return DupeStrokeActions.contains(a); else return false; } public static void checkForDupeStrokes() { if (AllStrokes != null) return; // already checked for (VueAction a : AllActionList) trackForDupeStrokes(a, (KeyStroke) a.getValue(ACCELERATOR_KEY)); } private static void trackForDupeStrokes(VueAction a, KeyStroke keyStroke) { if (AllStrokes == null) { AllStrokes = new java.util.HashMap(); DupeStrokeActions = new java.util.HashSet(); } if (keyStroke != null) { // this is more complicated than it needs to be because KeyStroke.hashCode // is imperfect (different KeyStroke's can have the same hash code) final VueAction existingAction = AllStrokes.get(keyStroke); if (existingAction != null) { final KeyStroke existingStroke = (KeyStroke) existingAction.getValue(ACCELERATOR_KEY); if (existingStroke.equals(keyStroke)) { Util.printStackTrace("WARNING; DUPLICATE KEYSTROKE: " + keyStroke + "; conflicting actions:" + "\n\t" + Util.tags(existingStroke) + "; " + Util.tags(existingAction) + "\n\t" + Util.tags(keyStroke) + "; " + Util.tags(a) ); DupeStrokeActions.add(existingAction); DupeStrokeActions.add(a); } } else { AllStrokes.put(keyStroke, a); } } } public VueAction(String name, String shortDescription, KeyStroke keyStroke, Icon icon) { super(name); this.keyStroke = keyStroke; this.permanentName = name; if (shortDescription != null) putValue(SHORT_DESCRIPTION, shortDescription); else putValue(SHORT_DESCRIPTION, name); if (keyStroke != null) putValue(ACCELERATOR_KEY, keyStroke); setSmallIcon(icon); //if (DEBUG.Enabled) System.out.println("Constructed: " + this + " icon=" + getValue(SMALL_ICON)); AllActionList.add(this); if (isSelectionWatcher()) getSelectionWatchers().add(this); if (DEBUG.Enabled) trackForDupeStrokes(this, keyStroke); } public VueAction(String name, KeyStroke keyStroke, String iconSpec) { this(name, null, keyStroke, null); setIcon(iconSpec); } public VueAction(String name, KeyStroke keyStroke) { this(name, null, keyStroke, null); } public VueAction(String name) { this(name, null, null, null); } public VueAction(String name, Icon icon) { this(name, null, null, icon); } private static int anonIndex = 0; public VueAction() { this(null, null, null, null); putValue(Action.NAME, getClass().getName() + anonIndex++); } /** add a button that supports toggle state (e.g., a /** JCheckBoxMenuItem) to be updated if this action represents the /** toogle of a boolean the checkbox should stay synced with */ public void trackToggler(AbstractButton toggler) { Collection list = (Collection) getValue(CHECKBOX_LIST); if (list == null) putValue(CHECKBOX_LIST, list = new java.util.ArrayList()); list.add(toggler); toggler.setSelected(getToggleState()); } protected void updateTogglers(boolean state) { final List<AbstractButton> toggles = (List<AbstractButton>) getValue(CHECKBOX_LIST); // JCheckBoxMenuItem's, which do add themselves as a property change // listener to the Action if you create them based on the Action, really // ought to override the default property change listener handler to // deal with this simple case: (then we wouldn't need the calls to trackToggler) //firePropertyChange("selected", state, !state); if (toggles != null) { for (AbstractButton item : toggles) { if (DEBUG.EVENTS) out("selected->" + state + " for " + tufts.vue.gui.GUI.name(item) + " (isSelected=" + item.isSelected() + ")"); item.setSelected(state); } } } private static final Boolean IS_NOT_A_TOGGLER = new Boolean(false); /** * if this is overridden to return a varying result, any AbstractButton watchers * of this action (added via trackToggler) will have their setSelected method * called with it's value. */ public Boolean getToggleState() { // returning a fixed boolean instance is a clever way of knowing if // this has been overridden return IS_NOT_A_TOGGLER; } public boolean isToggler() { return getToggleState() != IS_NOT_A_TOGGLER; } private void setIcon(String iconSpec) { Icon icon = null; if (iconSpec.startsWith(":")) { if (EnableSmallIconsForMenus) { icon = VueResources.getImageIconResource("/toolbarButtonGraphics/" + iconSpec.substring(1) + "16.gif"); setSmallIcon(icon); } icon = VueResources.getImageIconResource("/toolbarButtonGraphics/" + iconSpec.substring(1) + "24.gif"); putValue(LARGE_ICON, icon); } else { if (EnableSmallIconsForMenus) { icon = VueResources.getImageIconResource(iconSpec); setSmallIcon(icon); } } } private void setSmallIcon(Icon icon) { if (EnableSmallIconsForMenus) { if (icon != null) { if (icon != tufts.vue.gui.GUI.NO_ICON) putValue(SMALL_ICON, icon); } } } /** undoable: the undo manager already won't bother to create an * undo action if it didn't detect any changes. This method is * here as a backup just in case we know for sure we don't even * want to talk to the undo manager during an action, such as the * undo actions. */ boolean undoable() { return true; } /** @return false (the default) if this is an editing action - overide and return false * to enable as a non-editing action */ boolean isEditAction() { return false; } public String getActionName() { return (String) getValue(Action.NAME); } /** * @return the base name for this action that never changes. E.g.:, * for UndoAction, return just "Undo" instead of "Undo <most recent action>" */ public String getPermanentActionName() { return this.permanentName; } public void setActionName(String s) { putValue(Action.NAME, s); } public void revertActionName() { setActionName(getPermanentActionName()); } public boolean overrideIgnoreAllActions() { return false; } private void dumpEvent(ActionEvent actionEvent) { final StringBuffer m = new StringBuffer(512); m.append(this); m.append(Util.TERM_GREEN); m.append("; actionPerformed: " + getClass().getName()); m.append(";\n ActionEvent: (" + actionEvent.paramString() + ")"); String src; Object source = actionEvent.getSource(); for (int i = 0; i < 10; i++) { // looping failsafe: should never be more than 2 levels if (source instanceof EventWrap) { m.append(String.format("\n%15s: %s", "from", ((EventWrap)source).target)); source = ((EventWrap)source).event; } if (source instanceof EventObject) { EventObject e = (EventObject) source; //m.append('\n'); if (e instanceof InputEvent) { m.append(String.format("\n%14s%d: %s[%s]", "input source", i, e.getClass().getSimpleName(), ((InputEvent)e).paramString())); } else { m.append(String.format("\n%14s%d: %s", "source", i, Util.tags(e))); } source = e.getSource(); } else { m.append(String.format("\n%15s: %s", "source#", Util.tags(source))); break; } } m.append(Util.TERM_CLEAR); //m.append('\n'); Log.debug(m.toString()); } public void actionPerformed(ActionEvent ae) { if (DEBUG.EVENTS) { System.out.println("\n==============================================================================================================="); try { dumpEvent(ae); } catch (Throwable t) { t.printStackTrace(); } } if (allIgnored && !isUserEnabled()) { //if (DEBUG.Enabled) Log.debug("ALL ACTIONS DISABLED; " + this + "; " + ae); if (DEBUG.Enabled) { Util.printStackTrace("ALL ACTIONS DISABLED; " + this + "; " + ae); } else { Log.debug("all actions disabled; disallowed: " + this + "; " + ae); java.awt.Toolkit.getDefaultToolkit().beep(); } return; } currentActionTime = System.currentTimeMillis(); Throwable exception = null; try { if (isUserEnabled()) { act(); final Boolean state = getToggleState(); if (state != IS_NOT_A_TOGGLER) { //if (DEBUG.EVENTS) out("new toggle state: " + Util.tags(state)); updateTogglers(state); } } else { java.awt.Toolkit.getDefaultToolkit().beep(); Log.info(getActionName() + ": Not currently enabled"); } } catch (DeniedException e) { Log.info("Denied: " + this + "; " + e.getMessage()); } catch (Throwable t) { synchronized (System.err) { System.err.println("*** VueAction: exception during action [" + getActionName() + "]"); System.err.println("*** VueAction: " + getClass()); System.err.println("*** VueAction: selection is " + selection()); System.err.println("*** VueAction: event was " + ae); tufts.Util.printStackTrace(t); } exception = t; } establishMarkForUndo(ae, exception); updateSelectionWatchers(); //if (DEBUG.EVENTS) Log.debug(this + " END OF actionPerformed: ActionEvent=" + ae.paramString() + "\n"); if (DEBUG.EVENTS) Log.debug("\n===============================================================================================================\n"); // normally handled by updateActionListeners, but if someone has actually // defined "enabled" instead of enabledFor, we'll need this. // setEnabled(enabled()); // Okay, do NOT do this -- enabled sometimes use to just ring the bell when an // action is attempted that you can't actually do right now -- problem is if we // disable the action based on enabled(), it has no way of ever getting turned // back on! } protected LWSelection selection() { return VUE.getSelection(); } protected MapViewer viewer() { return VUE.getActiveViewer(); } protected boolean haveViewer() { return viewer() != null; } protected LWComponent focal() { return viewer().getFocal(); } protected void establishMarkForUndo(ActionEvent ae, Throwable exception) { if (VUE.getUndoManager() != null && undoable()) { VUE.getUndoManager().markChangesAsUndo(getUndoName(ae, exception)); } } public String getUndoName() { return null; } public String getUndoName(ActionEvent e, Throwable exception) { String undoName = getUndoName(); if (undoName == null) undoName = e.getActionCommand(); if (undoName == null) undoName = getActionName(); if (exception != null) { if (DEBUG.Enabled) undoName += " (!" + exception + ")"; else undoName += " (!)"; } return undoName; } public KeyStroke getKeyStroke() { return keyStroke; //return (KeyStroke) getValue(ACCELERATOR_KEY); } public String getKeyStrokeDescription() { final KeyStroke keyStroke = getKeyStroke(); if (keyStroke != null) return tufts.vue.action.ShortcutsAction.getDescription(keyStroke); else return "Menu item: " + getActionName(); } public void fire(java.awt.event.KeyEvent e) { e.consume(); fire((Object)e); } public void fire(Object source) { fire(source, getActionName()); } private static class EventWrap { final Object target; final EventObject event; EventWrap(Object t, EventObject e) { target = t; event = e; } } public void fire(Object source, EventObject event) { fire(new EventWrap(source, event), getActionName()); } private void fire(Object source, String name) { actionPerformed(new ActionEvent(source, 0, name)); } /** * Fire this action, but only if the given key event matches our accelerator key. * @return true if the action was fired */ public boolean fireIfMatching(Object source, java.awt.event.KeyEvent e) { if (KeyStroke.getKeyStrokeForEvent(e).equals(keyStroke)) { fire(source); return true; } else return false; } /** * Fire this action, but only if the given key event matches our accelerator key. * The KeyEvent will be used as the source of the action. * @return true if the action was fired */ public boolean fireIfMatching(java.awt.event.KeyEvent e) { return fireIfMatching(e, e); } private static final Collection<VueAction> _selectionWatchers = new java.util.ArrayList(); /** can be overridden for action subclass categories that may want to use a different selection */ protected Collection<VueAction> getSelectionWatchers() { return _selectionWatchers; } /** can be overridden for action subclass categories */ protected void updateSelectionWatchers() { updateSelectionWatchers(getSelectionWatchers(), selection()); } static { VUE.getSelection().addListener(new LWSelection.Listener() { public void selectionChanged(LWSelection s) { updateSelectionWatchers(_selectionWatchers, s); } @Override public String toString() { return "Global Handler for " + _selectionWatchers.size() + " " + VueAction.class.getSimpleName() + "s"; } }); } private static void updateSelectionWatchers(final Collection<VueAction> watchers, final LWSelection s) { final LWSelection selection = VUE.getActiveViewer() == null ? null : s; //if (DEBUG.SELECTION) Log.debug("updating " + _selectionWatchers.size() + " selection watchers..."); for (VueAction a : watchers) { try { // todo: this should ideally be run at any "user-mark" (any undo-manager possible checkpoint) // not just when selection changes -- would be faster / have to less often, tho would then be // subject to being out of date if we miss any user marks, tho those are bugs anyway. a.updateEnabled(selection); } catch (Throwable t) { Log.error("updateEnabled failed in: " + Util.tags(a) + "; with selection " + selection); } } //if (DEBUG.SELECTION) Log.debug("#UPDATED " + _selectionWatchers.size() + " selection watchers"); } /** @return false in this impl: override to change */ protected boolean isSelectionWatcher() { return false; } /** does nothing in this impl: override to make use of */ protected void updateEnabled(LWSelection s) { } /** generic public overridable call for specialized impl's */ public void update(Object arg) { } /** Note that overriding enabled() will not update the action's enabled * state based on what's in the selection -- you need to subclass * Actions.LWCAction and override enabledFor(LWSelection s) for * that -- it gets called whenever the selection changes and will * update the actions enabled state based on what it returns. If * you want an action to update it's enabled state based on any * other VUE application state, all enabled states are updated * after every action is performed, but if you need more than * that, the action will need it's own listener for whatever event * it's interested in. */ protected boolean enabled() { return VUE.getActiveViewer() != null; } /** @return true: must be overriden to be put to use */ boolean enabledFor(LWSelection s) { return true; } /** public access enabled checker that also checks master action enabled states */ public boolean isUserEnabled() { if (allIgnored && !overrideIgnoreAllActions()) return false; else return enabled(); } public void act() { System.err.println("Unhandled VueAction: " + getActionName()); } protected void out(String s) { if (DEBUG.Enabled) Log.debug(this + ": " + s); } protected void info(String s) { Log.info(this + " " + s); } public String toString() { Class c = getClass(); return Util.TERM_GREEN + (c.isAnonymousClass() ? c.getSuperclass().getSimpleName() : c.getSimpleName()) + "[" + getActionName() + "]" + Util.TERM_CLEAR; } }