1

I'm writing a program where there is a keyboard and a JTextArea. Once a button is pressed, the color of the button should change as well as the caret position. I used Key Binding. Now if I first change the color then move the caret, only the caret moves but color doesn't change and vise versa. Here is a sample of my code: (I have many panels to organize the rest of the buttons...)

import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.EventQueue;
import java.awt.Font;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.GridLayout;
import java.awt.Insets;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.KeyEvent;

import javax.swing.AbstractAction;
import javax.swing.ActionMap;
import javax.swing.ButtonGroup;
import javax.swing.InputMap;
import javax.swing.JButton;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JTextArea;
import javax.swing.KeyStroke;
import javax.swing.UIManager;
import javax.swing.UnsupportedLookAndFeelException;
import javax.swing.border.EmptyBorder;

public class KeyboardTest extends JFrame {

public static final JTextArea textToType = new JTextArea("jjj jjj jjj jjj kkk kkk kkk kkk jjj kkk jjj kkk jjj kkk jkj jkj jkj jkj kjk kjk kjk \r\nkjk jjj jjj jjj kkk kkk kkk jk jk jk kj kj kj jj kk jk kj kj jk jj jk kk kj j j j j k \r\nk k k j k k j j k k j jkj jjk kjj kkj jkk kkk jjj kjk");
public static final int caretPosition = 0;

public static void main(String[] args) {
    new KeyboardTest();
}

public KeyboardTest() {
    EventQueue.invokeLater(new Runnable() {
        @Override
        public void run() {
            try {
        UIManager.setLookAndFeel("com.sun.java.swing.plaf.nimbus.NimbusLookAndFeel");
            } catch (Exception ex) {
                try {
                    UIManager.setLookAndFeel(UIManager
                            .getSystemLookAndFeelClassName());
                } catch (ClassNotFoundException | InstantiationException
                        | IllegalAccessException
                        | UnsupportedLookAndFeelException e) {
                    e.printStackTrace();
                }
            }
            JFrame frame = new JFrame("Testing");
            frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
            frame.setLayout(new BorderLayout());
            frame.add(new TestPane());
            frame.setLocationRelativeTo(null);
            frame.setVisible(true);
            frame.setBounds(100, 100, 650, 390);
        }
    });

}

public class TestPane extends JPanel {

    public TestPane() {
        JPanel contentPane = new JPanel();
        contentPane.setBorder(new EmptyBorder(5, 5, 5, 5));
        add(contentPane);
        GridBagLayout gbl_contentPane = new GridBagLayout();
        gbl_contentPane.columnWidths = new int[] { 614, 0 };
        gbl_contentPane.rowHeights = new int[] { 31, 14, 51, 181, 25, 0 };
        gbl_contentPane.columnWeights = new double[] { 0.0,
                Double.MIN_VALUE };
        gbl_contentPane.rowWeights = new double[] { 0.0, 0.0, 0.0, 0.0,
                0.0, Double.MIN_VALUE };
        contentPane.setLayout(gbl_contentPane);

        JLabel title = new JLabel(
                "Our first touch typing lesson introduces 2 home row keys for the right hand: j k");
        GridBagConstraints gbc_title = new GridBagConstraints();
        gbc_title.fill = GridBagConstraints.BOTH;
        gbc_title.insets = new Insets(0, 0, 5, 0);
        gbc_title.gridx = 0;
        gbc_title.gridy = 1;
        contentPane.add(title, gbc_title);

        textToType.setEditable(false);
        textToType.setFont(new Font("Lucida Console", Font.PLAIN, 12));
        textToType.setBackground(new Color(255, 250, 205));
        textToType
                .setText("jjj jjj jjj jjj kkk kkk kkk kkk jjj kkk jjj kkk jjj kkk jkj jkj jkj jkj kjk kjk kjk \r\nkjk jjj jjj jjj kkk kkk kkk jk jk jk kj kj kj jj kk jk kj kj jk jj jk kk kj j j j j k \r\nk k k j k k j j k k j jkj jjk kjj kkj jkk kkk jjj kjk");
        textToType.getCaret().setVisible(true);
        textToType.setCaretPosition(caretPosition);
        textToType.setFocusable(false);
        GridBagConstraints gbc_textToType = new GridBagConstraints();
        gbc_textToType.fill = GridBagConstraints.BOTH;
        gbc_textToType.insets = new Insets(0, 0, 5, 0);
        gbc_textToType.gridx = 0;
        gbc_textToType.gridy = 2;
        contentPane.add(textToType, gbc_textToType);

        JPanel keyboardPanel = new JPanel();
        GridBagConstraints gbc_keyboardPanel = new GridBagConstraints();
        gbc_keyboardPanel.fill = GridBagConstraints.BOTH;
        gbc_keyboardPanel.insets = new Insets(0, 0, 5, 0);
        gbc_keyboardPanel.gridx = 0;
        gbc_keyboardPanel.gridy = 3;
        contentPane.add(keyboardPanel, gbc_keyboardPanel);
        keyboardPanel.setLayout(new GridLayout(5, 0, 0, 0));

        JPanel kPanel1 = new JPanel();
        keyboardPanel.add(kPanel1);
        kPanel1.setLayout(null);

        JPanel buttonspanel1 = new JPanel();
        buttonspanel1.setBounds(0, 0, 523, 36);
        kPanel1.add(buttonspanel1);
        buttonspanel1.setLayout(new GridLayout(0, 13, 0, 5));

        JButton one = new JButton("1");
        one.setFocusable(false);
        buttonspanel1.add(one);

        addKeyBindingGreen(one, "1", KeyEvent.VK_1);

        addKeyBinding(one, "1", KeyEvent.VK_1);
}
//Key Binding to change the button to green...

        protected void addKeyBindingGreen(JButton btn, String name,
            int virtualKey) {
        ActionMap am = getActionMap();
        InputMap im = getInputMap(WHEN_IN_FOCUSED_WINDOW);

        im.put(KeyStroke.getKeyStroke(virtualKey, 0, false), name
                + ".pressed");
        im.put(KeyStroke.getKeyStroke(virtualKey, 0, true), name
                + ".released");

        am.put(name + ".pressed", new KeyActionGreen(btn, true));
        am.put(name + ".released", new KeyActionGreen(btn, false));
    }

 //Key binding to move the caret

        protected void addKeyBinding(JButton btn, String name,
            int virtualKey) {
        ActionMap am = getActionMap();
        InputMap im = getInputMap(WHEN_IN_FOCUSED_WINDOW);

        im.put(KeyStroke.getKeyStroke(virtualKey, 0, false), name
                + ".pressed");
        im.put(KeyStroke.getKeyStroke(virtualKey, 0, true), name
                + ".released");

        am.put(name + ".pressed", new KeyAction(btn, true, caretPosition));
        am.put(name + ".released", new KeyAction(btn, false, caretPosition ));
    }
}
public class KeyAction extends AbstractAction {

    private JButton btn;
    private boolean cur;
    private int caretPosition;

    public KeyAction(JButton btn, boolean cur, int caretPosition) {
        this.btn = btn;
        this.cur = cur;
        this.caretPosition = caretPosition;
    }

    @Override
    public void actionPerformed(ActionEvent e) {
        if (cur) {
            btn.getModel().setPressed(true);
            textToType.setCaretPosition(caretPosition++);
        } else {
            btn.getModel().setPressed(false);
        }
    }

}
public class KeyActionGreen extends AbstractAction {

    private JButton btn;
    private boolean highlight;

    public KeyActionGreen(JButton btn, boolean highlight) {
        this.btn = btn;
        this.highlight = highlight;
    }

    @Override
    public void actionPerformed(ActionEvent e) {
        if (highlight) {
            btn.getModel().setPressed(true);
            btn.setBackground(Color.GREEN);
            btn.setOpaque(true);
        } else {
            btn.getModel().setPressed(false);
            btn.setBackground(null);
            btn.setOpaque(false);
        }
    }

    }
}

2 Answers2

1

You are adding an Action to the name + ".pressed" and name + ".released" key of the ActionMap, both in your addKeyBinding and addKeyBindingGreen methods.

AcationMap is a Map. The behavior of a Map is that each key can contain only one value. If you attempt to put two values to the same key, the first value you put will get pushed out by the second value, in your case, the value being whichever Action you add second.

Only choice is to combine the code you want into one Action

Paul Samsotha
  • 205,037
  • 37
  • 486
  • 720
  • it would be possible to define a "chained" Action, which took a list of Actions and wen executed, executed in Action in it's list, there by allowing a single Action to be registered, but perform, multiple actions...otherwise you'd need to setup a inheritance chain – MadProgrammer Aug 02 '14 at 02:37
1

Updated

There are three core issues,

Issue #1

You passing the value of caretPosition (from KeyboardTest) to the KeyAction, the problem with this, is the KeyAction will only ever update the class instance of caretPosition relative to itself, that is, if you have two instance of KeyAction, they will have two separate values and will be updated independently.

This is because parameters in Java are passed by value, not by reference...

You could pass an object which wrapped the int value, making sure you pass the same instance to all the buttons. This way the actions would be modifying the same value or you could simply make the action's reference the class instance field instead...

public int caretPosition = 0;

//...

public class KeyAction extends AbstractAction {

    private JButton btn;
    private boolean cur;

    public KeyAction(JButton btn, boolean cur) {
        this.btn = btn;
        this.cur = cur;
    }

    @Override
    public void actionPerformed(ActionEvent e) {
        if (cur) {
            btn.getModel().setPressed(true);
            textToType.setCaretPosition(caretPosition++);
        } else {
            btn.getModel().setPressed(false);
        }
    }

}

Issue #2

You are doing a post increment of the caretPosition instead of a pre increment...

textToType.setCaretPosition(caretPosition++);

What this means is, the value of caretPosition is only been changed AFTER the setCaretPosition returns, meaning that the first time the action is triggered, it's value is 0, still...

You should change this to something more like...

textToType.setCaretPosition(++caretPosition);

Issue #3

As, has already been pointed out, you can only attach a single Action to a KeyStroke, via the ActionMap/InputMap bindings.

This leaves you with at least three basic options...

One...

You could resign yourself to the fact that you can only specify a single Action and have to decide which one is the best choice...

In this case you "could" attach another Action or ActionListener to the individual buttons which could, for example, carry out the other Action...but you would need to ensure that the key binding Action triggered the button, using something like JButton#doClick, for example...

Two...

Devise a "chainable" Action, which was capable of taking a list of Actions and when triggered, would trigger these actions...

public class ChainableAction extends AbstractAction {

    private List<Action> actions;

    public ChainableAction(Action... actions) {
        this.actions = new ArrayList<>(Arrays.asList(actions));
    }

    @Override
    public void actionPerformed(ActionEvent e) {
        for (Action action : actions) {
            action.actionPerformed(e);
        }
    }

}

The above example does have one issue, it has not "configuration" properties, making it worthless to be used with things like JMenuItem. You can provide configuration information (such as icons and text) via the constructor though.

Three...

Set up an inheritance chain, where the color Action and text Action were capable of extending from each other, in form or other, then you would simply use the final child Action for the bindings...

public class KeyAction extends ColorActionGreen {

    private JButton btn;
    private boolean cur;
    private int caretPosition;

    public KeyAction(JButton btn, boolean cur, int caretPosition, boolean highlight) {
        super(btn, highlight);
        this.btn = btn;
        this.cur = cur;
        this.caretPosition = caretPosition;
    }

    @Override
    public void actionPerformed(ActionEvent e) {
        super.actionPerformed(e);
        if (cur) {
            btn.getModel().setPressed(true);
            textToType.setCaretPosition(caretPosition++);
        } else {
            btn.getModel().setPressed(false);
        }
    }

}

public class ColorActionGreen extends AbstractAction {

    private JButton btn;
    private boolean highlight;

    public ColorActionGreen(JButton btn, boolean highlight) {
        this.btn = btn;
        this.highlight = highlight;
    }

    @Override
    public void actionPerformed(ActionEvent e) {
        if (highlight) {
            btn.getModel().setPressed(true);
            btn.setBackground(Color.GREEN);
            btn.setOpaque(true);
        } else {
            btn.getModel().setPressed(false);
            btn.setBackground(null);
            btn.setOpaque(false);
        }
    }

}

Then you would be able to use the KeyAction for all the bindings and even the JButtons themselves...

MadProgrammer
  • 343,457
  • 22
  • 230
  • 366