0

I have a simple wish for my Swing UI: there is a standard Copy action that is mapped to the InputMap of a component. Next there is a popup menu in this same component and I would like to add a menu item that runs the copy action and of course would show the keyboard shortcut that is in the inputMap.

This is the mac-version of the mapping, which I finally managed to add as a generic rule with the help of this, by realising that some components use "copy", whereas others use DefaultEditorKit.copyAction:

inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_C, KeyEvent.META_DOWN_MASK), DefaultEditorKit.copyAction);
inputMap.put(KeyStroke.getKeyStroke(KeyEvent.VK_C, KeyEvent.META_DOWN_MASK), "copy");

Now, I can find the action for a table, for example with

ActionMap actionMap = myTable.getActionMap();
Action action = actionMap.get("copy");

Now, I use the Action to create the MenuItem:

JPopupMenu popupMenu = new JPopupMenu();
JMenuItem item = new JMenuItem(action);
popupMenu.add(item);
table.setComponentPopupMenu(popupMenu);

As a result, I see the menu item, but it does not copy anything, although the shortcut key that is mapped to the same action, does copy. I can even define the shortcut (which I seemingly have to define myself, but also just as a hint for the user that these things are somehow linked together):

int MASK = Toolkit.getDefaultToolkit().getMenuShortcutKeyMask();
KeyStroke keyStroke = KeyStroke.getKeyStroke(KeyEvent.VK_C, MASK);
item.setAccelerator(keyStroke);

So, what am I missing? I even tried to define the action listener specifically, to no avail:

item.addActionListener(myTable.getActionForKeyStroke(keyStroke));

Sounds funny that the keyboard shortcut works automatically (I just had to figure out how to make the Cmd-key work in Mac instead of Ctrl (which took only a couple of hours)) and now I cannot make the menu entry linked to the existing action by no means (even after working for another couple of hours).

trashgod
  • 203,806
  • 29
  • 246
  • 1,045
Jouni Aro
  • 2,099
  • 14
  • 30
  • See [*What is the JTable CTRL+C event's name?*](https://stackoverflow.com/q/14356859/230513) for some alternatives. – trashgod Aug 22 '17 at 09:54
  • Hmm, seems that myTable.getInputMap().get(KeyStroke.getKeyStroke(KeyEvent.VK_C, MASK)) gives 'null'. But "copy" is the name that is in the actionMap, and actionMap.get("copy") gives a valid Action. – Jouni Aro Aug 22 '17 at 10:31
  • You forgot `WHEN_ANCESTOR_OF_FOCUSED_COMPONENT`. – trashgod Aug 22 '17 at 10:39
  • Aha - no I didn't forget - I did not know about it :) I admit, this is the first time I am doing anything related, so I am struggling, but I also didn't expect anything this complicated, either... Well, now I get "copy" from the InputMap, as expected. - The question remains: how can I trigger that action from code or from a menu entry so that it works with mouse action also? – Jouni Aro Aug 22 '17 at 10:44
  • 1
    You might try @camickr's approach, cited above. – trashgod Aug 22 '17 at 10:47
  • Aha - again ;) I didn't notice that your question was a link. Seems I am a bit ignorant today. Anyways, @camrick's ActionMapAction really did the trick. I must admit that I would have never been able to solve this myself. Simple stuff... Thanks! – Jouni Aro Aug 22 '17 at 11:06

1 Answers1

1

To recap from the comments, here's a full implementation—modified from the original in the article cited here—with the addCopyAndSelectAll method that adds a standard "copy" and "selectAll" menu items. Note that this works mainly with the JTable controls, since they use these action names. Other component types may be initialized to other action names as I've mentioned in my answer to the other issue.

import java.awt.Toolkit;
import java.awt.event.ActionEvent;
import java.awt.event.KeyEvent;

import javax.swing.AbstractAction;
import javax.swing.Action;
import javax.swing.JComponent;
import javax.swing.JMenuItem;
import javax.swing.JPopupMenu;
import javax.swing.KeyStroke;

/*
 * The ActionMapAction class is a convenience class that allows you to use an installed Action as an
 * Action or ActionListener on a separate component.
 *
 * It can be used on components like JButton or JMenuItem that support an Action as a property of
 * the component. Or it can be added to the same above components as an ActionListener.
 *
 * The benefit of this class is that a new ActionEvent will be created such that the source of the
 * event is the component the Action belongs to, not the component that was "clicked". Otherwise in
 * many cases a ClassCastException will be thrown when the Action is invoked.
 */
@SuppressWarnings("serial")
public class ActionMapAction extends AbstractAction {
  /**
   * @param parent
   * @param popupMenu
   */
  public static void addCopyAndSelectAll(JComponent parent, JPopupMenu popupMenu) {
    // Must use ActionMapAction to link between the MenuItem and Table
    // The "copy" action, for example must be called from a Table, not from MenuItem
    Action copyAction = new ActionMapAction("Copy", parent, "copy");

    JMenuItem copyItem = new JMenuItem(copyAction);
    int MASK = Toolkit.getDefaultToolkit().getMenuShortcutKeyMask();
    copyItem.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_C, MASK));
    popupMenu.add(copyItem);

    Action selAction = new ActionMapAction(UiMessages.INSTANCE.get("Select All", parent, "selectAll");

    JMenuItem selItem = new JMenuItem(selAction);
    selItem.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_A, MASK));
    popupMenu.add(selItem);

    parent.setComponentPopupMenu(popupMenu);
  }

  private Action originalAction;
  private JComponent component;

  private String actionCommand = "";

  /**
   * Replace the default Action for the given KeyStroke with a custom Action
   *
   * @param name the name parameter of the Action
   * @param componet the component the Action belongs to
   * @param actionKey the key to identify the Action in the ActionMap
   */
  public ActionMapAction(String name, JComponent component, String actionKey) {
    super(name);

    originalAction = component.getActionMap().get(actionKey);

    if (originalAction == null) {
      String message = "no Action for action key: " + actionKey;
      throw new IllegalArgumentException(message);
    }

    this.component = component;
  }

  /**
   * Invoke the original Action using the original component as the source of the event.
   */
  @Override
  public void actionPerformed(ActionEvent e) {
    e = new ActionEvent(component, ActionEvent.ACTION_PERFORMED, actionCommand, e.getWhen(), e.getModifiers());

    originalAction.actionPerformed(e);
  }

  public void setActionCommand(String actionCommand) {
    this.actionCommand = actionCommand;
  }
}
trashgod
  • 203,806
  • 29
  • 246
  • 1,045
Jouni Aro
  • 2,099
  • 14
  • 30