/* * 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.explorer; import java.awt.*; import java.awt.event.*; import java.awt.datatransfer.*; import javax.swing.*; import java.io.IOException; import java.util.HashMap; import java.lang.reflect.*; import java.beans.PropertyChangeListener; import java.beans.PropertyChangeEvent; import org.openide.DialogDisplayer; import org.openide.util.datatransfer.*; import org.openide.ErrorManager; import org.openide.NotifyDescriptor; import org.openide.actions.CutAction; import org.openide.actions.CopyAction; import org.openide.actions.PasteAction; import org.openide.actions.DeleteAction; import org.openide.filesystems.FileObject; import org.openide.filesystems.FileStateInvalidException; import org.openide.filesystems.FileSystem; import org.openide.filesystems.Repository; import org.openide.loaders.DataObject; import org.openide.loaders.DataShadow; import org.openide.util.actions.ActionPerformer; import org.openide.nodes.Node; import org.openide.util.NbBundle; import org.openide.util.WeakListener; /** * This class contains the default implementation of reactions to the standard * explorer actions. It can be attached to any {@link ExplorerManager}. Then * this class will listen to changes of selected nodes or the explored context * of that manager, and update the state of cut/copy/paste/delete actions. <P> * An instance of this class can only be attached to one manager at a time. Use * {@link #attach} and {@link #detach} to make the connection. * * @author Jan Jancura, Petr Hamernik, Ian Formanek, Jaroslav Tulach */ public class ExplorerActions { /** copy action performer */ private final CopyCutActionPerformer copyActionPerformer = new CopyCutActionPerformer (true); /** cut action performer */ private final CopyCutActionPerformer cutActionPerformer = new CopyCutActionPerformer (false); /** delete action performer */ private final DeleteActionPerformer deleteActionPerformer = new DeleteActionPerformer(); /** own paste action */ private final OwnPaste pasteActionPerformer = new OwnPaste (); private ActionStateUpdater actionStateUpdater; /** the manager we are listening on */ private ExplorerManager manager; /** must the delete be confirmed */ private boolean confirmDelete = true; /** attach as performers or just update the actions */ private boolean attachPerformers; /** actions to work with */ private static CopyAction copy = null; private static CutAction cut = null; private static DeleteAction delete = null; private static PasteAction paste = null; /** Creates new instance. */ public ExplorerActions () { this (true); } /** Creates new instance with a decision whether the action should update * performers (the old behaviour) or only set the state of cut,copy,delete, * and paste actions. */ ExplorerActions (boolean attachPerformers) { this.attachPerformers = attachPerformers; } /** Getter for the copy action. */ final Action copyAction () { return copyActionPerformer; } /** The cut action */ final Action cutAction () { return cutActionPerformer; } /** The delete action */ final Action deleteAction () { return deleteActionPerformer; } /** Own paste action */ final Action pasteAction () { return pasteActionPerformer; } /** Attach to new manager. * @param m the manager to listen on */ public synchronized void attach (ExplorerManager m) { if (manager != null) { // first of all detach detach (); } manager = m; // Sets action state updater and registers listening on manager and // exclipboard. actionStateUpdater = new ActionStateUpdater(); manager.addPropertyChangeListener( WeakListener.propertyChange(actionStateUpdater, manager)); Clipboard c = getClipboard(); if (c instanceof ExClipboard) { ExClipboard clip = (ExClipboard)c; clip.addClipboardListener( (ClipboardListener)WeakListener.create( ClipboardListener.class, actionStateUpdater, clip)); } updateActions (); } /** Detach from manager currently being listened on. */ public synchronized void detach () { if (manager == null) return; // Unregisters (weak) listening on manager and exclipboard (see attach). actionStateUpdater = null; stopActions (); manager = null; } /** Access method for use from ExplorerPanel, and also * via reflection (!) from RegistryImpl in core. * @deprecated Kill me later; see #18137 for explanation. */ ExplorerManager getAttachedManager() { return manager; } /** Set whether to confirm deletions. * @param yes <code>true</code> to confirm deletions */ public final void setConfirmDelete (boolean yes) { confirmDelete = yes; } /** Should deletions be confirmed? * @return <code>true</code> if deletions must be confirmed */ public final boolean isConfirmDelete () { return confirmDelete; } /** Stops listening on all actions */ private void stopActions () { if (copyActionPerformer != null) { if (attachPerformers) { if (copy.getActionPerformer() instanceof CopyCutActionPerformer) { copy.setActionPerformer (null); } if (cut.getActionPerformer() instanceof CopyCutActionPerformer) { cut.setActionPerformer (null); } paste.setPasteTypes (null); if (delete.getActionPerformer() instanceof DeleteActionPerformer) { delete.setActionPerformer (null); } } else { copyActionPerformer.setEnabled (false); cutActionPerformer.setEnabled (false); deleteActionPerformer.setEnabled (false); pasteActionPerformer.setEnabled (false); } } } /** Updates the state of all actions. * @param path list of selected nodes */ private void updateActions () { if (manager == null) return; Node[] path = manager.getSelectedNodes(); if (copy == null) { copy = (CopyAction) CopyAction.findObject(CopyAction.class, true); cut = (CutAction) CutAction.findObject(CutAction.class, true); paste = (PasteAction) PasteAction.findObject(PasteAction.class, true); delete = (DeleteAction) DeleteAction.findObject(DeleteAction.class, true); } int i; int k = path != null ? path.length : 0; if (k > 0) { boolean incest = false; if (k > 1) { // Do a special check for parenthood. Affects delete (for a long time), // copy (#13418), cut (#13426). If one node is a parent of another, // assume that the situation is sketchy and prevent it. // For k==1 it is impossible so do not waste time on it. HashMap allNodes = new HashMap(101); for (i = 0; i < k; i++) { if (! checkParents(path[i], allNodes)) { incest = true; break; } } } for (i = 0; i < k; i++) { if (incest || !path[i].canCopy()) { if (attachPerformers) { copy.setActionPerformer (null); } else { copyActionPerformer.setEnabled (false); } break; } } if (i == k) { if (attachPerformers) { copy.setActionPerformer (copyActionPerformer); } else { copyActionPerformer.setEnabled (true); } } for (i = 0; i < k; i++) { if (incest || !path[i].canCut()) { if (attachPerformers) { cut.setActionPerformer (null); } else { cutActionPerformer.setEnabled (false); } break; } } if (i == k) { if (attachPerformers) { cut.setActionPerformer (cutActionPerformer); } else { cutActionPerformer.setEnabled (true); } } for (i = 0; i < k; i++) { if (incest || !path[i].canDestroy()) { if (attachPerformers) { delete.setActionPerformer (null); } else { deleteActionPerformer.setEnabled(false); } break; } } if (i == k) { if (attachPerformers) { delete.setActionPerformer (deleteActionPerformer); } else { deleteActionPerformer.setEnabled (true); } } } else { // k==0, i.e. no nodes selected if (attachPerformers) { copy.setActionPerformer (null); cut.setActionPerformer (null); delete.setActionPerformer (null); } else { copyActionPerformer.setEnabled(false); cutActionPerformer.setEnabled(false); deleteActionPerformer.setEnabled(false); } } updatePasteAction(path); } /** Adds all parent nodes into the set. * @param set set of all nodes * @param node the node to check * @return false if one of the nodes is parent of another */ private boolean checkParents (Node node, HashMap set) { if (set.get (node) != null) { return false; } // this signals that this node is the original one set.put (node, this); for (;;) { node = node.getParentNode (); if (node == null) { return true; } if (set.put (node, node) == this) { // our parent is a node that is also in the set return false; } } } /** Updates paste action. * @param path selected nodes */ private void updatePasteAction (Node[] path) { ExplorerManager man = manager; if (man == null) { if (attachPerformers) { paste.setPasteTypes (null); } else { pasteActionPerformer.setPasteTypes (null); } return; } if (path != null && ((path.length > 1)/* || ((path.length == 1) && (path [0].isLeaf()))*/) ) { if (attachPerformers) { paste.setPasteTypes(null); } else { pasteActionPerformer.setPasteTypes (null); } return; } else { Node node = man.getExploredContext (); Node[] selectedNodes = man.getSelectedNodes (); if (selectedNodes != null && (selectedNodes.length == 1) /*&& (!selectedNodes[0].isLeaf())*/) { node = selectedNodes[0]; } if(node != null) { Transferable trans = getClipboard().getContents(this); updatePasteTypes(trans, node); } } } /** Actually updates paste types. */ private void updatePasteTypes(Transferable trans, Node pan) { if (trans != null) { // First, just ask the node if it likes this transferable, whatever it may be. // If it does, then fine. PasteType[] pasteTypes = pan == null ? new PasteType[] { } : pan.getPasteTypes(trans); if (pasteTypes.length != 0) { if (attachPerformers) { paste.setPasteTypes(pasteTypes); } else { pasteActionPerformer.setPasteTypes (pasteTypes); } return; } boolean flavorSupported = false; try { flavorSupported = trans.isDataFlavorSupported(ExTransferable.multiFlavor); } catch (java.lang.Exception e) { // patch to get the Netbeans start under Solaris // [PENDINGworkaround] } if (flavorSupported) { // The node did not accept this multitransfer as is--try to break it into // individual transfers and paste them in sequence instead. try { MultiTransferObject obj = (MultiTransferObject) trans.getTransferData(ExTransferable.multiFlavor); int count = obj.getCount(); boolean ok = true; Transferable[] t = new Transferable[count]; PasteType[] p = new PasteType[count]; for (int i = 0; i < count; i++) { t[i] = obj.getTransferableAt(i); pasteTypes = pan == null ? new PasteType[] { } : pan.getPasteTypes(t[i]); if (pasteTypes.length == 0) { ok = false; break; } // [PENDING] this is ugly! ideally should be some way of comparing PasteType's for similarity? p[i] = pasteTypes[0]; } if (ok) { PasteType[] arrOfPaste = new PasteType[] { new MultiPasteType(t, p) }; if (attachPerformers) { paste.setPasteTypes(arrOfPaste); } else { pasteActionPerformer.setPasteTypes (arrOfPaste); } return; } } catch (UnsupportedFlavorException e) { // [PENDING] notify?! } catch (IOException e) { // [PENDING] notify?! } } } if (attachPerformers) { if(paste != null) { paste.setPasteTypes(null); } } else { pasteActionPerformer.setPasteTypes (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; } /** Paste type used when in clipbopard is MultiTransferable */ private static class MultiPasteType extends PasteType { /** Array of transferables */ Transferable[] t; /** Array of paste types */ PasteType[] p; /** Constructs new MultiPasteType for the given content of the clipboard */ MultiPasteType(Transferable[] t, PasteType[] p) { this.t = t; this.p = p; } /** Performs the paste action. * @return Transferable which should be inserted into the clipboard after * paste action. It can be null, which means that clipboard content * should be cleared. */ public Transferable paste() throws IOException { int size = p.length; Transferable[] arr = new Transferable[size]; for (int i = 0; i < size; i++) { Transferable newTransferable = p[i].paste(); if (newTransferable != null) { arr[i] = newTransferable; } else { // keep the orginal arr[i] = t[i]; } } return new ExTransferable.Multi (arr); } } /** Own implementation of paste action */ private class OwnPaste extends AbstractAction { private PasteType[] pasteTypes; OwnPaste() {} public boolean isEnabled() { updateActionsState(); return super.isEnabled(); } public void setPasteTypes (PasteType[] arr) { synchronized (this) { this.pasteTypes = arr; } setEnabled (arr != null); } public void actionPerformed(ActionEvent e) { throw new IllegalStateException ("Should not be invoked at all. Paste types: " + java.util.Arrays.asList (pasteTypes)); // NOI18N } public Object getValue (String s) { if ("delegates".equals (s)) { // NOI18N return pasteTypes; } return super.getValue (s); } } /** Class which performs copy and cut actions */ private class CopyCutActionPerformer extends AbstractAction implements org.openide.util.actions.ActionPerformer { /** determine if adapter is used for copy or cut action. */ private boolean copyCut; /** Create new adapter */ public CopyCutActionPerformer (boolean b) { copyCut = b; } public boolean isEnabled() { updateActionsState(); return super.isEnabled(); } /** Perform copy or cut action. */ public void performAction(org.openide.util.actions.SystemAction action) { Transferable trans = null; Node[] sel = manager.getSelectedNodes (); if (sel.length != 1) { Transferable[] arrayTrans = new Transferable[sel.length]; for (int i = 0; i < sel.length; i++) if ((arrayTrans[i] = getTransferableOwner(sel[i])) == null) return; trans = new ExTransferable.Multi (arrayTrans); } else { trans = getTransferableOwner(sel[0]); } if (trans != null) { Clipboard clipboard = getClipboard(); clipboard.setContents(trans, new StringSelection ("")); // NOI18N } } private Transferable getTransferableOwner(Node node) { try { return copyCut ? node.clipboardCopy() : node.clipboardCut(); } catch (java.io.IOException e) { ErrorManager.getDefault ().notify(ErrorManager.INFORMATIONAL, e); return null; } } /** Invoked when an action occurs. * */ public void actionPerformed(ActionEvent e) { performAction (null); } } /** Class which performs delete action */ private class DeleteActionPerformer extends AbstractAction implements ActionPerformer { DeleteActionPerformer() {} public boolean isEnabled() { updateActionsState(); return super.isEnabled(); } /** Perform delete action. */ public void performAction(org.openide.util.actions.SystemAction action) { final Node[] sel = manager.getSelectedNodes (); if ((sel == null) || (sel.length == 0)) return; // perform action if confirmed if (!confirmDelete || doConfirm(sel)) { // clear selected nodes try { if (manager != null) { manager.setSelectedNodes(new Node[] {}); } } catch (java.beans.PropertyVetoException e) { // never thrown, setting empty selected nodes cannot be vetoed } doDestroy(sel); if (attachPerformers) { delete.setActionPerformer (null); // fixes bug #673 } else { setEnabled (false); } } } private boolean doConfirm(Node[] sel) { String message, title; if (sel.length == 1) { if (sel[0].getCookie(DataShadow.class) != null) { title = NbBundle.getMessage(ExplorerActions.class, "MSG_ConfirmDeleteShadowTitle"); DataShadow obj = (DataShadow)sel[0].getCookie(DataShadow.class); message = NbBundle.getMessage(ExplorerActions.class, "MSG_ConfirmDeleteShadow", new Object[] { obj.getName(), // name of the shadow sel[0].getDisplayName(), // name of original fullName(obj), // full name of file for shadow fullName(obj.getOriginal()) // full name of original file }); } else if (sel[0].getCookie(org.openide.loaders.DataFolder.class) != null) { message = NbBundle.getMessage(ExplorerActions.class, "MSG_ConfirmDeleteFolder", sel[0].getDisplayName()); title = NbBundle.getMessage(ExplorerActions.class, "MSG_ConfirmDeleteFolderTitle"); } else { message = NbBundle.getMessage(ExplorerActions.class, "MSG_ConfirmDeleteObject", sel[0].getDisplayName()); title = NbBundle.getMessage(ExplorerActions.class, "MSG_ConfirmDeleteObjectTitle"); } } else { message = NbBundle.getMessage(ExplorerActions.class, "MSG_ConfirmDeleteObjects", new Integer(sel.length)); title = NbBundle.getMessage(ExplorerActions.class, "MSG_ConfirmDeleteObjectsTitle"); } NotifyDescriptor desc = new NotifyDescriptor.Confirmation(message, title, NotifyDescriptor.YES_NO_OPTION); return NotifyDescriptor.YES_OPTION.equals(DialogDisplayer.getDefault().notify(desc)); } private String fullName(DataObject obj) { FileObject f = obj.getPrimaryFile(); if (f.isRoot()) { try { return f.getFileSystem().getDisplayName(); } catch (FileStateInvalidException e) { return ""; //NOI18N } } else { return f.toString(); } } private void doDestroy(final Node[] sel) { try { Repository.getDefault().getDefaultFileSystem().runAtomicAction(new FileSystem.AtomicAction() { public void run() throws IOException { for (int i=0; i< sel.length; i++) { try { sel[i].destroy(); } catch (IOException e) { ErrorManager.getDefault().notify(e); } } } }); } catch (IOException ioe) { IllegalStateException ise = new IllegalStateException(); ErrorManager.getDefault().annotate(ise, ioe); throw ise; } } /** Invoked when an action occurs. * */ public void actionPerformed(ActionEvent e) { performAction (null); } } /** Updates actions state via updater (if the updater is present). */ private void updateActionsState() { ActionStateUpdater asu; synchronized(this) { asu = actionStateUpdater; } if(asu != null) { actionStateUpdater.update(); } } /** Class which register changes in manager, and clipboard, coalesces * them if they are frequent and performs the update of actions state. */ private class ActionStateUpdater implements PropertyChangeListener, ClipboardListener, ActionListener { private final Timer timer; ActionStateUpdater() { timer = new FixIssue29405Timer(150, this); timer.setCoalesce(true); timer.setRepeats(false); } public void propertyChange(PropertyChangeEvent e) { timer.restart(); } public void clipboardChanged(ClipboardEvent ev) { if (!ev.isConsumed()) { updatePasteAction(manager.getSelectedNodes()); } } public void actionPerformed(ActionEvent evt) { updateActions(); timer.stop(); } /** Updates actions states now if there is pending event. */ public void update() { if(timer.isRunning()) { timer.stop(); updateActions(); } } } /** Timer which fixes problem with running status (issue #29405). */ private static class FixIssue29405Timer extends javax.swing.Timer { private boolean running; public FixIssue29405Timer(int delay, ActionListener l) { super(delay, l); } public void restart() { super.restart(); running = true; } public void stop() { running = false; super.stop(); } public boolean isRunning() { return running; } } // End of FixIssue29405Timer class. }