All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.openide.actions.PasteAction Maven / Gradle / Ivy

The newest version!
/*
 * 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.openide.actions;

import java.awt.Component;
import java.awt.EventQueue;
import java.awt.MenuShortcut;
import java.awt.Toolkit;
import java.awt.datatransfer.Clipboard;
import java.awt.datatransfer.ClipboardOwner;
import java.awt.datatransfer.StringSelection;
import java.awt.datatransfer.Transferable;
import java.awt.event.ActionEvent;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.beans.PropertyVetoException;
import java.io.IOException;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.logging.Level;
import java.util.logging.Logger;
import javax.swing.AbstractAction;
import javax.swing.Action;
import javax.swing.ActionMap;
import javax.swing.JMenuItem;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import javax.swing.text.DefaultEditorKit;
import org.openide.awt.Actions;
import org.openide.explorer.ExplorerManager;
import org.openide.nodes.Children;
import org.openide.nodes.Node;
import org.openide.nodes.NodeEvent;
import org.openide.nodes.NodeListener;
import org.openide.nodes.NodeMemberEvent;
import org.openide.nodes.NodeReorderEvent;
import org.openide.util.ChangeSupport;
import org.openide.util.Exceptions;
import org.openide.util.HelpCtx;
import org.openide.util.Lookup;
import org.openide.util.LookupEvent;
import org.openide.util.LookupListener;
import org.openide.util.NbBundle;
import org.openide.util.RequestProcessor;
import org.openide.util.UserCancelException;
import org.openide.util.WeakListeners;
import org.openide.util.actions.CallbackSystemAction;
import org.openide.util.actions.Presenter;
import org.openide.util.actions.SystemAction;
import org.openide.util.datatransfer.PasteType;
import org.openide.windows.TopComponent;

/** Paste from clipboard. This is a callback system action,
* with enhanced behaviour. Others can plug in by adding
* 
* topcomponent.getActionMap ().put (javax.swing.text.DefaultEditorKit.pasteAction, theActualAction);
* 
* or by using the now deprecated setPasteTypes and setActionPerformer * methods. *

* There is a special support for more than one type of paste to be enabled at once. * If the theActualAction returns array of actions from * getValue ("delegates") than those actions are offered as * subelements by the paste action presenter. */ public final class PasteAction extends CallbackSystemAction { /** 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; } protected @Override void initialize() { super.initialize(); setEnabled(false); } public String getName() { return NbBundle.getMessage(PasteAction.class, "Paste"); } public HelpCtx getHelpCtx() { return new HelpCtx(PasteAction.class); } protected @Override String iconResource() { return "org/openide/resources/actions/paste.gif"; // NOI18N } public @Override JMenuItem getMenuPresenter() { return new Actions.SubMenu(this, model(), false); } public @Override JMenuItem getPopupPresenter() { return new Actions.SubMenu(this, model(), true); } public @Override Action createContextAwareInstance(Lookup actionContext) { return new DelegateAction(this, actionContext); } public @Override Object getActionMapKey() { return DefaultEditorKit.pasteAction; } public @Override void actionPerformed(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. Object obj = ac.getValue("delegates"); // NOI18N if (obj instanceof PasteType []) { PasteType [] arr = (PasteType []) obj; if (arr.length > 0) { t = arr [0]; } } else if (obj instanceof Action []) { Action [] arr = (Action []) obj; if (arr.length > 0) { arr [0].actionPerformed (ev); return; } } else { ac.actionPerformed(ev); return; } } } if (t != null) { // posts the action in RP thread new ActionPT(t, ev.getActionCommand()); } else { Toolkit.getDefaultToolkit().beep(); Logger.getLogger(PasteAction.class.getName()).log(Level.INFO, "No paste types available when performing paste action. ActionEvent: {0}", ev); // NOI18N } } protected @Override boolean asynchronous() { return false; } /** Set possible paste types. * Automatically enables or disables the paste action according to whether there are any. * @deprecated Use TopComponent.getActionMap ().put (javax.swing.text.DefaultEditorKit.pasteAction, yourPasteAction); * If you want register more paste types then use an action which delegates to * an array of PasteAction or also can delegate to an array of * org.openide.util.datatransfer.PasteType. * @param types the new types to allow, or null */ @Deprecated public void setPasteTypes(PasteType[] types) { PasteAction.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 null */ 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 Action findActionFromMap(ActionMap map) { if (map != null) { return map.get(DefaultEditorKit.pasteAction); } return null; } /** If our clipboard is not found return the default system clipboard. */ private static Clipboard getClipboard() { Clipboard c = Lookup.getDefault().lookup(Clipboard.class); if (c == null) { c = Toolkit.getDefaultToolkit().getSystemClipboard(); } return c; } /** Utility 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 Method m = c.getMethod("getRegistry"); // NOI18N Object o = m.invoke(null); c = Class.forName("org.openide.windows.TopComponent$Registry"); // NOI18N // use reflection now m = c.getMethod("getActivated"); // NOI18N o = m.invoke(o); 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 (InvocationTargetException x) { t = x; } if (t != null) { Logger.getLogger(PasteAction.class.getName()).log(Level.WARNING, null, t); } return null; } /** 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 implements Actions.SubMenuModel, LookupListener, PropertyChangeListener { private final ChangeSupport cs = new ChangeSupport(this); /** lookup we are attached to or null we we should work globally */ private Lookup.Result result; /** previous enabled state */ private boolean enabled; /** weak listener for action */ private PropertyChangeListener actionWeakL; /** weak listener for paste type */ private PropertyChangeListener pasteTypeWeakL; /** weak lookup listener */ private LookupListener weakLookup; /** @param lookup can be null */ public ActSubMenuModel(Lookup lookup) { 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) { TopComponent tc = TopComponent.getRegistry().getActivated(); if (tc != null) { return tc.getActionMap(); } } else { for (ActionMap am : result.allInstances()) { return am; } } 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) { TopComponent.getRegistry().addPropertyChangeListener(WeakListeners.propertyChange(this, TopComponent.getRegistry())); } else { result = lookup.lookupResult(ActionMap.class); weakLookup = WeakListeners.create(LookupListener.class, this, result); result.addLookupListener(weakLookup); } 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 = findObject(PasteAction.class); if (actionToWorkWith != null) { actionToWorkWith[0] = a; } Object[] arr = a == null ? null : 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 }; } } private boolean isEnabledImpl(Object[] pasteTypesOrActions) { if (pasteTypesOrActions == null) { pasteTypesOrActions = getPasteTypesOrActions(null); } if ((pasteTypesOrActions.length == 1) && pasteTypesOrActions[0] instanceof Action) { return ((Action) pasteTypesOrActions[0]).isEnabled(); } else { return pasteTypesOrActions.length > 0; } } public boolean isEnabled() { return isEnabledImpl(null); } 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) { performActionAt(index, null); } public void performActionAt(int index, ActionEvent ev) { Action[] action = new Action[1]; Object[] arr = getPasteTypesOrActions(action); if (arr.length <= index) { return; } if (arr[index] instanceof PasteType) { PasteType t = (PasteType) arr[index]; // posts the action is RP thread new ActionPT(t, (ev == null) ? null : ev.getActionCommand()); return; } else { // is action Action a = (Action) arr[index]; a.actionPerformed(new ActionEvent(a, ActionEvent.ACTION_PERFORMED, Action.NAME)); return; } } /** Registers .ChangeListener to receive events. *@param listener The listener to register. */ public synchronized void addChangeListener(ChangeListener listener) { cs.addChangeListener(listener); } /** Removes .ChangeListener from the list of listeners. *@param listener The listener to remove. */ public synchronized void removeChangeListener(ChangeListener listener) { cs.removeChangeListener(listener); } /** Notifies all registered listeners about the event. * *@param param1 Parameter #1 of the .ChangeEvent 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(pasteTypeWeakL); pasteTypeWeakL = WeakListeners.propertyChange(this, a); a.addPropertyChangeListener(pasteTypeWeakL); } // plus always make sure we are listening on the actions if (listen[0] != a) { listen[0].removePropertyChangeListener(actionWeakL); actionWeakL = WeakListeners.propertyChange(this, listen[0]); listen[0].addPropertyChangeListener(actionWeakL); } boolean en = isEnabledImpl(arr); if (en == enabled) { return; } enabled = en; // and fire if requested.... if (!fire) { return; } cs.fireChange(); } public void propertyChange(PropertyChangeEvent evt) { checkStateChanged(true); } public void resultChanged(LookupEvent ev) { checkStateChanged(true); } } /** 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 implements NodeListener, Runnable { /** All added children */ private List 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; } } // XXX [FindBugs] is this field good for something (never read)? needed to hold hard ref, perhaps? 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 Children.MUTEX.readAccess(this); } } public void run() { node.removeNodeListener(this); if (added.isEmpty()) { return; } // bugfix #22698, don't select the added nodes // when the nodes not under managed explorer's root node bigloop: for (Node n : added) { while (n != null) { if (n.equals(em.getRootContext())) { continue bigloop; } n = n.getParentNode(); } return; } try { em.setSelectedNodes(added.toArray(new Node[0])); } catch (PropertyVetoException ex) { Logger.getLogger(PasteAction.class.getName()).log(Level.WARNING, null, ex); } catch (IllegalStateException ex) { Logger.getLogger(PasteAction.class.getName()).log(Level.WARNING, null, 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 AbstractAction implements Presenter.Menu, Presenter.Popup, Presenter.Toolbar, 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 @Override String toString() { return super.toString() + "[delegate=" + delegate + "]"; // NOI18N } public @Override void putValue(String key, Object value) { } /** Invoked when an action occurs. */ public void actionPerformed(ActionEvent e) { if (model != null) { model.performActionAt(0, e); } } public @Override boolean isEnabled() { return (model != null) && model.isEnabled(); } public @Override Object getValue(String key) { return delegate.getValue(key); } public @Override void setEnabled(boolean b) { } public JMenuItem getMenuPresenter() { return new Actions.SubMenu(this, model, false); } public JMenuItem getPopupPresenter() { return new Actions.SubMenu(this, model, true); } public Component getToolbarPresenter() { return new Actions.ToolbarButton(this); } public void stateChanged(ChangeEvent evt) { firePropertyChange("enabled", null, null); } } // end of DelegateAction /** Action that wraps paste type. */ private static final class ActionPT extends AbstractAction implements Runnable { private static final RequestProcessor RP = new RequestProcessor("Pasting"); // NOI18N private PasteType t; private NodeSelector sel; private boolean secondInvocation; public ActionPT(PasteType t, String command) { this.t = t; ExplorerManager em = findExplorerManager(); if (em != null) { this.sel = new NodeSelector(em, null); } if ("waitFinished".equals(command)) { // NOI18N run(); } else { RP.post(this); } } public void actionPerformed(ActionEvent ev) { try { 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 (IOException e) { Exceptions.printStackTrace(e); } finally { EventQueue.invokeLater(this); } } public void run() { if (secondInvocation) { if (sel != null) { sel.select(); } } else { secondInvocation = true; ActionManager.getDefault().invokeAction( this, new ActionEvent(t, ActionEvent.ACTION_PERFORMED, Action.NAME) ); } } public @Override boolean isEnabled() { return SystemAction.get(PasteAction.class).isEnabled(); } public @Override Object getValue(String key) { return SystemAction.get(PasteAction.class).getValue(key); } } }