Home > database >  Detect if JSpinner change comes from typing value or clicking on arrow
Detect if JSpinner change comes from typing value or clicking on arrow

Time:01-09

When I receive a ChangeEvent from my JSpinner, I'd like to detect if user used the arrows to increase/decrease the number value, or directly typed a new number.

What would be the best approach to do this ?

EDIT: for now my non-reliable solution is just to save the last JSpinner changed value, and if new change value is /- equals to the step value, then I assume user clicked on an arrow. It works except if user typed a value which is equals to (oldValue /- step).

EDIT: why ? I want to reproduce the behavior found in Midi editors of several famous DAWs. The JSpinner represents the velocity (0-127) of selected notes. It shows the velocity of the first selected note. Usually notes velocity differ. When you increase with arrow, you want to increase all selected notes by the same amount. When you type in a new value, you want all velocities to be reset to this value.

CodePudding user response:

Distinguishing the trigger of a value change is not supported - the only value-related event fired by JSpinner is a ChangeEvent which carries no state except the source. We need another type of listener or a combination of listeners.

First thought: listen to changes of the editor's textField, f.i. an actionListener or a propertyChangeListener on the value property. This doesn't work, mainly because

  • both change- and propertyChangeListener are fired always (change before property)
  • actionListener is not fired on focusLost

Second thought: go dirty and dig into implementation details to hook into the action fired by the arrow buttons.

The idea:

  • look up the action for increment/decrement from the spinner's actionMap: this is the same as the arrows' actions and also used by up/down keys (which I assume not counting a "editing")
  • for each, create a wrapper that sets a flag before delegating to super
  • put that wrapper into the spinner's actionMap
  • look up the arrow buttons in the spinner's children and replace their respective actionListener with the wrapper

Client code would change the tweak's changeListener to acts according to the flag as appropriate.

Some code doing the tweaking (beware: not formally tested!):

public static class SpinnerTweaker {

    private JSpinner spinner;
    private boolean wasButtonAction;
    private Object oldValue;

    public SpinnerTweaker(JSpinner spinner) {
        this.spinner = spinner;

        AbstractAction incrementDelegate = createDelegate("increment");
        spinner.getActionMap().put("increment", incrementDelegate);

        AbstractAction decrementDelegate = createDelegate("decrement");
        spinner.getActionMap().put("decrement", decrementDelegate);

        // replacing arrow button's action
        Component[] components = spinner.getComponents();
        for (Component component : components) {
            if (component instanceof JButton) {
                if (component.getName() == "Spinner.nextButton") {
                    ActionListener[] actions = ((JButton) component).getActionListeners();
                    ActionListener uiAction = actions[0];
                    ((JButton) component).removeActionListener(uiAction);
                    ((JButton) component).addActionListener(incrementDelegate);
                }
                if (component.getName() == "Spinner.previousButton") {
                    ActionListener[] actions = ((JButton) component).getActionListeners();
                    ActionListener uiAction = actions[0];
                    ((JButton) component).removeActionListener(uiAction);
                    ((JButton) component).addActionListener(decrementDelegate);
                }
            }
        }

        spinner.addChangeListener(e -> {
            if (wasButtonAction) {
                System.out.println("value changed by button: "   spinner.getValue());
            } else {
                System.out.println("value changed by editing: "   spinner.getValue());
            }
            wasButtonAction = false;
       });

    }

    protected AbstractAction createDelegate(String actionCommand) {
        // hooking into original button action to set button flag
        AbstractAction action = (AbstractAction) spinner.getActionMap().get(actionCommand);
        AbstractAction delegate = new AbstractAction() {

            @Override
            public void actionPerformed(ActionEvent e) {
                oldValue = spinner.getValue();
                wasButtonAction = true;
                action.actionPerformed(e);
                // hit min/max - TBD: needs testing!
                if (oldValue.equals(spinner.getValue())) {
                    wasButtonAction = false;
                }
            }
        };

        return delegate;
    }
}
  • Related