javaswingjbuttonkey-bindings

Java swing - Processing simultaneous key presses with key bindings


Info

Having read through several related questions, I think I have a bit of a unique situation here.

I am building a Java swing application to help drummers make simple shorthand song charts. There's a dialog where the user can "key in" a rhythm, which is to be recorded to a MIDI sequence and then processed into either tabulature or sheet music. This is intended to be used with short sections of a song.

Setup

The idea is when the bound JButtons fire their action while the sequence is being recorded, they'll generate a MidiMessage with timing information. I also want the buttons to visually indicate that they've been activated.

The bound keys are currently firing correctly using the key bindings I've implemented (except for simultaneous keypresses)...

Problem

It's important that simultaneous keypresses are registered as a single event--and the timing matters here.

So, for example, if the user pressed H (hi-hat) and S (snare) at the same time, it would register as a unison hit at the same place in the bar.

I have tried using a KeyListener implementation similar to this: https://stackoverflow.com/a/13529058/13113770 , but with that setup I ran into issues with focus, and though it could detect simultaneous key presses, it would also process them individually.

Could anyone shed some light on this for me?

  // code omitted

  public PunchesDialog(Frame owner, Song partOwner, Part relevantPart)
  {
    super(owner, ModalityType.APPLICATION_MODAL);

    this.partOwner = partOwner;
    this.relevantPart = relevantPart;

    // code omitted

    /*
     * Voices Panel
     */

    voices = new LinkedHashMap<>() {{
      put("crash",    new VoiceButton("CRASH (C)",         crashHitAction));
      put("ride",     new VoiceButton("RIDE (R)",          rideHitAction));
      put("hihat",    new VoiceButton("HI-HAT (H)",        hihatHitAction));
      put("racktom",  new VoiceButton("RACK TOM (T)",      racktomHitAction));
      put("snare",    new VoiceButton("SNARE (S)",         snareHitAction));
      put("floortom", new VoiceButton("FLOOR TOM (F)",     floortomHitAction));
      put("kickdrum", new VoiceButton("KICK DRUM (SPACE)", kickdrumHitAction));
    }};

    Action crashHitAction = new CrashHitAction();
    Action rideHitAction = new RideHitAction();
    Action hihatHitAction = new HihatHitAction();
    Action racktomHitAction = new RacktomHitAction();
    Action snareHitAction = new SnareHitAction();
    Action floortomHitAction = new FloortomHitAction();
    Action kickdrumHitAction = new KickdrumHitAction();

    KeyStroke key;
    InputMap inputMap = ((JPanel) getContentPane()).
      getInputMap(JComponent.WHEN_IN_FOCUSED_WINDOW);
    ActionMap actionMap = ((JPanel) getContentPane()).getActionMap();

    key = KeyStroke.getKeyStroke(KeyEvent.VK_C, 0);
    inputMap.put(key, "crashHit");
    actionMap.put("crashHit", crashHitAction);

    key = KeyStroke.getKeyStroke(KeyEvent.VK_R, 0);
    inputMap.put(key, "rideHit");
    actionMap.put("rideHit", rideHitAction);

    key = KeyStroke.getKeyStroke(KeyEvent.VK_H, 0);
    inputMap.put(key, "hihatHit");
    actionMap.put("hihatHit", hihatHitAction);

    key = KeyStroke.getKeyStroke(KeyEvent.VK_T, 0);
    inputMap.put(key, "racktomHit");
    actionMap.put("racktomHit", racktomHitAction);

    key = KeyStroke.getKeyStroke(KeyEvent.VK_S, 0);
    inputMap.put(key, "snareHit");
    actionMap.put("snareHit", snareHitAction);

    key = KeyStroke.getKeyStroke(KeyEvent.VK_F, 0);
    inputMap.put(key, "floortomHit");
    actionMap.put("floortomHit", floortomHitAction);

    key = KeyStroke.getKeyStroke(KeyEvent.VK_SPACE, 0);
    inputMap.put(key, "kickdrumHit");
    actionMap.put("kickdrumHit", kickdrumHitAction);

    final JPanel pnlVoices = new JPanel(new MigLayout(
          "Insets 0, gap 0, wrap 2", "[fill][fill]", "fill"));
    pnlVoices.add(voices.get("crash"),    "w 100%, h 100%, grow");
    pnlVoices.add(voices.get("ride"),     "w 100%");
    pnlVoices.add(voices.get("hihat"),    "w 100%");
    pnlVoices.add(voices.get("racktom"),  "w 100%");
    pnlVoices.add(voices.get("snare"),    "w 100%");
    pnlVoices.add(voices.get("floortom"), "w 100%");
    pnlVoices.add(voices.get("kickdrum"), "span");

    // code omitted

  }

  private class CrashHitAction extends AbstractAction
  {
    @Override
    public void actionPerformed(ActionEvent e) {
      // voices.get("crash").doClick(100);
      kfMgr.clearFocusOwner();

      logger.debug("hit crash");
    }
  }

  private class RideHitAction extends AbstractAction
  {
    @Override
    public void actionPerformed(ActionEvent e) {
      // voices.get("ride").doClick(100);
      kfMgr.clearFocusOwner();

      logger.debug("hit ride");
    }
  }

  private class HihatHitAction extends AbstractAction
  {
    @Override
    public void actionPerformed(ActionEvent e) {
      // voices.get("hihat").doClick(100);
      kfMgr.clearFocusOwner();

      logger.debug("hit hihat");
    }
  }

  private class RacktomHitAction extends AbstractAction
  {
    @Override
    public void actionPerformed(ActionEvent e) {
      // voices.get("racktom").doClick(100);
      kfMgr.clearFocusOwner();

      logger.debug("hit racktom");
    }
  }

  private class FloortomHitAction extends AbstractAction
  {
    @Override
    public void actionPerformed(ActionEvent e) {
      // voices.get("floortom").doClick(100);
      kfMgr.clearFocusOwner();

      logger.debug("hit floortom");
    }
  }

  private class SnareHitAction extends AbstractAction
  {
    @Override
    public void actionPerformed(ActionEvent e) {
      // voices.get("snare").doClick(100);
      kfMgr.clearFocusOwner();

      logger.debug("hit snare");
    }
  }

  private class KickdrumHitAction extends AbstractAction
  {
    @Override
    public void actionPerformed(ActionEvent e) {
      // voices.get("kickdrum").doClick(100);
      kfMgr.clearFocusOwner();

      logger.debug("hit kickdrum");
    }
  }

Screenshot of dialog here: https://i.sstatic.net/n4RzY.png


Solution

  • I would personally use the KeyListener interface to keep track of what the user types, and register within a Set<Character> the keys that have been pressed but not released. Finally, collect the content of the set once any key has been released.

    The reason why I would use a Set instead of a List is because the keyPressed() event is fired multiple times when the user is holding down a key.

    Here, I've also attached a brief example to give you the idea.

    public class MyClass extends JFrame implements KeyListener {
    
        private JTextArea textArea;
        private List<Character> listKeys;
    
        public MyClass() {
            setTitle("test");
    
            listKeys = new ArrayList<>();
            textArea = new JTextArea();
            textArea.addKeyListener(this);
    
            setLayout(new BorderLayout());
            add(textArea, BorderLayout.CENTER);
    
            setLocation(50, 50);
            setSize(500, 500);
            setVisible(true);
        }
    
        @Override
        public void keyTyped(KeyEvent e) {
        }
    
        @Override
        public void keyPressed(KeyEvent e) {
            if (!listKeys.contains(e.getKeyChar())) {
                listKeys.add(e.getKeyChar());
            }
        }
    
        @Override
        public void keyReleased(KeyEvent e) {
            if (listKeys.isEmpty()) {
                return;
            }
    
            if (listKeys.size() > 1) {
                System.out.print("The key combination ");
            } else {
                System.out.print("The key ");
            }
            for (Character c : listKeys) {
                System.out.print(c + " ");
            }
            System.out.println("has been entered");
            listKeys.clear();
        }
    
        public static void main(String[] args) {
            new MyClass();
        }
    }