/* * Sun Public License Notice * * The contents of this file are subject to the Sun Public License * Version 1.0 (the "License"). You may not use this file except in * compliance with the License. A copy of the License is available at * http://www.sun.com/ * * The Original Code is NetBeans. The Initial Developer of the Original * Code is Sun Microsystems, Inc. Portions Copyright 1997-2003 Sun * Microsystems, Inc. All Rights Reserved. */ package org.openide.util.actions; import java.beans.*; import java.lang.ref.*; import java.util.*; import javax.swing.Action; import org.openide.awt.Actions; import org.openide.util.ContextAwareAction; import org.openide.nodes.Node; import org.openide.explorer.ExplorerManager; import org.openide.util.LookupListener; import org.openide.windows.TopComponent.Registry; import org.openide.util.Lookup; import org.openide.util.WeakSet; /** An action which can listen to the activated node selection. * This means that the set of nodes active in a window * may change the enabled state of the action according to {@link #enable}. * <p><strong>Note:</strong> if your action involves getting cookies * from nodes, which in many cases is the correct design, please use * {@link CookieAction} instead, as that permits sensitivity to cookies * and also listens to changes in supplied cookies. * @author Jan Jancura, Ian Formanek, Jaroslav Tulach */ public abstract class NodeAction extends CallableSystemAction implements ContextAwareAction { private static final long serialVersionUID = -5672895970450115226L; /** whether or not anyone is listening to PROP_ENABLED */ private static final String PROP_HAS_LISTENERS = "hasListeners"; // NOI18N /** last-used nodes, as a Reference<Node[]> */ private static final String PROP_LAST_NODES = "lastNodes"; // NOI18N /** last-computed enablement (Boolean) */ private static final String PROP_LAST_ENABLED = "lastEnabled"; // NOI18N /** the selection listener, if any */ private static NodesL l; /** set of actions with listeners */ private static final Set listeningActions = new WeakSet(100); // Set<NodeAction> /* Initialize the listener. */ protected void initialize () { super.initialize (); putProperty(PROP_HAS_LISTENERS, Boolean.FALSE); // Not yet determined: putProperty(PROP_ENABLED, null); } /** Initializes selection listener. * If you override this method, you must always call the super method first. */ protected void addNotify () { super.addNotify(); // initializes the listener putProperty(PROP_HAS_LISTENERS, Boolean.TRUE); synchronized (listeningActions) { if (l == null) { l = new NodesL(); } if (listeningActions.isEmpty()) { l.setActive(true); } listeningActions.add(this); } } /** Shuts down the selection listener. * If you override this method, you must always call the super method last. */ protected void removeNotify () { synchronized (listeningActions) { listeningActions.remove(this); if (listeningActions.isEmpty()) { l.setActive(false); } } putProperty(PROP_HAS_LISTENERS, Boolean.FALSE); // Previous results should no longer be cached: putProperty(PROP_ENABLED, null); super.removeNotify(); } /** Test for enablement based on {@link #enable}. * You probably ought not ever override this. * @return <code>true</code> to enable */ public boolean isEnabled () { Node[] ns = null; Boolean b = null; synchronized (getLock()) { b = (Boolean)getProperty(PROP_ENABLED); if (b == null) { ns = getActivatedNodes(surviveFocusChange()); Reference r = (Reference)getProperty(PROP_LAST_NODES); if (r != null && r.get() == ns) { // Still using the same Node[] we did last time. Remember the result. b = (Boolean)getProperty(PROP_LAST_ENABLED); if (((Boolean)getProperty(PROP_HAS_LISTENERS)).booleanValue()) { putProperty(PROP_ENABLED, b); } } else { // Really need to compute it. // #17433: do this outside the lock! } // if inactive, we cannot safely cache results because node selection might change } } if (b == null) { b = ((ns != null && enable(ns)) ? Boolean.TRUE : Boolean.FALSE); synchronized (getLock()) { putProperty(PROP_LAST_NODES, new WeakReference(ns)); putProperty(PROP_LAST_ENABLED, b); if (((Boolean)getProperty(PROP_HAS_LISTENERS)).booleanValue()) { putProperty(PROP_ENABLED, b); } } } return b.booleanValue(); } /* Change enablement state. * Clears our previous cache. * Some NodeAction subclasses (CookieAction, MoveUpAction, ...) may call this * when some aspect of the node selection other than the selection itself * changes, so we should clear the cache to ensure that the enablement status * is respected. */ public void setEnabled(boolean e) { putProperty(PROP_LAST_ENABLED, null); putProperty(PROP_LAST_NODES, null); if (((Boolean)getProperty(PROP_HAS_LISTENERS)).booleanValue()) { // Just set it; the next time selection chamges, we will recompute. super.setEnabled(e); } else { // Problematic. If we just set PROP_ENABLED then the next time isEnabled() // is called, even if the node selection is now different, we will be // in trouble; it will not bother to call enable() again. putProperty(PROP_ENABLED, null, true); } } /** Perform the action with a specific action event. * Normally this simply calls {@link #performAction()}, that is using * the global node selection. * However you may call this directly, with an action event whose * source is either a node or an array of nodes, to invoke the action * directly on that nodes or nodes. If you do this, the action must * be such that it would be enabled on that node selection, otherwise * the action is not required to behave correctly (that is, it can * be written to assume that it is never called with a node selection * it is not enabled on). * @param ev action event */ public void actionPerformed (java.awt.event.ActionEvent ev) { Object s = ev == null ? null : ev.getSource (); if (s instanceof Node) { performAction (new Node[] { (Node) s }); } else if (s instanceof Node[]) { performAction ((Node[])s); } else { performAction (); } } /** Performs the action. * In the default implementation, calls {@link #performAction(Node[])}. * In general you need not override this. */ public void performAction() { performAction (getActivatedNodes ()); } /** Get the currently activated nodes. * @return the nodes (may be empty but not <code>null</code>) */ public final Node[] getActivatedNodes () { Registry r = CallbackSystemAction.getRegistry (); if (r != null) { return r.getActivatedNodes (); } else { ExplorerManager em = CallbackSystemAction.getExplorerManager(); if (em != null) { return em.getSelectedNodes(); } } return new Node[0]; } /** Specify the behavior of the action when a window with no * activated nodes is selected. * If the action should then be disabled, * return <code>false</code> here; if the action should stay in the previous state, * return <code>true</code>. * <p>Note that {@link #getActivatedNodes} and {@link #performAction} are still * passed the set of selected nodes from the old window, if you keep this feature on. * This is useful, e.g., for an action like Compilation which should remain active * even if the user switches to a window like the Output Window that has no associated nodes; * then running the action will still use the last selection from e.g. an Explorer window * or the Editor, if there was one to begin with. * * @return <code>true</code> in the default implementation */ protected boolean surviveFocusChange() { return true; } /** * Perform the action based on the currently activated nodes. * Note that if the source of the event triggering this action was itself * a node, that node will be the sole argument to this method, rather * than the activated nodes. * * @param activatedNodes current activated nodes, may be empty but not <code>null</code> */ protected abstract void performAction (Node[] activatedNodes); /** * Test whether the action should be enabled based * on the currently activated nodes. * * @param activatedNodes current activated nodes, may be empty but not <code>null</code> * @return <code>true</code> to be enabled, <code>false</code> to be disabled */ protected abstract boolean enable (Node[] activatedNodes); /** Implements <code>ContextAwareAction</code> interface method. */ public Action createContextAwareInstance(Lookup actionContext) { return new DelegateAction(this, actionContext); } /** Fire PROP_ENABLE if the value is currently known (and clear that value). */ void maybeFireEnabledChange() { boolean fire = false; synchronized (getLock()) { if (getProperty(PROP_ENABLED) != null) { putProperty(PROP_ENABLED, null); fire = true; } } if (fire) { firePropertyChange(PROP_ENABLED, null, null); } } /** Getter for activated nodes * @return array */ private static Node[] getActivatedNodes (boolean survive) { Registry r = CallbackSystemAction.getRegistry (); if (r != null) { if (survive) { return r.getActivatedNodes (); } else { return r.getCurrentNodes (); } } else { ExplorerManager em = CallbackSystemAction.getExplorerManager(); if (em != null) { return em.getSelectedNodes(); } } return new Node[0]; } /** Node listener to check whether the action is enabled or not */ private static final class NodesL implements PropertyChangeListener { /** whether to change enablement of nodes marked to survive focus change */ private boolean chgSFC = false; /** and those marked to not survive */ private boolean chgNSFC = false; /** Constructor that checks the current state */ public NodesL() { } /** Activates/passivates the listener. */ void setActive (boolean active) { Registry r = CallbackSystemAction.getRegistry (); if (r != null) { if (active) { r.addPropertyChangeListener (this); } else { r.removePropertyChangeListener(this); // Any saved PROP_ENABLED will be bogus now: forget(true); forget(false); } } else { ExplorerManager em = CallbackSystemAction.getExplorerManager(); if (em != null) { if (active) { em.addPropertyChangeListener(this); } else { em.removePropertyChangeListener(this); forget(true); forget(false); } } } } /** Property change listener. */ public void propertyChange (PropertyChangeEvent ev) { String p = ev.getPropertyName(); boolean schedule = false; if (p == null || p.equals(Registry.PROP_ACTIVATED_NODES)) { chgSFC = true; schedule = true; } if (p == null || p.equals(Registry.PROP_CURRENT_NODES)) { chgNSFC = true; schedule = true; } if (p == null || p.equals(ExplorerManager.PROP_SELECTED_NODES)) { chgSFC = true; chgNSFC = true; schedule = true; } if (schedule) { update(); } } /** Updates the state of the action. */ public void update() { if (chgSFC) { forget(true); chgSFC = false; } if (chgNSFC) { forget(false); chgNSFC = false; } } /** Checks the state of the action. * Or rather, it just forgets it ever knew. * @param sfc if true, only survive-focus-change actions affected, else only not-s-f-c */ private void forget(boolean sfc) { List as; synchronized (listeningActions) { as = new ArrayList(listeningActions.size()); for (Iterator it = listeningActions.iterator(); it.hasNext(); ) { as.add(it.next()); } } Iterator it = as.iterator(); while (it.hasNext()) { NodeAction a = (NodeAction)it.next(); if (a.surviveFocusChange() == sfc) { a.maybeFireEnabledChange(); } } } } // end of NodesL /** A delegate action that is usually associated with a specific lookup and * extract the nodes it operates on from it. Otherwise it delegates to the * regular NodeAction. */ private static final class DelegateAction extends Object implements javax.swing.Action, org.openide.util.LookupListener, Presenter.Menu, Presenter.Popup, Presenter.Toolbar { /** action to delegate too */ private NodeAction delegate; /** lookup we are associated with (or null) */ private org.openide.util.Lookup.Result result; /** previous state of enabled */ private boolean enabled = true; /** support for listeners */ private PropertyChangeSupport support = new PropertyChangeSupport (this); public DelegateAction (NodeAction a, Lookup actionContext) { this.delegate = a; this.result = actionContext.lookup (new org.openide.util.Lookup.Template ( Node.class )); this.result.addLookupListener ((LookupListener)org.openide.util.WeakListener.create ( LookupListener.class, this, this.result )); resultChanged (null); } /** Overrides superclass method, adds delegate description. */ public String toString() { return super.toString() + "[delegate=" + delegate + "]"; // NOI18N } /** Nodes are taken from the lookup if any. */ private synchronized Node[] nodes () { Node[] empty = new Node[0]; if (result != null) { return (Node[])result.allInstances ().toArray (empty); } else { return empty; } } /** Invoked when an action occurs. */ public void actionPerformed(java.awt.event.ActionEvent e) { delegate.performAction (nodes ()); } public void addPropertyChangeListener(PropertyChangeListener listener) { support.addPropertyChangeListener (listener); } public void removePropertyChangeListener(PropertyChangeListener listener) { support.removePropertyChangeListener (listener); } public void putValue(String key, Object o) {} public Object getValue(String key) { return delegate.getValue(key); } public boolean isEnabled() { return delegate.enable(nodes ()); } public void setEnabled(boolean b) { } public void resultChanged(org.openide.util.LookupEvent ev) { boolean newEnabled = delegate.enable(nodes ()); if (newEnabled != enabled) { support.firePropertyChange (PROP_ENABLED, enabled, newEnabled); enabled = newEnabled; } } public javax.swing.JMenuItem getMenuPresenter() { if (isMethodOverriden (delegate, "getMenuPresenter")) { // NOI18N return delegate.getMenuPresenter (); } else { return new Actions.MenuItem(this, true); } } public javax.swing.JMenuItem getPopupPresenter() { if (isMethodOverriden (delegate, "getPopupPresenter")) { // NOI18N return delegate.getPopupPresenter (); } else { return new Actions.MenuItem(this, false); } } public java.awt.Component getToolbarPresenter() { if (isMethodOverriden (delegate, "getToolbarPresenter")) { // NOI18N return delegate.getToolbarPresenter (); } else { return new Actions.ToolbarButton (this); } } private boolean isMethodOverriden (NodeAction d, String name) { try { java.lang.reflect.Method m = d.getClass ().getMethod(name, new Class[0]); return m.getDeclaringClass() != CallableSystemAction.class; } catch (java.lang.NoSuchMethodException ex) { ex.printStackTrace(); throw new IllegalStateException ("Error searching for method " + name + " in " + d); // NOI18N } } } // end of DelegateAction }