The Source for Java Technology Collaboration
User: Password:



Scott Violet

Scott Violet's Blog

Cut, Copy and Paste

Posted by zixle on August 07, 2006 at 09:27 PM | Comments (6)

After a long hiatus I'm returning to a series of blogs on architecting applications. This time around I'm covering a simple way to provide rich cut, copy and paste behavior in an application.

As usual, for those wishing to cut to the chase, here's the app:

Many Swing components provide cut, copy and paste support. For example, JTextField supports cut, copy and paste out the box. This is hardly revolutionary; todays developers expect such behavior out of the box.

Swing's cut, copy and paste support is provided by javax.swing.TransferHandler. TransferHandler provides Actions for cut, copy and paste. Unfortunately the enabled state of the actions is not updated based on context, and the actions target the source of the event. In other words, the actions provided by TransferHandler are not appropriate for use on menus or toolbars.

While the Actions provided by TransferHandler have limited usage, the remaining portions of TransferHandler are imminently more useful (even more so in 1.6). Rather than reinvent the wheel, I'll provide new Actions that target TransferHandler.

Here's the list of requirements for cut, copy and paste actions:

  • The actions should track the permanent focus owner. For example, you would not want the copy action to target a menu when it has focus.
  • The paste action should only be enabled if the clipboard has a data flavor supported by the component.
  • The cut and copy actions need to update based on the selection of the component. For example, if a text field has focus, the cut and copy actions should only be enabled if the selection is not empty. *
Ideally it would be easy to enable this support in an existing application with minimal fuss. To that end a helper class, CutCopyPasteHelper, with all static methods is used. The static methods operate on any JComponent and do not look for specific interface in determining cut, copy and paste support.

Targetting the focused component is easily done with the KeyboardFocusManager. The KeyboardFocusManager maintains a bound property for the permanent focus owner. To listen for changes one need only install a PropertyChangeListener. The following code illustrates this:

  PropertyChangeListener focusListener = new PropertyChangeListener {
    public void propertyChange(PropertyChangeEvent e) {
      if (e.getPropertyName() == "permanentFocusOwner") {
        // The permanent focus owner has changed.
        newPermanentFocusOwner((Component)e.getNewValue());
      }
    }
  };
  KeyboardFocusManager.getCurrentKeyboardFocusManager().
      addPropertyChangeListener(focusListener);
The paste action is the trickiest. The paste action is enabled if the paste action is enabled on the focus component, and a data flavor on the clipboard matches the one supplied for the focused component. Tracking the set of data flavors on the clipboard is easily done using a FlavorListener. The following code illustrates this:
  FlavorListener flavorListener = new FlavorListener {
    public void flavorsChanged(FlavorEvent e) {
      flavorsChanged();
    }
  };
  Clipboard clipboard = Toolkit.getDefaultToolkit().getSystemClipboard();
  clipboard.addFlavorListener(flavorListener);
The set of data flavors a component supports for paste must be provided by the developer. This is done with the following method:
  CutCopyPasteHelper.registerDataFlavors(component,  DataFlavor...dataFlavors);
In addition to setting the set of flavors, you must explicitly enable the ability for a component to paste. This is done with the setPasteEnabled method. Seperating the set of flavors from the enable state makes it easier to operate on each independently. For example, once you've registered the flavors, if you subsequently need to change the paste enabled state you need only invoke setPasteEnabled.

Once you've enabled paste on the component and registered the set of flavors, CutCopyPasteHelper has all the information needed to update the paste action appropriately for you.

The cut and copy actions are simpler. These are updated based on the focused component, and whether or not you've enabled cut and copy. For example, a text field with an empty selection would disable the cut and copy actions. On the other hand, a text field with a non-empty selection would enable the cut and copy actions. CutCopyPasteHelper provides the setCutEnabled and setCopyEnabled methods for this.

Many Swing components provide keyboard bindings that target the actions provided by TransferHandler. CutCopyPasteHelper provides replacements for these actions. For a component to use CutCopyPasteHelper actions you must change the bindings registered on each component to target CutCopyPasteHelper. This is done using the ActionMap and InputMap. CutCopyPasteHelper provides the registerCutCopyPasteBindings that takes care of this.

Here's a cheat sheet for using these actions with a component:

  1. Provide a TransferHandler for the component.
  2. Replace the bindings provided by Swing using the registerCutCopyPasteBindings method.
  3. Register the set of data flavors supported during paste by way of the registerDataFlavors method.
  4. Install a listener on the component that updates the cut and copy state based on the component's selection. The cut and copy state is changed using the setCutEnabled and setCopyEnabled methods.
Here's the relevant portions of the code for the password store app that enable cut, copy and paste on entryList (a JList):
  // Step 1
  // Install the TransferHandler.
  entryList.setTransferHandler(new ListTransferHandler());

  // Step 2
  // Register key bindings that target the cut, copy, and paste actions
  // provided by CutCopyPasteHelper.
  CutCopyPasteHelper.registerCutCopyPasteBindings(entryList, true);

  // Step 3
  // Register the data flavors on the list:
  CutCopyPasteHelper.registerDataFlavors(entryList,
                PASSWORD_ENTRY_DATA_FLAVOR);
  // Enable paste
  CutCopyPasteHelper.setPasteEnabled(entryList, true);

  // Step 4
  // Install listener to update cut/copy state based on selection
  ListSelectionListener selectionListener = new ListSelectionListener {
    public void valueChanged(ListSelectionEvent e) {
      if (!e.getValueIsAdjusting()) {
        boolean hasSelection = (entryList.getMinSelectionIndex() != -1);
        CutCopyPasteHelper.setCopyEnabled(entryList, hasSelection);
        CutCopyPasteHelper.setCutEnabled(entryList, hasSelection);
      }
    }
  };
  entryList.addListSelectionListener(new ListSelectionHandler());
To use the actions provided by CutCopyPasteHelper in a menu is no different than any other action, invoke setAction or use a constructor that takes an Action. Please note that the name, accelerator, mnemonic, and icon are not provided. It is expected you'll set these directly on the menu or button using the action.

* Selection

You'll notice these actions target the focused component. Targetting the focused component is problematic if you need another component to get focus while firing the action. For example, if a button wired to the cut action gets focus, the cut action targets the button; this is not what you want. This is trivially fixed by invoking setFocusable(false) on the button, but that's a stop-gap. The larger issue is how to determine the target of these actions. In nearly all cases targetting the permanent focus owner is what you want, but there are exceptions. This is a larger issue, to which many folks have proposed big solutions. It's something that needs more study, and has been bothering both Hans and myself...

The Sandbox

For security reasons a sandboxed app can't get at the system clipboard. This is most definitely a major pain! I've primarily concerned myself with apps that have full access to the clipboard. I suspect there are some things that could be done to make support of these actions better when in the sandbox. That's for another day though.

Early on I mentioned I wanted it to be trivial to wire these actions up to existing components. I've done the work for the text components. As this blog is now gargantuan in size, I'll save that for a later blog. In the mean time, here's the latest source for the password store app, and source for fabric. For those that look at the source, you'll see a bunch of stuff I haven't gotten to. That's for a later day as well... Oh, and yes, CutCopyPasteHelper will eventually makes it's way into Swing in some form or another.

    -Scott


Bookmark blog post: del.icio.us del.icio.us Digg Digg DZone DZone Furl Furl Reddit Reddit
Comments
Comments are listed in date ascending order (oldest first) | Post Comment

  • Why do you choose shared actions which require reconfiguration instead of simply installing cut/copy/paste actions tailored to a given component into that component's ActionMap?

    Posted by: twalljava on August 08, 2006 at 10:30 AM

  • twalljava,

    I can't think of a reason why *not* to go with shared actions. Here's some benefits:

    The Actions attached to menus/toolbars need to be dynamic, in so far as they must target the focused component. I suppose you could continually promote the actions from the focused component to the menu/toolbar.
    The TransferHandler should have all the logic for cut/copy/paste. So, what would the action need to do?

        -Scott

    Posted by: zixle on August 09, 2006 at 09:07 AM

  • In the NetBeans platform framework, we have global actions (like toolbar buttons) as well as context-dependent actions.

    Global actions generally have a single instance whose enablement state and other properties are updated in response to things like focus changes. (We normally track changes in focussed window, but a window can further notify global actions of selection changes according to changes in focus on components it owns, changes in list/tree selections,. etc.) Global actions may declare that they wish to be responsive to the last focussed window/component with a capacity for having a selection: i.e. a text field has a capacity for a text selection (whether it currently has a selection or not), but an image label does not. This solves the problem Scott mentioned re. giving focus to a button which has no selection.

    Context-dependent actions will generally have a singleton factory, and real actions are created from this factory when you supply a context, which could encompass things like text selections and so on. Each component may install its own context-dependent actions in its ActionMap. There may be an associated global action (e.g. Copy) which finds the last focussed component to have a given ActionMap entry and runs that action; this style permits every component to substitute its own implementation for e.g. Copy if it wishes.

    Posted by: jglick on August 09, 2006 at 09:28 AM

  • One reason *not* to go with shared actions is if you're depending on the action to have an "enabled" state which reflects whether or not the action can be performed, e.g. cut/paste should be disabled on an uneditable text field.

    I guess adjusting the state of the shared action is not much different than adjusting the state of a private one. You can't have any non-focus-based components depending on the action, though. If you had a "copy table" button next to a table, it couldn't depend on the table's shared copy action to reflect its state, or the state is going to be incorrect when some other component has focus.

    Posted by: twalljava on October 03, 2006 at 07:14 AM

  • That code doesn't quite seem to work. Brackets are needed after the ListSelectionListener and ListSelectionHandler can't be instantiated in the last line as it is an abstract class.

    So I think the last line should be:

    entryList.addListSelectionListener(selectionListener);

    Posted by: casebash on January 16, 2008 at 02:42 AM

  • Also the source code for CutCopyPasteHelper has a bug that might break this component completly in some applications. In the code for PasteAction.actionPerformed it calls the old importData with the old method:

    importData(JComponent comp, Transferable t)

    which only works for JComponents and breaks if you want to call it on a JFrame (and doesn't even throw an exception.

    You should first construct a TransferSupport with comp and t and then use

    importData(TransferHandler.TransferSupport support).

    TransferHandler.TransferSupport ts=new TransferHandler.TransferSupport(
    target,t);
    th.importData(ts);

    Posted by: casebash on January 18, 2008 at 06:35 PM



Only logged in users may post comments. Login Here.


Powered by
Movable Type 3.01D
 Feed java.net RSS Feeds