How to set an accelerator for a JMenu sub menu?
Asked Answered
K

2

5

I have a user request to add an accelerator to a sub menu (JMenu) which would allow the user to press the short cut and have the corresponding sub menu "fold out", showing its contained menu items.

I don't recall every having seen something like this (either in Java or any other language). Our application is written in Java using Swing. We have a number of JMenuItems with accelerators that work well, but when I attempted to add an accelerator to JMenu I get the following exception:

java.lang.Error: setAccelerator() is not defined for JMenu. Use setMnemonic() instead.

I've tried to use the MenuDemo! code to experiment with this a bit further.

This is what I tried:

//a submenu
menu.addSeparator();
submenu = new JMenu("A submenu");
submenu.setMnemonic(KeyEvent.VK_S);
submenu.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_U, InputEvent.CTRL_MASK));

The last line is the one added by me, which causes the exception.

I've tried extensive googling but all I can find is articles on how to add accelerators to JMenuItem.

It seems the JMenu does not support this natively. Is there any workaround to achieve this behaviour?

Krishna answered 16/10, 2012 at 7:47 Comment(0)
S
9

Another option is to override the accelerator get/set and reproduce the JMenuItem behaviour. Then the UI will do the rest of the job.

The important thing is to fire the property change and have a consistent get/set for the accelerator. The advantage of this solution is that it also provides a visual indication of the shortcut/accelerator.

Here is a small demo code:

import java.awt.event.KeyEvent;

import javax.swing.JFrame;
import javax.swing.JMenu;
import javax.swing.JMenuBar;
import javax.swing.JMenuItem;
import javax.swing.KeyStroke;
import javax.swing.SwingUtilities;
import javax.swing.UIManager;
import javax.swing.UnsupportedLookAndFeelException;

public class TestMenu {

    protected void initUI() {
        JFrame frame = new JFrame(TestMenu.class.getSimpleName());
        frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        JMenuBar bar = new JMenuBar();
        JMenu topMenu = new JMenu("Top Menu");
        JMenu subMenu = new JMenu("Sub menu") {
            private KeyStroke accelerator;

            @Override
            public KeyStroke getAccelerator() {
                return accelerator;
            }

            @Override
            public void setAccelerator(KeyStroke keyStroke) {
                KeyStroke oldAccelerator = accelerator;
                this.accelerator = keyStroke;
                repaint();
                revalidate();
                firePropertyChange("accelerator", oldAccelerator, accelerator);
            }
        };
        subMenu.setAccelerator(KeyStroke.getKeyStroke(KeyEvent.VK_F4, KeyEvent.CTRL_MASK));
        JMenuItem item1 = new JMenuItem("Item 1");
        JMenuItem item2 = new JMenuItem("Item 2");
        subMenu.add(item1);
        subMenu.addSeparator();
        subMenu.add(item2);
        topMenu.add(subMenu);
        bar.add(topMenu);
        frame.setJMenuBar(bar);
        frame.setSize(400, 300);
        frame.setVisible(true);
    }

    public static void main(String[] args) {
        SwingUtilities.invokeLater(new Runnable() {

            @Override
            public void run() {
                try {
                    UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
                } catch (ClassNotFoundException e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                } catch (InstantiationException e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                } catch (IllegalAccessException e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                } catch (UnsupportedLookAndFeelException e) {
                    // TODO Auto-generated catch block
                    e.printStackTrace();
                }
                new TestMenu().initUI();
            }
        });
    }

}
Skaw answered 16/10, 2012 at 10:54 Comment(3)
cool as well - there are still surprises in the old lady Swing:-)Kimikokimitri
Thanks, this solves my problems. So easy, yet you have to think of it :-)Krishna
Unfortunately, this doesn't work with the OSX system menubar. If I add an accelerator key to a menu like this, the key brings up a semi-functional and old-fashioned looking new menu on the corner of the JFrame that duplicates the system menu. The keyboard (enter key) doesn't work to activate menu items in the odd extra menu, and the real system menu does not show the keystroke assigned to the menu item.Flotow
C
6

I don't think that is possible just like that. But what you could do is adding an AbstractAction, which simulates a click.

submenu.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW).put(KeyStroke.getKeyStroke("control U"), "expand");
    submenu.getActionMap().put("expand", new AbstractAction("expand") {
        private static final long serialVersionUID = 1L;

        @Override
        public void actionPerformed(ActionEvent evt) {
            submenu.doClick();
        }
    }
);

I hope this also works for you.

Celerity answered 16/10, 2012 at 8:9 Comment(7)
my first guess was that wouldn't work - but was wrong :-) +1 - the usability catch is that there's no visual indication of which keyStroke to useKimikokimitri
It works like a charm as far as opening the sub menu goes, thanks a lot for that. As kleopatra says, there is no indicator to show which key-binding is used. Is it possible, to add this information somehow? Then, the solution would be perfect :-)Krishna
The only choices you have are afaik either juggling around with fonts so it looks like the Accelerators indicator or adding a JMenuItem instead of a JMenu and adding a PopUp or something for the selection... (But then you dont have a "real" menu anymore) Greetings from Switzerland ;-)Celerity
@Kimikokimitri Actually, by simply overriding get/set accelerator and firing the appropriate property change event, you can have the keystroke indication and let the UI do the rest of the work (see also my example below).Skaw
@UrsBeeli See my answer below which also provides a visual feedback of the keystroke.Skaw
don't unserstand, focus and selection could be painted, with little and API built_in effort ....Artemisa
Thanks everyone for your help. @1r0n1k: Greetings from CH to CH :-)Krishna

© 2022 - 2024 — McMap. All rights reserved.