org.openide.util.actions.CallbackSystemAction Maven / Gradle / Ivy
The newest version!
/*
* DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
*
* Copyright 1997-2010 Oracle and/or its affiliates. All rights reserved.
*
* Oracle and Java are registered trademarks of Oracle and/or its affiliates.
* Other names may be trademarks of their respective owners.
*
* 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. Oracle designates this
* particular file as subject to the "Classpath" exception as provided
* by Oracle 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]"
*
* Contributor(s):
*
* The Original Software is NetBeans. The Initial Developer of the Original
* Software is Sun Microsystems, Inc. Portions Copyright 1997-2008 Sun
* Microsystems, Inc. All Rights Reserved.
*
* 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.
*/
package org.openide.util.actions;
import java.awt.Component;
import java.awt.Toolkit;
import java.awt.event.ActionEvent;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.beans.PropertyChangeSupport;
import java.lang.ref.Reference;
import java.lang.ref.WeakReference;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.swing.Action;
import javax.swing.ActionMap;
import org.openide.util.ContextAwareAction;
import org.openide.util.Lookup;
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;
/** Not preferred anymore, the replacement is
* Actions.callback factory method.
* To migrate to the new API just remove the definition of your action in
*
* layer file and replace it with:
*
* <file name="action-pkg-ClassName.instance">
* <attr name="instanceCreate" methodvalue="org.openide.awt.Actions.callback"/>
* <attr name="key" stringvalue="KeyInActionMap"/>
* <attr name="surviveFocusChange" boolvalue="false"/> <!-- defaults to false -->
* <attr name="fallback" newvalue="action.pkg.DefaultAction"/> <!-- may be missing -->
* <attr name="displayName" bundlevalue="your.pkg.Bundle#key"/>
* <attr name="iconBase" stringvalue="your/pkg/YourImage.png"/>
* <!-- if desired: <attr name="noIconInMenu" boolvalue="false"/> -->
* </file>
*
*
*
* @author Ian Formanek, Jaroslav Tulach, Petr Hamernik
*/
public abstract class CallbackSystemAction extends CallableSystemAction implements ContextAwareAction {
/** action performer */
private static final String PROP_ACTION_PERFORMER = "actionPerformer"; // NOI18N
/** a list of all actions that has survive focus change set to false */
private static final WeakSet> notSurviving = new WeakSet>(37);
/** a list of actions surviving focus change */
private static final WeakSet> surviving = new WeakSet>(37);
/** key to access listener */
private static final Object LISTENER = new Object();
static final long serialVersionUID = -6305817805474624653L;
/** logging */
private static final Logger err = Logger.getLogger(
"org.openide.util.actions.CallbackSystemAction"
); // NOI18N
/** Initialize the action to have no performer.
*/
protected void initialize() {
super.initialize();
updateEnabled();
setSurviveFocusChange(false);
}
/** Get the current action performer.
* @return the current action performer, or null
if there is currently no performer
* @deprecated use TopComponent.getActionMap() as described in the javadoc
*/
@Deprecated
public ActionPerformer getActionPerformer() {
return (ActionPerformer) getProperty(PROP_ACTION_PERFORMER);
}
/** Set the action performer.
* The specified value can be null
, which means that the action will have no performer
* and is disabled. ({@link #isEnabled} will return false
regardless its previous state.)
*
* This method is too dynamic it depends on the actuall order of callers and
* is for example very fragile with respect to focus switching and correct delivering of
* focus change events. That is why an alternative based on
* ActionMap proposal
* has been developed.
*
* So if you are providing a TopComponent
* and want to provide
* your own handling of CopyAction use following code:
*
* TopComponent tc = ...;
* javax.swing.Action yourCopyAction = ...; // the action to invoke instead of Copy
*
* CopyAction globalCopyAction = SystemAction.get (CopyAction.class);
* Object key = globalCopyAction.getActionMapKey(); // key is a special value defined by all CallbackSystemActions
*
* // and finally:
* tc.getActionMap ().put (key, yourCopyAction);
*
* This code registers yourCopyAction
with tc
* top component, so whenever a globalCopyAction
is invoked,
* your action is being delegated to.
*
* @param performer the new action performer or null
to disable
*
* @deprecated use TopComponent.getActionMap() as described in the javadoc
*/
@Deprecated
public void setActionPerformer(ActionPerformer performer) {
putProperty(PROP_ACTION_PERFORMER, performer);
updateEnabled();
}
/** Updates the enabled state by checking performer and ActionMap
*/
private void updateEnabled() {
Action action = GlobalManager.getDefault().findGlobalAction(
getActionMapKey(), getSurviveFocusChange()
);
if (action != null) {
setEnabled(action.isEnabled());
synchronized (LISTENER) {
ActionDelegateListener l = (ActionDelegateListener) getProperty(LISTENER);
if ((l == null) || (l.get() != this)) {
l = new ActionDelegateListener(this, action);
putProperty(LISTENER, l);
} else {
l.attach(action);
}
}
} else {
if (getActionPerformer() != null) {
// we have performer
setEnabled(true);
} else {
setEnabled(false);
}
clearListener();
}
}
/** Clears the listener.
*/
private void clearListener() {
synchronized (LISTENER) {
// remove listener on any action
ActionDelegateListener l = (ActionDelegateListener) getProperty(LISTENER);
if (l != null) {
l.clear();
putProperty(LISTENER, null);
}
}
}
/** Perform the action. Tries the performer and then scans the ActionMap
* of selected topcomponent.
*/
public void actionPerformed(final ActionEvent ev) {
// First try global context action.
final Action action = GlobalManager.getDefault().findGlobalAction(getActionMapKey(), getSurviveFocusChange());
if (action != null) {
if (action.isEnabled()) {
action.actionPerformed(ev);
} else {
Toolkit.getDefaultToolkit().beep();
}
return;
}
final Object ap = getActionPerformer();
if (ap != null) {
org.openide.util.actions.ActionInvoker.invokeAction(
this, ev, asynchronous(), new Runnable() {
public void run() {
if (ap == getActionPerformer()) {
getActionPerformer().performAction(CallbackSystemAction.this);
}
}
}
);
return;
}
Toolkit.getDefaultToolkit().beep();
}
/** Perform the action.
* This default implementation calls the assigned action performer if it
* exists, otherwise does nothing.
* @deprecated This only uses {@link ActionPerformer}. Use {@link #actionPerformed} instead.
*/
@Deprecated
public void performAction() {
ActionPerformer ap = getActionPerformer();
if (ap != null) {
ap.performAction(this);
}
}
/** Getter for action map key, which is used to find action from provided
* context (i.e. ActionMap
provided by the context),
* which acts as a callback.
* Override this method in subclasses to provide 'nice' key.
* @return key which is used to find the action which performs callback,
* default returned key is a class name.
* @since 3.29 */
public Object getActionMapKey() {
return getClass().getName();
}
/** Test whether the action will survive a change in focus.
* By default, it will not.
* @return true
if the enabled state of the action survives focus changes
*/
public boolean getSurviveFocusChange() {
getProperty(null); // force initialization
return !notSurviving.contains(getClass());
}
/** Implements ContextAwareAction
interface method. */
public Action createContextAwareInstance(Lookup actionContext) {
return new DelegateAction(this, actionContext);
}
/** Set whether the action will survive a change in focus.
* If false
, then the action will be automatically
* disabled (using {@link #setActionPerformer}) when the window
* focus changes.
*
* @param b true
to survive focus changes, false
to be sensitive to them
*/
public void setSurviveFocusChange(boolean b) {
synchronized (notSurviving) {
if (b) {
notSurviving.remove(getClass());
surviving.add(getClass());
} else {
notSurviving.add(getClass());
surviving.remove(getClass());
}
}
}
/** Array of actions from a set of classes.
*/
private static List toInstances(java.util.Set> s) {
List actions;
synchronized (notSurviving) {
actions = new ArrayList(s.size());
for (Class extends CallbackSystemAction> c : s) {
CallbackSystemAction a = SystemAction.findObject(c, false);
if (a != null) {
actions.add(a);
}
}
}
return actions;
}
/** Clears all action performers for those that has setSurviveFocusChange
* on true.
*/
private static void clearActionPerformers() {
List actions = toInstances(notSurviving);
// clear the performers out of any loop
for (CallbackSystemAction a : actions) {
a.setActionPerformer(null);
}
actions = toInstances(surviving);
// clear the performers out of any loop
for (CallbackSystemAction a : actions) {
if (err.isLoggable(Level.FINE)) {
err.fine("updateEnabled: " + a); // NOI18N
}
a.updateEnabled();
}
}
/** Listener on a global context.
*/
private static final class GlobalManager implements LookupListener {
private static GlobalManager instance;
private Lookup.Result result;
private List> actionMaps = new ArrayList>(2);
private final ActionMap survive = new ActionMap();
private GlobalManager() {
result = Utilities.actionsGlobalContext().lookup(new Lookup.Template(ActionMap.class));
result.addLookupListener(this);
resultChanged(null);
}
public synchronized static GlobalManager getDefault() {
if (instance != null) {
return instance;
}
instance = new GlobalManager();
return instance;
}
public Action findGlobalAction(Object key, boolean surviveFocusChange) {
// search action in all action maps from global context
Action a = null;
for (Reference ref : actionMaps) {
ActionMap am = ref.get();
a = am == null ? null : am.get(key);
if (a != null) {
break;
}
}
if (surviveFocusChange) {
if (a == null) {
a = survive.get(key);
if (a != null) {
a = ((WeakAction) a).getDelegate();
}
if (err.isLoggable(Level.FINE)) {
err.fine("No action for key: " + key + " using delegate: " + a); // NOI18N
}
} else {
if (err.isLoggable(Level.FINE)) {
err.fine("New action for key: " + key + " put: " + a);
}
survive.put(key, new WeakAction(a));
}
}
if (err.isLoggable(Level.FINE)) {
err.fine("Action for key: " + key + " is: " + a); // NOI18N
}
return a;
}
/** Change all that do not survive ActionMap change */
public void resultChanged(org.openide.util.LookupEvent ev) {
Collection extends ActionMap> ams = result.allInstances();
if (err.isLoggable(Level.FINE)) {
err.fine("changed maps : " + ams); // NOI18N
err.fine("previous maps: " + actionMaps); // NOI18N
}
// do nothing if maps are actually the same
if (ams.size() == actionMaps.size()) {
boolean theSame = true;
int i = 0;
for (Iterator extends ActionMap> newMaps = ams.iterator(); newMaps.hasNext(); i++) {
ActionMap oldMap = actionMaps.get(i).get();
if (oldMap == null || oldMap != newMaps.next()) {
theSame = false;
break;
}
}
if (theSame) {
return;
}
}
// update actionMaps
List> tempActionMaps = new ArrayList>(2);
for (ActionMap actionMap : ams) {
tempActionMaps.add(new WeakReference(actionMap));
}
actionMaps = tempActionMaps;
if (err.isLoggable(Level.FINE)) {
err.fine("clearActionPerformers"); // NOI18N
}
Mutex.EVENT.readAccess(new Runnable() {
public void run() {
clearActionPerformers();
}
});
}
}
// end of LookupListener
/** An action that holds a weak reference to other action.
*/
private static final class WeakAction extends WeakReference implements Action {
public WeakAction(Action delegate) {
super(delegate);
}
public Action getDelegate() {
return get();
}
public Object getValue(String key) {
throw new UnsupportedOperationException();
}
public void putValue(String key, Object value) {
throw new UnsupportedOperationException();
}
public void actionPerformed(ActionEvent e) {
throw new UnsupportedOperationException();
}
public void removePropertyChangeListener(PropertyChangeListener listener) {
throw new UnsupportedOperationException();
}
public void addPropertyChangeListener(PropertyChangeListener listener) {
throw new UnsupportedOperationException();
}
public void setEnabled(boolean b) {
throw new UnsupportedOperationException();
}
public boolean isEnabled() {
throw new UnsupportedOperationException();
}
}
/** A class that listens on changes in enabled state of an action
* and updates the state of the action according to it.
*/
private static final class ActionDelegateListener extends WeakReference implements PropertyChangeListener {
private Reference delegate;
public ActionDelegateListener(CallbackSystemAction c, Action delegate) {
super(c);
this.delegate = new WeakReference(delegate);
delegate.addPropertyChangeListener(this);
}
public void clear() {
Action a;
Reference d = delegate;
a = d == null ? null : d.get();
if (a == null) {
return;
}
delegate = null;
a.removePropertyChangeListener(this);
}
public void attach(Action action) {
Reference d = delegate;
if ((d == null) || (d.get() == action)) {
return;
}
Action prev = d.get();
// reattaches to different action
if (prev != null) {
prev.removePropertyChangeListener(this);
}
this.delegate = new WeakReference(action);
action.addPropertyChangeListener(this);
}
public void propertyChange(java.beans.PropertyChangeEvent evt) {
synchronized (LISTENER) {
Reference d = delegate;
if ((d == null) || (d.get() == null)) {
return;
}
}
CallbackSystemAction c = get();
if (c != null) {
c.updateEnabled();
}
}
}
/** 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 Action,
LookupListener, Presenter.Menu, Presenter.Popup, Presenter.Toolbar, PropertyChangeListener {
/** action to delegate too */
private CallbackSystemAction delegate;
/** lookup we are associated with (or null) */
private Lookup.Result result;
/** previous state of enabled */
private boolean enabled;
/** support for listeners */
private PropertyChangeSupport support = new PropertyChangeSupport(this);
/** listener to check listen on state of action(s) we delegate to */
private PropertyChangeListener weakL;
/** last action we were listening to */
private Reference lastRef;
public DelegateAction(CallbackSystemAction a, Lookup actionContext) {
this.delegate = a;
this.weakL = org.openide.util.WeakListeners.propertyChange(this, null);
this.enabled = a.getActionPerformer() != null;
this.result = actionContext.lookup(new Lookup.Template(ActionMap.class));
this.result.addLookupListener(WeakListeners.create(LookupListener.class, this, this.result));
resultChanged(null);
}
/** Overrides superclass method, adds delegate description. */
public String toString() {
return super.toString() + "[delegate=" + delegate + "]"; // NOI18N
}
/** Invoked when an action occurs.
*/
public void actionPerformed(final java.awt.event.ActionEvent e) {
final Action a = findAction();
if (a != null) {
Runnable run = new Runnable() {
public void run() {
a.actionPerformed(e);
}
};
org.openide.util.actions.ActionInvoker.invokeAction(delegate, e, delegate.asynchronous(), run);
} else {
// XXX #30303 if the action falls back to the old behaviour
// it may not be performed in case it is in dialog and
// is not transmodal.
// This is just a hack, see TopComponent.processKeyBinding.
Object source = e.getSource();
if (
source instanceof Component &&
javax.swing.SwingUtilities.getWindowAncestor((Component) source) instanceof java.awt.Dialog
) {
Object value = delegate.getValue("OpenIDE-Transmodal-Action"); // NOI18N
if (!Boolean.TRUE.equals(value)) {
return;
}
}
delegate.actionPerformed(e);
}
}
public boolean isEnabled() {
Action a = findAction();
if (a == null) {
a = delegate;
}
// 40915 - hold last action weakly
Action last = lastRef == null ? null : lastRef.get();
if (a != last) {
if (last != null) {
last.removePropertyChangeListener(weakL);
}
lastRef = new WeakReference(a);
a.addPropertyChangeListener(weakL);
}
return a.isEnabled();
}
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 void setEnabled(boolean b) {
}
public void resultChanged(org.openide.util.LookupEvent ev) {
boolean newEnabled = isEnabled();
if (newEnabled != enabled) {
support.firePropertyChange(PROP_ENABLED, enabled, newEnabled);
enabled = newEnabled;
}
}
public void propertyChange(PropertyChangeEvent evt) {
resultChanged(null);
}
/*** Finds an action that we should delegate to
* @return the action or null
*/
private Action findAction() {
Collection extends ActionMap> c = result != null ? result.allInstances() : Collections.emptySet();
if (!c.isEmpty()) {
Object key = delegate.getActionMapKey();
for (ActionMap map : c) {
Action action = map.get(key);
if (action != null) {
return action;
}
}
}
return null;
}
public javax.swing.JMenuItem getMenuPresenter() {
if (isMethodOverridden(delegate, "getMenuPresenter")) { // NOI18N
return delegate.getMenuPresenter();
} else {
return org.openide.util.actions.ActionPresenterProvider.getDefault().createMenuPresenter(this);
}
}
public javax.swing.JMenuItem getPopupPresenter() {
if (isMethodOverridden(delegate, "getPopupPresenter")) { // NOI18N
return delegate.getPopupPresenter();
} else {
return org.openide.util.actions.ActionPresenterProvider.getDefault().createPopupPresenter(this);
}
}
public java.awt.Component getToolbarPresenter() {
if (isMethodOverridden(delegate, "getToolbarPresenter")) { // NOI18N
return delegate.getToolbarPresenter();
} else {
return org.openide.util.actions.ActionPresenterProvider.getDefault().createToolbarPresenter(this);
}
}
private boolean isMethodOverridden(CallableSystemAction 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
}
}
protected void finalize() {
Action last = lastRef == null ? null : lastRef.get();
if (last != null) {
last.removePropertyChangeListener(weakL);
}
}
}
// end of DelegateAction
}