JMenuItem.setSelected() does not alter appearance of selected item?
Asked Answered
A

6

3

I wish to programmatically cause a particular item on a menu to be selected and to display as such so that if Enter is pressed, the corresponding action will be performed. Ubnfortunately I find that neither JMenuItem.setSelected(), nor JPopupMenu.setSelectedItem() does what I want. Basically I want to happen what happens when either the arrow key is pressed or the mouse moves into the space of a particular item - the background color alters, indicating the selection. I did not program that, it just happens. Why don't these APIs do the same thing? This is driving me nuts. It should not be this hard. Is there some API that does what I want?

Attain answered 12/2, 2013 at 18:14 Comment(3)
The setSelected() method controls whether a JCheckBoxMenuItem or JRadioButtonMenuItem has a check next to it. It does not relate to whether the item is highlighted.Braze
Is there an API that does?Attain
After reading the comments, I think I don't like your approach for many reasons: (1) it doesn't seem to hold up if you have multiple menus with only 1 enabled item (2) it's unexpected behavior to have a menu auto-expand with the only enabled item armed and ready (3) What if, during the change of state in your app that causes this, the focus on your frame is also on a clickable component, such as a JButton? Who wins the spacebar war? (4) I've been doing UIs for a long time, and have never seen this. I try not to reinvent the wheel as far as user interaction is concerned. Just my 2 cents.Promise
P
3

This kinda worked:

JMenuItem#setArmed(boolean);

although you don't see it unless you traverse the JMenus to get there. Perhaps if you call that on each menu above it?

EDIT: Perhaps you want an accelerator for your menu item? See: How To Use Menus: Enabling Keyboard Operation

Promise answered 12/2, 2013 at 18:35 Comment(11)
This partially works. It does alter the appearance. What it doesn't do is fire the state change that enables the keypress (<space>) that executes the current armed action to fire. In other words, I have to move the mouse out of the armed item and then back in, and once I do that <space> will execute the action referenced by the armed item. All I'm trying to do is "preselect" an item for default action. There must be an easy way to do this. Sheesh!Attain
I must admit, this seems like an unusual requirement. Could you be more specific as to what you're trying to accomplish?Promise
Have a preselected item on a menu for which nothing other than pressing the space bar will execute that item's action without requiring mouse movement or arrow key press to "get there". This application is very keyboard-centric. Use of the mouse, while possible, is deprecated, and every action must be accomplishable with keystrokes.Attain
See my link above on how to use accelerators.Promise
thanks but no. I want a way to preselect a default action, that will be done when the "do the selected item" keystroke is pressed.Attain
adding splungebob's suggestion as useful, even though it was not the full solution. It was still useful.Attain
@Steve: "Have a preselected item on a menu for which ..." How is the item "preseleted": by default or by some other user interaction? If it's the former, then just magically hitting spacebar to do some hidden action doesn't seem like a good UI. If it's the latter, then wouldn't an accelerator do the trick? I guess I'm trying to see a real world example of what you're trying to accomplish. Perhaps there's a better way.Promise
Basically, my real use case is this: If, due to application state, only one item on a menu is enabled, we want to be able to execute that item with a single keystroke without "navigating" to it. Alternatively we could "just do it" without showing the menu, but this would be even more confusing to the user.Attain
So, if you have a menu with 5 items on it, and at any given time due to application state all but one is disabled, then hitting spacebar will trigger the sole enabled item? How will the user know which item is getting triggered? And what if there's another menu with the only one enable item? Who wins? Perhaps I'm not understanding.Promise
The user will know that because my solution also requires your idea of setting that item armed to give it the visual indication.Attain
I am trying to solve the same issue. In my case, I have a "Show All" and "Hide All" menu items that will trigger showing or not showing panels on a status bar. I want to be able to select and deselect the corresponding JCheckBoxMenuItem so that it is in sync with the GUI. I can trigger the action, but I cannot get the menu items to select or deselect.Cormac
G
1

java.awt.Robot can do Trick ;)
Consider the code Given below:

import javax.swing.JFrame;
import javax.swing.JButton;
import javax.swing.JMenu;
import javax.swing.JMenuItem;
import javax.swing.JMenuBar;
import javax.swing.JOptionPane;
import javax.swing.JPopupMenu;
import javax.swing.SwingUtilities;
import java.awt.Dimension;
import java.awt.BorderLayout;
import java.awt.event.ActionListener;
import java.awt.event.ActionEvent;
import java.awt.event.KeyEvent;
import java.awt.Robot;
public class JMenuFrame extends JFrame implements ActionListener
{
    JMenuBar bar;
    JMenu menu ;
    String[] items;
    JMenuItem[] menuItems;
    JButton start;
    Robot robot;
    public void prepareAndShowGUI()
    {
        try
        {
            robot = new Robot();    
        }
        catch (Exception ex){}
        bar = new JMenuBar();
        menu = new JMenu("File");
        items =  new String[]{"Open","Save","Save As","Quit"};
        menuItems = new JMenuItem[items.length];
        start = new JButton("Click me");
        for (int i = 0 ; i < items.length ; i++)
        {
            menuItems[i] = new JMenuItem(items[i]);
            menuItems[i].addActionListener(this);
            menu.add(menuItems[i]);
        }
        bar.add(menu);
        setJMenuBar(bar);
        start.addActionListener(this);
        getContentPane().add(start,BorderLayout.SOUTH);
        setPreferredSize(new Dimension(300,400));
        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        pack();
        setVisible(true);
    }
    @Override
    public void actionPerformed(ActionEvent evt)
    {
        if ("Click me".equals(evt.getActionCommand()))
        {
            menu.doClick();
            if (robot!=null)
            {
                for (int i = 0 ; i<=2 ; i++) //Suppose you want to select 3rd MenuItem
                {
                    if (!menuItems[i].isEnabled())
                    {
                        continue;
                    }
                    robot.keyPress(KeyEvent.VK_DOWN);
                    robot.keyRelease(KeyEvent.VK_UP);
                }
            }
        }
        else
        {
            JOptionPane.showMessageDialog(this,evt.getActionCommand()+" is pressed","Information",JOptionPane.INFORMATION_MESSAGE);
        }
    }
    public static void main(String st[])
    {
        SwingUtilities.invokeLater( new Runnable()
        {
            public void run()
            {
                JMenuFrame mf = new JMenuFrame();
                mf.prepareAndShowGUI();
            }
        });
    }
}
Gebhardt answered 12/2, 2013 at 19:42 Comment(2)
Interesting. I never heard of java.awt.Robot before.Attain
+1 for doClick(), also seen here and here.Courlan
A
0

It's ugly as sin, but this, in connection with splungebob's setArmed() answer above is the full solution: First, make the menu a MenuKeyListener and add it a a MenuKeyListener to itself. Then:

    public void menuKeyReleased(MenuKeyEvent e) {
    if (e.getModifiers() == 0) {
        switch (e.getKeyCode()) {
        case KeyEvent.VK_ENTER:
        case KeyEvent.VK_SPACE:
            for (MenuElement elem : this.getSubElements()) {
                if (elem instanceof JMenuItem) {
                    JMenuItem item = (JMenuItem) elem;
                    if (item.isArmed()) {
                        Action action = item.getAction();
                        if (action != null) {
                            action.actionPerformed(new ActionEvent(this, 0, null));
                            e.consume();
                            setVisible(false);
                        }
                    }
                }
            }
        }
    }   
}

I can't believe that this was this difficult. Swing has definite limitations when it comes to building keyboard-centric interfaces.

Attain answered 12/2, 2013 at 19:37 Comment(0)
P
0

Although I'm not sure I agree with the requirements (see my comment in the OP), I still wanted to give it a crack. The first half of the code is just setting up the GUI so that the condition can be creted by the user.

- A menu is created with 3 items, and 3 separate checkboxes are created to control the state of the menu items.

- If at any time only one menu item is enabled, auto-expand the menu and "pre-select" the item. The code to auto-expand the menu was ripped from JMenu.

- Add a MenuKeyListener to the menu to capture the user hitting the space bar while the menu tree is expanded.

import java.awt.*;
import java.awt.event.*;
import java.util.*;

import javax.swing.*;
import javax.swing.event.*;

public class JMenuDemo implements Runnable
{
  private final String[] ACTIONS = new String[]{"Open", "Print", "Close"};

  private JMenu fileMenu;
  private JMenuItem[] menuItems;
  private JCheckBox[] checkBoxes;

  public static void main(String args[])
  {
    SwingUtilities.invokeLater(new JMenuDemo()); 
  }

  public void run()
  {
    JPanel panel = new JPanel(new GridLayout(0,1));
    panel.setBorder(BorderFactory.createTitledBorder("Enabled"));

    menuItems = new JMenuItem[ACTIONS.length];
    checkBoxes = new JCheckBox[ACTIONS.length];

    for (int i = 0; i < ACTIONS.length; i++)
    {
      final int index = i;
      final String action = ACTIONS[i];

      menuItems[i] = new JMenuItem(action);
      menuItems[i].setAccelerator(KeyStroke.getKeyStroke(action.charAt(0),
                                  ActionEvent.ALT_MASK));
      menuItems[i].setMnemonic(action.charAt(0));
      menuItems[i].addActionListener(new ActionListener()
      {
        public void actionPerformed(ActionEvent event)
        {
          System.out.println(action);
        }
      });

      checkBoxes[i] = new JCheckBox(action);
      checkBoxes[i].setSelected(true);
      checkBoxes[i].addItemListener(new ItemListener()
      {
        public void itemStateChanged(ItemEvent event)
        {
          checkBoxChanged(index);
        }
      });
      panel.add(checkBoxes[i]);
    }

    JFrame frame = new JFrame("Demo");
    frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    frame.setJMenuBar(createJMenuBar());
    frame.add(panel, BorderLayout.SOUTH);
    frame.setSize(300, 400);
    frame.setLocationRelativeTo(null);
    frame.setVisible(true);
  }

  private void checkBoxChanged(int index)
  {
    menuItems[index].setEnabled(checkBoxes[index].isSelected());
    evaluate();
  }

  private JMenuBar createJMenuBar()
  {
    fileMenu = new JMenu("File");
    fileMenu.setMnemonic('F');
    fileMenu.addMenuKeyListener(new MenuKeyListener()
    {
      @Override
      public void menuKeyTyped(MenuKeyEvent event)
      {
        autoClick(event);
      }

      @Override
      public void menuKeyPressed(MenuKeyEvent event) {}

      @Override
      public void menuKeyReleased(MenuKeyEvent event) {}
    });

    for (int i = 0; i < menuItems.length; i++)
    {
      fileMenu.add(menuItems[i]);
    }

    JMenuBar menuBar = new JMenuBar();
    menuBar.add(fileMenu);

    return menuBar;
  }

  private void autoClick(MenuKeyEvent event)
  {
    if (event.getModifiers() == 0 && event.getKeyChar() == KeyEvent.VK_SPACE)
    {
      for (JMenuItem menuItem : menuItems)
      {
        if (menuItem.isArmed())
        {
          menuItem.doClick();
          MenuSelectionManager.defaultManager().setSelectedPath(null);
        }
      }
    }
  }

  private void evaluate()
  {
    JMenuItem onlyOne = null;

    for (JMenuItem menuItem : menuItems)
    {
      menuItem.setArmed(false);

      if (menuItem.isEnabled())
      {
        if (onlyOne == null)
        {
          onlyOne = menuItem;
        }
        else
        {
          onlyOne = null;
          break;
        }
      }
    }

    // Show the path if only one is enabled
    if (onlyOne != null)
    {
      onlyOne.setArmed(true);
      MenuElement me[] = buildMenuElementArray(fileMenu);
      MenuSelectionManager.defaultManager().setSelectedPath(me);
    }
  }

  /*
   * Copied from JMenu
   */
  private MenuElement[] buildMenuElementArray(JMenu leaf)
  {
    Vector<JComponent> elements = new Vector<JComponent>();
    Component current = leaf.getPopupMenu();

    while (true)
    {
      if (current instanceof JPopupMenu)
      {
        JPopupMenu pop = (JPopupMenu) current;
        elements.insertElementAt(pop, 0);
        current = pop.getInvoker();
      }
      else if (current instanceof JMenu)
      {
        JMenu menu = (JMenu) current;
        elements.insertElementAt(menu, 0);
        current = menu.getParent();
      }
      else if (current instanceof JMenuBar)
      {
        JMenuBar bar = (JMenuBar) current;
        elements.insertElementAt(bar, 0);
        MenuElement me[] = new MenuElement[elements.size()];
        elements.copyInto(me);
        return me;
      }
    }
  }
}
Promise answered 13/2, 2013 at 15:55 Comment(0)
D
0
    jMenu1.doClick(); // this open the menu again
Diwan answered 18/5, 2015 at 14:32 Comment(0)
D
0

I found this setSelectedPath() works well in unison of mouse hover, unlike using menuItem.setArmed()

MenuSelectionManager.defaultManager().setSelectedPath(new MenuElement[]{popupMenu, menuItem});
Drandell answered 22/9, 2022 at 7:39 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.