Saturday, April 13, 2013

Reusing Context Menus in Swing (Java)

I just spent hours (hours I can ill afford at my age!) resolving a very frustrating and in my opinion obscure problem with a graphical user interface I'm building using Swing (Java 7). My interface has a bunch of lists scattered around, using a customized class (extending Swing's JList) that includes tool tips for individual items in the list, pop up context menus (JPopupMenu), and tool tips for the menu items in the context menus. In one such list, everything worked: item tool tips displayed, a right click popped up the context menu, and hovering on a context menu displayed its tool tip. Everywhere else, item tool tips displayed and context menus popped up (and their actions correctly fired if I clicked them), but the bleeping menu tool tips were nowhere to be seen.

I'll skip the long and painful history of blind alleys I went down. (Somewhere, some Google employee is desperately trying to cool down a server I overheated during my search.) I ultimately realized that the meaningful distinction between the one list that worked and the many that did not had nothing to do with the lists themselves, nor with their parent containers. The one list that worked properly had a context menu that was only used in that one place. All the other lists popped up one of a few menus that could be invoked from more than one place.

My understanding is that you cannot add the same Swing component to more than one container, but that's not the issue with instances of JPopupMenu. You don't actually add the context menu to more than one component; you just invoke it (set it visible and position it) from more than one place. The fact that the same menu correctly displayed in more than one place, and the menu item actions correctly fired from each place, seems to support that. So I'm baffled why menu item tool tips appear if there is only one location that listens for the pop-up action -- right-click on most (but not all?) systems -- but fail to appear if more than one location listens for the pop-up action. Is this an "undocumented feature" of Swing?

The solution was to change my customized JList class so that, rather than using the original JPopupMenu, it uses a deep copy of the menu. The only thing multiple copies of the menu share is the action method. In case it will help anyone, here is my code for making the clone menu.

  /**
   * Clone a popup menu (deep copy).
   * @param m the menu to clone
   * @return the clone
   */
  private static JPopupMenu cloneMenu(JPopupMenu m) {
    if (m == null) {
      return null;
    }
    JPopupMenu menu = new JPopupMenu();
    for (Component i : m.getComponents()) {
      if (i instanceof JMenuItem) {
        JMenuItem item = new JMenuItem();
        JMenuItem old = (JMenuItem) i;
        item.setText(old.getText());
        item.setToolTipText(old.getToolTipText());
        item.setMnemonic(old.getMnemonic());
        for (ActionListener a : old.getActionListeners()) {
          item.addActionListener(a);
        }
        menu.add(item);
      }
    }
    return menu;
  }

I should note that I only copied the bits I use (text, tool tip, mnemonic and action listener). If you use other bits (accelerators, for instance), you'll need to copy those as well.

There's one other little piece of the puzzle. Since clones of a given menu all use the same action listeners, you need to give the action listeners a way of knowing which clone invoked them. Here's my tweak to a prototype action listener:

private void someMenuItemActionPerformed(java.awt.event.ActionEvent evt) {                                                          
  JPopupMenu m = (JPopupMenu) ((Component) evt.getSource()).getParent();
  // m.getInvoker() is the component on which the context menu was invoked
  // TODO add your handling code here:
} 

In my case, m.getInvoker() will be an instance of my modified JList class.

No comments:

Post a Comment

Due to intermittent spamming, comments are being moderated. If this is your first time commenting on the blog, please read the Ground Rules for Comments. In particular, if you want to ask an operations research-related question not relevant to this post, consider asking it on Operations Research Stack Exchange.