/*
* 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.actions;
import java.awt.*;
import java.awt.datatransfer.Clipboard;
import java.awt.datatransfer.ClipboardOwner;
import java.awt.datatransfer.StringSelection;
import java.awt.datatransfer.Transferable;
import java.awt.event.*;
import java.beans.*;
import java.util.ArrayList;
import java.util.Arrays;
import javax.swing.Action;
import javax.swing.ActionMap;
import javax.swing.event.*;
import org.openide.util.datatransfer.*;
import org.openide.util.HelpCtx;
import org.openide.util.actions.*;
import org.openide.awt.Actions;
import org.openide.ErrorManager;
import org.openide.explorer.ExplorerManager;
import org.openide.nodes.NodeListener;
import org.openide.util.UserCancelException;
import org.openide.nodes.Node;
import org.openide.nodes.NodeEvent;
import org.openide.nodes.NodeMemberEvent;
import org.openide.nodes.NodeReorderEvent;
import org.openide.util.Lookup;
import org.openide.util.LookupListener;
import org.openide.util.NbBundle;
import org.openide.windows.TopComponent;
/** Paste from clipboard. This is a callback system action,
* with enhanced behaviour. Others can plug in by adding
* <PRE>
* topcomponent.getActionMap ().put (javax.swing.text.DefaultEditorKit.pasteAction, theActualAction);
* </PRE>
* or by using the now deprecated <code>setPasteTypes</code> and <code>setActionPerformer</code>
* methods.
* <P>
* There is a special support for more than one type of paste to be enabled at once.
* If the <code>theActualAction</code> returns array of actions from
* <code>getValue ("delegates")</code> than those actions are offered as
* subelements by the paste action presenter.
*/
public final class PasteAction extends CallbackSystemAction {
/** generated Serialized Version UID */
static final long serialVersionUID = -6620328110138256516L;
/** Imlementation of ActSubMenuInt */
private static ActSubMenuModel globalModel;
/** All currently possible paste types. */
private static PasteType[] types;
/** Lazy initializtion of the global model */
private static synchronized ActSubMenuModel model () {
if (globalModel == null) {
globalModel = new ActSubMenuModel (null);
}
return globalModel;
}
/* Overrides superclass initialization */
protected void initialize () {
super.initialize();
setEnabled (false);
}
/* Human presentable name of the action. This should be
* presented as an item in a menu.
* @return the name of the action
*/
public String getName() {
return NbBundle.getMessage(PasteAction.class, "Paste");
}
/* Help context where to find more about the action.
* @return the help context for this action
*/
public HelpCtx getHelpCtx() {
return new HelpCtx (PasteAction.class);
}
/* Icon resource.
* @return name of resource for icon
*/
protected String iconResource () {
return "org/openide/resources/actions/paste.gif"; // NOI18N
}
/* Returns a JMenuItem that presents the Action, that implements this
* interface, in a MenuBar.
* @return the JMenuItem representation for the Action
*/
public javax.swing.JMenuItem getMenuPresenter() {
return new Actions.SubMenu(this, model (), false);
}
/* Returns a JMenuItem that presents the Action, that implements this
* interface, in a PopupMenu.
* @return the JMenuItem representation for the Action
*/
public javax.swing.JMenuItem getPopupPresenter() {
return new Actions.SubMenu(this, model (), true);
}
/** Overrides superclass method. */
public Action createContextAwareInstance(Lookup actionContext) {
return new DelegateAction(this, actionContext);
}
/** Gets action map key, overrides superclass method.
* @return key used to find an action from context's ActionMap */
public Object getActionMapKey() {
return javax.swing.text.DefaultEditorKit.pasteAction;
}
public void actionPerformed(java.awt.event.ActionEvent ev) {
PasteType t;
if (ev.getSource() instanceof PasteType) {
t = (PasteType)ev.getSource ();
} else {
PasteType[] arr = getPasteTypes ();
if (arr != null && arr.length > 0) {
t = arr[0];
} else {
t = null;
}
}
if (t == null) {
// Try to find paste action 'performer' from activated TopComponent.
Action ac = findActionFromActivatedTopComponentMap();
if(ac != null) {
// XXX Hack to get paste types from action 'performer',
// which in fact doesn't perform the paste.
// Look at ExplorerActions.OwnPaste#getValue method.
PasteType[] arr = (PasteType[])ac.getValue("delegates"); // NOI18N
if(arr != null && arr.length > 0) {
t = arr[0];
} else {
ac.actionPerformed(ev);
return;
}
}
}
if(t != null) {
executePasteType (t);
} else {
ErrorManager.getDefault().notify(
ErrorManager.INFORMATIONAL,
new IllegalStateException(
"No paste types available when performing paste action")); // NOI18N
}
}
/** Does the execution of a paste type with all handling around
*/
private static void executePasteType (PasteType t) {
NodeSelector sel = null;
try {
ExplorerManager em = findExplorerManager ();
if (em != null) {
sel = new NodeSelector (em, null);
}
Transferable trans = t.paste();
Clipboard clipboard = getClipboard();
if (trans != null) {
ClipboardOwner owner = trans instanceof ClipboardOwner ?
(ClipboardOwner)trans
:
new StringSelection (""); // NOI18N
clipboard.setContents(trans, owner);
}
} catch (UserCancelException exc) {
// ignore - user just pressed cancel in some dialog....
} catch (java.io.IOException e) {
ErrorManager.getDefault().notify(e);
} finally {
if (sel != null) {
sel.select ();
}
}
}
/** Set possible paste types.
* Automatically enables or disables the paste action according to whether there are any.
* @param types the new types to allow, or <code>null</code>
*/
public void setPasteTypes(PasteType[] types) {
this.types = types;
if ((types == null) || (types.length == 0)) {
setEnabled(false);
}
else {
setEnabled(true);
}
model ().checkStateChanged (true);
}
/** Get all paste types.
* @return all possible paste types, or <code>null</code> */
public PasteType[] getPasteTypes() {
return types;
}
/** Finds paste action from currently activated TopComponent's action map. */
private static Action findActionFromActivatedTopComponentMap() {
TopComponent tc = TopComponent.getRegistry().getActivated();
if (tc != null) {
ActionMap map = tc.getActionMap ();
return findActionFromMap(map);
}
return null;
}
/** Finds paste action from provided map. */
private static javax.swing.Action findActionFromMap (ActionMap map) {
if (map != null) {
return map.get (javax.swing.text.DefaultEditorKit.pasteAction);
}
return null;
}
/** If our clipboard is not found return the default system clipboard. */
private static Clipboard getClipboard() {
Clipboard c = (java.awt.datatransfer.Clipboard)
org.openide.util.Lookup.getDefault().lookup(java.awt.datatransfer.Clipboard.class);
if (c == null) {
c = java.awt.Toolkit.getDefaultToolkit().getSystemClipboard();
}
return c;
}
/** General implementation of Actions.SubMenuModel that works
* with provided lookup or without it. With lookup it attaches
* to changes in the lookup and updates its state according to
* it. Without it listens on TopComponent.getActivated() and
* works with it.
*/
private static class ActSubMenuModel extends EventListenerList
implements Actions.SubMenuModel, LookupListener, PropertyChangeListener {
/** lookup we are attached to or null we we should work globally */
private Lookup.Result result;
/** previous enabled state */
private boolean enabled;
/** our weak listener */
private PropertyChangeListener weakL;
/** @param lookup can be null */
public ActSubMenuModel (Lookup lookup) {
weakL = org.openide.util.WeakListener.propertyChange(this, null);
attachListenerToChangesInMap (lookup);
}
/** Finds appropriate map to work with.
* @return map from lookup or from activated TopComponent, null no available
*/
private ActionMap map () {
if (result == null) {
org.openide.windows.TopComponent tc = org.openide.windows.TopComponent.getRegistry().getActivated();
if (tc != null) {
return tc.getActionMap ();
}
} else {
java.util.Iterator it = result.allItems ().iterator ();
while (it.hasNext()) {
Object o = ((Lookup.Item)it.next ()).getInstance ();
if (o instanceof ActionMap) {
return (ActionMap)o;
}
}
}
return null;
}
/** Adds itself as a listener for changes in current ActionMap.
* If the lookup is null then it means to listen on TopComponent
* otherwise to listen on the lookup itself.
*
* @param lookup lookup to listen on or null
*/
private void attachListenerToChangesInMap (Lookup lookup) {
if (lookup == null) {
org.openide.windows.TopComponent.getRegistry().addPropertyChangeListener(
org.openide.util.WeakListener.propertyChange (
this, org.openide.windows.TopComponent.getRegistry()
)
);
} else {
result = lookup.lookup (new Lookup.Template (ActionMap.class));
result.addLookupListener (this);
}
checkStateChanged (false);
}
/** Finds the currently active items this method should delegate to.
* For historical reasons one can use PasteType by PasteAction.setPasteTypes
* in the new implementation it is expected that such paste types
* will be replaced by Actions (obtained from getValue("delegates")).
*
*
* @param actionToWorkWith array of size 1 or null. Will be filled
* with action that we actually delegate to (either the global or local
* found in action map)
* @return array of either PasteTypes or Actions
*/
private Object[] getPasteTypesOrActions (Action[] actionToWorkWith) {
Action x = findActionFromMap(map ());
if (x == null) {
// No context action use the global one.
PasteAction a = (PasteAction)findObject (PasteAction.class);
if (actionToWorkWith != null) {
actionToWorkWith[0] = a;
}
Object[] arr = a.getPasteTypes();
if (arr != null) {
return arr;
} else {
return new Object[0];
}
}
if (actionToWorkWith != null) {
actionToWorkWith[0] = x;
}
Object obj = x.getValue ("delegates"); // NOI18N
if (obj instanceof Object[]) {
return (Object[])obj;
} else {
return new Object[] { x };
}
}
public boolean isEnabled() {
Object[] arr = getPasteTypesOrActions(null);
if(arr.length == 1 && arr[0] instanceof Action) {
return ((Action)arr[0]).isEnabled();
} else {
return arr.length > 0;
}
}
public int getCount() {
return getPasteTypesOrActions (null).length;
}
public String getLabel(int index) {
Object[] arr = getPasteTypesOrActions (null);
if (arr.length <= index) {
return null;
}
if (arr[index] instanceof PasteType) {
return ((PasteType)arr[index]).getName();
} else {
// is Action
return (String) ((Action)arr[index]).getValue(Action.NAME);
}
}
public HelpCtx getHelpCtx (int index) {
Object[] arr = getPasteTypesOrActions (null);
if (arr.length <= index) {
return null;
}
if (arr[index] instanceof PasteType) {
return ((PasteType)arr[index]).getHelpCtx ();
} else {
// is action
Object helpID = ((Action)arr[index]).getValue ("helpID"); // NOI18N
if(helpID instanceof String) {
return new HelpCtx((String)helpID);
} else {
return null;
}
}
}
public MenuShortcut getMenuShortcut(int index) {
return null;
}
public void performActionAt(int index) {
Action[] action = new Action[1];
Object[] arr = getPasteTypesOrActions (action);
if (arr.length <= index) {
return;
}
if (arr[index] instanceof PasteType) {
PasteType t = (PasteType)arr[index];
invokeAction(new ActionPT (t),new ActionEvent (t,ActionEvent.ACTION_PERFORMED, javax.swing.Action.NAME));
return;
} else {
// is action
Action a = (Action)arr[index];
invokeAction (a, new ActionEvent (a, ActionEvent.ACTION_PERFORMED, a.NAME));
return;
}
}
/** Registers .ChangeListener to receive events.
*@param listener The listener to register.
*/
public synchronized void addChangeListener(javax.swing.event.ChangeListener listener) {
add (javax.swing.event.ChangeListener.class, listener);
}
/** Removes .ChangeListener from the list of listeners.
*@param listener The listener to remove.
*/
public synchronized void removeChangeListener(javax.swing.event.ChangeListener listener) {
remove (javax.swing.event.ChangeListener.class, listener);
}
/** Notifies all registered listeners about the event.
*
*@param param1 Parameter #1 of the <CODE>.ChangeEvent<CODE> constructor.
*/
protected void checkStateChanged(boolean fire) {
Action[] listen = new Action[1];
Object[] arr = getPasteTypesOrActions(listen);
Action a = null;
if (arr.length == 1 && arr[0] instanceof Action) {
a = (Action)arr[0];
a.removePropertyChangeListener(weakL);
a.addPropertyChangeListener(weakL);
}
// plus always make sure we are listening on the actions
if (listen[0] != a) {
listen[0].removePropertyChangeListener(weakL);
listen[0].addPropertyChangeListener (weakL);
}
boolean en = isEnabled ();
if (en == enabled) {
return;
}
enabled = en;
// and fire if requested....
if (!fire) {
return;
}
Object[] listeners = getListenerList ();
if (listeners.length == 0) {
return;
}
javax.swing.event.ChangeEvent e = new javax.swing.event.ChangeEvent (
this
);
for (int i = listeners.length-1; i>=0; i-=2) {
((javax.swing.event.ChangeListener)listeners[i]).stateChanged (e);
}
}
public void propertyChange(java.beans.PropertyChangeEvent evt) {
checkStateChanged (true);
}
public void resultChanged(org.openide.util.LookupEvent ev) {
checkStateChanged (true);
}
}
/** Utility method for invoking actions in separate thread. Note:
* it uses reflection because it should work without
* the rest of the IDE classes.
*/
static void invokeAction(Action sa, ActionEvent ev) {
Throwable t = null;
try {
Class c = Class.forName("org.openide.actions.ActionManager"); // NOI18N
Object o = org.openide.util.Lookup.getDefault ().lookup(c);
if (o != null) {
// lookup has found the instance
// use reflection now
java.lang.reflect.Method m = c.getMethod("invokeAction", // NOI18N
new Class[] {
javax.swing.Action.class,
java.awt.event.ActionEvent.class });
m.invoke(o, new Object[] { sa, ev } );
// everything went ok -->
return;
}
}
// exceptions from forName:
catch (ClassNotFoundException x) { }
catch (ExceptionInInitializerError x) { }
catch (LinkageError x) { }
// exceptions from getMethod:
catch (SecurityException x) { t = x; }
catch (NoSuchMethodException x) { t = x;}
// exceptions from invoke
catch (IllegalAccessException x) { t = x;}
catch (IllegalArgumentException x) { t = x;}
catch (java.lang.reflect.InvocationTargetException x) {
t = x;
}
if (t != null) {
ErrorManager.getDefault ().notify(ErrorManager.INFORMATIONAL, t);
}
// something went wrong --> invoke the action directly
sa.actionPerformed(ev);
}
/** Utilitity method for finding the currently selected explorer manager.
* it uses reflection because it should work without
* the rest of the IDE classes.
*
* @return current explorer manager or null
*/
static ExplorerManager findExplorerManager () {
Throwable t = null;
try {
Class c = Class.forName("org.openide.windows.TopComponent"); // NOI18N
// use reflection now
java.lang.reflect.Method m = c.getMethod("getRegistry", // NOI18N
new Class[0]
);
Object o = m.invoke(null, new Object[0] );
c = Class.forName("org.openide.windows.TopComponent$Registry"); // NOI18N
// use reflection now
m = c.getMethod("getActivated", // NOI18N
new Class[0]
);
o = m.invoke (o, new Object[0]);
if (o instanceof ExplorerManager.Provider) {
return ((ExplorerManager.Provider)o).getExplorerManager();
}
}
// exceptions from forName:
catch (ClassNotFoundException x) { }
catch (ExceptionInInitializerError x) { }
catch (LinkageError x) { }
// exceptions from getMethod:
catch (SecurityException x) { t = x; }
catch (NoSuchMethodException x) { t = x;}
// exceptions from invoke
catch (IllegalAccessException x) { t = x;}
catch (IllegalArgumentException x) { t = x;}
catch (java.lang.reflect.InvocationTargetException x) {
t = x;
}
if (t != null) {
ErrorManager.getDefault ().notify(ErrorManager.INFORMATIONAL, t);
}
return null;
}
/** Class that listens on a given node and when invoked listen on changes
* and after that tries to select the desired node.
*/
static final class NodeSelector extends Object
implements NodeListener, Runnable {
/** All added children */
private ArrayList added;
/** node we are listening to */
private Node node;
/** manager to work with */
private ExplorerManager em;
/** children */
private Node[] children;
/** @param em explorer manager to work with
* @param n nodes to attach to or null if em's nodes should be used
*/
public NodeSelector (ExplorerManager em, Node[] n) {
this.em = em;
if (n != null && n.length > 0) {
this.node = n[0];
} else {
Node[] arr = em.getSelectedNodes ();
if (arr.length != 0) {
this.node = arr[0];
} else {
// do not initialize
return;
}
}
this.children = node.getChildren().getNodes(true);
this.added = new ArrayList ();
this.node.addNodeListener (this);
}
/** Selects the added nodes */
public void select () {
if (added != null) {
// if initialized => wait till finished update
node.getChildren().getNodes(true);
// and select the right nodes
org.openide.nodes.Children.MUTEX.readAccess (this);
}
}
public void run () {
this.node.removeNodeListener (this);
if (added.isEmpty()) {
return;
}
Node[] arr = (Node[])added.toArray (new Node[0]);
// bugfix #22698, don't select the added nodes
// when the nodes not under managed explorer's root node
bigloop: for (int i = 0; i < arr.length; i++) {
Node node = arr[i];
while (node != null) {
if (node.equals(em.getRootContext ())) {
continue bigloop;
}
node = node.getParentNode ();
}
return;
}
try {
em.setSelectedNodes (arr);
} catch (PropertyVetoException ex) {
ErrorManager.getDefault().notify (ErrorManager.INFORMATIONAL, ex);
} catch (IllegalStateException ex) {
ErrorManager.getDefault().notify (ErrorManager.INFORMATIONAL, ex);
}
}
/** Fired when a set of new children is added.
* @param ev event describing the action
*/
public void childrenAdded(NodeMemberEvent ev) {
added.addAll (Arrays.asList (ev.getDelta()));
}
/** Fired when a set of children is removed.
* @param ev event describing the action
*/
public void childrenRemoved(NodeMemberEvent ev) {
}
/** Fired when the order of children is changed.
* @param ev event describing the change
*/
public void childrenReordered(NodeReorderEvent ev) {
}
/** Fired when the node is deleted.
* @param ev event describing the node
*/
public void nodeDestroyed(NodeEvent ev) {
}
/** This method gets called when a bound property is changed.
* @param evt A PropertyChangeEvent object describing the event source
* and the property that has changed.
*/
public void propertyChange(PropertyChangeEvent evt) {
}
} // end of NodeSelector
/** 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 javax.swing.AbstractAction
implements Presenter.Menu, Presenter.Popup, Presenter.Toolbar, javax.swing.event.ChangeListener {
/** action to delegate too */
private PasteAction delegate;
/** model to work with */
private ActSubMenuModel model;
public DelegateAction (PasteAction a, Lookup actionContext) {
this.delegate = a;
this.model = new ActSubMenuModel (actionContext);
this.model.addChangeListener(this);
}
/** Overrides superclass method, adds delegate description. */
public String toString() {
return super.toString() + "[delegate=" + delegate + "]"; // NOI18N
}
public void putValue(String key, Object value) { }
/** Invoked when an action occurs.
*/
public void actionPerformed(java.awt.event.ActionEvent e) {
if (model != null) {
model.performActionAt(0);
}
}
public boolean isEnabled() {
return model != null && model.isEnabled();
}
public Object getValue(String key) {
return delegate.getValue(key);
}
public void setEnabled(boolean b) {
}
public javax.swing.JMenuItem getMenuPresenter() {
return new org.openide.awt.Actions.SubMenu (this, model, false);
}
public javax.swing.JMenuItem getPopupPresenter() {
return new org.openide.awt.Actions.SubMenu (this, model, true);
}
public java.awt.Component getToolbarPresenter() {
return new Actions.ToolbarButton (this);
}
public void stateChanged (javax.swing.event.ChangeEvent evt) {
super.firePropertyChange("enabled", null, null);
}
} // end of DelegateAction
/** Action that wraps paste type.
*/
private static final class ActionPT extends javax.swing.AbstractAction {
private PasteType t;
public ActionPT (PasteType t) {
this.t = t;
}
public void actionPerformed (java.awt.event.ActionEvent ev) {
executePasteType (t);
}
}
}