Make JSpinner Select Text When Focused
Asked Answered
G

2

9

I would like to change the behavior of a JSpinner so that when you click on the text, it selects it. This makes it easier to replace the field with the value that you want. Unfortunately, I can't get the behavior to work and instead, it just inserts the cursor in the text without selecting what is already there.

I have tried adding a focus Listener to both the JSpinner itself and the text area itself, via ((DefaultEditor) this.getEditor()).getTextField(), yet neither of these seem to have the intended effect. My code (for the JSpinner itself) is as follows:

spinner.addFocusListener(new FocusAdapter(){
            @Override
            public void focusGained(FocusEvent e) {
                ((DefaultEditor) ((JSpinner) e.getSource()).getEditor()).getTextField().selectAll();
            }
        }); 

I'm not sure what the problem is. If it matters, I'm running Mac OS 10.7.5 and Java 6u43.

EDIT: I put a System.out.println right at the beginning of the focusGained method and discovered that it was never called. So it looks like getting focus on the JSpinner isn't registering. Again, I tried putting the focusAdpater both on the spinner and on the text field (not at the same time though).

Gemeinschaft answered 10/3, 2013 at 21:58 Comment(0)
L
8

Much of the problem you are facing has to do with how the spinner validates any values within the spinner AFTER a focus event (and several other state events), which will circumvent the highlight.

The MacOS is even worse.

What I ended up doing was starting a Thread that waited a very short time period (around 25 milliseconds) and then used SwingUtilities.invokeLater to actually perform the selection...

Updated with a example

import java.awt.Component;
import java.awt.EventQueue;
import java.awt.GridBagLayout;
import java.awt.event.FocusAdapter;
import java.awt.event.FocusEvent;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import javax.swing.JButton;
import javax.swing.JComponent;
import javax.swing.JFrame;
import javax.swing.JSpinner;
import javax.swing.SpinnerNumberModel;
import javax.swing.SwingUtilities;
import javax.swing.UIManager;
import javax.swing.UnsupportedLookAndFeelException;
import javax.swing.text.JTextComponent;

public class AutoFocusSpinner {

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

    public static final SelectOnFocusGainedHandler SHARED_INSTANCE = new SelectOnFocusGainedHandler();

    public AutoFocusSpinner() {
        EventQueue.invokeLater(new Runnable() {
            @Override
            public void run() {
                try {
                    UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
                } catch (ClassNotFoundException ex) {
                } catch (InstantiationException ex) {
                } catch (IllegalAccessException ex) {
                } catch (UnsupportedLookAndFeelException ex) {
                }

                JSpinner spinner = new JSpinner(new SpinnerNumberModel(1, 0, 100, 1));
                installFocusListener(spinner);

                JFrame frame = new JFrame("Test");
                frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
                frame.setLayout(new GridBagLayout());
                frame.add(spinner);
                frame.add(new JButton("Here for testing"));
                frame.pack();
                frame.setLocationRelativeTo(null);
                frame.setVisible(true);
            }

        });
    }

    public void installFocusListener(JSpinner spinner) {

        JComponent spinnerEditor = spinner.getEditor();

        if (spinnerEditor != null) {

            // This is me spending a few days trying to make this work and 
            // eventually throwing a hissy fit and just grabbing all the 
            // JTextComponent components contained within the editor....
            List<JTextComponent> lstChildren = findAllChildren(spinner, JTextComponent.class);
            if (lstChildren != null && lstChildren.size() > 0) {

                JTextComponent editor = lstChildren.get(0);
                editor.addFocusListener(SHARED_INSTANCE);

            }

        }

    }

    public static <T extends Component> List<T> findAllChildren(JComponent component, Class<T> clazz) {

        List<T> lstChildren = new ArrayList<T>(5);
        for (Component comp : component.getComponents()) {

            if (clazz.isInstance(comp)) {

                lstChildren.add((T) comp);

            } else if (comp instanceof JComponent) {

                lstChildren.addAll(findAllChildren((JComponent) comp, clazz));

            }

        }

        return Collections.unmodifiableList(lstChildren);

    }

    public static class SelectOnFocusGainedHandler extends FocusAdapter {

        @Override
        public void focusGained(FocusEvent e) {

            Component comp = e.getComponent();
            if (comp instanceof JTextComponent) {
                final JTextComponent textComponent = (JTextComponent) comp;
                new Thread(new Runnable() {
                    @Override
                    public void run() {
                        try {
                            Thread.sleep(25);
                        } catch (InterruptedException ex) {
                        }
                        SwingUtilities.invokeLater(new Runnable() {
                            @Override
                            public void run() {
                                textComponent.selectAll();
                            }
                        });
                    }
                }).start();
            }            
        }        
    }
}

Now, right about now, I'm praying for some really nice, simple, undocumented property that we can set that will mean we don't need to do all this :P

Leboeuf answered 10/3, 2013 at 22:21 Comment(20)
This wasn't working for me, but I did a little bit of testing and discovered that focusGained never gets called. Perhaps there is a bigger issue going on?Gemeinschaft
You're not shadowing the variables?Leboeuf
Ah, actually, the problem is, the spinner doesn't recieve focus, the editor does. Bug, that dependsLeboeuf
I tried adding the focus listener on the spinner's editor, but that doesn't seem to gain focus either. In fact, focusGained doesn't seem to be called when there is a listener on the spinner, editor, or textField!Gemeinschaft
Yeah, there's a real trick to it. I've added a working exampleLeboeuf
Your example does work, but I had to increase the delay from 25ms to 75ms in order to do it. Fortunately I've extended JSpinner so it shouldn't be all that bad adapting your code for my purposes, but I'm having a hard time getting it to work in my program. Trial and error I guess. Once you clean up your answer, I'll be happy to accept it as the best answer.Gemeinschaft
Yeah, it's a hack. This works on MiniMac and MacBook Pro, but there might be other considerations to take into account with the delayLeboeuf
I found that it doesn't work if you make changes to the spinner model, which I do in my code. But you can override setModel to install the FocusListener again. It may be a hack, but it currently works for my needs!Gemeinschaft
Could you please update your answer so that the first part no longer refers to code that doesn't work? I.e. spinner.addFocusListenerGemeinschaft
Off the top of my head, you should be use something like commitEdit on the spinner, which would be better then trying to mix view/model code toegtherLeboeuf
I figured out that you can bypass the whole deal of finding the children with ((DefaultEditor) spinner.getEditor()).getTextField().addFocusListener(new SelectOnFocusGainedAdapter());. You also don't need to have a shared instance if you don't want to. It might be helpful to put just that and the code for the SelectOnFocusGainedAdapter in the answer so somebody coming in can more easily find the solution (perhaps with a note saying you may need to adjust the time delay).Gemeinschaft
I tend to use a shared instance in this way, as creating lots of FocusListeners that do exactly the same thing all the time seems like a waste. The reason for the findAllChildren comes down to how different editors are installed (I think I had an issue with a DateEditor or something, can't remember) and allowing for different platforms. That's just me, but if it works for then goodLeboeuf
One final note: I've switched to using a MouseListener (checking for MousePressed) instead of a FocusListener so that the text does not get highlighted when you press the JSpinner buttons.Gemeinschaft
So what happens when the use tabs into the field?Leboeuf
It doesn't work if you tab into the field. So it looks like either you get tabs and the buttons (which is not wanted) or you don't get either. I haven't found a way to determine if the focus was gained as a result of pressing the JSpinner buttons.Gemeinschaft
@Leboeuf This has helped me a lot at first but now somehow it will not recognize that it is focused when that happens with the tab-key. The FocusTraversal happens correctly (when moving through all JSpinners I get out at the next correct component), but neither appears a cursor nor gets the text selected. Any clue why this might happen? As well shows the KeyBoardFocusManager that the component was selected.Prelature
@Prelature You may need to monitor the focusLost event and stop the thread (although a Swing Timer would be easier), you should also check to see if the editor has focus when thread finally wants to update the selected stateLeboeuf
Hm I tried around a bit and it seems that the OP's original problem occurs for me as well. The focusGained-method is never executed.... I will investigate further and probably ask an SO question later. Thanks for your quick response.Prelature
@Prelature The timers could be off as well, they are "guesses" and "observations" of what seems to work :PLeboeuf
@Leboeuf So I think I found the root of the problem now (maybe). I made up a question right here. If you could have a look on that, that would be much appreciated!Prelature
D
10

Don't know about a Mac but I've used this code on Windows:

JSpinner.DefaultEditor editor = (JSpinner.DefaultEditor)spinner.getEditor();
JTextField textField = editor.getTextField();
textField.addFocusListener( new FocusAdapter()
{
    public void focusGained(final FocusEvent e)
    {
        SwingUtilities.invokeLater(new Runnable()
        {
            @Override
            public void run()
            {
                JTextField tf = (JTextField)e.getSource();
                tf.selectAll();
            }
        });

    }

});

I've also used this code for selecting text on a JFormattedTextField.

Dwan answered 11/3, 2013 at 1:15 Comment(4)
This doesn't seem to work on a Mac. When focus is gained, there is a brief flicker of everything being highlighted, and then it goes away.Gemeinschaft
@Thunderforge, ok it seems like the Mac is using multiple invokeLater() pieces of code itself. So instead of hardcoding a sleep of 75ms you could try wrapping the above code in another invokeLater() to add the code back to the end of the EDT again. Bother are ugly solutions but this may avoid hardcoding a sleep time.Dwan
I'd love to avoid hardcoding in a time, but unfortunately wrapping the code into another invokeLater() didn't do the trick. (I assume you meant the part starting with SwingUtilities). I get the same behavior as before.Gemeinschaft
This answer also overrides focusLost to do the same thing, and somehow that solves the issue on OS X.Seaddon
L
8

Much of the problem you are facing has to do with how the spinner validates any values within the spinner AFTER a focus event (and several other state events), which will circumvent the highlight.

The MacOS is even worse.

What I ended up doing was starting a Thread that waited a very short time period (around 25 milliseconds) and then used SwingUtilities.invokeLater to actually perform the selection...

Updated with a example

import java.awt.Component;
import java.awt.EventQueue;
import java.awt.GridBagLayout;
import java.awt.event.FocusAdapter;
import java.awt.event.FocusEvent;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import javax.swing.JButton;
import javax.swing.JComponent;
import javax.swing.JFrame;
import javax.swing.JSpinner;
import javax.swing.SpinnerNumberModel;
import javax.swing.SwingUtilities;
import javax.swing.UIManager;
import javax.swing.UnsupportedLookAndFeelException;
import javax.swing.text.JTextComponent;

public class AutoFocusSpinner {

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

    public static final SelectOnFocusGainedHandler SHARED_INSTANCE = new SelectOnFocusGainedHandler();

    public AutoFocusSpinner() {
        EventQueue.invokeLater(new Runnable() {
            @Override
            public void run() {
                try {
                    UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName());
                } catch (ClassNotFoundException ex) {
                } catch (InstantiationException ex) {
                } catch (IllegalAccessException ex) {
                } catch (UnsupportedLookAndFeelException ex) {
                }

                JSpinner spinner = new JSpinner(new SpinnerNumberModel(1, 0, 100, 1));
                installFocusListener(spinner);

                JFrame frame = new JFrame("Test");
                frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
                frame.setLayout(new GridBagLayout());
                frame.add(spinner);
                frame.add(new JButton("Here for testing"));
                frame.pack();
                frame.setLocationRelativeTo(null);
                frame.setVisible(true);
            }

        });
    }

    public void installFocusListener(JSpinner spinner) {

        JComponent spinnerEditor = spinner.getEditor();

        if (spinnerEditor != null) {

            // This is me spending a few days trying to make this work and 
            // eventually throwing a hissy fit and just grabbing all the 
            // JTextComponent components contained within the editor....
            List<JTextComponent> lstChildren = findAllChildren(spinner, JTextComponent.class);
            if (lstChildren != null && lstChildren.size() > 0) {

                JTextComponent editor = lstChildren.get(0);
                editor.addFocusListener(SHARED_INSTANCE);

            }

        }

    }

    public static <T extends Component> List<T> findAllChildren(JComponent component, Class<T> clazz) {

        List<T> lstChildren = new ArrayList<T>(5);
        for (Component comp : component.getComponents()) {

            if (clazz.isInstance(comp)) {

                lstChildren.add((T) comp);

            } else if (comp instanceof JComponent) {

                lstChildren.addAll(findAllChildren((JComponent) comp, clazz));

            }

        }

        return Collections.unmodifiableList(lstChildren);

    }

    public static class SelectOnFocusGainedHandler extends FocusAdapter {

        @Override
        public void focusGained(FocusEvent e) {

            Component comp = e.getComponent();
            if (comp instanceof JTextComponent) {
                final JTextComponent textComponent = (JTextComponent) comp;
                new Thread(new Runnable() {
                    @Override
                    public void run() {
                        try {
                            Thread.sleep(25);
                        } catch (InterruptedException ex) {
                        }
                        SwingUtilities.invokeLater(new Runnable() {
                            @Override
                            public void run() {
                                textComponent.selectAll();
                            }
                        });
                    }
                }).start();
            }            
        }        
    }
}

Now, right about now, I'm praying for some really nice, simple, undocumented property that we can set that will mean we don't need to do all this :P

Leboeuf answered 10/3, 2013 at 22:21 Comment(20)
This wasn't working for me, but I did a little bit of testing and discovered that focusGained never gets called. Perhaps there is a bigger issue going on?Gemeinschaft
You're not shadowing the variables?Leboeuf
Ah, actually, the problem is, the spinner doesn't recieve focus, the editor does. Bug, that dependsLeboeuf
I tried adding the focus listener on the spinner's editor, but that doesn't seem to gain focus either. In fact, focusGained doesn't seem to be called when there is a listener on the spinner, editor, or textField!Gemeinschaft
Yeah, there's a real trick to it. I've added a working exampleLeboeuf
Your example does work, but I had to increase the delay from 25ms to 75ms in order to do it. Fortunately I've extended JSpinner so it shouldn't be all that bad adapting your code for my purposes, but I'm having a hard time getting it to work in my program. Trial and error I guess. Once you clean up your answer, I'll be happy to accept it as the best answer.Gemeinschaft
Yeah, it's a hack. This works on MiniMac and MacBook Pro, but there might be other considerations to take into account with the delayLeboeuf
I found that it doesn't work if you make changes to the spinner model, which I do in my code. But you can override setModel to install the FocusListener again. It may be a hack, but it currently works for my needs!Gemeinschaft
Could you please update your answer so that the first part no longer refers to code that doesn't work? I.e. spinner.addFocusListenerGemeinschaft
Off the top of my head, you should be use something like commitEdit on the spinner, which would be better then trying to mix view/model code toegtherLeboeuf
I figured out that you can bypass the whole deal of finding the children with ((DefaultEditor) spinner.getEditor()).getTextField().addFocusListener(new SelectOnFocusGainedAdapter());. You also don't need to have a shared instance if you don't want to. It might be helpful to put just that and the code for the SelectOnFocusGainedAdapter in the answer so somebody coming in can more easily find the solution (perhaps with a note saying you may need to adjust the time delay).Gemeinschaft
I tend to use a shared instance in this way, as creating lots of FocusListeners that do exactly the same thing all the time seems like a waste. The reason for the findAllChildren comes down to how different editors are installed (I think I had an issue with a DateEditor or something, can't remember) and allowing for different platforms. That's just me, but if it works for then goodLeboeuf
One final note: I've switched to using a MouseListener (checking for MousePressed) instead of a FocusListener so that the text does not get highlighted when you press the JSpinner buttons.Gemeinschaft
So what happens when the use tabs into the field?Leboeuf
It doesn't work if you tab into the field. So it looks like either you get tabs and the buttons (which is not wanted) or you don't get either. I haven't found a way to determine if the focus was gained as a result of pressing the JSpinner buttons.Gemeinschaft
@Leboeuf This has helped me a lot at first but now somehow it will not recognize that it is focused when that happens with the tab-key. The FocusTraversal happens correctly (when moving through all JSpinners I get out at the next correct component), but neither appears a cursor nor gets the text selected. Any clue why this might happen? As well shows the KeyBoardFocusManager that the component was selected.Prelature
@Prelature You may need to monitor the focusLost event and stop the thread (although a Swing Timer would be easier), you should also check to see if the editor has focus when thread finally wants to update the selected stateLeboeuf
Hm I tried around a bit and it seems that the OP's original problem occurs for me as well. The focusGained-method is never executed.... I will investigate further and probably ask an SO question later. Thanks for your quick response.Prelature
@Prelature The timers could be off as well, they are "guesses" and "observations" of what seems to work :PLeboeuf
@Leboeuf So I think I found the root of the problem now (maybe). I made up a question right here. If you could have a look on that, that would be much appreciated!Prelature

© 2022 - 2024 — McMap. All rights reserved.