HomeDigital EditionSys-Con RadioSearch Java Cd
Advanced Java AWT Book Reviews/Excerpts Client Server Corba Editorials Embedded Java Enterprise Java IDE's Industry Watch Integration Interviews Java Applet Java & Databases Java & Web Services Java Fundamentals Java Native Interface Java Servlets Java Beans J2ME Libraries .NET Object Orientation Observations/IMHO Product Reviews Scalability & Performance Security Server Side Source Code Straight Talking Swing Threads Using Java with others Wireless XML
 

Last month we discussed the use of Swing's Document model to create a syntax-highlighting Document model that we could just plug into JTextPane and use. This month we'll continue with that and add complete support for comments, strings and numbers. We'll also cover how easy it is to actually use the model we've developed, and test things out as we go along.

To start things out let's try and use what we have so far. If we look at the code below we can see that plugging the Document we've created into a JTextPane component is quite easy. Defining the keywords is also very easy.

JTextPane editor = new JTextPane();
CodeDocument doc = new CodeDocument();
Vector keywords = new Vector();
keywords.addElement("abstract");
keywords.addElement("boolean");
...
doc.setKeywords(keywords);
editor.setDocument(doc);

We could have just created the CodeDocument on the fly and passed it in as an argument to the constructor of the JTextPane class, but then the keywords for the Document wouldn't have been set and we'd have had to retrieve the Document and then add them.

By running the code above and typing in some text, we get the window in Figure 1 to come up.

Figure 1
Figure 1:

A word of caution: If you use this and the first word you type in is in your keywords list, youšll get an exception. This is a known bug on the Swing Web site (for more information look at http://developer.java.sun.com/developer/bugParade/bugs/4128967.html) with no known workaround. You may also notice that when you type for a while and then move the cursor back to a previous position and start typing again, the JTextPane doesn't quite repaint itself correctly. This is also a known bug of the JTextPane (for more information see http://developer.java.sun.com/developer/ bugParade/bugs/4127974.html). This has been fixed in Swing version 1.1.

Now you can test the modifications we'll be making to the CodeDocument class.

Changing the insertString Method
Some of you may have noticed that the Document Model code we developed in the previous article worked only if you actually typed in the text manually. If we had called the insertString method and passed in a string with a length longer than one character, the syntax highlighting wouldn't have worked. To fix that we'll make a simple change, moving the code that actually did the comparing to another method, and changing the insertString method to support any size string. We'll create a method called processChar that will actually do the work of checking the kind of character we're dealing with and, in turn, call the correct method to handle that character. To make the whole thing work, we can then just loop through each of the characters in the string that's passed into the insertString method, starting at the offset position (the offs variable passed into the insertString method) and ending at the offset plus the length of the string passed in. For each character we find we can call the processChar method, and voila! We can now handle any size string. To get an idea of the code, take a look below.

public void insertString(int offs,
String str,
AttributeSet a) throws BadLocationException{
super.insertString(offs, str, normal);

int strLen = str.length();
int endpos = offs + strLen;
int strpos;
for (int i=offs;i<endpos;i++){
currentPos = i;
strpos = i - offs;
processChar(str.charAt(strpos));
}
currentPos = offs;
}

The only problem with the solution above is that when reading large chunks of text you may notice a slowdown in JTextPane's performance while it loads and parses all the text. A possible solution would be to use a thread to handle any text not immediately visible to the user, parse it in the background and then...well, that'll be the subject of a future article!

Adding String Support
Now that we can support entered text of any size, let's add highlighting support for strings. Adding string support isn't too hard now that we have the basic pieces in place. We'll add a variable that we'll use to keep track of whether we're entering a string. If this flag is turned on, any text we enter following the double quote ("") will be colored in a different foreground color. Also, if this flag is turned on, no keyword processing will occur. A carriage return will automatically shut the flag off. We'll need to keep track of the start position where the flag was first turned on. This will allow us to type in some text representing a string, hit the carriage return and type other stuff, and then put the caret back on the line where the string was and resume entering the string text ­ all the while keeping the formats correct. We'll also need a variable that represents the style attribute we want for strings.

In addition to variables we'll need some new methods. For strings we'll create a checkForString() method that will determine whether we're inside a string. If it finds that we are, the mode variable will be set appropriately (this will be explained in more detail a few paragraphs down). We'll also need a method called insertTextString() that will actually reinsert the properly formatted string.

Adding Number Support
Adding support for numbers is similar to adding support for strings. We'll add a variable to hold the style attribute for numbers, and also add two more methods: checkForNumber() and insertNumberString().

Like strings, the checkForNumber() method determines whether we're actually entering valid digits. If we are, it sets the CodeDocument's mode accordingly. Like insertTextString, insertNumberString inserts the properly formatted text as a number.

Adding Comment Support
Comments are handled in a similar manner. Two new methods are needed, checkForComment() and insertCommentString(). Like numbers and text, the checkForComments method determines if a comment block has been started (it checks for the "/*" combination to start a comment and the "*/" to end a comment block). If it has, it changes the attributes of the entered text by calling the insertCommentString() method. Again, like numbers and strings, the insertCommentString() changes the formatting attri-butes of the string accordingly, and then inserts into the document. Another private variable is needed to hold the formatting attributes for comments.

Putting It All Together
Now it's time to get down and dirty. We're finally going to look at the whole process, first in general terms and then in more detail using code examples. As we mentioned earlier, the original insertString method of the CodeDocument class was changed, and much of the logic was moved to a new method called processChar(), thus allowing us to handle not only single keystrokes, but also to programmatically insert multiple character strings. ProcessChar() works by making certain assumptions about what characters will follow other characters. Based on this, it sets the insert mode for the current position of incoming text. Based on this mode (which is simply a private integer variable), it can then determine which insertXXXString() method to call.

For example, when it encounters the character '9', it figures there's a very good chance that this is the start of a number or that a number is currently in the process of being entered. To verify this it calls the checkForNumber() method. If checkForNumber() determines that we're still entering a number, it sets the mode to the number entry mode. If it discovers any other character present, however, such as a space, a parenthesis or a letter, it sets the mode to the default text entry mode (the number entry mode is represented by the static constant NUMBER_MODE, while the text entry mode is represented by the static constant TEXT_MODE). The other checkForXXX() methods work in a similar fashion. After the checkForXXX() method has returned, the mode will have been set correctly and, based on this, the proper insertXXXString() method can be called. If the mode is in TEXT_MODE, the formatting is left alone.

Now look at the code in Listing 1 for method processChar. The first thing the method does is to check if we're in COMMENT_MODE. Because comments can include anything (aside from the comment-terminating characters), we'll let the mode default to COMMENT_MODE; otherwise we'll change it to default to TEXT_MODE, which is the standard text entry mode (no formatting). Next, a switch statement is created based on the character passed to the processChar method. As mentioned before, the method works on the assumption that the character being entered belongs to one of five groups (generic text, keywords, strings, numbers or comments), and that the case statements are grouped accordingly. Characters equaling the numbers '1' through '9' cause a check for numbers; characters equaling '*' or '/' suggest the possible start or end of a comment block, and a check is made; and so on. Once the switch statement is finished executing, a final check is made for quoted strings if we're still in TEXT_MODE. Finally, depending on which mode we're in, a call is made to the appropriate insertXXXString method.

There are four check methods: checkForComment(), checkForKeyword(), checkForNumber() and checkForString(). While there are some differences from the original checkForKeywords() method, the basic idea, as discussed last month, is the same. We retrieve the current element, get the text from it, find our position in the element and then, starting at the end, we walk backward until a delimiter of some sort is found and then set the mode accordingly. Let's look at some of the checkForString() method's code to examine this more closely. We'll assume we already have the correct offset from the Document's element.

..
..
int quoteCount = 0;
if ((offs >= 0) && (offs <= strLen-1)){
i = offs;
while (i >0){
//the while loop walks back until we hit a delimiter

char charAt = elementText.charAt(i);
if ((charAt == '"')){
quoteCount ++;
}
i--;
}
int rem = quoteCount % 2;
mode = (rem == 0) ? TEXT_MODE: STRING_MODE;
}

The code is fairly simple: we loop backward until we run out of characters to process, each time comparing a character from the element text. If the character is a double quote (" "), we add one to the local variable called quoteCount. Once we're done with the loop, we check the remainder of the quoteCount divided by two and assign the results to another local variable called rem. Why do this? Again, this is another assumption about the way words are put together. Since quotes always come in pairs, if our remainder isn't equal to zero, we know we're "inside" a string quotation. Otherwise we can safely assume that we're "outside" and are just entering normal text.

And Finally...
I think at this point I've probably run out of column space. We now have a syntax-highlighting Document class that supports any user-defined set keywords, plus string, number and comment highlighting (see Listings 2 and 3). We've also seen how easy it is to incorporate our Document class into the JTextPane control to test our results as we go along. In the next article we'll add some more user-definable properties so you can change the color of highlighted string, comments, keywords and so on. We'll also look at adding support for a kind of "smart editor" ŕ la Borland's Code Insight features in their IDE editors.

References

  1. Topley, K. (1998). Core Java Foundation Classes. Prentice Hall PTR. Prentice-Hall Inc.
  2. Eckstein, R., Loy, R, and Wood, D. (1998) Java Swing. O'Reilly and Associates Inc.
About the Author
Jim Crafton is a staff consultant with Computer Sciences Corporation where he specializes in object-oriented development. He also develops advanced graphics software for Windows and the BeOS. He can be reached at [email protected] and has a Web site at www.one.net/~ddiego/.

	

Listing 1. for method processChar 

private void processChar(String str){ 
    char strChar = str.charAt(0); 
    if (mode != this.COMMENT_MODE){ 
      mode = TEXT_MODE; 
    } 
      switch (strChar){ 
        case ('{'):case ('}'):case (' '): case('\n'): 
        case ('('):case (')'):case (';'):case ('.'):{ 
          checkForKeyword(); 
          if (mode == STRING_MODE && strChar == '\n'){ 
            mode = TEXT_MODE; 
          } 
        } 
        break; 
        case ('"'):{ 
          insertTextString(str, currentPos); 
          this.checkForString(); 
        } 
        break; 
        case ('0'):case ('1'):case ('2'):case ('3'):case ('4'): 
        case ('5'):case ('6'):case ('7'):case ('8'):case ('9'):{ 
          checkForNumber(); 
        } 
        break; 
        case ('*'):case ('/'):{ 
          checkForComment(); 
        } 
        break; 
      } 
      if (mode == this.TEXT_MODE){ 
        this.checkForString(); 
      } 
      if (mode == this.STRING_MODE){ 
        insertTextString(str, this.currentPos); 
      } 
      else if (mode == this.NUMBER_MODE){ 
        insertNumberString(str, this.currentPos); 
      } 
      else if (mode == this.COMMENT_MODE){ 
        insertCommentString(str, this.currentPos); 
      } 
  

  } 

Listing 2. 

import com.sun.java.swing.*; 
import java.util.*; 

public class Test{ 

  public static void main(String[] args) { 
    JFrame frame = new JFrame(); 
    JTextPane editor = new JTextPane(); 
    CodeDocument doc = new CodeDocument(); 
    Vector keywords = new Vector(); 
    keywords.addElement("abstract"); 
    keywords.addElement("boolean"); 
    keywords.addElement("break"); 
    keywords.addElement("byte"); 
    keywords.addElement("byvalue"); 
    keywords.addElement("case"); 
    keywords.addElement("cast"); 
    keywords.addElement("catch"); 
    keywords.addElement("char"); 
    keywords.addElement("class"); 
    keywords.addElement("const"); 
    keywords.addElement("continue"); 
    keywords.addElement("default"); 
    keywords.addElement("do"); 
    keywords.addElement("double"); 
    keywords.addElement("extends"); 
    keywords.addElement("else"); 
    keywords.addElement("false"); 
    keywords.addElement("final"); 
    keywords.addElement("finally"); 
    keywords.addElement("float"); 
    keywords.addElement("for"); 
    keywords.addElement("future"); 
    keywords.addElement("generic"); 
    keywords.addElement("if"); 
    keywords.addElement("implements"); 
    keywords.addElement("import"); 
    keywords.addElement("inner"); 
    keywords.addElement("instanceof"); 
    keywords.addElement("int"); 
    keywords.addElement("interface"); 
    keywords.addElement("long"); 
    keywords.addElement("native"); 
    keywords.addElement("new"); 
    keywords.addElement("null"); 
    keywords.addElement("operator"); 
    keywords.addElement("outer"); 
    keywords.addElement("package"); 
    keywords.addElement("private"); 
    keywords.addElement("protected"); 
    keywords.addElement("public"); 
    keywords.addElement("rest"); 
    keywords.addElement("return"); 
    keywords.addElement("short"); 
    keywords.addElement("static"); 
    keywords.addElement("super"); 
    keywords.addElement("switch"); 
    keywords.addElement("synchronized"); 
    keywords.addElement("this"); 
    keywords.addElement("throw"); 
    keywords.addElement("throws"); 
    keywords.addElement("transient"); 
    keywords.addElement("true"); 
    keywords.addElement("try"); 
    keywords.addElement("var"); 
    keywords.addElement("void"); 
    keywords.addElement("volatile"); 
    keywords.addElement("while"); 
    doc.setKeywords(keywords); 
    editor.setDocument(doc); 
    frame.setSize(400,400); 
    frame.getContentPane().add(editor); 
    frame.show(); 
  } 
} 

Listing 3. 

import com.sun.java.swing.*; 
import com.sun.java.swing.text.*; 
import java.util.*; 
import java.awt.*; 

public class CodeDocument extends DefaultStyledDocument{ 
  private String word = ""; 
  private SimpleAttributeSet bold = new SimpleAttributeSet(); 
  private SimpleAttributeSet string = new SimpleAttributeSet(); 
  private SimpleAttributeSet normal = new SimpleAttributeSet(); 
  private SimpleAttributeSet number = new SimpleAttributeSet(); 
  private SimpleAttributeSet comments = new SimpleAttributeSet(); 
  private int currentPos = 0; 
  private Vector keywords = new Vector(); 
  public static int STRING_MODE = 10; 
  public static int TEXT_MODE = 11; 
  public static int NUMBER_MODE = 12; 
  public static int COMMENT_MODE = 13; 
  private int mode = TEXT_MODE; 
  

  public CodeDocument() { 
   //set the bold attribute 
   StyleConstants.setBold(bold, true); 
   StyleConstants.setForeground(string, Color.red); 
   StyleConstants.setForeground(number, Color.magenta); 
   StyleConstants.setForeground(comments, Color.blue); 
   StyleConstants.setItalic(comments, true); 
  } 
  

  private void insertKeyword(String str, int pos){ 
    try{ 
      //remove the old word and formatting 
      this.remove(pos - str.length(), str.length()); 
      /*replace it with the same word, but new formatting 
      *we MUST call the super class insertString method here, otherwise we 
      *would end up in an infinite loop !!!!!*/ 
      super.insertString(pos - str.length(), str, bold); 
    } 
    catch (Exception ex){ 
      ex.printStackTrace(); 
    } 
  } 
  

  private void insertTextString(String str, int pos){ 
    try{ 
      //remove the old word and formatting 
      this.remove(pos,str.length()); 
      super.insertString(pos, str, string); 
    } 
    catch (Exception ex){ 
      ex.printStackTrace(); 
    } 
  } 
  

  private void insertNumberString(String str, int pos){ 
    try{ 
      //remove the old word and formatting 
      this.remove(pos,str.length()); 
      super.insertString(pos, str, number); 
    } 
    catch (Exception ex){ 
      ex.printStackTrace(); 
    } 
  } 
  

  private void insertCommentString(String str, int pos){ 
    try{ 
      //remove the old word and formatting 
      this.remove(pos,str.length()); 
      super.insertString(pos, str, comments); 
    } 
    catch (Exception ex){ 
      ex.printStackTrace(); 
    } 
  } 
  

  private void checkForString(){ 
    int offs = this.currentPos; 
    Element element = this.getParagraphElement(offs); 
    String elementText = ""; 
    try{ 
      //this gets our chuck of current text for the element we're on 
      elementText = this.getText(element.getStartOffset(), 
                                 element.getEndOffset() - 
element.getStartOffset()); 
    } 
    catch(Exception ex){ 
      //whoops! 
      System.out.println("no text"); 
    } 
    int strLen = elementText.length(); 
    if (strLen == 0) {return;} 
    int i = 0; 
  

    if (element.getStartOffset() > 0){ 
      //translates backward if neccessary 
      offs = offs - element.getStartOffset(); 
    } 
    int quoteCount = 0; 
    if ((offs >= 0) && (offs <= strLen-1)){ 
      i = offs; 
      while (i >0){ 
      //the while loop walks back until we hit a delimiter 
  

        char charAt = elementText.charAt(i); 
        if ((charAt == '"')){ 
         quoteCount ++; 
        } 
        i--; 
      } 
      int rem = quoteCount % 2; 
      //System.out.println(rem); 
      mode = (rem == 0) ? TEXT_MODE: STRING_MODE; 
    } 
  } 
  

  private void checkForKeyword(){ 
    if (mode != TEXT_MODE) { 
      return; 
    } 
    int offs = this.currentPos; 
    Element element = this.getParagraphElement(offs); 
    String elementText = ""; 
    try{ 
      //this gets our chuck of current text for the element we're on 
      elementText = this.getText(element.getStartOffset(), 
element.getEndOffset() - element.getStartOffset()); 
    } 
    catch(Exception ex){ 
      //whoops! 
      System.out.println("no text"); 
    } 
    int strLen = elementText.length(); 
    if (strLen == 0) {return;} 
    int i = 0; 
  

    if (element.getStartOffset() > 0){ 
      //translates backward if neccessary 
      offs = offs - element.getStartOffset(); 
    } 
    if ((offs >= 0) && (offs <= strLen-1)){ 
      i = offs; 
      while (i >0){ 
      //the while loop walks back until we hit a delimiter 
        i--; 
        char charAt = elementText.charAt(i); 
        if ((charAt == ' ') | (i == 0) | (charAt =='(') | (charAt ==')') | 
            (charAt == '{') | (charAt == '}')){ //if i == 0 then we're at 
the begininng 
          if(i != 0){ 
            i++; 
          } 
          word = elementText.substring(i, offs);//skip the period 
  

          String s = word.trim().toLowerCase(); 
          //this is what actually checks for a matching keyword 
          if (keywords.contains(s)){ 
            insertKeyword(word, currentPos); 
          } 
          break; 
        } 
      } 
    } 
  } 
  

  private void checkForNumber(){ 
    int offs = this.currentPos; 
    Element element = this.getParagraphElement(offs); 
    String elementText = ""; 
    try{ 
      //this gets our chuck of current text for the element we're on 
      elementText = this.getText(element.getStartOffset(), 
element.getEndOffset() - element.getStartOffset()); 
    } 
    catch(Exception ex){ 
      //whoops! 
      System.out.println("no text"); 
    } 
    int strLen = elementText.length(); 
    if (strLen == 0) {return;} 
    int i = 0; 
  

    if (element.getStartOffset() > 0){ 
      //translates backward if neccessary 
      offs = offs - element.getStartOffset(); 
    } 
    mode = TEXT_MODE; 
    if ((offs >= 0) && (offs <= strLen-1)){ 
      i = offs; 
      while (i >0){ 
      //the while loop walks back until we hit a delimiter 
        char charAt = elementText.charAt(i); 
        if ((charAt == ' ') | (i == 0) | (charAt =='(') | (charAt ==')') | 
            (charAt == '{') | (charAt == '}') /*|*/){ //if i == 0 then 
we're at the begininng 
          if(i != 0){ 
            i++; 
          } 
          mode = NUMBER_MODE; 
          break; 
        } 
        else if (!(charAt >= '0' & charAt <= '9' | charAt=='.' 
                  | charAt=='+' | charAt=='-' 
                  | charAt=='/' | charAt=='*'| charAt=='%' | charAt=='=')){ 
          mode = TEXT_MODE; 
          break; 
        } 
        i--; 
      } 
    } 
  } 
  

  private void checkForComment(){ 
    int offs = this.currentPos; 
    Element element = this.getParagraphElement(offs); 
    String elementText = ""; 
    try{ 
      //this gets our chuck of current text for the element we're on 
      elementText = this.getText(element.getStartOffset(), 
element.getEndOffset() - element.getStartOffset()); 
    } 
    catch(Exception ex){ 
      //whoops! 
      System.out.println("no text"); 
    } 
    int strLen = elementText.length(); 
    if (strLen == 0) {return;} 
    int i = 0; 
  

    if (element.getStartOffset() > 0){ 
      //translates backward if neccessary 
      offs = offs - element.getStartOffset(); 
    } 
    if ((offs >= 1) && (offs <= strLen-1)){ 
      i = offs; 
      char commentStartChar1 = elementText.charAt(i-1); 
      char commentStartChar2 = elementText.charAt(i); 
      if (commentStartChar1 == '/' && commentStartChar2 == '*'){ 
          mode = COMMENT_MODE; 
          this.insertCommentString("/*", currentPos-1); 
      } 
      else if (commentStartChar1 == '*' && commentStartChar2 == '/'){ 
          mode = TEXT_MODE; 
          this.insertCommentString("*/", currentPos-1); 
      } 
    } 
  } 
  

  private void processChar(String str){ 
    char strChar = str.charAt(0); 
    if (mode != this.COMMENT_MODE){ 
      mode = TEXT_MODE; 
    } 
      switch (strChar){ 
        case ('{'):case ('}'):case (' '): case('\n'): 
        case ('('):case (')'):case (';'):case ('.'):{ 
          checkForKeyword(); 
          if (mode == STRING_MODE && strChar == '\n'){ 
            mode = TEXT_MODE; 
          } 
        } 
        break; 
        case ('"'):{ 
          insertTextString(str, currentPos); 
          this.checkForString(); 
        } 
        break; 
        case ('0'):case ('1'):case ('2'):case ('3'):case ('4'): 
        case ('5'):case ('6'):case ('7'):case ('8'):case ('9'):{ 
          checkForNumber(); 
        } 
        break; 
        case ('*'):case ('/'):{ 
          checkForComment(); 
        } 
        break; 
      } 
      if (mode == this.TEXT_MODE){ 
        this.checkForString(); 
      } 
      if (mode == this.STRING_MODE){ 
        insertTextString(str, this.currentPos); 
      } 
      else if (mode == this.NUMBER_MODE){ 
        insertNumberString(str, this.currentPos); 
      } 
      else if (mode == this.COMMENT_MODE){ 
        insertCommentString(str, this.currentPos); 
      } 
  

  } 
  

  private void processChar(char strChar){ 
      char[] chrstr = new char[1]; 
      chrstr[0] = strChar; 
      String str = new String(chrstr); 
      processChar(str); 
  } 
  

  public void insertString(int offs, 
                          String str, 
                          AttributeSet a) throws BadLocationException{ 
    super.insertString(offs, str, normal); 
  

    int strLen = str.length(); 
    int endpos = offs + strLen; 
    int strpos; 
    for (int i=offs;i<endpos;i++){ 
      currentPos = i; 
      strpos = i - offs; 
      processChar(str.charAt(strpos)); 
    } 
    currentPos = offs; 
  } 
  

  public Vector getKeywords(){ 
    return this.keywords; 
  } 
  

  public void setKeywords(Vector aKeywordList){ 
    if (aKeywordList != null){ 
      this.keywords = aKeywordList; 
    } 
  } 
} 
  

 

All Rights Reserved
Copyright ©  2004 SYS-CON Media, Inc.
  E-mail: [email protected]

Java and Java-based marks are trademarks or registered trademarks of Sun Microsystems, Inc. in the United States and other countries. SYS-CON Publications, Inc. is independent of Sun Microsystems, Inc.