All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.openide.util.actions.NodeAction Maven / Gradle / Ivy

There is a newer version: RELEASE240
Show newest version
/*
 * Licensed to the Apache Software Foundation (ASF) under one
 * or more contributor license agreements.  See the NOTICE file
 * distributed with this work for additional information
 * regarding copyright ownership.  The ASF licenses this file
 * to you under the Apache 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.apache.org/licenses/LICENSE-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 org.openide.util.actions;


import java.awt.Component;
import java.awt.event.ActionEvent;
import java.beans.PropertyChangeListener;
import java.beans.PropertyChangeSupport;
import java.lang.ref.Reference;
import java.lang.ref.WeakReference;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.swing.Action;
import javax.swing.JMenuItem;
import org.openide.awt.Actions;
import org.openide.nodes.Node;
import org.openide.util.ContextAwareAction;
import org.openide.util.Exceptions;
import org.openide.util.Lookup;
import org.openide.util.LookupEvent;
import org.openide.util.LookupListener;
import org.openide.util.Mutex;
import org.openide.util.Utilities;
import org.openide.util.WeakListeners;
import org.openide.util.WeakSet;

/**
 * A type of action that listens on change in activated nodes selection and
 * allows its subclasses to simply change their enabled state and handle
 * action invocation requests.
 * 

* Whenever a list of activated nodes changes (a new * TopComponent is selected or * its internal selection changes like in * explorer) * the overriden method {@link #enable} * is called and state of the action is updated * according to the result. When the action is performed, the subclasses are * notified by a call to their {@link #performAction(Node[])} where they * can perform their operation on the currently selected array of nodes. * *

Note: if your action involves getting cookies * from nodes, which in many cases is the correct design, please use * CookieAction instead, as that permits sensitivity to cookies * and also listens to changes in supplied cookies. */ 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 */ 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); /* Initialize the listener. */ @Override 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. */ @Override 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. */ @Override 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 true to enable */ @Override public boolean isEnabled() { Node[] ns = null; Boolean b = null; synchronized (getLock()) { b = (Boolean) getProperty(PROP_ENABLED); if (NodeAction.l == null) { NodeAction.l = new NodesL (); l.setActive (true); } NodesL listener = NodeAction.l; if (b == null) { ns = listener.getActivatedNodes(surviveFocusChange()); Reference r = (Reference) getProperty(PROP_LAST_NODES); if ((r != null) && java.util.Arrays.equals((Node[]) 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. */ @Override public void setEnabled(boolean e) { putProperty(PROP_LAST_ENABLED, null); putProperty(PROP_LAST_NODES, null); Boolean propHasListener = (Boolean) getProperty(PROP_HAS_LISTENERS); if (propHasListener != null && propHasListener.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 * @deprecated Using a special action event in this way is deprecated. * Better is to use {@link #createContextAwareInstance} and pass * a lookup containing all desired {@link Node} instances. */ @Deprecated @Override public void actionPerformed(final ActionEvent ev) { final Object s = (ev == null) ? null : ev.getSource(); if (s instanceof Node) { ActionInvoker.invokeAction(this, ev, amIasynchronous(), new Runnable() { @Override public void run() { performAction(new Node[] { (Node) s }); } }); } else if (s instanceof Node[]) { ActionInvoker.invokeAction(this, ev, amIasynchronous(), new Runnable() { @Override public void run() { performAction((Node[]) s); } }); } else { super.actionPerformed(ev); } } /** Performs the action. * In the default implementation, calls {@link #performAction(Node[])}. * @deprecated Do not call this programmatically. * Use {@link #createContextAwareInstance} to pass in a node selection. * Do not override this method. */ @Deprecated public void performAction() { performAction(getActivatedNodes()); } /** Get the currently activated nodes. * @return the nodes (may be empty but not null) */ public final Node[] getActivatedNodes() { NodesL listener = NodeAction.l; return (listener == null) ? new Node[0] : listener.getActivatedNodes(true); } /** Specify the behavior of the action when a window with no * activated nodes is selected. * If the action should then be disabled, * return false here; if the action should stay in the previous state, * return true. *

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 true 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 null */ 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 null * @return true to be enabled, false to be disabled */ protected abstract boolean enable(Node[] activatedNodes); /** Implements ContextAwareAction interface method. * * Returns a delegate action that is associated with a specific lookup and * extracts the nodes it operates on from it. Otherwise it delegates to the * regular NodeAction (especially to {@link #enable} and {@link #performAction} methods). * Note: Never call directly methods setEnabled or putValue * of this delegate action, it is useless, they are empty. The enablement * state of the action is driven by the content of the actionContext. * * @param actionContext a lookup contains action context, cannot be null * @return a delegate action */ 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) { try { firePropertyChange(PROP_ENABLED, null, null); } catch (NullPointerException e) { // Probably because of a JDK bug that AbstractButton$ButtonActionPropertyChangeListener.propertyChange does not grok null values for "enabled" prop: Exceptions.attachMessage(e, "You cannot add " + getClass().getName() + " directly to a JMenu etc.; use org.openide.awt.Actions.connect instead"); // NOI18N Logger.getLogger(NodeAction.class.getName()).log(Level.WARNING, null, e); } } } final boolean amIasynchronous() { return asynchronous(); } /** Node listener to check whether the action is enabled or not */ private static final class NodesL implements LookupListener { /** result with Nodes we listen to */ private volatile Lookup.Result result; /** 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; /** pointer to previously activated nodes (via Reference to Node) */ private Reference[] activatedNodes; /** Constructor that checks the current state */ public NodesL() { } /** Computes the list of activated nodes. */ public Node[] getActivatedNodes(boolean survive) { OUTER: if (survive && (activatedNodes != null)) { Node[] arr = new Node[activatedNodes.length]; for (int i = 0; i < arr.length; i++) { if ((arr[i] = activatedNodes[i].get()) == null) { break OUTER; } } return arr; } Lookup.Result r = result; return (r == null) ? new Node[0] : r.allInstances().toArray(new Node[0]); } /** Activates/passivates the listener. */ synchronized void setActive(boolean active) { Lookup context = Utilities.actionsGlobalContext(); if (active) { if (result == null) { result = context.lookupResult(Node.class); result.addLookupListener(this); } } else { // result.removeLookupListener (this); // result = null; // Any saved PROP_ENABLED will be bogus now: forget(true); forget(false); } } /** Property change listener. */ public void resultChanged(LookupEvent ev) { Lookup.Result r = result; if (r == null) { return; } chgSFC = true; chgNSFC = true; Collection> items = result.allItems(); boolean updateActivatedNodes = true; if (items.size() == 1) { Lookup.Item item = items.iterator().next(); if ("none".equals(item.getId()) && (item.getInstance() == null)) { // this is change of selected node to null, // do not update activatedNodes updateActivatedNodes = false; } } if (updateActivatedNodes) { Iterator it = result.allInstances().iterator(); ArrayList> list = new ArrayList>(); while (it.hasNext()) { list.add(new WeakReference(it.next())); } activatedNodes = list.toArray(new Reference[list.size()]); } 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()) { final NodeAction a = it.next(); if (a.surviveFocusChange() == sfc) { Mutex.EVENT.readAccess(new Runnable() { public void run() { 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. */ static class DelegateAction implements Action, LookupListener, Presenter.Menu, Presenter.Popup, Presenter.Toolbar { private static final Node[] EMPTY_NODE_ARRAY = new Node[0]; /** 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.lookupResult(Node.class); this.result.addLookupListener(WeakListeners.create(LookupListener.class, this, this.result)); resultChanged(null); } /** Overrides superclass method, adds delegate description. */ @Override public String toString() { return super.toString() + "[delegate=" + delegate + "]"; // NOI18N } /** Nodes are taken from the lookup if any. */ public final synchronized Node[] nodes() { if (result != null) { return result.allInstances().toArray(EMPTY_NODE_ARRAY); } else { return EMPTY_NODE_ARRAY; } } /** Invoked when an action occurs. */ @Override public void actionPerformed(ActionEvent e) { ActionInvoker.invokeAction(delegate, e, delegate.amIasynchronous(), new Runnable() { @Override public void run() { 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 enabled; } public void setEnabled(boolean b) { } public void resultChanged(LookupEvent ev) { final boolean old = enabled; enabled = delegate.enable(nodes()); Mutex.EVENT.readAccess(new Runnable() { @Override public void run () { support.firePropertyChange(PROP_ENABLED, old, enabled); } }); } public JMenuItem getMenuPresenter() { if (isMethodOverridden(delegate, "getMenuPresenter")) { // NOI18N return delegate.getMenuPresenter(); } else { return new Actions.MenuItem(this, true); } } public JMenuItem getPopupPresenter() { if (isMethodOverridden(delegate, "getPopupPresenter")) { // NOI18N return delegate.getPopupPresenter(); } else { return new Actions.MenuItem(this, false); } } public Component getToolbarPresenter() { if (isMethodOverridden(delegate, "getToolbarPresenter")) { // NOI18N return delegate.getToolbarPresenter(); } else { return new Actions.ToolbarButton(this); } } private boolean isMethodOverridden(NodeAction d, String name) { try { 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 }





© 2015 - 2025 Weber Informatics LLC | Privacy Policy