How do I change the color of specific words in a JTextPane
just while the user is typing?
Should I override JTextPane
paintComponent
method?
- 163,965
- 36
- 203
- 405
- 1,610
- 6
- 31
- 63
-
1This answer regarding [**How to colour specific word in JTextPane**](http://stackoverflow.com/a/9652143/1057230), might can help you too. – nIcE cOw Jan 18 '13 at 14:59
5 Answers
No. You are not supposed to override the paintComponent() method. Instead, you should use StyledDocument
. You should also delimit the words by your self.
Here is the demo, which turns "public", "protected" and "private" to red when typing, just like a simple code editor:
import javax.swing.*;
import java.awt.*;
import javax.swing.text.*;
public class Test extends JFrame {
private int findLastNonWordChar (String text, int index) {
while (--index >= 0) {
if (String.valueOf(text.charAt(index)).matches("\\W")) {
break;
}
}
return index;
}
private int findFirstNonWordChar (String text, int index) {
while (index < text.length()) {
if (String.valueOf(text.charAt(index)).matches("\\W")) {
break;
}
index++;
}
return index;
}
public Test () {
setDefaultCloseOperation(EXIT_ON_CLOSE);
setSize(400, 400);
setLocationRelativeTo(null);
final StyleContext cont = StyleContext.getDefaultStyleContext();
final AttributeSet attr = cont.addAttribute(cont.getEmptySet(), StyleConstants.Foreground, Color.RED);
final AttributeSet attrBlack = cont.addAttribute(cont.getEmptySet(), StyleConstants.Foreground, Color.BLACK);
DefaultStyledDocument doc = new DefaultStyledDocument() {
public void insertString (int offset, String str, AttributeSet a) throws BadLocationException {
super.insertString(offset, str, a);
String text = getText(0, getLength());
int before = findLastNonWordChar(text, offset);
if (before < 0) before = 0;
int after = findFirstNonWordChar(text, offset + str.length());
int wordL = before;
int wordR = before;
while (wordR <= after) {
if (wordR == after || String.valueOf(text.charAt(wordR)).matches("\\W")) {
if (text.substring(wordL, wordR).matches("(\\W)*(private|public|protected)"))
setCharacterAttributes(wordL, wordR - wordL, attr, false);
else
setCharacterAttributes(wordL, wordR - wordL, attrBlack, false);
wordL = wordR;
}
wordR++;
}
}
public void remove (int offs, int len) throws BadLocationException {
super.remove(offs, len);
String text = getText(0, getLength());
int before = findLastNonWordChar(text, offs);
if (before < 0) before = 0;
int after = findFirstNonWordChar(text, offs);
if (text.substring(before, after).matches("(\\W)*(private|public|protected)")) {
setCharacterAttributes(before, after - before, attr, false);
} else {
setCharacterAttributes(before, after - before, attrBlack, false);
}
}
};
JTextPane txt = new JTextPane(doc);
txt.setText("public class Hi {}");
add(new JScrollPane(txt));
setVisible(true);
}
public static void main (String args[]) {
new Test();
}
}
The code is not so beautiful since I typed it quickly but it works. And I hope it will give you some hint.
- 5,007
- 2
- 14
- 27
-
-
@SachinKumar, Do you mean the loops in `findLastNonWordChar` and `findFirstNonWordChar`? Yes, you can find the first non word character without looping, see the answer [here](http://stackoverflow.com/questions/11214245/java-find-index-of-first-regex), but there is no direct way to find the 'last index of' a non-word character. You can try using `split()` method. – shuangwhywhy Oct 07 '13 at 03:10
-
I am trying same thing using DocumentListener(), but i got an exception "Attempt to mutate in notification". How can i accomplish this using DocumentListener(). – Sachin Kumar Oct 08 '13 at 08:48
-
@SachinKumar, listener is not the same thing here. All the methods in `DocumentListener` are triggered after updating is happen. – shuangwhywhy Oct 08 '13 at 10:12
-
-
@shuangwhywhy do you know whether there is a possibility to have colored text in JTextPane ? – Tomasz Waszczyk Dec 03 '13 at 10:40
-
1@MrPhi, Yes, but you can only set the same color (the same style, including font size, font family, bold, italic, etc.) to all of the characters in JTextPane. – shuangwhywhy Dec 03 '13 at 14:20
-
Overwriting paintComponent
will not help you.
This is not an easy one, but not impossible either. Something like this will help you:
DefaultStyledDocument document = new DefaultStyledDocument();
JTextPane textpane = new JTextPane(document);
StyleContext context = new StyleContext();
// build a style
Style style = context.addStyle("test", null);
// set some style properties
StyleConstants.setForeground(style, Color.BLUE);
// add some data to the document
document.insertString(0, "", style);
You may need to tweak this, but at least it shows you where to start.
- 31,836
- 4
- 59
- 77
-
now let me ask other small question here, is it possible to make a JTextPane with only 1 row? just like a JTextField – Soheil Jan 18 '13 at 21:15
Another solution is to use a DocumentFilter
.
Here is an example:
Create a class that extends DocumentFilter:
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);
}
}
}
All you need to do then is apply the DocumentFilter
that you created to your JTextPane
as follows:
((AbstractDocument) yourTextPane.getDocument()).setDocumentFilter(new CustomDocumentFilter());
- 3,668
- 32
- 28
-
This answer deserves more likes! It works perfectly for me, whereas the other answers are more confusing to use. – AMACB Mar 27 '16 at 02:58
-
1Since the DocumentFilter doesn’t actually do any filtering, it probably makes more sense to use a [DocumentListener](https://docs.oracle.com/javase/9/docs/api/javax/swing/text/Document.html#addDocumentListener-javax.swing.event.DocumentListener-) instead. – VGR Sep 29 '17 at 19:15
You can extend DefaultStyledDocument like I did here for an SQL editor I am building with keyword text coloring ...
import java.util.ArrayList;
import java.util.List;
import javax.swing.text.AttributeSet;
import javax.swing.text.BadLocationException;
import javax.swing.text.DefaultStyledDocument;
import javax.swing.text.Style;
public class KeywordStyledDocument extends DefaultStyledDocument {
private static final long serialVersionUID = 1L;
private Style _defaultStyle;
private Style _cwStyle;
public KeywordStyledDocument(Style defaultStyle, Style cwStyle) {
_defaultStyle = defaultStyle;
_cwStyle = cwStyle;
}
public void insertString (int offset, String str, AttributeSet a) throws BadLocationException {
super.insertString(offset, str, a);
refreshDocument();
}
public void remove (int offs, int len) throws BadLocationException {
super.remove(offs, len);
refreshDocument();
}
private synchronized void refreshDocument() throws BadLocationException {
String text = getText(0, getLength());
final List<HiliteWord> list = processWords(text);
setCharacterAttributes(0, text.length(), _defaultStyle, true);
for(HiliteWord word : list) {
int p0 = word._position;
setCharacterAttributes(p0, word._word.length(), _cwStyle, true);
}
}
private static List<HiliteWord> processWords(String content) {
content += " ";
List<HiliteWord> hiliteWords = new ArrayList<HiliteWord>();
int lastWhitespacePosition = 0;
String word = "";
char[] data = content.toCharArray();
for(int index=0; index < data.length; index++) {
char ch = data[index];
if(!(Character.isLetter(ch) || Character.isDigit(ch) || ch == '_')) {
lastWhitespacePosition = index;
if(word.length() > 0) {
if(isReservedWord(word)) {
hiliteWords.add(new HiliteWord(word,(lastWhitespacePosition - word.length())));
}
word="";
}
}
else {
word += ch;
}
}
return hiliteWords;
}
private static final boolean isReservedWord(String word) {
return(word.toUpperCase().trim().equals("CROSS") ||
word.toUpperCase().trim().equals("CURRENT_DATE") ||
word.toUpperCase().trim().equals("CURRENT_TIME") ||
word.toUpperCase().trim().equals("CURRENT_TIMESTAMP") ||
word.toUpperCase().trim().equals("DISTINCT") ||
word.toUpperCase().trim().equals("EXCEPT") ||
word.toUpperCase().trim().equals("EXISTS") ||
word.toUpperCase().trim().equals("FALSE") ||
word.toUpperCase().trim().equals("FETCH") ||
word.toUpperCase().trim().equals("FOR") ||
word.toUpperCase().trim().equals("FROM") ||
word.toUpperCase().trim().equals("FULL") ||
word.toUpperCase().trim().equals("GROUP") ||
word.toUpperCase().trim().equals("HAVING") ||
word.toUpperCase().trim().equals("INNER") ||
word.toUpperCase().trim().equals("INTERSECT") ||
word.toUpperCase().trim().equals("IS") ||
word.toUpperCase().trim().equals("JOIN") ||
word.toUpperCase().trim().equals("LIKE") ||
word.toUpperCase().trim().equals("LIMIT") ||
word.toUpperCase().trim().equals("MINUS") ||
word.toUpperCase().trim().equals("NATURAL") ||
word.toUpperCase().trim().equals("NOT") ||
word.toUpperCase().trim().equals("NULL") ||
word.toUpperCase().trim().equals("OFFSET") ||
word.toUpperCase().trim().equals("ON") ||
word.toUpperCase().trim().equals("ORDER") ||
word.toUpperCase().trim().equals("PRIMARY") ||
word.toUpperCase().trim().equals("ROWNUM") ||
word.toUpperCase().trim().equals("SELECT") ||
word.toUpperCase().trim().equals("SYSDATE") ||
word.toUpperCase().trim().equals("SYSTIME") ||
word.toUpperCase().trim().equals("SYSTIMESTAMP") ||
word.toUpperCase().trim().equals("TODAY") ||
word.toUpperCase().trim().equals("TRUE") ||
word.toUpperCase().trim().equals("UNION") ||
word.toUpperCase().trim().equals("UNIQUE") ||
word.toUpperCase().trim().equals("WHERE"));
}
}
Simply add it to your class like so:
import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Font;
import javax.swing.JFrame;
import javax.swing.JScrollPane;
import javax.swing.JTextPane;
import javax.swing.text.BadLocationException;
import javax.swing.text.Style;
import javax.swing.text.StyleConstants;
import javax.swing.text.StyleContext;
public class SQLEditor extends JFrame {
private static final long serialVersionUID = 1L;
public SQLEditor() {
StyleContext styleContext = new StyleContext();
Style defaultStyle = styleContext.getStyle(StyleContext.DEFAULT_STYLE);
Style cwStyle = styleContext.addStyle("ConstantWidth", null);
StyleConstants.setForeground(cwStyle, Color.BLUE);
StyleConstants.setBold(cwStyle, true);
final JTextPane pane = new JTextPane(new KeywordStyledDocument(defaultStyle, cwStyle));
pane.setFont(new Font("Courier New", Font.PLAIN, 12));
JScrollPane scrollPane = new JScrollPane(pane);
getContentPane().add(scrollPane, BorderLayout.CENTER);
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
setSize(375, 400);
}
public static void main(String[] args) throws BadLocationException {
SQLEditor app = new SQLEditor();
app.setVisible(true);
}
}
Here's the missing HiliteWord class ...
public class HiliteWord {
int _position;
String _word;
public HiliteWord(String word, int position) {
_position = position;
_word = word;
}
}
- 1,418
- 10
- 16
-
-
-
-
1Given that `word.toUpperCase().trim()` is not a cheap operation, copying and converting the string contents two times in the worst case, it is not a good idea to do it up to 70 times in row. Considering that `word` should not contain white-space, `trim()` is obsolete and `equalsIgnoreCase` can perform the desired operation directly without the creation of new strings. – Holger Apr 09 '21 at 15:20
@Constantin
Dear Constantin, I used Your fine Solution for my little Project and after only a few Adjustments your solution worked well for me.
If you allow, my Changes were:
My Use of your Class KeywordStyledDocument in my own JFrame:
StyleContext styleContext = new StyleContext();
Style defaultStyle = styleContext.getStyle(StyleContext.DEFAULT_STYLE);
This line I Have changed: MutableAttributeSet cwStyle = Functions.style(true, false, Color.RED);
private JTextPane jTextPaneNumbers = new JTextPane(new KeywordStyledDocument(defaultStyle, cwStyle));
I outsourced the supply of the cwStyle Instance in a static Function called style:
public static MutableAttributeSet style(boolean boldness, boolean italic, Color color) {
MutableAttributeSet s = new SimpleAttributeSet();
StyleConstants.setLineSpacing(s, -0.2f);
StyleConstants.setBold(s, boldness);
StyleConstants.setItalic(s, italic);
StyleConstants.setForeground(s, color);
return s;
}
Furthermore as you see above the cwStyle Class is not longer an Instance of StyleConstants but an Inctance of MutableAttributeSet. Therefore naturally I had to change the Constructor of your KeywordStyledDocumentClass as well:
public KeywordStyledDocument(Style defaultStyle, MutableAttributeSet cwStyle) {
_defaultStyle = defaultStyle;
_cwStyle = cwStyle;
}
After this litle changes and adding my own "words" in your isReservedWord Function and add my Characters ' and * to your processWord Function:
...word.toUpperCase().trim().equals("UNION") ||
word.toUpperCase().trim().equals("UNIQUE") ||
word.toUpperCase().trim().equals("WHERE") ||
word.trim().equals("''''''") ||
word.trim().equals("******")
);
if(!(Character.isLetter(ch) || Character.isDigit(ch) || ch == '_' || ch == '\'' || ch == '*')) {
I became my whished Result:
Thank you very much for showing your Code here.
- 33
- 6