Search code examples
javaswingjlabelmouselistener

How to get the part of the text from JLabel according mouse pointer


Does anyone knows how to get the part of the text from the beginning of JLabel to the pointer of the mouse? For example, let's say we have a JLabel with text 'C:\aaa\bbb\ccc'. The user points mouse pointer on characters 'bbb', so I would like to get the text 'C:\aaa\bbb'. Now, when I have this part of the text, I can change its color. I think will use html for that.


Solution

  • The Java Accessibility API conveniently includes a getIndexAtPoint method as part of the AccessibleText interface that converts a location (such as that of the mouse pointer) to the index of the character at that location:

    Given a point in local coordinates, return the zero-based index of the character under that Point. If the point is invalid, this method returns -1.

    Here is a test program that uses this method to get the part of the string you asked for:

    import java.awt.BorderLayout;
    import java.awt.Point;
    import java.awt.event.MouseEvent;
    import java.awt.event.MouseMotionAdapter;
    
    import javax.accessibility.AccessibleText;
    import javax.swing.JFrame;
    import javax.swing.JLabel;
    
    public class JLabelMouseDemo {
        private static String labelText = "<html>C:\\aaa\\bbb\\ccc</html>";
        private static JLabel label;
        private static JLabel substringDisplayLabel;
    
        public static void main(String[] args) {
            JFrame frame = new JFrame();
            label = new JLabel(labelText);
            label.addMouseMotionListener(new MouseMotionAdapter() {
                public void mouseMoved(MouseEvent e) {
                    AccessibleText accessibleText =
                            label.getAccessibleContext().getAccessibleText();
                    Point p = e.getPoint();
                    int index = accessibleText.getIndexAtPoint(p);
                    if (index >= 0) {
                        // The index is with respect to the actually displayed
                        // characters rather than the entire HTML string, so we
                        // must add six to skip over "<html>", which is part of
                        // the labelText String but not actually displayed on
                        // the screen. Otherwise, the substrings could end up
                        // something like "tml>C:\aaa"
                        index += 6;
    
                        // Strangely, in my testing, index was a one-based index
                        // (for example, mousing over the 'C' resulted in an
                        // index of 1), but this makes getting the part of the
                        // string up to that character easier.
                        String partOfText = labelText.substring(0, index);
    
                        // Display for demonstration purposes; you could also
                        // figure out how to highlight it or use the string or
                        // just the index in some other way to suit your needs.
                        // For example, you might want to round the index to
                        // certain values so you will line up with groups of
                        // characters, only ever having things like
                        // "C:\aaa\bbb", and never "C:\aaa\b"
                        substringDisplayLabel.setText(partOfText);
                    }
                }
            });
            frame.add(label);
            substringDisplayLabel = new JLabel();
            frame.add(substringDisplayLabel, BorderLayout.SOUTH);
            frame.setSize(200, 200);
            frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
            frame.setVisible(true);
        }
    }
    

    Actually obtaining an object of type AccessibleText that corresponds to a particular JLabel may not always work: as far as I can tell, it will only be possible when the JLabel is displaying HTML text. This also seems to be supported by the JLabel source:

    public AccessibleText getAccessibleText() {
                View view = (View)JLabel.this.getClientProperty("html");
                if (view != null) {
                    return this;
                } else {
                    return null;
                }
            }
    

    I don't claim to fully understand what's going on in that code or why accessibility is not available for non-HTML text, but my test program did not work when the JLabel contained plain rather than HTML text: label.getAccessibleContext().getAccessibleText() would return null, and using a forced cast of (AccessibleText) label.getAccessibleContext() would yield an object that only ever returned -1 from getIndexAtPoint.


    Edit: It is possible to get the part of the text without worrying about adjusting the indices based on the location of HTML tags that aren't displayed as visible text. You just have to maintain two copies of the string on the label: one containing only the characters to be displayed (rawText in the example below) that will be sliced according to the index, and one containing a formatted HTML version that will actually be used as the text of the label (the result of formatLabelText below). Because getIndexAtPoint returns an index relative only to the displayed characters, getting the desired substring is easier in the second example than my original one. The only adjustment made to index is rounding it up so that highlighted text lines up with the backslash-delimited groups.

    import java.awt.Point;
    import java.awt.event.MouseEvent;
    import java.awt.event.MouseMotionAdapter;
    
    import javax.accessibility.AccessibleText;
    import javax.swing.JFrame;
    import javax.swing.JLabel;
    
    public class JLabelMouseHighlightDemo {
        private static String rawText = "C:\\aaa\\bbb\\ccc";
        private static JLabel label;
    
        private static String formatLabelText(int index) {
            if (index < 0) {
                index = 0;
            }
            if (index > rawText.length()) {
                index = rawText.length();
            }
            StringBuilder sb = new StringBuilder();
            sb.append("<html>");
            sb.append("<font color='red'>");
            sb.append(rawText.substring(0, index));
            sb.append("</font>");
            sb.append(rawText.substring(index));
            sb.append("</html>");
            return sb.toString();
        }
    
        private static int roundIndex(int index) {
            // This method rounds up index to always align with a group of
            // characters delimited by a backslash, so the red text will be
            // "C:\aaa\bbb" instead of just "C:\aaa\b".
            while (index < rawText.length() && rawText.charAt(index) != '\\') {
                index++;
            }
            return index;
        }
    
        public static void main(String[] args) {
            JFrame frame = new JFrame();
            label = new JLabel(formatLabelText(0));
            label.addMouseMotionListener(new MouseMotionAdapter() {
                public void mouseMoved(MouseEvent e) {
                    AccessibleText accessibleText =
                            label.getAccessibleContext().getAccessibleText();
                    Point p = e.getPoint();
                    int index = accessibleText.getIndexAtPoint(p);
                    index = roundIndex(index);
                    label.setText(formatLabelText(index));
                }
            });
            frame.add(label);
            frame.setSize(200, 200);
            frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
            frame.setVisible(true);
        }
    }