Using DocumentFilter iteratively

130 views Asked by At

I am trying to run this code:

How to change the color of specific words in a JTextPane?

private final class CustomDocumentFilter extends DocumentFilter
{
        private final StyledDocument styledDocument = yourTextPane.getStyledDocument();

    private final StyleContext styleContext = StyleContext.getDefaultStyleContext();
    private final AttributeSet greenAttributeSet = styleContext.addAttribute(styleContext.getEmptySet(), StyleConstants.Foreground, Color.GREEN);
    private final AttributeSet blackAttributeSet = styleContext.addAttribute(styleContext.getEmptySet(), StyleConstants.Foreground, Color.BLACK);

// Use a regular expression to find the words you are looking for
Pattern pattern = buildPattern();

@Override
public void insertString(FilterBypass fb, int offset, String text, AttributeSet attributeSet) throws BadLocationException {
    super.insertString(fb, offset, text, attributeSet);

    handleTextChanged();
}

@Override
public void remove(FilterBypass fb, int offset, int length) throws BadLocationException {
    super.remove(fb, offset, length);

    handleTextChanged();
}

@Override
public void replace(FilterBypass fb, int offset, int length, String text, AttributeSet attributeSet) throws BadLocationException {
    super.replace(fb, offset, length, text, attributeSet);

    handleTextChanged();
}

/**
 * Runs your updates later, not during the event notification.
 */
private void handleTextChanged()
{
    SwingUtilities.invokeLater(new Runnable() {
        @Override
        public void run() {
            updateTextStyles();
        }
    });
}

/**
 * Build the regular expression that looks for the whole word of each word that you wish to find.  The "\\b" is the beginning or end of a word boundary.  The "|" is a regex "or" operator.
 * @return
 */
private Pattern buildPattern()
{
    StringBuilder sb = new StringBuilder();
    for (String token : ALL_WORDS_THAT_YOU_WANT_TO_FIND) {
        sb.append("\\b"); // Start of word boundary
        sb.append(token);
        sb.append("\\b|"); // End of word boundary and an or for the next word
    }
    if (sb.length() > 0) {
        sb.deleteCharAt(sb.length() - 1); // Remove the trailing "|"
    }

    Pattern p = Pattern.compile(sb.toString());

    return p;
}


private void updateTextStyles()
{
    // Clear existing styles
    styledDocument.setCharacterAttributes(0, yourTextPane.getText().length(), blackAttributeSet, true);

    // Look for tokens and highlight them
    Matcher matcher = pattern.matcher(yourTextPane.getText());
    while (matcher.find()) {
        // Change the color of recognized tokens
        styledDocument.setCharacterAttributes(matcher.start(), matcher.end() - matcher.start(), greenAttributeSet, false);
    }
}
}

And

((AbstractDocument) yourTextPane.getDocument()).setDocumentFilter(new CustomDocumentFilter());

I would like to use it iteratively, that is, that any new string ALL_WORDS_THAT_YOU_WANT_TO_FIND will be automatically colored. I thought of deleting

styledDocument.setCharacterAttributes(0, yourTextPane.getText().length(), blackAttributeSet, true);

(that is, to not destroy the previous colored words) but it does not work: it only keeps colored the input words given at the last iteration. How could I do that?

1

There are 1 answers

3
Freek de Bruijn On BEST ANSWER

Edit: updated after two questions in the comments

So you want to add words to the list and update the JTextPane? In that case you would want to make sure that the list gets updated and used each time the updateTextStyles method runs.

You can use multiple lists of words that can apply unique formatting to the text. The code that you started with uses a regular expression, which you could expand to multiple regular expressions. You can also search for exact case sensitive matches of sub strings (or text fragments) without looking at word boundaries, as is used in the code below.

This means that the formatting of some text might be changed multiple times by matches from different groups. The order in which you search will determine the end result. For example, this small example allows you to fill a text pane and add new words to three highlight groups (with colors red, orange, and blue):

Screenshot of the example program

Here is the code of the three classes in the example (using Java 8):

IterativeDocumentFilter.java:

import java.awt.*;
import java.util.List;
import javax.swing.*;
import javax.swing.border.*;
import javax.swing.text.*;

public class IterativeDocumentFilter {
    public static void main(String[] arguments) {
        SwingUtilities.invokeLater(
                () -> new IterativeDocumentFilter().createAndShowGui()
        );
    }

    private void createAndShowGui() {
        JFrame frame = new JFrame("Stack Overflow");
        frame.setBounds(100, 100, 1000, 600);
        frame.setDefaultCloseOperation(WindowConstants.EXIT_ON_CLOSE);

        JPanel mainPanel = new JPanel(new BorderLayout());

        JTextPane textPane = new JTextPane(new DefaultStyledDocument());
        CustomDocumentFilter documentFilter = new CustomDocumentFilter(textPane);
        textPane.setBorder(new LineBorder(Color.BLACK, 1));
        enlargeFont(textPane);

        mainPanel.add(textPane, BorderLayout.CENTER);
        mainPanel.add(createBottomPanels(documentFilter), BorderLayout.PAGE_END);

        frame.getContentPane().add(mainPanel);
        frame.setVisible(true);
    }

    private JPanel createBottomPanels(CustomDocumentFilter documentFilter) {
        JPanel bottomPanels = new JPanel();
        bottomPanels.setLayout(new BoxLayout(bottomPanels, BoxLayout.PAGE_AXIS));
        for (HighlightGroup highlightGroup : documentFilter.getHighlightGroups()) {
            List<String> textFragments = highlightGroup.getTextFragments();
            JPanel groupPanel = new JPanel(new FlowLayout(FlowLayout.LEADING));
            JLabel textFragmentsLabel = new JLabel("Current text fragments: "
                                                   + textFragments);
            textFragmentsLabel.setForeground(highlightGroup.getColor());
            JLabel addTextFragmentLabel = new JLabel("Additional text fragment:");
            addTextFragmentLabel.setForeground(highlightGroup.getColor());
            JTextField addTextFragmentTextField = new JTextField(28);
            JButton addTextFragmentButton = new JButton("Add text fragment");
            addTextFragmentButton.setForeground(highlightGroup.getColor());

            addTextFragmentButton.addActionListener(actionEvent -> {
                String newTextFragment = addTextFragmentTextField.getText().trim();
                if (!textFragments.contains(newTextFragment)) {
                    textFragments.add(newTextFragment);
                    documentFilter.handleTextChanged();
                    textFragmentsLabel.setText("Current text fragments: "
                                               + textFragments);
                }
                addTextFragmentTextField.setText("");
            });

            groupPanel.add(addTextFragmentLabel);
            groupPanel.add(addTextFragmentTextField);
            groupPanel.add(addTextFragmentButton);
            textFragmentsLabel.setBorder(new EmptyBorder(0, 42, 0, 0));
            groupPanel.add(textFragmentsLabel);

            enlargeFont(addTextFragmentLabel);
            enlargeFont(addTextFragmentTextField);
            enlargeFont(addTextFragmentButton);
            enlargeFont(textFragmentsLabel);

            bottomPanels.add(groupPanel);
        }

        return bottomPanels;
    }

    private void enlargeFont(Component component) {
        component.setFont(component.getFont().deriveFont(16f));
    }
}

CustomDocumentFilter.java:

import java.awt.*;
import java.util.*;
import java.util.List;
import javax.swing.*;
import javax.swing.text.*;

public class CustomDocumentFilter extends DocumentFilter
{
    private final JTextPane textPane;
    private final List<HighlightGroup> highlightGroups;

    private final StyleContext styleContext = StyleContext.getDefaultStyleContext();
    private final AttributeSet blackAttributeSet
            = styleContext.addAttribute(styleContext.getEmptySet(),
                                        StyleConstants.Foreground, Color.BLACK);

    public CustomDocumentFilter(JTextPane textPane) {
        this.textPane = textPane;

        highlightGroups = createHighlightGroups();
        ((AbstractDocument) textPane.getDocument()).setDocumentFilter(this);
    }

    private List<HighlightGroup> createHighlightGroups() {
        List<HighlightGroup> groups = new ArrayList<>();
        groups.add(new HighlightGroup(Arrays.asList("one", "two", "three"), Color.RED));
        groups.add(new HighlightGroup(Arrays.asList("a", "the"), Color.ORANGE));
        groups.add(new HighlightGroup(Arrays.asList("th", "o"), Color.BLUE));
        return groups;
    }

    public List<HighlightGroup> getHighlightGroups() {
        return highlightGroups;
    }

    @Override
    public void insertString(FilterBypass fb, int offset, String text,
                             AttributeSet attributeSet) throws BadLocationException {
        super.insertString(fb, offset, text, attributeSet);

        handleTextChanged();
    }

    @Override
    public void remove(FilterBypass fb, int offset, int length)
            throws BadLocationException {
        super.remove(fb, offset, length);

        handleTextChanged();
    }

    @Override
    public void replace(FilterBypass fb, int offset, int length, String text,
                        AttributeSet attributeSet) throws BadLocationException {
        super.replace(fb, offset, length, text, attributeSet);

        handleTextChanged();
    }

    /**
     * Runs your updates later, not during the event notification.
     */
    public void handleTextChanged()
    {
        SwingUtilities.invokeLater(this::updateTextStyles);
    }

    private void updateTextStyles()
    {
        // Reset the existing styles by using the default black style for all text.
        StyledDocument document = textPane.getStyledDocument();
        document.setCharacterAttributes(0, textPane.getText().length(),
                                        blackAttributeSet, true);

        // Apply styling for the different groups (the order of the groups is relevant).
        for (HighlightGroup highlightGroup : highlightGroups) {
            highlightGroup.highlightWords(textPane);
        }
    }
}

HighlightGroup.java:

import java.awt.*;
import java.util.*;
import java.util.List;
import javax.swing.*;
import javax.swing.text.*;

public class HighlightGroup {
    private final List<String> textFragments;
    private final Color color;
    private final AttributeSet attributeSet;

    public HighlightGroup(List<String> textFragments, Color color) {
        this.textFragments = new ArrayList<>(textFragments);
        this.color = color;

        StyleContext styleContext = StyleContext.getDefaultStyleContext();
        this.attributeSet = styleContext.addAttribute(styleContext.getEmptySet(),
                                                      StyleConstants.Foreground,
                                                      color);
    }

    public List<String> getTextFragments() {
        return textFragments;
    }

    public Color getColor() {
        return color;
    }

    public void highlightWords(JTextPane textPane) {
        String text = textPane.getText();
        StyledDocument styledDocument = textPane.getStyledDocument();
        for (String textFragment : textFragments) {
            int fromIndex = 0;
            int startIndex = text.indexOf(textFragment, fromIndex);
            while (startIndex != -1) {
                // Change the color of recognized text fragments.
                styledDocument.setCharacterAttributes(startIndex, textFragment.length(),
                                                      attributeSet, false);

                fromIndex = startIndex + textFragment.length();
                startIndex = text.indexOf(textFragment, fromIndex);
            }
        }
    }
}