0

As per title, is it possible to change a JCheckBox selection state using a mnemonic key without transferring the focus to the component?

Current behavior as per GIF.
The wanted behavior would be the focus remaining on the "Type" text field.

enter image description here

LppEdd
  • 20,274
  • 11
  • 84
  • 139
  • 1
    With the standard JCheckBox you can only achieve this with `setFocusable(false);` - but then you can no longer use the tab key to select the checkbox and then press the space key to toggle it. – Thomas Kläger Feb 11 '21 at 22:13
  • @ThomasKläger if only I could intercept the mnemonic event. But I couldn't find a way – LppEdd Feb 11 '21 at 22:35
  • @ThomasKläger btw, workarounds are ok. – LppEdd Feb 11 '21 at 22:39
  • 2
    Instead of using a mnemonic you can use [Key Bindings](https://docs.oracle.com/javase/tutorial/uiswing/misc/keybinding.html). – camickr Feb 12 '21 at 00:58
  • @camickr is it still possible to highlight the "fake mnemonic" letter on ALT with your strategy? – LppEdd Feb 12 '21 at 01:10
  • Does `setDisplayedMnemonicIndex(..)` work? If not, then maybe try to use HTML for the text. Then you can use the underline tag for the "B". – camickr Feb 12 '21 at 01:33
  • @camickr I've posted another answer, could you give your opinion? – LppEdd Feb 12 '21 at 13:27
  • Whatever works for you. Thomas's solution seems simpler since both involve extending JCheckBox. My solution was a suggestion was a solution what would not involve extending a component so it could be used on any AbstractButton (JButton, JCheckBox, JRadioButton). In any case, it would be nice if your suggestion was posted as an [mre] so other could use the solution. We don't know what the UIUtilities method does. – camickr Feb 12 '21 at 15:35
  • @camickr the fact is the other solution, while it works, introduces mutable state, while overriding key bindings seems more focused on that single thing. The static method returns ALT or whatever key is assigned to the mnemonic system. – LppEdd Feb 12 '21 at 15:40

2 Answers2

1

The problem are these lines from the BasicButtonListener.Actions:

        if (key == PRESS) {
            ButtonModel model = b.getModel();
            model.setArmed(true);
            model.setPressed(true);
            if(!b.hasFocus()) {
                b.requestFocus();  // here it requests the focus
            }
        }

This is buried deep in the handling of the key binding for checkbox and this code was never meant to be customized.

A workaround would be to create your own class that extends JCheckBox and specifically doesn't request the focus while a key binding is processed:

public class MyCheckBox extends JCheckBox {

    private boolean requestFocusAllowed = true;

    public MyCheckBox() {
    }

    @Override
    public void requestFocus() {
        if (requestFocusAllowed) {
            super.requestFocus();
        }
    }

    @Override
    protected boolean processKeyBinding(KeyStroke ks, KeyEvent e, int condition, boolean pressed) {
        requestFocusAllowed = false;
        try {
            return super.processKeyBinding(ks, e, condition, pressed);
        } finally {
            requestFocusAllowed = true;
        }
    }
}
Thomas Kläger
  • 17,754
  • 3
  • 23
  • 34
  • Thanks! This seems an idea similar to what camickr proposed in the comments. I suppose then the mnemonic is registered as keybinding, then what if I simply override it by registering a new one with the same key combination? – LppEdd Feb 12 '21 at 09:55
  • I've implemented what I mentioned above, feel free to give your opinion. – LppEdd Feb 12 '21 at 13:27
0

Given that the mnemonic is registered as a key binding, in the scope of

JComponent.WHEN_IN_FOCUSED_WINDOW

See BasicButtonListener#updateMnemonicBinding


We can simply override the key binding actions with our own ones.

private class MnemonicCheckBox : JCheckBox(...) {
  override fun setMnemonic(mnemonic: Int) {
    super.setMnemonic(mnemonic)

    if (mnemonic != 0) {
      overrideMnemonicActions()
    }
  }

  private fun overrideMnemonicActions() {
    val mnemonicMask = UIUtilities.getSystemMnemonicKeyMask()
    val altGraphDownMask = InputEvent.ALT_GRAPH_DOWN_MASK

    getInputMap(WHEN_IN_FOCUSED_WINDOW).apply {
      // Pressed
      put(KeyStroke.getKeyStroke(mnemonic, mnemonicMask), "mnemonicPressed")
      put(KeyStroke.getKeyStroke(mnemonic, mnemonicMask or altGraphDownMask), "mnemonicPressed")

      // Released
      put(KeyStroke.getKeyStroke(mnemonic, mnemonicMask, true), "mnemonicReleased")
      put(KeyStroke.getKeyStroke(mnemonic, mnemonicMask or altGraphDownMask, true), "mnemonicReleased")
    }

    actionMap.apply {
      put("mnemonicPressed", object : AbstractAction() {
        override fun actionPerformed(event: ActionEvent) {
          model.isArmed = true
          model.isPressed = true
        }
      })
      put("mnemonicReleased", object : AbstractAction() {
        override fun actionPerformed(event: ActionEvent) {
          model.isPressed = false
          model.isArmed = false
        }
      })
    }
  }
}

To maintain the semantic expressed in BasicButtonListener.Actions, we have overridden both the pressed and released actions to change the ButtonModel state.

Example:

enter image description here

LppEdd
  • 20,274
  • 11
  • 84
  • 139