In Java Swing, can I receive Caret events in real time?

965 Views Asked by At

I'm writing a hex editor-like view, which is composed of two JTextComponents (hex and ASCII). I'd like to synchronize the selection between the two views, so I've implemented a CaretListener for the two components. This works well for responding to selection events where the user has long-pressed, dragged, and released the mouse. The components receive the caretUpdate when the user releases the mouse.

How can the components receive incremental caretUpdate events as the user presses the mouse and drags the mouse around without releasing?

1

There are 1 best solutions below

4
On

There is, unfortunately, no selection model for JTextArea, otherwise this would have being really easy...

Instead, I was forced to add a ChangeListener to the Caret of each text area. This allowed me to see when the caret position was changed in real time.

The next problem occurred when I realised that only the current text area would actually show it's selection highlight's....(nb This can be easily rectified, check next update)

I had to then apply a highlighter to the unfocused text area...

enter image description here

import java.awt.BorderLayout;
import java.awt.EventQueue;
import java.awt.GridLayout;
import java.awt.event.FocusAdapter;
import java.awt.event.FocusEvent;
import java.io.File;
import java.io.FileReader;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTextArea;
import javax.swing.UIManager;
import javax.swing.UnsupportedLookAndFeelException;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import javax.swing.text.BadLocationException;
import javax.swing.text.DefaultHighlighter;

public class CaretTest {

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

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

                JFrame frame = new JFrame("Testing");
                frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
                frame.setLayout(new BorderLayout());
                frame.add(new TestPane());
                frame.pack();
                frame.setLocationRelativeTo(null);
                frame.setVisible(true);
            }
        });
    }

    public class TestPane extends JPanel {

        public TestPane() {
            setLayout(new GridLayout(1, 2));

            JTextArea left = new JTextArea(10, 20);
            JTextArea right = new JTextArea(10, 20);

            left.setEditable(false);
            right.setEditable(false);

            left.getCaret().addChangeListener(new ChangeHandler(left, right));
            right.getCaret().addChangeListener(new ChangeHandler(right, left));

            left.addFocusListener(new FocusHandler(left, right));
            right.addFocusListener(new FocusHandler(right, left));

            JScrollPane leftSP = new JScrollPane(left);
            JScrollPane rightSP = new JScrollPane(right);

            leftSP.getHorizontalScrollBar().setModel(rightSP.getHorizontalScrollBar().getModel());
            leftSP.getVerticalScrollBar().setModel(rightSP.getVerticalScrollBar().getModel());

            add(leftSP);
            add(rightSP);

            FileReader reader = null;
            try {

                reader = new FileReader(new File("Ni.txt"));
                left.read(reader, null);
                reader.close();
                reader = new FileReader(new File("Ni.txt"));
                right.read(reader, null);

            } catch (Exception exp) {
                exp.printStackTrace();
            } finally {
                try {
                    reader.close();
                } catch (Exception e) {
                }
            }
        }

        protected void updateHighlighting(JTextArea source, JTextArea target) {
            DefaultHighlighter.DefaultHighlightPainter painter = new DefaultHighlighter.DefaultHighlightPainter(target.getSelectionColor());
            int start = source.getSelectionStart();
            int end = source.getSelectionEnd();
            try {
                target.getHighlighter().addHighlight(start, end, painter);
            } catch (BadLocationException ex) {
                ex.printStackTrace();
            }
        }

        public class ChangeHandler implements ChangeListener {

            private final JTextArea source;
            private final JTextArea target;

            public ChangeHandler(JTextArea source, JTextArea target) {
                this.source = source;
                this.target = target;
            }

            @Override
            public void stateChanged(ChangeEvent e) {
                if (e.getSource() == source.getCaret()) {
                    target.getHighlighter().removeAllHighlights();
                    updateHighlighting(source, target);
                }
            }
        }

        public class FocusHandler extends FocusAdapter {

            private final JTextArea source;
            private final JTextArea target;

            public FocusHandler(JTextArea source, JTextArea target) {
                this.source = source;
                this.target = target;
            }

            @Override
            public void focusGained(FocusEvent e) {
                source.getHighlighter().removeAllHighlights();
                target.getHighlighter().removeAllHighlights();
                updateHighlighting(source, target);
            }

        }
    }

}

ps- You'll need to supply your own text ;)

Update with "non-highlighter" example

Thanks to StanislavL for pointing out that you can use JTextComponent#getCaret()#setSelectionVisible(true) to make a non focused text component show it's selected text.

I did find that focus changes made this false again, so I've updated within the the change handler to always be true

import java.awt.BorderLayout;
import java.awt.EventQueue;
import java.awt.GridLayout;
import java.awt.event.FocusAdapter;
import java.awt.event.FocusEvent;
import java.io.File;
import java.io.FileReader;
import javax.swing.JFrame;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTextArea;
import javax.swing.UIManager;
import javax.swing.UnsupportedLookAndFeelException;
import javax.swing.event.ChangeEvent;
import javax.swing.event.ChangeListener;
import javax.swing.text.BadLocationException;
import javax.swing.text.DefaultHighlighter;

public class CaretTest {

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

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

                JFrame frame = new JFrame("Testing");
                frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
                frame.setLayout(new BorderLayout());
                frame.add(new TestPane());
                frame.pack();
                frame.setLocationRelativeTo(null);
                frame.setVisible(true);
            }
        });
    }

    public class TestPane extends JPanel {

        public TestPane() {
            setLayout(new GridLayout(1, 2));

            JTextArea left = new JTextArea(10, 20);
            JTextArea right = new JTextArea(10, 20);

            left.setEditable(false);
            right.setEditable(false);

            left.getCaret().setSelectionVisible(true);
            right.getCaret().setSelectionVisible(true);

            left.getCaret().addChangeListener(new ChangeHandler(left, right));
            right.getCaret().addChangeListener(new ChangeHandler(right, left));

            JScrollPane leftSP = new JScrollPane(left);
            JScrollPane rightSP = new JScrollPane(right);

            leftSP.getHorizontalScrollBar().setModel(rightSP.getHorizontalScrollBar().getModel());
            leftSP.getVerticalScrollBar().setModel(rightSP.getVerticalScrollBar().getModel());

            add(leftSP);
            add(rightSP);

            FileReader reader = null;
            try {

                reader = new FileReader(new File("Ni.txt"));
                left.read(reader, null);
                reader.close();
                reader = new FileReader(new File("Ni.txt"));
                right.read(reader, null);

            } catch (Exception exp) {
                exp.printStackTrace();
            } finally {
                try {
                    reader.close();
                } catch (Exception e) {
                }
            }
        }
    }

    public static class ChangeHandler implements ChangeListener {

        private static boolean ignoreUpdates = false;

        private final JTextArea source;
        private final JTextArea target;

        public ChangeHandler(JTextArea source, JTextArea target) {
            this.source = source;
            this.target = target;
        }

        @Override
        public void stateChanged(ChangeEvent e) {
            if (e.getSource() == source.getCaret()) {
                if (!ignoreUpdates) {
                    ignoreUpdates = true;
                    try {
                        target.getCaret().setSelectionVisible(true);
                        source.getCaret().setSelectionVisible(true);
                        target.setSelectionStart(source.getSelectionStart());
                        target.setSelectionEnd(source.getSelectionEnd());
                    } finally {
                        ignoreUpdates = false;
                    }
                }
            }
        }
    }

}