1

Focus traversal only seems to work for enabled Swing components (tabbing with TAB or CTRL+TAB). How would one treat both enabled and disabled components as significant and enable keyboard traversal through them?

Why would I want this? I have a form where each textfield, textarea or checkbox may have a set and unset state in addition to its value. Currently users are required to set and unset the components using mouse clicks (left click on unset component to set, CTRL+left click on a set component to unset it), but I'd like to provide keyboard access for doing the same thing too. The reason I chose this mechanism is that empty values may have meaning - an empty text component string is significant and cannot be used for denoting "unsetness". I also didn't want to precede each settable value with a checkbox instead, since that would just look awkward.

Here's an example form to clarify the set/unset state (the checkbox and textarea start off as unset, while the field as set):

import java.awt.BorderLayout;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import javax.swing.JButton;
import javax.swing.JCheckBox;
import javax.swing.JComponent;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTextArea;
import javax.swing.JTextField;
import javax.swing.SwingUtilities;
import javax.swing.text.JTextComponent;

public class TraverseDisabled extends JFrame {

    public static final String VALUE_NOT_SET_MESSAGE = "Click to set this value.";
    public static final String VALUE_SET_DEFAULT_MESSAGE = "Edit value or use CTRL + click to unset this value.";
    public static final String VALUE_NOT_SET_DEFAULT_VALUE = "<not-set>";

    private JPanel panel;
    private JLabel label1;
    private JTextField textfield1;
    private JCheckBox checkbox1;
    private JLabel label2;
    private JTextArea textarea1;
    private JButton button;

    public TraverseDisabled() {
        setTitle("Form");
        initComponents();

        pack();
        setLocationRelativeTo(null);
        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    }

    private void initComponents() {
        panel = new JPanel();
        panel.setLayout(new GridBagLayout());
        setLayout(new BorderLayout());
        add(panel);

        GridBagConstraints gbc;

        label1 = new JLabel("Field1:");
        gbc = new GridBagConstraints();
        gbc.gridx = 0;
        gbc.gridy = 0;
        gbc.anchor = GridBagConstraints.WEST;
        panel.add(label1, gbc);

        textfield1 = new JTextField(20);
        gbc = new GridBagConstraints();
        gbc.gridx = 1;
        gbc.gridy = 0;
        panel.add(textfield1, gbc);
        textfield1.addMouseListener(new MouseAdapter() {

            @Override
            public void mouseClicked(MouseEvent e) {
                onJComponentClicked(e, textfield1, null);
            }

        });

        checkbox1 = new JCheckBox("Checkbox1");
        gbc = new GridBagConstraints();
        gbc.gridx = 0;
        gbc.gridy = 1;
        gbc.anchor = GridBagConstraints.WEST;
        panel.add(checkbox1, gbc);
        checkbox1.addMouseListener(new MouseAdapter() {

            @Override
            public void mouseClicked(MouseEvent e) {
                onJComponentClicked(e, checkbox1, false);
            }

        });
        enableEditingForJCheckBoxComponent(checkbox1, false, false);

        label2 = new JLabel("Area1:");
        gbc = new GridBagConstraints();
        gbc.gridx = 0;
        gbc.gridy = 2;
        gbc.anchor = GridBagConstraints.NORTHWEST;
        panel.add(label2, gbc);

        textarea1 = new JTextArea(5, 20);
        JScrollPane scrollPane1 = new JScrollPane(textarea1);
        gbc = new GridBagConstraints();
        gbc.gridx = 1;
        gbc.gridy = 2;
        panel.add(scrollPane1, gbc);
        textarea1.addMouseListener(new MouseAdapter() {

            @Override
            public void mouseClicked(MouseEvent e) {
                onJComponentClicked(e, textarea1, null);
            }

        });
        enableEditingForJTextComponent(textarea1, false, VALUE_NOT_SET_DEFAULT_VALUE);

        button = new JButton("Apply");
        gbc = new GridBagConstraints();
        gbc.gridx = 1;
        gbc.gridy = 3;
        gbc.anchor = GridBagConstraints.EAST;
        panel.add(button, gbc);
    }

    private Object onJComponentClicked(java.awt.event.MouseEvent evt, JComponent component, Object previousValue) {
        if (!component.isEnabled()) {
            if (component instanceof JCheckBox) {
                enableEditingForJCheckBoxComponent((JCheckBox)component, true, (Boolean)previousValue);
            } else if (component instanceof JTextComponent) {
                enableEditingForJTextComponent((JTextComponent)component, true, (String)previousValue);
            }
            evt.consume();
        } else if (evt.isControlDown()) {
            if (component instanceof JCheckBox) {
                JCheckBox cb = (JCheckBox)component;
                previousValue = !cb.isSelected();
                enableEditingForJCheckBoxComponent((JCheckBox) component, false, (Boolean)previousValue);
            } else if (component instanceof JTextComponent) {
                previousValue = ((JTextComponent)component).getText();
                enableEditingForJTextComponent((JTextComponent)component, false, VALUE_NOT_SET_DEFAULT_VALUE);
            }
            evt.consume();
        } 
        return previousValue;
    }

    private void enableEditingForJTextComponent(JTextComponent textComponent, boolean enable, String text) {
        if (!enable) {
            textComponent.setEnabled(false);
            textComponent.setText(text);
            textComponent.setToolTipText(VALUE_NOT_SET_MESSAGE);
        } else {
            textComponent.setEnabled(true);
            textComponent.setText(text);
            textComponent.setToolTipText(VALUE_SET_DEFAULT_MESSAGE);
            textComponent.requestFocusInWindow();
        }
    }

    private void enableEditingForJCheckBoxComponent(JCheckBox checkBox, boolean enable, boolean value) {
        if (!enable) {
            checkBox.setEnabled(false);
            checkBox.setSelected(value);
            checkBox.setToolTipText(VALUE_NOT_SET_MESSAGE);
        } else {
            checkBox.setEnabled(true);
            checkBox.setSelected(value);
            checkBox.setToolTipText(VALUE_SET_DEFAULT_MESSAGE);
        }
    }

    public static void main(String[] args) {
        SwingUtilities.invokeLater(new Runnable() {

            public void run() {
                new TraverseDisabled().setVisible(true);
            }
        });
    }

}
predi
  • 5,528
  • 32
  • 60
  • One possible solution: don't disable the component, at least not via `setEnabled(false)`. Instead prevent user input in other ways, perhaps by using a boolean within key model methods. For instance one way would be to use a DocumentFilter for text components such as JTextFields, and to disable the filter with a boolean variable when you want the component to be "disabled", but still allowing it to gain focus. – Hovercraft Full Of Eels Oct 07 '15 at 14:37

2 Answers2

4

I believe the default FocusTraverslPolicy used by Swing will ultimately end up invoking a method in the Component class that has a final method:

final boolean canBeFocusOwner() {
    // It is enabled, visible, focusable.
    if (isEnabled() && isDisplayable() && isVisible() && isFocusable()) {
        return true;
    }
    return false;
}

So it definitely checks to make sure the component is enabled.

So the only option I can suggest is to try and create your own FocusTraversalPolicy.

The section from the Swing tutorial on Customizing Focus Traversal has an example you might be able to use/modify to meet your requirements.

camickr
  • 321,443
  • 19
  • 166
  • 288
  • This method made me realize just how futile my efforts really are. Receiving keyboard events on disabled components would probably require dark magic or at least a crossroads deal. – predi Oct 08 '15 at 08:20
1

I ended up considering what Hovercraft Full Of Eels suggested and used an alternative. Since the form does (or can) consist of label-component pairs, I decided to abuse the labels by making them focusable if the paired component is disabled. The labels then handle keyboard events.

import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.event.ActionEvent;
import java.awt.event.FocusAdapter;
import java.awt.event.FocusEvent;
import java.awt.event.FocusListener;
import java.awt.event.KeyEvent;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.util.HashMap;
import java.util.Map;
import javax.swing.AbstractAction;
import javax.swing.Action;
import javax.swing.ActionMap;
import javax.swing.BorderFactory;
import javax.swing.InputMap;
import javax.swing.JButton;
import javax.swing.JCheckBox;
import javax.swing.JComponent;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTextArea;
import javax.swing.JTextField;
import javax.swing.KeyStroke;
import javax.swing.SwingUtilities;
import javax.swing.text.JTextComponent;

public class TraverseDisabled extends JFrame {

    public static final String VALUE_NOT_SET_MESSAGE = "Click to set this value.";
    public static final String VALUE_SET_DEFAULT_MESSAGE = "Edit value or use CTRL + click to unset this value.";
    public static final String VALUE_NOT_SET_DEFAULT_VALUE = "<not-set>";

    private JPanel panel;
    private JTextField textfield1;
    private JCheckBox checkbox1;
    private JTextArea textarea1;
    private JLabel label1;
    private JLabel label2;
    private JLabel label3;
    private JButton button;

    // associate labels with components
    final private Map<JComponent, JLabel> bindings = new HashMap<JComponent, JLabel>();

    public TraverseDisabled() {
        setTitle("Form");
        initComponents();

        pack();
        setLocationRelativeTo(null);
        setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    }

    private void initComponents() {
        panel = new JPanel();
        panel.setLayout(new GridBagLayout());
        setLayout(new BorderLayout());
        add(panel);

        GridBagConstraints gbc;

        label1 = new JLabel("Field1:");
        gbc = new GridBagConstraints();
        gbc.gridx = 0;
        gbc.gridy = 0;
        gbc.anchor = GridBagConstraints.WEST;
        panel.add(label1, gbc);

        textfield1 = new JTextField(20);
        gbc = new GridBagConstraints();
        gbc.gridx = 1;
        gbc.gridy = 0;
        panel.add(textfield1, gbc);
        textfield1.addMouseListener(new MouseAdapter() {

            @Override
            public void mouseClicked(MouseEvent e) {
                onJComponentClicked(e, textfield1, null);
            }

        });
        bindings.put(textfield1, label1);
        enableEditingForJTextComponent(textfield1, true, null);

        label2 = new JLabel("Checkbox1:");
        gbc = new GridBagConstraints();
        gbc.gridx = 0;
        gbc.gridy = 1;
        gbc.anchor = GridBagConstraints.WEST;
        panel.add(label2, gbc);

        checkbox1 = new JCheckBox();
        gbc = new GridBagConstraints();
        gbc.gridx = 1;
        gbc.gridy = 1;
        gbc.anchor = GridBagConstraints.WEST;
        panel.add(checkbox1, gbc);
        checkbox1.addMouseListener(new MouseAdapter() {

            @Override
            public void mouseClicked(MouseEvent e) {
                onJComponentClicked(e, checkbox1, false);
            }

        });
        bindings.put(checkbox1, label2);
        enableEditingForJCheckBoxComponent(checkbox1, false, false);

        label3 = new JLabel("Area1:");

        gbc = new GridBagConstraints();
        gbc.gridx = 0;
        gbc.gridy = 2;
        gbc.anchor = GridBagConstraints.NORTHWEST;
        panel.add(label3, gbc);

        textarea1 = new JTextArea(5, 20);
        JScrollPane scrollPane1 = new JScrollPane(textarea1);
        gbc = new GridBagConstraints();
        gbc.gridx = 1;
        gbc.gridy = 2;
        panel.add(scrollPane1, gbc);
        textarea1.addMouseListener(new MouseAdapter() {

            @Override
            public void mouseClicked(MouseEvent e) {
                onJComponentClicked(e, textarea1, null);
            }

        });
        bindings.put(textarea1, label3);
        enableEditingForJTextComponent(textarea1, false, VALUE_NOT_SET_DEFAULT_VALUE);

        button = new JButton("Apply");
        gbc = new GridBagConstraints();
        gbc.gridx = 1;
        gbc.gridy = 3;
        gbc.anchor = GridBagConstraints.EAST;
        panel.add(button, gbc);

        // react to label focus
        FocusListener labelFocusListener = new FocusAdapter() {

            @Override
            public void focusGained(FocusEvent e) {
                JLabel label = (JLabel) e.getSource();
                label.setBorder(BorderFactory.createMatteBorder(1, 1, 1, 1, Color.black));
            }

            @Override
            public void focusLost(FocusEvent e) {
                JLabel label = (JLabel) e.getSource();
                label.setBorder(BorderFactory.createMatteBorder(1, 1, 1, 1, label.getBackground()));
            }

        };

        // action for unsetting values
        Action unsetAction = new AbstractAction() {

            public void actionPerformed(ActionEvent e) {
                JComponent comp = (JComponent) e.getSource();
                MouseEvent evt = new MouseEvent(
                        comp, MouseEvent.MOUSE_CLICKED,
                        1, MouseEvent.CTRL_DOWN_MASK, 0, 0, 1, false);
                onJComponentClicked(evt, comp, comp instanceof JCheckBox ? false : null);
            }

        };

        // action for setting values
        Action setAction = new AbstractAction() {

            public void actionPerformed(ActionEvent e) {
                for (Map.Entry<JComponent, JLabel> entry : bindings.entrySet()) {
                    if (e.getSource() == entry.getValue()) {
                        MouseEvent evt = new MouseEvent(
                                entry.getKey(), MouseEvent.MOUSE_CLICKED, 
                                1, 0, 0, 0, 1, false);
                        onJComponentClicked(evt, entry.getKey(), entry.getKey() instanceof JCheckBox ? false : null);
                        break;
                    }
                }
            }

        };

        // initialize them labels
        for (JLabel label : bindings.values()) {
            label.setBorder(BorderFactory.createMatteBorder(1, 1, 1, 1, label1.getBackground()));
            label.addFocusListener(labelFocusListener);

            InputMap inputMap = label.getInputMap();
            ActionMap actionMap = label.getActionMap();
            KeyStroke key;
            key = KeyStroke.getKeyStroke(KeyEvent.VK_ENTER, 0);
            inputMap.put(key, "set-value");
            actionMap.put("set-value", setAction);
        }

        // make it possible to unset from focused component
        for (JComponent comp : bindings.keySet()) {            
            InputMap inputMap = comp.getInputMap();
            ActionMap actionMap = comp.getActionMap();
            KeyStroke key;
            key = KeyStroke.getKeyStroke(KeyEvent.VK_U, KeyEvent.CTRL_DOWN_MASK);
            inputMap.put(key, "unset-value");
            actionMap.put("unset-value", unsetAction);
        }
    }

    private Object onJComponentClicked(java.awt.event.MouseEvent evt, JComponent component, Object previousValue) {
        if (!component.isEnabled()) {
            if (component instanceof JCheckBox) {
                enableEditingForJCheckBoxComponent((JCheckBox)component, true, (Boolean)previousValue);
            } else if (component instanceof JTextComponent) {
                enableEditingForJTextComponent((JTextComponent)component, true, (String)previousValue);
            }
            evt.consume();
        } else if (evt.isControlDown()) {
            if (component instanceof JCheckBox) {
                JCheckBox cb = (JCheckBox)component;
                previousValue = !cb.isSelected();
                enableEditingForJCheckBoxComponent((JCheckBox) component, false, (Boolean)previousValue);
            } else if (component instanceof JTextComponent) {
                previousValue = ((JTextComponent)component).getText();
                enableEditingForJTextComponent((JTextComponent)component, false, VALUE_NOT_SET_DEFAULT_VALUE);
            }
            evt.consume();
        } 
        return previousValue;
    }

    private void enableEditingForJTextComponent(JTextComponent textComponent, boolean enable, String text) {
        if (!enable) {
            textComponent.setEnabled(false);
            textComponent.setText(text);
            textComponent.setToolTipText(VALUE_NOT_SET_MESSAGE);
        } else {
            textComponent.setEnabled(true);
            textComponent.setText(text);
            textComponent.setToolTipText(VALUE_SET_DEFAULT_MESSAGE);
            textComponent.requestFocusInWindow();
        }
        bindings.get(textComponent).setFocusable(!enable); // change focusable
    }

    private void enableEditingForJCheckBoxComponent(JCheckBox checkBox, boolean enable, boolean value) {
        if (!enable) {
            checkBox.setEnabled(false);
            checkBox.setSelected(value);
            checkBox.setToolTipText(VALUE_NOT_SET_MESSAGE);
        } else {
            checkBox.setEnabled(true);
            checkBox.setSelected(value);
            checkBox.setToolTipText(VALUE_SET_DEFAULT_MESSAGE);
        }
        bindings.get(checkBox).setFocusable(!enable); // change focusable
    }

    public static void main(String[] args) {
        SwingUtilities.invokeLater(new Runnable() {

            public void run() {
                new TraverseDisabled().setVisible(true);
            }
        });
    }

}

I don't think it is possible to achieve what I originally wanted, but this is close and simple enough.

predi
  • 5,528
  • 32
  • 60
  • To bind the label/text field pairs you might be able use the setLabelFor(...) and getLabelFor() methods. If may simplify the code a little since the event will be generated on the label and you just invoke the getLabelFor() method to get the bound component. Will save you creating your own Map. – camickr Oct 08 '15 at 14:46