-1

I have a keyboard shortcut in my Java Swing application to save some inputs to a server and go to the next screen if the user presses Alt+V. The action is tied to a JButton, which can alternatively be clicked to save/proceed.

I found that when Alt+V was held down, it triggered the action multiple times instead of just once, using the following code:

proceedBttn.getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW)
            .put(KeyStroke.getKeyStroke(KeyEvent.VK_V, InputEvent.ALT_DOWN_MASK), "proceedBtn");

My thought was to fix with the following, which instead only triggers the action on release of the keyboard shortcut:

proceedBttn().getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW)
            .put(KeyStroke.getKeyStroke(KeyEvent.VK_V, InputEvent.ALT_DOWN_MASK, true), "proceedBtn");

However, the above code only triggers if the user is still holding Alt when they let go of V. When a user types Alt+V quickly, it's common that they let go of Alt prior to V (or at the same time), which causes the action not to trigger at all.

Is there a good Java-native option to handle this problem, rather than some potentially complex latch logic on every shortcut in my application?

Mark Rotteveel
  • 100,966
  • 191
  • 140
  • 197
Graham Meehan
  • 445
  • 5
  • 18
  • 1
    If you can get away with using a `KeyListener`, it can do exactly this for you. Let me know if that is a viable solution for you and then I can type up an answer. – davidalayachew Apr 20 '23 at 16:48
  • 1
    Absolutely! We are not tied to any particular constraints for this task. I appreciate it. – Graham Meehan Apr 20 '23 at 16:52
  • Ok, typing up now – davidalayachew Apr 20 '23 at 17:12
  • 2
    @davidalayachew: and I'm going to tell you right now that a KeyListener is not the way to solve this problem since it will only work if the listened-to component has and maintains keyboard focus. The OP has already started to use Key Bindings, which is the right path, why offer a solution that has them moving backwards? – Hovercraft Full Of Eels Apr 20 '23 at 17:49
  • 2
    No you should NOT use a KeyListener. Swing was designed to be used with Key Bindings. *it triggered the action multiple times instead of just once* - you will have the same issue with a KeyListener. *Is there a good Java-native option to handle this problem* If you have a "Proceed" button, why not set a mnemonic for the button to use "Alt p". Then the "p" on the button will be underlined to give the user a visual cue how to invoke the button with the keyboard. Check out the Swing tutorial on [How to Use Actions](https://docs.oracle.com/javase/tutorial/uiswing/misc/action.html) for an example. – camickr Apr 20 '23 at 17:51
  • Seems like multiple people disagree, so I will leave it alone. – davidalayachew Apr 20 '23 at 18:20
  • @HovercraftFullOfEels to answer your questions, KeyListener is very convenient for me whenever I need to simply parse and handle basic key press behaviour. Focus issues never really came up for me that couldn't be easily dealt with. Perhaps the nature of OP's project makes it less than ideal, but I've used them to great effect. – davidalayachew Apr 20 '23 at 18:22
  • @camickr I wasn't implying that the KeyListener alone would solve the problem. Clearly, more effort than that is required. I suggested KeyListener because I solved this exact same problem multiple times before, and KeyListener was an important part of the solution. – davidalayachew Apr 20 '23 at 18:23
  • 1
    @davidalayachew *because I solved this exact same problem multiple times before* - there is no problem so solve if you use the proper API. Using Actions and Key Bindings is the newer solution used by all Swing components. I suggest you also read the tutorial I linked to so you can better understand the benefits of this approach. – camickr Apr 20 '23 at 21:05
  • @camickr I understand the benefits of the approach just fine, I have used bindings multiple times before as well. And when I say problem, I am trying to say "achieve a specific functionality". I am not saying that Key Bindings are in any way problematic, or that they are somehow incapable of solving this problem. I brought up KeyListener because it is a perfectly serviceable solution that is easy to explain and meets basic needs. When I wrote my first comment, I was very much aware of the fact that the same could be achieved using Key Bindings. – davidalayachew Apr 20 '23 at 22:08
  • 1
    @GrahamMeehan since everyone feels so strongly against using KeyListener, I will type up a solution instead that uses Key Bindings – davidalayachew Apr 20 '23 at 22:13
  • 1
    @davidalayachew There was no reason to suggest using a KeyListener since Key Bindings is the preferred Swing solution. We attempt to answer question with with current Swing design approaches. There was no reason for you to suggest a KeyListener to confuse the issue (or do continue to dwell on the point) I already gave the solution in my first comment. Have you not yet read the tutorial? Swing will built the Key Bindings for you. – camickr Apr 20 '23 at 22:33
  • [mcve] please . – kleopatra Apr 20 '23 at 22:39
  • @camickr - your first comment only links to a broad tutorial and mentions changing which letter I use for the shortcut, which is not something I have flexibility over due to the business requirements imposed on me. – Graham Meehan Apr 20 '23 at 22:44
  • @camickr *"We attempt to answer questions with current Swing design approaches"* -- If this is the reason why I should not have suggested KeyListener in the first place, then I accept this reason. And when I say I am typing up a solution, I am not saying that your link doesn't provide the functionality they are aiming for. I am saying that a fully coded example similar to what the OP is trying to do would likely be helpful to both them and everyone else who sees this post, so I will do that. In fact, I am going to be linking the tutorial you linked in my answer as well. – davidalayachew Apr 20 '23 at 22:57
  • Answer has been posted. – davidalayachew Apr 21 '23 at 02:02
  • @GrahamMeehan *...and mentions changing which letter I use for the shortcut,* - the suggestion was to use a mnemonic. The logical choice is to do what a standard UI does an pick a mnemonic that matches the text of the button. However, that is not a requirement. I guess you didn't try it. All you had to do was copy the demo code and change a single character for one of the mnemonics the see if "Alt-V" would work. We should not have to explain the code in the tutorial. If there is something about the tutorial you don't understand then ask a question. We can't guess what you might find confusing. – camickr Apr 21 '23 at 04:45

1 Answers1

1

Short Answer

You should use Mnemonics. They don't run into the problem you ran into because, with Mnemonics, holding Alt+V is the same thing as holding the button with your computer mouse. The button remains pressed until you let go of V, regardless of if you let go of Alt before or after V.

That solves the multiple click problem, but please note that that means that input only happens when you let go of the button, not when you push it down. I think that I am solving your actual issue here (sounds like an XY Problem), but please note that the button event will not occur on push, but on release.


Long Answer

So, what you are trying to do is to have your button be triggered only once upon key press. If that is your goal, then we can accomplish that more easily using Mnemonics and javax.swing.Action. Following the suggestion from @camickr above, I would also encourage you to look at the "How to Use Actions" tutorial provided by Oracle themselves. Their Swing tutorials are excellent.

So to begin, Swing provides developers the ability to attach a Mnemonic to many of the key Swing components. Mnemonics are an accessibility feature that allows the user to perform more complex actions through key combinations. In Swing, that is commonly done via Alt+SomeKey. Mnemonics can be activated by attaching an Action to the component that you want to make easily accessible.

According to the Action supported Swing Components diagram, all buttons that subtype JComponent have the ability to use Mnemonics (PLEASE NOTE that not all Swing components can use Mnemonics). They each have a method on them called setAction(Action action). The method definition for it is AbstractButton::setAction. And since JButton is a child of AbstractButton, that means that JButton also has this method too.

So, we know how to achieve what we want (using an Action on a JButton) and we know that it is possible for buttons (according to the diagram I listed above). Let's build the Action.

In order to make a button get triggered by an Action, we must first create an Action, set the Mnemonic on the Action, then attach it to the button via JButton::setAction. Here is a quick and dirty demo that shows that off.

import javax.swing.JFrame;
import javax.swing.JButton;
import javax.swing.AbstractAction;
import javax.swing.Action;
import java.awt.event.ActionEvent;
import java.awt.event.KeyEvent;

public class Example1
{

   public static void main(String[] args)
   {
    
      JFrame frame = new JFrame();
   
      JButton button = new JButton();
   
      Action action = 
         new AbstractAction()
         {
         
            public void actionPerformed(ActionEvent event)
            {
            
               System.out.println("You pressed Alt-V");
            
            }
         
         };
   
      action.putValue(Action.NAME, "This is the button text -- press Alt-V");
   
      action.putValue(Action.MNEMONIC_KEY, KeyEvent.VK_V);
   
      button.setAction(action);
   
      frame.getContentPane().add(button);
   
      frame.pack();
   
      frame.setVisible(true);
   
    
   }

}

As you can see if you run the above, it creates a tiny little JFrame holding nothing but a button. If you press Alt-V when the window is in focus, you can trigger the button presses. And, by default, the keyboard combination doesn't run into any of the potholes that InputMap and ActionMap brought you into.

The code is mostly self-explanatory, but the 4 lines that matter most are the 2 Action::putValue lines, the AbstractButton::setAction line, and the line inside of the actionPerformed(ActionEvent event) method.

The actionPerformed(ActionEvent event) method is what happens whenever the button is pressed. The Action::putValue lines configure the action. The first one specifies the text that should show up on the JButton. The second one attaches the keyboard combination to the Action itself. Then, we attach this Action to the JButton using the setAction(Action action) method. Once that is done, then the JButton can be triggered by using Alt-V.

Here is a more complicated example that shows how this might look when nested inside of multiple JPanel instances.


import javax.swing.AbstractAction;
import javax.swing.Action;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import java.awt.BorderLayout;
import java.awt.CardLayout;
import java.awt.event.ActionEvent;
import java.awt.event.KeyEvent;

public class KeyboardControls
{

   public static void main(String[] args)
   {
   
      new KeyboardControls();
   
   }
 
   public KeyboardControls()
   {
   
      final JFrame frame = makeBasicJFrame();
   
      final JPanel parentPanel = makeParentPanel();
   
      frame.add(parentPanel);
   
      frame.setVisible(true);
   
   }
 
   private JFrame makeBasicJFrame()
   {
   
      final JFrame frame = new JFrame();
   
      frame.setSize(500, 500);
      frame.setLocationByPlatform(true);
      frame.setTitle("Keyboard Controls");
      frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
   
      return frame;
   
   }
 
   private JPanel makeParentPanel()
   {
   
      final JPanel parentPanel = new JPanel();
      parentPanel.setLayout(new BorderLayout());
   
      final CardLayout cardLayout = new CardLayout();
   
      final JPanel cardLayoutPanel = makeCardLayoutPanel(cardLayout);
   
      final Action nextAction =
               new AbstractAction()
               {
               
                  public void actionPerformed(final ActionEvent event)
                  {
                  
                     cardLayout.next(cardLayoutPanel);
                  
                  }
               
               };
   
      final JButton nextButton = makeNextButton(nextAction);
   
      parentPanel.add(cardLayoutPanel, BorderLayout.CENTER);
      parentPanel.add(nextButton, BorderLayout.SOUTH);
   
      return parentPanel;
   
   }
 
   private void addPageToGivenPanel(final JPanel givenPanel, final String pageName)
   {
   
      final JPanel pagePanel = new JPanel();
   
      final JLabel pageLabel = new JLabel();
      pageLabel.setText(pageName);
   
      pagePanel.add(pageLabel);
   
      givenPanel.add(pagePanel, pageName);
   
   }
 
   private JPanel makeCardLayoutPanel(final CardLayout cardLayout)
   {
   
      final JPanel cardLayoutPanel = new JPanel();
   
      cardLayoutPanel.setLayout(cardLayout);
   
      addPageToGivenPanel(cardLayoutPanel, "PAGE_1");
      addPageToGivenPanel(cardLayoutPanel, "PAGE_2");
      addPageToGivenPanel(cardLayoutPanel, "PAGE_3");
      addPageToGivenPanel(cardLayoutPanel, "PAGE_4");
      addPageToGivenPanel(cardLayoutPanel, "PAGE_5");
   
      return cardLayoutPanel;
   
   }
 
   private JButton makeNextButton(final Action nextAction)
   {
   
      final JButton nextButton = new JButton();
      nextButton.setAction(nextAction);
   
      final String buttonText = "NEXT -- Press Alt-V on your keyboard";
   
      nextAction.putValue(Action.NAME, buttonText);
      nextAction.putValue(Action.MNEMONIC_KEY, KeyEvent.VK_V);
   
      return nextButton;
   
   }
 
}
davidalayachew
  • 1,279
  • 1
  • 11
  • 22