Search code examples
javaswingjbuttonjtextfieldjtextarea

JButton that highlights next/previous match in JTextArea JAVA SWING


I am trying to implement buttons that search a specific word in JTextArea in my Java Swing application. Can anyone help me to implement buttons to highlight the next and previous match ? Please review my code for searchbutton aswell. For now it works fine.

PS. I want run searchButton in new Thread. Is it okay like that ???

searchButton.addActionListener(e -> new Thread(() -> {
        int pos = 0;
        // Get the text to find...convert it to lower case for eaiser comparision
        String find = searchField.getText().toLowerCase();
        // Focus the text area, otherwise the highlighting won't show up
        textArea.requestFocusInWindow();
        // Make sure we have a valid search term
        if (find != null && find.length() > 0) {
            Document document = textArea.getDocument();
            int findLength = find.length();
            try {
                boolean found = false;
                // Rest the search position if we're at the end of the document
                if (pos + findLength > document.getLength()) {
                    pos = 0;
                }
                // While we haven't reached the end...
                // "<=" Correction
                while (pos + findLength <= document.getLength()) {
                    // Extract the text from the docuemnt
                    String match = document.getText(pos, findLength).toLowerCase();
                    // Check to see if it matches or request
                    if (match.equals(find)) {
                        found = true;
                        break;
                    }
                    pos++;
                }
                // Did we find something...
                if (found) {
                    // Get the rectangle of the where the text would be visible...
                    Rectangle viewRect = textArea.modelToView(pos);
                    // Scroll to make the rectangle visible
                    textArea.scrollRectToVisible(viewRect);
                    // Highlight the text
                    textArea.setCaretPosition(pos + findLength);
                    textArea.select(pos, pos + findLength);
                    textArea.grabFocus();

                }

            } catch (Exception exp) {
                exp.printStackTrace();
            }
        }
    }));

Solution

    1. Don't use a Thread. You only use a Thread for time consuming tasks. Searching a text string is not a time consuming task. Also, updates to Swing components need to be done on the EDT. So you want the highlighting to be done on the EDT.

    2. Don't use grabFocus(). You have already used requestFocusInWindow() which is the correct method to use.

    3. Use the String.indexOf(…) method (as suggested by @ControlAltDel). Then there is no need for looping code. You just search for the text from the current caret position and you either find the word or you don't.

    For what its worth I happen to have old code lying around that does this:

    import java.awt.*;
    import java.awt.event.*;
    import java.io.*;
    import javax.swing.*;
    import javax.swing.event.*;
    import javax.swing.text.*;
    
    public class TextComponentFindNext extends JFrame implements ActionListener
    {
        JTextComponent textComponent;
        JTextField textField;
    
        public TextComponentFindNext()
            throws Exception
        {
            textComponent = new JTextPane();
            JScrollPane scrollPane = new JScrollPane( textComponent );
            scrollPane.setPreferredSize( new Dimension(500, 400) );
            getContentPane().add(scrollPane, BorderLayout.NORTH);
    
            textField = new JTextField(10);
            textField.setText("im");
            textField.addActionListener( this );
            getContentPane().add(textField, BorderLayout.WEST);
    
            JButton button = new JButton("Find Next");
            button.addActionListener( this );
            getContentPane().add(button, BorderLayout.EAST);
    
            FileReader reader = new FileReader( "TextComponentFindNext.java" );
            BufferedReader br = new BufferedReader(reader);
            textComponent.read(br, null);
            br.close();
        }
    
        public void actionPerformed(ActionEvent e)
        {
            String searchString = "";
    
            //  this works
            try
            {
                Document doc = textComponent.getDocument();
                searchString = doc.getText(0, doc.getLength());
            }
            catch(BadLocationException ble) {}
    
            //  this doesn't work
    //      searchString = textComponent.getText();
    
            int offset = textComponent.getCaretPosition();
            String searchText = textField.getText();
    
            int start = searchString.indexOf(searchText, offset);
    
            if (start != -1)
            {
                textComponent.select(start, start + searchText.length());
                textComponent.requestFocusInWindow();
            }
        }
    
        public static void main(String[] args)
            throws Exception
        {
            JFrame frame = new TextComponentFindNext();
            frame.setDefaultCloseOperation( EXIT_ON_CLOSE );
            frame.pack();
            frame.setLocationRelativeTo( null );
            frame.setVisible(true);
        }
    }
    

    There is no need for the scroll logic, it will scroll automatically when the text is selected.

    If you want to get fancy with scrolling you can "center" the line the contains the text. Check out Text Utilities for some helper methods that allow you to do this.

    Note, it is better to get the text from the Document, instead of the components. The text from the Document only contains a "\n" string for the end of line string. This means it will work for both a JTextArea and a JTextPane. See: Text and New Lines for more information.