diff -r cfdd8230bdb4 openide.util/src/org/openide/util/ContextAction.java --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/openide.util/src/org/openide/util/ContextAction.java Wed Nov 19 02:57:18 2008 +0000 @@ -0,0 +1,557 @@ +/* + * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER. + * + * Copyright 2008 Sun Microsystems, Inc. All rights reserved. + * + * The contents of this file are subject to the terms of either the GNU + * General Public License Version 2 only ("GPL") or the Common + * Development and Distribution License("CDDL") (collectively, the + * "License"). You may not use this file except in compliance with the + * License. You can obtain a copy of the License at + * http://www.netbeans.org/cddl-gplv2.html + * or nbbuild/licenses/CDDL-GPL-2-CP. See the License for the + * specific language governing permissions and limitations under the + * License. When distributing the software, include this License Header + * Notice in each file and include the License file at + * nbbuild/licenses/CDDL-GPL-2-CP. Sun designates this + * particular file as subject to the "Classpath" exception as provided + * by Sun in the GPL Version 2 section of the License file that + * accompanied this code. If applicable, add the following below the + * License Header, with the fields enclosed by brackets [] replaced by + * your own identifying information: + * "Portions Copyrighted [year] [name of copyright owner]" + * + * If you wish your version of this file to be governed by only the CDDL + * or only the GPL Version 2, indicate your decision by adding + * "[Contributor] elects to include this software in this distribution + * under the [CDDL or GPL Version 2] license." If you do not indicate a + * single choice of license, a recipient has the option to distribute + * your version of this file under either the CDDL, the GPL Version 2 or + * to extend the choice of license to its licensees as provided above. + * However, if you add GPL Version 2 code and therefore, elected the GPL + * Version 2 license, then the option applies only if the new code is + * made subject to such option by the copyright holder. + * + * Contributor(s): + * + * Portions Copyrighted 2008 Sun Microsystems, Inc. + */ +package org.openide.util; + +import java.awt.Image; +import java.awt.event.ActionEvent; +import java.beans.PropertyChangeEvent; +import java.beans.PropertyChangeListener; +import java.beans.PropertyChangeSupport; +import java.util.Arrays; +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; +import javax.swing.AbstractAction; +import javax.swing.Action; +import javax.swing.ImageIcon; + +/** + * An action which operates in the global selection context (a + * Lookup such as the return value of Utilities.actionsGlobalContext()). + * Context actions are sensitive to the presence or absence of a particular + * Java type in a Lookup. The presence or absence of objects of that type + * can changed based on things like what the user has selected, or what + * logical window contains focus. + *
+ * Context actions make it possible to have a global action (i.e. it doesn't + * have to know anything in particular about what it's going to operate on + * to be created) which nonetheless acts on whatever happens to be selected + * at some moment in time. + * + * This class is a replacement for most uses of NodeAction and CookieAction. + * @see org.openide.util.ContextAwareAction + * @see org.openide.util.Lookup + * @see org.openide.util.Utilities.actionsGlobalContext() + * @author Tim Boudreau + */ +public abstract class ContextActiontype
+ * @param type The type this action needs in its context in order to be
+ * invoked
+ */
+ protected ContextAction(Classtype
,
+ * with the specified display name and icon.
+ *
+ * @param type The type this action needs in its context in order to be
+ * invoked
+ * @param displayName A localized display name
+ * @param icon An image to use as the action's icon
+ */
+ protected ContextAction(Classtype
in the selection context
+ * lookup, and if there are, returns true. To refine this behavior further,
+ * override enabled (java.util.Collection)
.
+ * @return
+ */
+ @Override
+ public final boolean isEnabled() {
+ ActionStub stubAction = getStub();
+ Collection extends T> targets = stubAction.collection();
+ boolean result = checkQuantity(targets) && stubAction.isEnabled();
+ return result;
+ }
+
+ boolean checkQuantity(Collection> targets) {
+ return !targets.isEmpty();
+ }
+
+ /**
+ * Determine if this action should be enabled. This method will only be
+ * called if the size of the collection is > 0. The default implementation
+ * returns true
. If you need to do some further
+ * test on the collection of objects to determine if the action should
+ * really be enabled or not, override this method do that here.
+ *
+ * @param targets A collection of objects of type type
+ * @return Whether or not the action should be enabled.
+ */
+ protected boolean enabled(Collection extends T> targets) {
+ return true;
+ }
+
+ /**
+ * Overridden to throw an UnsupportedOperationException. Do not call.
+ * @param newValue
+ */
+ @Override
+ public final void setEnabled(boolean newValue) {
+ throw new UnsupportedOperationException();
+ }
+
+ /**
+ * Override to actually do whatever this action does. This method is
+ * passed the current collection of objects present in the selection
+ * context.
+ * @param targets The objects of type type
, which
+ * this action will use to do whatever it does
+ */
+ protected abstract void actionPerformed(Collection extends T> targets);
+
+ /**
+ * Fetches the collection of objects this action will act on and passes
+ * them to actionPerformed(Collection extends T>).
+ * @param e The action event. Ignored.
+ */
+ public final void actionPerformed(ActionEvent ignored) {
+ getStub().actionPerformed(null);
+ }
+
+ /**
+ * Create an instance of this action over a particular context. This is
+ * used to handle cases such as popup menus, where a popup menu is created
+ * against whatever the selection is at the time of its creation; if the
+ * selection changes while the popup is onscreen, we do not want
+ * the popup to operate on the new selection; it should operate on the thing
+ * that the user right-clicked. So for a popup menu, an instance of this
+ * action is created over a snapshot-lookup - a snapshot
+ * of the context at the moment it is created.
+ * @param actionContext The context this action instance should operate on.
+ * @return
+ */
+ public final Action createContextAwareInstance(Lookup actionContext) {
+ return createStub (actionContext);
+ }
+
+ ActionStub createStub(Lookup actionContext) {
+ return new ActionStub(actionContext, this);
+ }
+
+ private ActionStub createInternalStub () {
+ //Don't synchronize, just ensure we are only called from sync methods
+ assert Thread.holdsLock(this);
+ ActionStub result = (ActionStub)
+ createContextAwareInstance (Utilities.actionsGlobalContext());
+ return result;
+ }
+
+ private synchronized ActionStub getStub() {
+ if (stub == null && attached) {
+ stub = createInternalStub();
+ stub.addPropertyChangeListener(stubListener);
+ }
+ return stub == null ? createInternalStub() : stub;
+ }
+ /**
+ * Get the help context, if any. Returns HelpCtx.DEFAULT_HELP
+ * by default.
+ * @return A help context
+ */
+ public HelpCtx getHelpCtx() {
+ return HelpCtx.DEFAULT_HELP;
+ }
+
+ @Override
+ public String toString() {
+ return super.toString() + "[type=" + type.getName() + "]";
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ if (obj == null) {
+ return false;
+ }
+ if (getClass() != obj.getClass()) {
+ return false;
+ }
+ final ContextAction other = (ContextAction) obj;
+ if (this.type != other.type && (this.type == null || !this.type.equals(other.type))) {
+ return false;
+ }
+ return true;
+ }
+
+ @Override
+ public int hashCode() {
+ int hash = 7;
+ hash = 47 * hash + (this.type != null ? this.type.hashCode() : 0);
+ return hash;
+ }
+
+ @Override
+ public synchronized void addPropertyChangeListener(PropertyChangeListener listener) {
+ super.addPropertyChangeListener(listener);
+ int count = getPropertyChangeListeners().length;
+ if (count == 1) {
+ addNotify();
+ }
+ }
+
+ @Override
+ public synchronized void removePropertyChangeListener(PropertyChangeListener listener) {
+ super.removePropertyChangeListener(listener);
+ int count = getPropertyChangeListeners().length;
+ if (count == 0) {
+ removeNotify();
+ }
+ }
+
+ volatile boolean attached;
+ void addNotify() {
+ assert Thread.holdsLock(this);
+ attached = true;
+ stub = getStub();
+ }
+
+ void removeNotify() {
+ assert Thread.holdsLock(this);
+ attached = false;
+ stub.removePropertyChangeListener(stubListener);
+ stub = null;
+ }
+
+ //for unit tests
+ synchronized Collection extends T> stubCollection() {
+ return stub == null ? null : stub.collection();
+ }
+
+ private class StubListener implements PropertyChangeListener {
+
+ public void propertyChange(PropertyChangeEvent evt) {
+ if ("enabled".equals(evt.getPropertyName())) { //NOI18N
+ firePropertyChange (evt.getPropertyName(),
+ evt.getOldValue(), evt.getNewValue());
+ }
+ }
+
+ }
+
+ //Inner stub action class which delegates to the parent action's methods.
+ //Used both for context aware instances, and for internal state for
+ //ContextAction instances
+ private static class ActionStub implements Action, LookupListener {
+ private final Lookup.Result lkpResult;
+ private final Map pairs = new HashMap();
+ private final PropertyChangeSupport supp = new PropertyChangeSupport(this);
+ private final Lookup context;
+ protected final ContextAction parent;
+ protected boolean enabled;
+
+ ActionStub(Lookup context, ContextAction parent) {
+ assert context != null;
+ this.context = context;
+ this.parent = parent;
+ lkpResult = context.lookupResult(parent.type);
+ lkpResult.addLookupListener(this);
+ if (getClass() == ActionStub.class) {
+ //avoid superclass call to Retained.collection() before
+ //it has initialized
+ enabled = isEnabled();
+ }
+ }
+
+ public Object getValue(String key) {
+ Object result = pairs.get(key);
+ if (result == null) {
+ result = parent.getValue(key);
+ }
+ return result;
+ }
+
+ Collection extends T> collection() {
+ return lkpResult.allInstances();
+ }
+
+ public void putValue(String key, Object value) {
+ Object old = pairs.put(key, value);
+ boolean fire = (old == null) != (value == null);
+ if (fire) {
+ fire = value != null && !value.equals(old);
+ if (fire) {
+ supp.firePropertyChange (key, old, value);
+ }
+ }
+ }
+
+ public void setEnabled(boolean b) {
+ //Will throw exception
+ parent.setEnabled(b);
+ }
+
+ public boolean isEnabled() {
+ Collection extends T> targets = collection();
+ assert targets != null;
+ assert parent != null;
+ return targets.isEmpty() ? false : parent.checkQuantity(targets) &&
+ parent.enabled(targets);
+ }
+
+ public void addPropertyChangeListener(PropertyChangeListener listener) {
+ supp.addPropertyChangeListener(listener);
+ }
+
+ public void removePropertyChangeListener(PropertyChangeListener listener) {
+ supp.removePropertyChangeListener(listener);
+ }
+
+ public void actionPerformed(ActionEvent e) {
+ assert isEnabled() : "Not enabled: " + this;
+ Collection extends T> targets = collection();
+ parent.actionPerformed(targets);
+ }
+
+ void enabledChanged(final boolean enabled) {
+ Mutex.EVENT.readAccess(new Runnable() {
+ public void run() {
+ supp.firePropertyChange("enabled", !enabled, enabled); //NOI18N
+ if (unitTest) {
+ synchronized (parent) {
+ parent.notifyAll();
+ }
+ synchronized (this) {
+ this.notifyAll();
+ }
+ }
+ }
+ });
+ }
+
+ public void resultChanged(LookupEvent ev) {
+ boolean old = enabled;
+ enabled = isEnabled();
+ if (old != enabled) {
+ enabledChanged (enabled);
+ }
+ }
+
+ @Override
+ public String toString() {
+ return super.toString() + "[name=" + getValue (NAME) + //NOI18N
+ "delegating={"+ parent + "} context=" + //NOI18N
+ context +"]"; //NOI18N
+ }
+ }
+
+ /**
+ * Subclass of ContextAction which does not support multi-selection -
+ * like ContextAction, it is sensitive to a particular type. However,
+ * it only is enabled if there is exactly one object of type type
+ * in the selection.
+ * @param The type this action is sensitive to
+ */
+ public static abstract class Single extends ContextAction {
+ protected Single (Class type) {
+ super (type);
+ }
+
+ protected Single(Class type, String displayName, Image icon) {
+ super (type, displayName, icon);
+ }
+
+ /**
+ * Delegates to actionePerformed(T)
with the first and
+ * only element of the collection.
+ * @param targets The objects this action may operate on
+ */
+ @Override
+ protected final void actionPerformed(Collection extends T> targets) {
+ actionPerformed (targets.iterator().next());
+ }
+
+ /**
+ * Actually perform the action.
+ * @param target The only instance of T
in the action
+ * context.
+ */
+ protected abstract void actionPerformed (T target);
+
+ @Override
+ boolean checkQuantity(Collection> targets) {
+ return targets.size() == 1;
+ }
+
+ /**
+ * Determine if this action should be enabled. This method will only be
+ * called if the size of the collection == 1. The default implementation
+ * returns true
. If you need to do some further
+ * test on the collection of objects to determine if the action should
+ * really be enabled or not, override this method do that here.
+ *
+ * @param targets A collection of objects of type type
+ * @return Whether or not the action should be enabled.
+ */
+ @Override
+ protected boolean enabled(Collection extends T> targets) {
+ //Overridden only in order to have different javadoc
+ return super.enabled(targets);
+ }
+ }
+
+ /**
+ * A context action which, once enabled, remains enabled.
+ *
+ * The canonical example of this sort of action in the NetBeans IDE is
+ * NextErrorAction: It becomes enabled when the output window gains
+ * focus. But it should remain enabled when focus goes back to the
+ * editor, and still work against whatever context the output window
+ * gave it to work on. Such cases are rare but legitimate.
+ *
+ * Use judiciously - such actions are temporary memory
+ * leaks - the action will retain the last usable collection of
+ * objects it had to work on as long as there are any property
+ * change listeners attached to it.
+ *
+ * @param count
+ * instances of type
present in the selection
+ * context.
+ * @param type The type this action is sensitive too
+ * @param count Precisely how many instances of type
+ * need to be in the selection context for isEnabled() to be true
+ * or actionPerformed()
to be called.
+ */
+ protected ExactCount (Class