gustafc
gustafc

Reputation: 28865

How do I make JScrollPane scroll to follow input focus?

I have a Swing app with a large panel which is wrapped in a JScrollPane. Users normally move between the panel's subcomponents by tabbing, so when they tab to something out view, I want the scroll pane to autoscroll so the component with input focus is always visible.

I've tried using KeyboardFocusManager to listen for input focus changes, and then calling scrollRectToVisible.

Here's an SSCCE displaying my current strategy (just copy/paste and run!):

import java.awt.KeyboardFocusManager;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import javax.swing.*;

public class FollowFocus {

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

      public void run() {
        final int ROWS = 100;
        final JPanel content = new JPanel();
        content.setLayout(new BoxLayout(content, BoxLayout.Y_AXIS));
        content.add(new JLabel(
          "Thanks for helping out. Use tab to move around."));
        for (int i = 0; i < ROWS; i++) {
          JTextField field = new JTextField("" + i);
          field.setName("field#" + i);
          content.add(field);
        }

        KeyboardFocusManager.getCurrentKeyboardFocusManager()
                            .addPropertyChangeListener("focusOwner", 
                     new PropertyChangeListener() {

          @Override
          public void propertyChange(PropertyChangeEvent evt) {
            if (!(evt.getNewValue() instanceof JComponent)) {
              return;
            }
            JComponent focused = (JComponent) evt.getNewValue();
            if (content.isAncestorOf(focused)) {
              System.out.println("Scrolling to " + focused.getName());
              focused.scrollRectToVisible(focused.getBounds());
            }
          }
        });

        JFrame window = new JFrame("Follow focus");
        window.setContentPane(new JScrollPane(content));
        window.setSize(200, 200);
        window.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        window.setVisible(true);
      }
    });
  }
}

If you run this example, you'll notice it doesn't work very well. It does get the focus change notifications, but the call to scrollRectToVisible doesn't appear to have any effect. In my app (which is too complex to show here), scrollRectToVisible works about half the time when I tab into something outside of the viewport.

Is there an established way to solve this problem? If it makes any difference, the Swing app is built on Netbeans RCP (and most of our customers run Windows).

Upvotes: 11

Views: 15403

Answers (5)

Nolle
Nolle

Reputation: 261

Here my short summary. Add this to your Tools class:

public static void addOnEnter(Component c, Consumer<FocusEvent> onEnter) {
    FocusListener fl = new FocusListener() {
        @Override
        public void focusGained(FocusEvent e) {
            onEnter.accept(e);
        }
        @Override
        public void focusLost(FocusEvent e) {  }
    };
    c.addFocusListener(fl);
}

public static void scrollToFocus(FocusEvent e) {
    ((JComponent) e.getComponent().getParent()).scrollRectToVisible(
            e.getComponent().getBounds());
}

and use it like this:

Tools.addOnEnter(component, Tools::scrollToFocus);

component can be JTextField, JButton, ...

Upvotes: 1

user2822578
user2822578

Reputation: 1

Here jtextbox is the component you want to focus and jscrollpane is your scrollpane:

jScrollpane.getVerticalScrollBar().setValue(jtextbox.getLocation().x);

Upvotes: 0

kleopatra
kleopatra

Reputation: 51524

My comment to the other answer:

scrollRectToVisible on the component itself is the whole point of that method ;-) It's passed up the hierarchy until a parent doing the scroll is found

... except when the component itself handles it - as JTextField does: it's implemented to scroll horizontally to make the caret visible. The way out is to call the method on the field's parent.

Edit

just for clarity, the replaced line is

    content.scrollRectToVisible(focused.getBounds());

Upvotes: 16

mKorbel
mKorbel

Reputation: 109815

you have to take Rectangle from JPanel and JViewPort too, then compare, for example

notice (against down-voting) for final and nice output required some work for positions in the JViewPort

import java.awt.KeyboardFocusManager;
import java.awt.Rectangle;
import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import javax.swing.*;
//http://stackoverflow.com/questions/8245328/how-do-i-make-jscrollpane-scroll-to-follow-input-focus
public class FollowFocus {

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

            public void run() {
                final int ROWS = 100;
                final JPanel content = new JPanel();
                content.setLayout(new BoxLayout(content, BoxLayout.Y_AXIS));
                content.add(new JLabel(
                        "Thanks for helping out. Use tab to move around."));
                for (int i = 0; i < ROWS; i++) {
                    JTextField field = new JTextField("" + i);
                    field.setName("field#" + i);
                    content.add(field);
                }
                final JScrollPane scroll = new JScrollPane(content);
                KeyboardFocusManager.getCurrentKeyboardFocusManager().
                        addPropertyChangeListener("focusOwner", new PropertyChangeListener() {

                    @Override
                    public void propertyChange(PropertyChangeEvent evt) {
                        if (!(evt.getNewValue() instanceof JComponent)) {
                            return;
                        }
                        JViewport viewport = (JViewport) content.getParent();
                        JComponent focused = (JComponent) evt.getNewValue();
                        if (content.isAncestorOf(focused)) {
                            System.out.println("Scrolling to " + focused.getName());
                            Rectangle rect = focused.getBounds();
                            Rectangle r2 = viewport.getVisibleRect();
                            content.scrollRectToVisible(new Rectangle(rect.x, rect.y, (int) r2.getWidth(), (int) r2.getHeight()));
                        }
                    }
                });

                JFrame window = new JFrame("Follow focus");
                window.setContentPane(new JScrollPane(content));
                window.setSize(200, 200);
                window.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
                window.setVisible(true);
            }
        });
    }
}

Upvotes: 3

Ashwinee K Jha
Ashwinee K Jha

Reputation: 9307

One major issue in your code is:

focused.scrollRectToVisible(focused.getBounds());

You are calling scrollRectToVisible on the component itself! Presumably a typo. Make your JScrollPane a final variable and call

scrollPane.getViewport().scrollRectToVisible(focused.getBounds());

Upvotes: 0

Related Questions