2

I am trying to get my JMenuBar to model the behavior of the menu bars of Firefox and iTunes. The behavior: the menu bar is initially hidden. But, when you press Alt, the menu bar appears (with the first item selected) and when you don't have a menu item selected, the menu bar disappears. My idea was to listen for selection changes to the JMenuBar via a ChangeListener on its SelectionModel.

However, the behavior of the attached SSCCE is not as desired. When the frame loads, the JMenuBar is not visible. When you press Alt, the menu bar appears with the first menu selected (thanks to the WindowsLookAndFeel). However, every subsequent Alt pressed fires no ChangeEvents. I can't figure out why...

Anyone have light to shed?

public class MenuBarTest extends javax.swing.JFrame {

    public MenuBarTest() {
        initComponents();
        jMenuBar1.setVisible(false);
        jMenuBar1.getSelectionModel().addChangeListener(new ChangeListener() {
            @Override
            public void stateChanged(ChangeEvent e) {
                System.out.println(e.toString());
                jMenuBar1.setVisible(jMenuBar1.isSelected());
                System.out.println(jMenuBar1.isSelected());
                System.out.println(jMenuBar1.getSelectionModel().isSelected());
            }
        });
    }

    private void initComponents() {

        jMenuBar1 = new javax.swing.JMenuBar();
        jMenu1 = new javax.swing.JMenu();
        jMenuItem1 = new javax.swing.JMenuItem();
        jMenu2 = new javax.swing.JMenu();
        jMenuItem2 = new javax.swing.JMenuItem();

        setDefaultCloseOperation(javax.swing.WindowConstants.EXIT_ON_CLOSE);

        jMenu1.setText("File");
        jMenuItem1.setText("jMenuItem1");
        jMenu1.add(jMenuItem1);
        jMenuBar1.add(jMenu1);
        jMenu2.setText("Edit");
        jMenuItem2.setText("jMenuItem2");
        jMenu2.add(jMenuItem2);
        jMenuBar1.add(jMenu2);
        setJMenuBar(jMenuBar1);
        javax.swing.GroupLayout layout = new javax.swing.GroupLayout(getContentPane());
        getContentPane().setLayout(layout);
        layout.setHorizontalGroup(
                layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
                .addGap(0, 400, Short.MAX_VALUE));
        layout.setVerticalGroup(
                layout.createParallelGroup(javax.swing.GroupLayout.Alignment.LEADING)
                .addGap(0, 279, Short.MAX_VALUE));

        pack();
    }

    public static void main(String args[]) {
        try {
            UIManager.setLookAndFeel("com.sun.java.swing.plaf.windows.WindowsLookAndFeel");
        } catch (ClassNotFoundException | InstantiationException | IllegalAccessException | javax.swing.UnsupportedLookAndFeelException ex) {
            ex.printStackTrace();
        }
        java.awt.EventQueue.invokeLater(new Runnable() {
            public void run() {
                new NewClass().setVisible(true);
            }
        });
    }
    private javax.swing.JMenu jMenu1;
    private javax.swing.JMenu jMenu2;
    private javax.swing.JMenuBar jMenuBar1;
    private javax.swing.JMenuItem jMenuItem1;
    private javax.swing.JMenuItem jMenuItem2;
}
mKorbel
  • 109,525
  • 20
  • 134
  • 319
ryvantage
  • 13,064
  • 15
  • 63
  • 112

2 Answers2

3

Looks like the menubar is never unselected, once it had been selected. Not sure if that's a bug or not.

Could be a better idea to listen directly to the MenuSelectionManager as that's where you are notified about all changes to menu selection anywhere. Needs some logic to filter out those unrelated to the menuBar, something similar to:

ChangeListener listener = new ChangeListener() {
    @Override
    public void stateChanged(ChangeEvent e) {
        MenuElement[] elements = MenuSelectionManager.defaultManager().getSelectedPath();
        jMenuBar1.setVisible(elements.length > 0 && elements[0] == jMenuBar1);
    }
};
MenuSelectionManager.defaultManager().addChangeListener(listener);

Update

A hefty drawback of hiding the menubar is that accelerators to its menuItems stop working. The reason is that only componentInputMaps of components which are showing are asked to handle them. This is done deep down in the bowels of the swing package, namely by the package private class KeyboardManager. No way to hook-in a custom manager (which might be implemented to handle menubars that are not showing).

At the other end of the chain, we can interfer, though. Basically two options, both subclassing menubar:

  • (extremely dirty trick!) override isShowing to always return true. I've seen this done, but can't really recommend because there might be side-effects that I don't know
  • a slightly dirty trick: add a property hidden and implement getPreferredSize to return a 0 height if hidden. The dirtyness is its reliance on RootPaneLayout respecting the pref height ...

The revised ChangeListener:

bar.setHidden(true);
ChangeListener listener = new ChangeListener() {
    @Override
    public void stateChanged(ChangeEvent e) {
        MenuElement[] elements = MenuSelectionManager.defaultManager().getSelectedPath();
        bar.setHidden(!(elements.length >0 && elements[0] == bar));
    }
};
MenuSelectionManager.defaultManager().addChangeListener(listener);

The custom menuBar:

public static class JHideableMenuBar extends JMenuBar {

    private boolean hidden;

    public void setHidden(boolean hidden) {
        if (this.hidden == hidden) return;
        this.hidden = hidden;
        revalidate();
    }

    @Override
    public Dimension getPreferredSize() {
        Dimension pref = super.getPreferredSize();
        if (hidden) {
            pref.height = 0;
        }
        return pref;
    }

}
kleopatra
  • 51,061
  • 28
  • 99
  • 211
  • Excellent, that works. I would've never found `MenuSelectionManager` on my own. I would still like to know why a `ChangeListener` on the `SelectionManager` directly from the `JMenuBar` doesn't work, though. But it works so I'm happy! Thanks. – ryvantage Aug 07 '13 at 16:27
  • for some reason this approach disables menu item accelerators. Even though the docs for `setAccelerator` specifically says: "Note that when the keyboard accelerator is typed, it will work whether or not the menu is currently displayed." but this is not the case right now... – ryvantage Aug 08 '13 at 19:21
  • If you add this code: `jMenuItem2.setAccelerator(javax.swing.KeyStroke.getKeyStroke(java.awt.event.KeyEvent.VK_F, java.awt.event.InputEvent.CTRL_MASK)); jMenuItem2.setText("Find"); jMenuItem2.addActionListener(new java.awt.event.ActionListener() { public void actionPerformed(java.awt.event.ActionEvent evt) { String what = JOptionPane.showInputDialog(MenuBarTest.this, "Search for what?"); System.out.println(what); } });` to the above SSCCE – ryvantage Aug 08 '13 at 19:21
  • The accelerator set is ctrl+f, which only works when the menu bar is visible. – ryvantage Aug 08 '13 at 19:22
  • hmm ..are you saying that acceleator keys are not working if the the menubar is invisible? Unfortunately, that's a different problem of hiding (sorry, on IDE handý to think of a solution) – kleopatra Aug 08 '13 at 21:00
  • ok the new question is here: http://stackoverflow.com/questions/18155493/jmenuitem-accelerator-not-working-when-menu-bar-is-hidden – ryvantage Aug 09 '13 at 20:59
2

However, every subsequent Alt pressed fires no ChangeEvents. I can't figure out why...

  • ChangeListener firing events from SelectionModel, Mouse or Key events, those events are expected

  • you can to simulating events from ChangeListener e.g. reset selection on menu (put that instead on moving Focus to JTextField)

  • there are accesible another listeners, that firing own events and correctly

see

import java.awt.BorderLayout;
import java.awt.Dimension;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import javax.swing.ButtonModel;
import javax.swing.JTextField;
import javax.swing.UIManager;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import javax.swing.event.MenuEvent;
import javax.swing.event.MenuListener;

public class MenuBarTest extends javax.swing.JFrame {

    private javax.swing.JMenu jMenu1;
    private javax.swing.JMenu jMenu2;
    private javax.swing.JMenuBar jMenuBar1;
    private javax.swing.JMenuItem jMenuItem1;
    private javax.swing.JMenuItem jMenuItem2;
    private JTextField text = new JTextField("text", 10);

    public MenuBarTest() {
        jMenuBar1 = new javax.swing.JMenuBar();
        jMenu1 = new javax.swing.JMenu();
        jMenu1.addMenuListener(new MenuListener() {
            @Override
            public void menuSelected(MenuEvent e) {
                System.out.println("MenuListener - Selected: " + e.toString());
            }

            @Override
            public void menuDeselected(MenuEvent e) {
                System.out.println("MenuListener - Deselected: " + e.toString());
            }

            @Override
            public void menuCanceled(MenuEvent e) {
                System.out.println("MenuListener - Canceled: " + e.toString());
            }
        });
        jMenu1.getModel().addChangeListener(new ChangeListener() {
            @Override
            public void stateChanged(ChangeEvent e) {
                ButtonModel model = (ButtonModel) e.getSource();
                if (model.isArmed()) {
                    System.out.println("ButtonModel - Armed: " + e.toString());
                } else if (model.isEnabled()) {
                    System.out.println("ButtonModel - Enabled: " + e.toString());
                } else if (model.isPressed()) {
                    System.out.println("ButtonModel - Pressed: " + e.toString());
                } else if (model.isRollover()) {
                    System.out.println("ButtonModel - Rollover: " + e.toString());
                } else if (model.isSelected()) {
                    System.out.println("ButtonModel - Selected: " + e.toString());
                } else {
                    System.out.println("ButtonModel - !!!!!????: " + e.toString());
                }
            }
        });
        jMenuItem1 = new javax.swing.JMenuItem();
        jMenu2 = new javax.swing.JMenu();
        jMenuItem2 = new javax.swing.JMenuItem();
        jMenu1.setText("File");
        jMenuItem1.setText("jMenuItem1");
        jMenu1.add(jMenuItem1);
        jMenuBar1.add(jMenu1);
        jMenu2.setText("Edit");
        jMenuItem2.setText("jMenuItem2");
        jMenu2.add(jMenuItem2);
        jMenuBar1.add(jMenu2);
        jMenuBar1.setVisible(false);
        jMenuBar1.addPropertyChangeListener(new PropertyChangeListener() {
            @Override
            public void propertyChange(PropertyChangeEvent evt) {
                String strPropertyName = evt.getPropertyName();
                System.out.println("PropertyChangeListener - NewValue: " + evt.getNewValue());
                System.out.println("PropertyChangeListener - OldValue: " + evt.getOldValue());
                System.out.println("PropertyChangeListener - PropagationId: " + evt.getPropagationId());
                System.out.println("PropertyChangeListener - PropertyName: " + evt.getPropertyName());
                if ("MENU.MP_BARBACKGROUND".equals(strPropertyName)) {
                    System.out.println("PropertyChangeListener - MENU.MP_BARBACKGROUND: " + evt.getNewValue());
                }
            }
        });
        jMenuBar1.getSelectionModel().addChangeListener(new ChangeListener() {
            @Override
            public void stateChanged(ChangeEvent e) {
                System.out.println("ChangeListener - " + e.toString());
                jMenuBar1.setVisible(jMenuBar1.isSelected());
                System.out.println("ChangeListener - " + jMenuBar1.isSelected());
                System.out.println("ChangeListener - " + jMenuBar1.getSelectionModel().isSelected());
                java.awt.EventQueue.invokeLater(new Runnable() {
                    @Override
                    public void run() {
                        text.grabFocus();
                        text.requestFocusInWindow();
                        text.setText(text.getText());
                        text.selectAll();
                    }
                });
            }
        });
        setJMenuBar(jMenuBar1);
        add(text, BorderLayout.NORTH);
        add(new JTextField("text", 10), BorderLayout.SOUTH);
        setDefaultCloseOperation(javax.swing.WindowConstants.EXIT_ON_CLOSE);
        setPreferredSize(new Dimension(400, 300));
        pack();
    }

    public static void main(String args[]) {
        try {
            UIManager.setLookAndFeel("com.sun.java.swing.plaf.windows.WindowsLookAndFeel");
        } catch (ClassNotFoundException | InstantiationException | IllegalAccessException | javax.swing.UnsupportedLookAndFeelException ex) {
            ex.printStackTrace();
        }
        java.awt.EventQueue.invokeLater(new Runnable() {
            @Override
            public void run() {
                new MenuBarTest().setVisible(true);
            }
        });
    }
}
mKorbel
  • 109,525
  • 20
  • 134
  • 319
  • hmm ... or maybe I don't understand what you are suggesting? – kleopatra Aug 07 '13 at 07:18
  • @kleopatra 1. I'm not answering something, there any any answer 2. playing with accesible events 3. simulations that JMenuBar always returns non_expected value from methods implemented in API, maybe bug, maybe feature, 4. only its child e.g. JMenu can do that, jMenuBar1.setVisible(jMenu1.isSelected()); accesible from ButtonModel EDIT (isEnabled, rest of events is accesible from MenuListener), 5. I have to test events from PropertyChangeListener, – mKorbel Aug 07 '13 at 07:51
  • @kleopatra PropertyChangeListener ---> could be, but with woodoo wrapped into Boolean value (PropertyChangeListener firing opposite event and JMenuBar never will be visible on start_up) – mKorbel Aug 07 '13 at 07:58
  • @mKorbel, I'm sorry but I am having a lot of trouble understanding you. – ryvantage Aug 07 '13 at 16:31