Question

The last days I have been trying to implement a highlighting feature in a small text editor. For some reason I get a strange result:

enter image description here

The given example should highlight each "dolor" - the first occurences are correctly found and highlighted but the next ones aren't.

Here is the code I wrote so far:

import java.awt.Color;
import java.awt.Dimension;
import java.awt.event.MouseEvent;
import java.awt.event.MouseListener;

import javax.swing.JFrame;
import javax.swing.JScrollPane;
import javax.swing.JTextPane;
import javax.swing.text.BadLocationException;
import javax.swing.text.DefaultHighlighter;
import javax.swing.text.DefaultHighlighter.DefaultHighlightPainter;
import javax.swing.text.DefaultStyledDocument;

/**
 * Highlighting created on 04.11.2013<br>
 * <br>
 * Specification:<br>
 */
public class Highlighting extends JFrame implements MouseListener {

    private JScrollPane scrollPane;
    private JTextPane textPane;

    private DefaultHighlighter highlighter;
    private DefaultHighlightPainter painter;

    public static void main(String[] args) {
        new Highlighting().setVisible(true);
    }

    /**
     * 
     */
    public Highlighting() {
        this.initialize();
        this.build();
        this.configure();
    }

    /**
     *
     */
    public void initialize() {
        this.scrollPane = new JScrollPane();
        this.textPane = new JTextPane();
        this.highlighter = new DefaultHighlighter();
        this.painter = new DefaultHighlightPainter(Color.RED);
    }

    /**
     *
     */
    public void build() {
        this.add(this.scrollPane);
    }

    /**
     *
     */
    public void configure() {
        this.scrollPane.setViewportView(this.textPane);
        this.textPane.setHighlighter(this.highlighter);
        this.textPane.addMouseListener(this);
        this.textPane.setDocument(new DefaultStyledDocument());

        this.setPreferredSize(new Dimension(400, 500));
        this.pack();
        this.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
    }

    /**
     * 
     */
    private void highlight() {
        this.highlighter.removeAllHighlights();

        String selectedText = this.textPane.getSelectedText();
        String text = this.textPane.getText();

        int wordlength = selectedText.length();

        int index = 0;
        while ((index = text.indexOf(selectedText, index)) != -1) {

            try {
                this.highlighter.addHighlight(index, index + wordlength, this.painter);
            } catch (BadLocationException e) {
                e.printStackTrace();
            }

            index += wordlength;
        }
    }

    @Override
    public void mouseClicked(MouseEvent e) {
        if (e.getClickCount() == 2) {
            this.highlight();
        }
    }

    @Override
    public void mousePressed(MouseEvent e) {}

    @Override
    public void mouseReleased(MouseEvent e) {}

    @Override
    public void mouseEntered(MouseEvent e) {}

    @Override
    public void mouseExited(MouseEvent e) {}

}

Does this has something to do with the line separators (\r\n) ?

Was it helpful?

Solution

A JTextComponent's getText() and A JTextPane/JEditorPane's getText() has different implementation. JTextPane/JEditorPane uses EditorKit to write the document content(text) to a StringWriter and then return the text with formatting and inserting a line/paragraph break into the document. But the JTextCompoent returns document content directly by:

document.getText(0, document.getLength());

You will better understand if you try to compare the length : jTextPane1.getText().length() and jTextPane1().getDocument().getLength().

Reproducing the difference in length by inserting string with:

DefaultStyleDocument.insertString(0, str, primaryStyle)

when str = "I\n not"   ; document length = 6, getText().length = 7
when str = "I\r\n not" ; document length = 7, getText().length = 8
when str = "I\n\n not" ; document length = 7, getText().length = 9!

So, in your high-lighting text program try reading the content text using:

DefaultStyledDocument document = (DefaultStyledDocument) jTextPane1.getDocument();
try {
    contText = document.getText(0, document.getLength());
} catch (BadLocationException ex) {
     Logger.getLogger(JTextPaneTest.class.getName()).log(Level.SEVERE, null, ex);
 }

Then search for your selected text position in the contText as you were doing and you should be good to go. Because, highlighter.addHighlight(int p0, int p1, Highlighter.HighlightPainter p) uses the document for position offset.

Use CaretListener:

To Highlight upon text selection, It is better to use CaretListener, no need to add mouse and key board selection handling code at all:

enter image description here

jTextPane1.addCaretListener(new CaretListener() {
        public void caretUpdate(CaretEvent evt) {
            if(evt.getDot() == evt.getMark())return;

    JTextPane txtPane = (JTextPane) evt.getSource();
    DefaultHighlighter highlighter = (DefaultHighlighter) txtPane.getHighlighter();
    highlighter.removeAllHighlights();
    DefaultHighlightPainter hPainter = new DefaultHighlightPainter(new Color(0xFFAA00));
    String selText = txtPane.getSelectedText();
    String contText = "";// = jTextPane1.getText();

    DefaultStyledDocument document = (DefaultStyledDocument) txtPane.getDocument();

    try {
        contText = document.getText(0, document.getLength());
    } catch (BadLocationException ex) {
        Logger.getLogger(JTextPaneTest.class.getName()).log(Level.SEVERE, null, ex);
    }

    int index = 0;

    while((index = contText.indexOf(selText, index)) > -1){

        try {
            highlighter.addHighlight(index, selText.length()+index, hPainter);
            index = index + selText.length();
        } catch (BadLocationException ex) {
            Logger.getLogger(JTextPaneTest.class.getName()).log(Level.SEVERE, null, ex);
           //System.out.println(index);
        }
       }
        }
    });

OTHER TIPS

for example, see

  • how to remove Highlighter highlighter.removeHighlight(h);

  • View.modelToView for a new Highlighter

  • note I don't know how to determine a new line \n inside selection, not possible from this code

enter image description here enter image description here

import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.EventQueue;
import java.awt.FontMetrics;
import java.awt.Graphics;
import java.awt.Rectangle;
import java.awt.Shape;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import javax.swing.JFrame;
import javax.swing.JLabel;
import javax.swing.JPanel;
import javax.swing.JScrollPane;
import javax.swing.JTextField;
import javax.swing.JTextPane;
import javax.swing.UIManager;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;
import javax.swing.text.BadLocationException;
import javax.swing.text.DefaultHighlighter;
import javax.swing.text.Document;
import javax.swing.text.Highlighter;
import javax.swing.text.JTextComponent;
import javax.swing.text.LayeredHighlighter;
import javax.swing.text.Position;
import javax.swing.text.View;

public class HighlightExample {

    private JFrame f = new JFrame("Highlight example");
    private JPanel panel = new JPanel();
    private JTextPane textPane = new JTextPane();
    private JTextField tf = new JTextField("wrapping!");
    private String word;
    private Highlighter highlighter = new UnderlineHighlighter(null);

    public HighlightExample() {
        textPane.setHighlighter(highlighter);
        textPane.setText("This text pane contains no html. It supports letter wrapping, "
                + "\nThis text pane contains no html. It supports letter wrapping!, "
                + "\nThis text pane contains no html. It supports letter wrapping?, "
                + "\nThis text pane contains no html. It supports letter wrapping-, "
                + "\nThis text pane contains no html. It supports letter wrapping!, "
                + "\nThis text pane contains no html. It supports letter wrapping_, "
                + "\nThis text pane contains no html. It supports letter wrapping!, "
                + "\nThis text pane contains no html. It supports letter wrapping?, "
                + "\nThis text pane contains no html. It supports letter wrapping!, "
                + "\nThis text pane contains no html. It supports letter wrapping, "
                + "\nThis text pane contains no html. It supports letter wrapping!, "
                + "\nThis text pane contains no html. It supports letter wrapping-, "
                + "\nThis text pane contains no html. It supports letter wrapping!, "
                + "\nThis text pane contains no html. It supports letter wrapping?");
        panel.setLayout(new BorderLayout());
        panel.add(new JLabel("Enter word, then press ENTER key: "), "West");
        panel.add(tf, "Center");
        /*try {
         textPane.read(new FileReader("links1.html"), null);
         } catch (Exception e) {
         System.out.println("Failed to load file " + args[0]);
         System.out.println(e);
         }*/
        final WordSearcher searcher = new WordSearcher(textPane);
        tf.addActionListener(new ActionListener() {
            @Override
            public void actionPerformed(ActionEvent evt) {
                word = tf.getText().trim();
                int offset = searcher.search(word);
                if (offset != -1) {
                    try {
                        textPane.scrollRectToVisible(textPane
                                .modelToView(offset));
                    } catch (BadLocationException e) {
                    }
                }
            }
        });
        textPane.getDocument().addDocumentListener(new DocumentListener() {
            @Override
            public void insertUpdate(DocumentEvent evt) {
                searcher.search(word);
            }

            @Override
            public void removeUpdate(DocumentEvent evt) {
                searcher.search(word);
            }

            @Override
            public void changedUpdate(DocumentEvent evt) {
            }
        });
        f.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
        f.add(panel, "South");
        f.add(new JScrollPane(textPane), "Center");
        f.setSize(400, 400);
        f.setVisible(true);
    }

    public static void main(String[] args) {
        UIManager.put("TextPane.caretForeground", Color.yellow);
        UIManager.put("TextPane.selectionBackground", Color.green);
        UIManager.put("TextPane.selectionForeground", Color.blue);
        /*try {
         UIManager.setLookAndFeel("com.sun.java.swing.plaf.windows.WindowsLookAndFeel");
         } catch (Exception evt) {
         }*/
        EventQueue.invokeLater(new Runnable() {
            @Override
            public void run() {
                new HighlightExample();
            }
        });
    }
}

// A simple class that searches for a word in
// a document and highlights occurrences of that word
class WordSearcher {

    protected JTextComponent comp;
    protected Highlighter.HighlightPainter painter;

    public WordSearcher(JTextComponent comp) {
        this.comp = comp;
        this.painter = new UnderlineHighlighter.UnderlineHighlightPainter(Color.red);
    }

    // Search for a word and return the offset of the first occurrence. 
    // Highlights are added for all occurrences found.
    public int search(String word) {
        int firstOffset = -1;
        Highlighter highlighter = comp.getHighlighter();
        // Remove any existing highlights for last word
        Highlighter.Highlight[] highlights = highlighter.getHighlights();
        for (int i = 0; i < highlights.length; i++) {
            Highlighter.Highlight h = highlights[i];
            if (h.getPainter() instanceof UnderlineHighlighter.UnderlineHighlightPainter) {
                highlighter.removeHighlight(h);
            }
        }
        if (word == null || word.equals("")) {
            return -1;
        }
        String content = null; // Look for the word we are given - insensitive search
        try {
            Document d = comp.getDocument();
            content = d.getText(0, d.getLength()).toLowerCase();
        } catch (BadLocationException e) {
            return -1; // Cannot happen
        }
        word = word.toLowerCase();
        int lastIndex = 0;
        int wordSize = word.length();
        while ((lastIndex = content.indexOf(word, lastIndex)) != -1) {
            int endIndex = lastIndex + wordSize;
            try {
                highlighter.addHighlight(lastIndex, endIndex, painter);
            } catch (BadLocationException e) {
                // Nothing to do
            }
            if (firstOffset == -1) {
                firstOffset = lastIndex;
            }
            lastIndex = endIndex;
        }
        return firstOffset;
    }
}

class UnderlineHighlighter extends DefaultHighlighter {

    protected static final Highlighter.HighlightPainter sharedPainter = new UnderlineHighlightPainter(null);// Shared painter used for default highlighting   
    protected Highlighter.HighlightPainter painter; // Painter used for this highlighter

    public UnderlineHighlighter(Color c) {
        painter = (c == null ? sharedPainter : new UnderlineHighlightPainter(c));
    }

    // Convenience method to add a highlight with the default painter.
    public Object addHighlight(int p0, int p1) throws BadLocationException {
        return addHighlight(p0, p1, painter);
    }

    @Override
    public void setDrawsLayeredHighlights(boolean newValue) {
        if (newValue == false) {// Illegal if false - we only support layered highlights
            throw new IllegalArgumentException(
                    "UnderlineHighlighter only draws layered highlights");
        }
        super.setDrawsLayeredHighlights(true);
    }

    // Painter for underlined highlights
    public static class UnderlineHighlightPainter extends LayeredHighlighter.LayerPainter {

        protected Color color; // The color for the underline

        public UnderlineHighlightPainter(Color c) {
            color = c;
        }

        @Override
        public void paint(Graphics g, int offs0, int offs1, Shape bounds,
                JTextComponent c) {// Do nothing: this method will never be called            
        }

        @Override
        public Shape paintLayer(Graphics g, int offs0, int offs1, Shape bounds,
                JTextComponent c, View view) {
            g.setColor(color == null ? c.getSelectionColor() : color);
            Rectangle alloc = null;
            if (offs0 == view.getStartOffset() && offs1 == view.getEndOffset()) {
                if (bounds instanceof Rectangle) {
                    alloc = (Rectangle) bounds;
                } else {
                    alloc = bounds.getBounds();
                }
            } else {
                try {
                    Shape shape = view.modelToView(offs0, Position.Bias.Forward, offs1,
                            Position.Bias.Backward, bounds);
                    alloc = (shape instanceof Rectangle) ? (Rectangle) shape : shape.getBounds();
                } catch (BadLocationException e) {
                    return null;
                }
            }
            FontMetrics fm = c.getFontMetrics(c.getFont());
            int baseline = alloc.y + alloc.height - fm.getDescent() + 1;
            g.drawLine(alloc.x, baseline, alloc.x + alloc.width, baseline);
            g.drawLine(alloc.x, baseline + 1, alloc.x + alloc.width, baseline + 1);
            return alloc;
        }
    }
}
Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top