blob: 92158ecae69c610f09a17fcae3596d2aa986ee99 [file] [log] [blame]
/*
* 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.netbeans.spi.actions;
import org.openide.util.*;
import java.awt.Image;
import java.awt.event.ActionEvent;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.util.Collection;
import javax.swing.Action;
import javax.swing.ImageIcon;
/**
* An action which operates in the global <i>selection context</i> (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, or both of those things.
* <p/>
* Context actions implement <code>org.openide.util.ContextAwareAction</code>,
* which has the method <code>createContextAwareInstance()</code> to create
* an instance of the action for a specific context (for example, a popup
* menu - the selection might change while the popup is on-screen, but what
* the action in the popup menu operates on should not).
* <p/>
* Context actions make it possible to have a global action which doesn't
* have to know anything in particular about which object going to operate on;
* it just knows the type of that object. When invoked, it acts on whatever
* is selected at that moment in time.
* <p/>
* This class and its subclasses are a replacement for most uses of
* NodeAction and CookieAction from the Nodes API.
* In the NetBeans IDE, to declaratively install a global ContextAction, simply write
* a static method that contains the above code, and reference it in your
* layer file as the <code>methodvalue</code> for your <code>.instance</code>
* or similar file.
* <p/>
* <h4>Using ContextAction in place of NodeAction or CookieAction</h4>
* Replacing CookieAction with ContextAction is fairly straightforward.
* <p/>
* The most straightforward replacement for <code>CookieAction</code> is
* <code>LookupProviderAction</code>. It handles one layer of indirection -
* that is, a <code>CookieAction</code> first needs to have at least one <code>Node</code>
* selected; a <code>Node</code> is itself a <code>Lookup.Provider</code>,
* and so the <code>CookieAction</code>
* is sensitive to some object type in the lookup of one or more selected
* <code>Node</code>s. So a <code>CookieAction</code> is an action sensitive
* to an object (the cookie) in the lookup of an object in the selection (the node).
* <p/>
* <code>LookupProviderAction</code> accomplishes the same thing, without
* requiring that the second <code>Lookup.Provider</code> be a <code>Node</code>.
* <p>
* CookieAction supported a number of different <i>mode</i>s
* which determined its enablement, based on how many objects were in the
* selection and if there were any lookups in the selection that did <i>not</i>
* contain an instance of the type it was sensitive to. In <code>LookupProviderAction</code>,
* this distinction is split into its two constituents: To disallow enablement
* if there is some Lookup.Provider that does <i>not</i> contain the type you
* care about, pass true to the superclass constructor for the <code>all</code>
* parameter. To control for the number of selected items, override
* <code>checkQuantity(int)</code>.
* <p/>
* It is also possible with <code>ContextAction</code>s to do deeper indirection.
* E.g., say the selection will contain a <code>Node</code>. <code>Node</code>
* implements <code>Lookup.Provider</code>. You are interested in <code>Node</code>s
* which have a <code>Project</code> in their lookup, and <code>Project</code>
* is also a <code>Lookup.Provider</code> too. What you really want to do is
* write an action that is enabled
* <ul><li>When the selection contains a <code>Node</code>
* <ul><li>which contains a <code>Project</code> in its lookup</code>
* <ul><li>which contains a <code>Foo</code> in its lookup</code>
* </ul</li>
* </ul></li>
* </ul></li>
* The only actually interesting logic here is what you will do with a
* <code>Foo</code> if you can get hold of one. Handling this is easy:
* <pre>
* ContextAction<Foo> theRealAction = new MyContextAction(); //this is what you wrote
* Action<Node> theGlobalAction = createIndirectAction(Node.class,
* createIndirectAction(Project.class, theRealAction));
* </pre>
* If you had a CookieAction which was sensitive to more than one cookie type,
* that scenario is not supported directly by ContextAction. But what you
* do instead will likely be simpler and more testable than your original
* code, and will do the same thing:
* <ul>
* <li>Write one ContextAction subclass for each of the types you want to
* support</li>
* <li>Create your global action by calling
* <code>ContextAction.merge (actionOne, actionTwo, actionThree);
* </li>
* </ul>
* <h4>A Note about Equality</h4>
* ContextAction and any subclasses implement equals() and hashCode() such that
* if the <em>type</em> of the passed object is the same, then the objects are treated
* as identical. This enables multi-selection to work. This means that any
* instance of a subclass of ContextAction is effectively a singleton and any
* other instance of the same class may be substituted for it.
* <p/>
* If you want to create a single subclass of ContextAction which is passed
* different parameters or create instances of the same subtype which
* have different behaviors, you
* are probably choosing the wrong parent class for your action. The point
* of ContextAction is that the entirety of the &quot;context&quot; is the
* content of the Lookup that is passed to it.
* <p/>
* To, for example, change the display name depending on number of objects
* selected, simply override <code>change()</code> to do that.
*
* @see org.openide.util.ContextAwareAction
* @see org.openide.util.Lookup
* @see org.openide.util.Utilities#actionsGlobalContext
* @author Tim Boudreau
*/
public abstract class ContextAction<T> extends NbAction {
final Class<T> type;
private final StubListener stubListener = new StubListener();
//A context aware instance which we use internally to trigger
//enabled changes as long as there is at least one property change
//listener attached to us.
//By having the same thing we return from createContextAwareInstance handle
//all internal state, we make it easy to make a survives-focus-change
//subclass just by overriding createStub() to make a stub which retains
//the last usable collection of objects
//The action stub is the thing that really does the heavy lifting for
//all ContextActions.
ActionStub<T> stub;
static boolean unitTest;
/**
* Create a new ContextAction which will operate on instances of
* type <code>type</code>
* @param type The type this action needs in its context in order to be
* invoked
*/
protected ContextAction(Class<T> type) {
this (type, null, null);
}
/**
* Create a new ContextAction which will operate on a type <code>type</code>,
* 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(Class<T> type, String displayName, Image icon) {
this.type = type;
Parameters.notNull("type", type);
if (displayName != null) {
putValue (Action.NAME, displayName);
}
if (icon != null) {
putValue (Action.SMALL_ICON, new ImageIcon (icon));
}
putValue ("noIconInMenu", true);
}
/**
* Whether or not the action is enabled. By default, determines if there
* are any instances of type <code>type</code> in the selection context
* lookup, and if there are, returns true. To refine this behavior further,
* override <code>enabled (java.util.Collection)</code>.
* @return whether or not the action should be enabled
*/
@Override
public final boolean isEnabled() {
return _isEnabled();
}
boolean _isEnabled() { //For override in MergeAction
ActionStub<T> stubAction = getStub();
Collection <? extends T> targets = stubAction.collection();
boolean result = checkQuantity(targets) && stubAction.isEnabled();
return result;
}
boolean checkQuantity(Collection<? extends T> targets) {
return checkQuantity (targets.size());
}
/**
* Determine if this action should be enabled, based on the number
* of objects of type <code>type</code> available in the selection.
* Some actions will want to be enabled only if there is a single
* object of a given type, or some specific number (for example,
* an action which diffs two files should only be enabled if exactly
* two files are selected).
* <p/>
* This method is called prior to the (potentially)
* more expensive check of <code>enabled(Collection&lt;T&gt;)</code>. If it
* returns false, no further tests are done; the action is disabled.
*
* @param numberOfObjects The number of objects of type <code>T</code>
* in the selection
* @return True if the action should be enabled.
*/
protected boolean checkQuantity (int numberOfObjects) {
return numberOfObjects > 0;
}
/**
* 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 <code>true</code>. 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.
*
* @param targets A collection of objects of type <code>type</code>
* @return Whether or not the action should be enabled.
*/
protected boolean isEnabled(Collection<? extends T> targets) {
return true;
}
/**
* Override to actually do whatever this action does. This method is
* passed the collection of all objects of type <code>&lt;T&gt;
* present in the selection context (the lookup).
* @param targets The objects of type <code>type</code>, 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 <code>actionPerformed(Collection<? extends T>).
* @param ignored 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 <i>while</i> the popup is onscreen, we do not want
* the popup to operate on the new selection; it should operate on the thing
* the menu was created for. 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 An action specific to the passed Lookup
*/
@Override
protected final NbAction internalCreateContextAwareInstance(Lookup actionContext) {
return createStub (actionContext);
}
/**
* Equals in ContextAction and its subclasses will return true if the
* passed object is of the same <em>type</em> as the passed object. This
* means that two instances of the same subtype of ContextAction are
* equal if they are the same subtype (this enables actions to work over
* multiple selection).
* <p/>
* If this is not the behavior you want, it is very likely that
* ContextAction is not the correct superclass for your action.
*
* @param o The foreign object
* @return whether or not this object is functionally equivalent to the
* passed object
*/
@Override
public boolean equals (Object o) {
return o != null && o.getClass() == getClass();
}
/**
* Returns getClass().hashCode();
* @return The hash code of this type.
*/
@Override
public int hashCode() {
return getClass().getName().hashCode();
}
ActionStub<T> createStub(Lookup actionContext) {
return new ActionStub<T>(actionContext, this);
}
private ActionStub<T> createInternalStub () {
//Don't synchronize, just ensure we are only called from sync methods
assert Thread.holdsLock(lock());
ActionStub<T> result = createStub (Utilities.actionsGlobalContext());
return result;
}
ActionStub<T> getStub() {
synchronized (lock()) {
if (stub == null && attached()) {
stub = createInternalStub();
stub.addPropertyChangeListener(stubListener);
}
return stub == null ? createInternalStub() : stub;
}
}
@Override
public String toString() {
return super.toString() + "[name=" + getValue(NAME) + " type=" + type.getName() + "]";
}
/**
* Called when the set of available objects of type <code>type</code>
* changes. Override if you incorporate some aspect of the selection
* in the display name, and need to change it when the collection changes.
* <p/>
* Note that this method may be called with an empty collection.
* @param collection The collection of objects currently in the selection
* @param instance The action instance which encountered the change. <i>
* Note that <code>instance</code> may or may not == this.</i> It might
* be an action returned by createContextAwareInstance() for display in
* a popup menu, which is responding to a change in the context it
* operates on. If you need to change the display name, call
* <code>putValue(NAME, newName)</code> on <code>instance</code>,
* not on <code>this</code>. Do not assume <code>instance</code> is
* an instance of your <code>ContextAction</code> subclass - it may or
* it may not be.
*/
protected void change (Collection <? extends T> collection, Action instance) {
//do nothing
}
@Override
void internalAddNotify() {
stub = getStub();
stub.resultChanged(null);
super.internalAddNotify();
}
@Override
void internalRemoveNotify() {
try {
super.internalRemoveNotify();
} finally {
stub.removePropertyChangeListener(stubListener);
stub = null;
}
}
/**
* Recompute the enabled state of this action. Call this method if your
* action depends on some external value to compute its enabled state,
* when that value changes.
*/
protected final void refresh() {
getStub().resultChanged(null);
}
//for unit tests
Collection<? extends T> stubCollection() {
synchronized (lock()) {
return stub == null ? null : stub.collection();
}
}
private class StubListener implements PropertyChangeListener {
public void propertyChange(PropertyChangeEvent evt) {
firePropertyChange (evt.getPropertyName(),
evt.getOldValue(), evt.getNewValue());
}
}
/**
* Create an action which looks for a particular type <code>T</code> in
* the selection, where T itself is a subclass <code>Lookup.Provider</code>
* (such as a Node or Project in the NetBeans IDE), and delegates to
* another action which is sensitive to some object in that lookup.
* <p/>
* Such actions can be chained to any depth of lookup providers;
* you only need to write a ContextAction that is sensitive to the thing
* you are actually interested in at the end of the chain.
* So, if you want to write an action which is sensitive to, say, a
* ClassPathProvider that belongs to the Project found in the selected
* Node in the global selection, you would do as follows:
* <pre>
* ContextAction<ClassPathProvider> theRealAction = new MyContextAction();
* Action<Node> theGlobalAction = createIndirectAction(Node.class,
* createIndirectAction(Project.class, theRealAction));
* </pre>
* The returned action will pick up its properties (name, icon, enabled
* state, etc.) from the action passed in to this one, and it will re-fire
* property changes from that one.
* <p>
* If you need more specific control of enablement logic, either override
* <code>enabled()</code> in your <code>ContextAction</code>, or use
* one of the subclasses such as <code>Single</code> or
* <code>SurviveSelectionChange</code> which provide
* specific enablement behavior.
*
* @param <T> The Lookup.Provider subclass (for example,
* <code>Node</code>, <code>DataObject</code> or <code>Project</code>)
* @param lkpProviderType An object which implements Lookup.Provider which
* will be in the lookup.
* @param theRealAction The action to invoke if all the conditions are met
* @param allLookupsMustBeUsable if true, then if any of the Lookup.Providers
* cannot provide an object of the type <code>theRealAction</code> cares
* about, do not enable the action. Otherwise the action will be enabled
* if any objects <code>theRealAction</code> is interested in are present.
* If you want to write an action that works on multi-selection, but only
* if the action can work against all of the selected objects, pass true.
* If the action should be enabled if only some of the objects are interesting
* to <code>theRealAction</code>, pass false.
* @return A ContextAction
*/
public static <T extends Lookup.Provider, R> ContextAction<T>
createIndirectAction(Class<T> lkpProviderType, ContextAction<R> theRealAction, boolean allLookupsMustBeUsable) {
return new IndirectAction<T,R> (lkpProviderType, theRealAction,
allLookupsMustBeUsable);
}
/**
* Same as <code>createIndirectAction (lkpProviderType, theRealAction,
* <i>true</i>)</code>.
* @param <T> The Lookup.Provider subclass (for example,
* <code>Node</code>, <code>DataObject</code> or <code>Project</code>)
* @param lkpProviderType An object which implements Lookup.Provider which
* will be in the lookup.
* @param theRealAction The action to invoke if all the conditions are met
* @return A contextAction
*/
public static <T extends Lookup.Provider, R> ContextAction<T>
createIndirectAction(Class<T> lkpProviderType, ContextAction<R> theRealAction) {
return createIndirectAction (lkpProviderType, theRealAction, true);
}
}