/*
* 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.awt.Component;
import java.awt.AWTEvent;
import java.awt.event.AWTEventListener;
import java.awt.event.FocusEvent;
import java.lang.ref.*;
import java.util.ArrayList;
import java.util.Iterator;
import javax.swing.Action;
import org.openide.util.ContextAwareAction;
import org.openide.util.Lookup;
import org.openide.explorer.ExplorerManager;
import org.openide.windows.TopComponent.Registry;
import org.openide.util.WeakSet;
import org.openide.awt.Actions;
import org.openide.util.LookupListener;
import org.openide.windows.TopComponent;
/* enabled is old; action perf prop is not public --jglick
*
* <P>
* <TABLE BORDER COLS=3 WIDTH=100%>
* <TR><TH WIDTH=15%>Property<TH WIDTH=15%>Property Type<TH>Description
* <TR><TD> Enabled <TD> boolean <TD> The explicite enabled/disabled
* state of the action.
* <TR><TD> ActionPerformer <TD> ActionPerformer <TD> The class that performs the action
* </TABLE>
*/
/** Action that can have a performer of the action attached to it at any time,
* or changed.
* The action will be automatically disabled
* when it has no performer.
* <p>Also may be made sensitive to changes in window focus.
* @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 CallableSystemAction actions surviving focus change */
private static final WeakSet surviving = new WeakSet (37);
/** key to access listener */
private static final Object LISTENER = new Object ();
/** If the IDE is running (with the window system) we remember
* a reference to the registry.
*/
private static Registry registry;
// try to initialize as soon as this class is loaded,
// getRegistry adds a listener to java.awt.Toolkit
// we have to ensure that if the IDE is running (more precisely
// window system) the next calls to getRegistry remove this
// ugly listener from Toolkit.
static {
getRegistry();
}
/** If there is no registry we keep reference to an ExplorerManager.
*/
private static ExplorerManager explorerManager;
private static FocusTracker ft;
static final long serialVersionUID =-6305817805474624653L;
/** 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 <code>null</code> if there is currently no performer
*/
public ActionPerformer getActionPerformer() {
return (ActionPerformer)getProperty (PROP_ACTION_PERFORMER);
}
/** Set the action performer.
* The specified value can be <code>null</code>, which means that the action will have no performer
* and is disabled. ({@link #isEnabled} will return <code>false</code> regardless its previous state.)
* <P>
* This method is <em>too dynamic</em> 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
* <a href="http://openide.netbeans.org/proposals/actions/design.html#callback">ActionMap proposal</a>
* has been developed.
* <P>
* So if you are providing a {@link TopComponent} and want to provide
* your own handling of {@link org.openide.actions.CopyAction} use following code:
* <PRE>
* 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);
* </PRE>
* This code registers <code>yourCopyAction</code> with <code>tc</code>
* top component, so whenever a <code>globalCopyAction</code> is invoked,
* your action is being delegated to.
*
* @param performer the new action performer or <code>null</code> to disable
*
* @deprecated use TopComponent.getActionMap() as described in the javadoc
*/
public void setActionPerformer(ActionPerformer performer) {
putProperty (
PROP_ACTION_PERFORMER, performer
);
updateEnabled ();
}
/** Updates the enabled state by checking performer and ActionMap
*/
private void updateEnabled () {
javax.swing.Action action = findGlobalContextAction ();
if(action != null) {
setEnabled (action.isEnabled());
synchronized (LISTENER) {
ActionDelegateListener l = (ActionDelegateListener)getProperty(LISTENER);
if (l == null) {
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(java.awt.event.ActionEvent ev) {
// First try global context action.
javax.swing.Action action = findGlobalContextAction ();
if (action != null) {
action.actionPerformed(ev);
return;
}
ActionPerformer ap = getActionPerformer ();
if (ap != null) {
ap.performAction (this);
return;
}
}
/** Finds global context action (the one from activated component).
* @return action for key of activated component or null
*/
private javax.swing.Action findGlobalContextAction () {
TopComponent tc = TopComponent.getRegistry().getActivated();
if (tc != null) {
javax.swing.ActionMap map = tc.getActionMap();
if (map != null) {
Object key = getActionMapKey();
return map.get (key);
}
}
return null;
}
/** Perform the action.
* This default implementation calls the assigned action performer if it
* exists, otherwise does nothing.
*/
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. <codeActionMap</code> 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 <code>true</code> if the enabled state of the action survives focus changes
*/
public boolean getSurviveFocusChange () {
getProperty(null); // force initialization
return !notSurviving.contains (getClass ());
}
/** Implements <code>ContextAwareAction</code> interface method. */
public Action createContextAwareInstance(Lookup actionContext) {
return new DelegateAction(this, actionContext);
}
/** Set whether the action will survive a change in focus.
* If <code>false</code>, then the action will be automatically
* disabled (using {@link #setActionPerformer}) when the window
* focus changes.
*
* @param b <code>true</code> to survive focus changes, <code>false</code> 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 ());
}
}
}
/** Getter for component registry.
* @returns active registry or null if there is none
*/
static Registry getRegistry() {
if (registry != null) {
return registry;
}
try {
Class c = Class.forName("org.openide.windows.TopComponent$Registry"); // NOI18N
registry = (Registry)Lookup.getDefault().lookup(c);
} catch (Exception x) {
// ignore any exception and return null
}
if ((registry == null) && (ft == null)) {
startTrackingFocus();
}
if (registry != null) {
// lookup succesfull --> no need to globally listen on focus changes
stopTrackingFocus();
registry.addPropertyChangeListener (new FocusTracker ());
}
return registry; // if the IDE is present we should return non null
}
/** If there is no TopComponent.Registry use this
* instance of explorerManager to get selected nodes.
*/
static ExplorerManager getExplorerManager() {
return explorerManager;
}
/** Setter for the current explorer manager.*/
static void setExplorerManager(ExplorerManager e) {
explorerManager = e;
}
/** Attach a listener (FocusTracker) to global event delivery
* mechanism (Toolkit).
*/
private static void startTrackingFocus() {
ft = new FocusTracker();
java.awt.Toolkit.getDefaultToolkit().addAWTEventListener(
ft, AWTEvent.FOCUS_EVENT_MASK);
}
/** Unregisters FocusTracker from Toolkit. */
private static void stopTrackingFocus() {
if (ft != null) {
java.awt.Toolkit.getDefaultToolkit().removeAWTEventListener(ft);
ft = null;
}
}
/** Array of actions from a set of classes.
*/
private static ArrayList toInstances (java.util.Set s) {
ArrayList actions;
synchronized (notSurviving) {
actions = new ArrayList (s.size ());
Iterator it = s.iterator ();
while (it.hasNext ()) {
Class c = (Class)it.next ();
Object 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 () {
ArrayList actions = toInstances (notSurviving);
// clear the performers out of any loop
Iterator it = actions.iterator ();
while (it.hasNext ()) {
CallbackSystemAction a = (CallbackSystemAction)it.next ();
a.setActionPerformer (null);
}
actions = toInstances (surviving);
// clear the performers out of any loop
it = actions.iterator ();
while (it.hasNext ()) {
CallbackSystemAction a = (CallbackSystemAction)it.next ();
a.updateEnabled ();
}
}
/** This listener is attached to java.awt.Toolkit. If window
* manager is present (if it registers TopComponent.Registry
* in global lookup (Lookup.getDefault()) this listener should
* be immediatelly detached from the Toolkit.
*/
private static class FocusTracker implements AWTEventListener, PropertyChangeListener {
FocusTracker() {}
/**
* Invoked when an event is dispatched in the AWT.
*/
public void eventDispatched(AWTEvent event) {
if (event instanceof FocusEvent) {
FocusEvent fe = (FocusEvent)event;
if ((fe.getID() & FocusEvent.FOCUS_GAINED) == 0) {
// if it is not focus gained --> return
return;
}
}
Object source = event.getSource();
if (source instanceof Component) {
Component c = (Component)source;
while ((c != null) && (! (c instanceof ExplorerManager.Provider))) {
c = c.getParent();
}
if (c instanceof ExplorerManager.Provider) {
setExplorerManager(((ExplorerManager.Provider)c).getExplorerManager());
}
}
}
public void propertyChange(java.beans.PropertyChangeEvent ev) {
if (Registry.PROP_ACTIVATED.equals(ev.getPropertyName())) {
// deletes the performer
clearActionPerformers ();
}
}
} // end of FocusTracker
/** 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 Object
implements PropertyChangeListener {
private CallbackSystemAction action;
private javax.swing.Action delegate;
public ActionDelegateListener (CallbackSystemAction c, javax.swing.Action delegate) {
this.action = c;
this.delegate = delegate;
delegate.addPropertyChangeListener(this);
}
public void clear () {
javax.swing.Action a;
a = delegate;
if (a == null) return;
delegate = null;
a.removePropertyChangeListener (this);
}
public void attach (javax.swing.Action action) {
if (delegate == action) {
return;
}
// reattaches to different action
if (this.delegate != null) {
this.delegate.removePropertyChangeListener(this);
}
this.delegate = action;
action.addPropertyChangeListener(this);
}
public void propertyChange(java.beans.PropertyChangeEvent evt) {
synchronized (LISTENER) {
if (delegate == null) return;
}
action.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 javax.swing.Action, org.openide.util.LookupListener,
Presenter.Menu, Presenter.Popup, Presenter.Toolbar, PropertyChangeListener {
/** action to delegate too */
private CallbackSystemAction delegate;
/** lookup we are associated with (or null) */
private org.openide.util.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 javax.swing.Action last;
public DelegateAction (CallbackSystemAction a, Lookup actionContext) {
this.delegate = a;
this.weakL = org.openide.util.WeakListener.propertyChange (this, null);
this.enabled = a.getActionPerformer () != null;
this.result = actionContext.lookup (new org.openide.util.Lookup.Template (
javax.swing.ActionMap.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
}
/** Invoked when an action occurs.
*/
public void actionPerformed(java.awt.event.ActionEvent e) {
javax.swing.Action a = findAction ();
if (a != null) {
a.actionPerformed(e);
} 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() {
javax.swing.Action a = findAction ();
if (a == null) {
a = delegate;
}
if (a != last) {
if (last != null) {
last.removePropertyChangeListener (weakL);
}
last = a;
last.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 javax.swing.Action findAction () {
java.util.Collection c = result != null ?
result.allInstances() : java.util.Collections.EMPTY_LIST;
if (!c.isEmpty()) {
Object key = delegate.getActionMapKey();
for(Iterator it = c.iterator(); it.hasNext(); ) {
javax.swing.ActionMap map = (javax.swing.ActionMap)it.next();
javax.swing.Action action = map.get (key);
if(action != null) {
return action;
}
}
}
return null;
}
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 (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
}
}
} // end of DelegateAction
}