Drag-and-Drop Overview

Java > Core SWING advanced programming > 3. TEXT COMPONENTS WITH CUSTOM VIEWS > Highlighting and Highlighters

 

Highlighting and Highlighters

In the development of ExtendedParagraphView, we glossed over one important point. To see what the problem is, run the example program again using the command:

 java AdvancedSwing.Chapter3.ExtendedParagraphExample 

With the example running, create a selection by clicking somewhere with the mouse and dragging over the text you want to select. You'll see that the selection appears, highlighted in the usual selection color, as shown in Figure 3-14. You probably won't be surprised by this, but if you refer to Figure 1-10 in Chapter 1, you'll see that the selection highlighter is drawn on the background of the component, before the Views get a chance to draw the text and other content. This means that the selection will be drawn and then the ExtendedParagraphView will fill the paragraphs background. This should obscure the selection, so you shouldn't be able to see it, yet the selection still appears.

Figure 3-14. A custom ParagraphView with text selected.
graphics/03fig14.gif

What actually happens is exactly what was described in the last paragraph, but there's one more operation that gets performed that we haven't yet told you about. There are two kinds of highlighters ordinary and layered. Ordinary highlighters are drawn directly on the background of the component, as shown in Figure 2-10, by the painting code in BasicTextUI, just before the Views' paint methods are called. In contrast, layered highlighters are drawn by the text-rendering Views before they carry out their own drawing. The selection appears in this example because the default highlighter used to draw the selection is actually a layered highlighter and is drawn by the Labelviews that render the paragraph's text. Had this not been the case, the implementation of ExtendedParagraphView would have had to avoid painting the paragraph background over the selection. In this section, we'll discuss highlighters in more detail and show you how to use a custom highlighter to do more than simply show selected text.

The Highlighter and Highlight Painter Interfaces

Every highlighter implements the Highlighter interface defined in the javax.swing.text package. Highlighter consists of a relatively small set of methods and two nested interfaces:

 public interface Highlighter {    public void install(JTextComponent c);    public void deinstall(JTextComponent c);    public void paint(Graphics g);    public Object addHighlight(int p0, int p1,                               HighlightPainter p)                               throws BadLocationException;    public void removeHighlight(Object tag);    public void removeAllHighlights();    public void changeHighlight(Object tag, int p0, int p1)                                throws BadLocationException;    public Highlight[] getHighlights();       public interface HighlightPainter {          public void paint(Graphics g, int p0, int p1,                            Shape bounds, JTextComponent c);    }    public interface Highlight {       public int getStartOffset();       public int getEndOffset();       public HighlightPainter getPainter();    } } 

The install and deinstall methods are used to connect a Highlighter to a JTextComponent and to subsequently break the association. A Highlighter is associated with only one text component at a time and would usually save a reference to that component in the install method, for later use. The paint method requests that the Highlighter draw all its highlights on its related text component, using the Graphics object passed as its argument to perform the rendering. Well say more about how highlight painting is performed later.

A Highlighter can actually support several highlighted areas at the same time, each of them represented internally by an object that implements the Highlighter.Highlight interface. New highlights are added using the addHighlight method, which is given the start and end offsets of the part of the text components model to which the highlight is to be applied and an object that can perform the painting of that highlight. There is no need for different highlights connected to a single Highlighter to use the same painter, because the painter to be used is stored along with the offsets. This method returns an Object of unspecified type, which the caller of addHighlight can use to remove the highlight later using the removeHighlight method, or to change the start and/or end offset of the highlighted area using changeHighlight. You should not make any assumption about the type of the Object returned by addHighlight. The removeAllHighlights method, as its name suggests, removes all the highlights associated with a Highlighter. In the case of the default Highlighter used by the Swing text components, this method is called each time a Highlighter is installed into a text component, so that Highlighters can be reused without accidentally carrying over highlights from one text component to another one. Finally, the getHighlights method returns an array of objects that all implement the Highlight interface. Each of these objects represents one highlight that has been added to the Highlighter. The parameters associated with a Highlight can be obtained using its getstartoffset, getEndOffset, and get-Painter methods. You'll see how these methods, and many of the Highlighter methods, are typically used in the example shown later in this section (Listing 3-6).

Every highlight is actually drawn by an object that implements the HighlightPainter interface, which has only a paint method. The paint method of Highlighter actually renders its highlights by looping through them and calling the paint method of the HighlightPainter associated with each of them. The Swing text package provides a single implementation of the Highlighter.HighlightPainter interface (called DefaultHighlighter) that renders highlights by drawing a block of solid color over the area of the text component used to display the portion of the model between the two offsets (p0 and p1) given to its paint method. This highlighter is the one that draws the selection that you see when you drag the mouse over some text, or move the cursor with the SHIFT key pressed. Later in this section, you'll see how to implement a custom highlighter that is slightly more interesting than the default one.

Core Note

Actually, the highlighter installed by default in all the Swing text components is an instance of an inner class of BasicTextUI called BasicHighlighter. This class is, however, a direct subclass of DefaultHighlighter that adds nothing other than a declaration that it implements the interface UIResource, which marks it as having been installed automatically rather than by explicit programmer action.



The LayeredHighlighter Class

As well as the Highlighter interface, the Swing text package also contains an abstract class called LayeredHighlighter, which is defined as follows:

 public abstract class LayeredHighlighter implements Highlighter {    public abstract void paintLayeredHighlights(Graphics g, int                                  p0,int p1.                                  Shape viewBounds,                                  TextComponent editor,                                  View view);    static public abstract class LayerPainter                        implements Highlighter.HighlightPainter {       public abstract Shape paintLayer(Graphics g, int p0,                                  int p1,Shape viewBounds,                                  JTextComponenteditor,                                    View view);    } } 

This class claims to implement the Highlighter interface but does not actually provide implementations of any of its methods; this simply obliges any concrete subclass of LayeredHighlighter to implement the Highlighter methods and ensures that a LayeredHighlighter can be treated as a Highlighter. Such a subclass must also implement the paintLayeredHighlights method.

A layered highlighter is simply a highlighter that can support layered highlights as well as nonlayered highlights. In implementation terms, the difference between a layered highlight and a nonlayered highlight is that the former uses a layered highlight painter, that is, a painter that is derived from the inner class LayeredHighlighter.LayerPainter, while the latter uses a nonlayered highlight painter. A layered highlighter is capable of supporting layered highlights because it provides the paintLayeredHighlights method as well as the paint method used to draw nonlayered highlights.

Here's a summary of the way in which highlights are drawn from within the paint method of BasicTextUI:

  • The Highlighter is obtained from the host text component using its getHighlighter method. At this stage, nothing has yet been drawn on the component.

  • BasicTextUI's paint method calls the paint method of the Highlighter. This is a request to draw any nonlayered highlights that have been added to the Highlighter. It implements this by looping through all its installed highlights and calling the paint method of all those that have a painter that is not derived from LayeredHighlighter.LayerPainter.

  • The top level View is asked to paint itself. This ripples down through all the nested Views, which all have their paint methods called at the appropriate times. If a View supports the rendering of layered highlights, before it renders the text that it maps it gets the text components Highlighter and checks whether it is derived from LayeredHighlighter. If it is, it calls the Highlighter's paintLayeredHighlights method. This causes the layered highlighter to loop through all of its highlights and call the paintLayer method of all those that have highlight painters derived from LayeredHighlighter .LayerPainter that is, the layered highlights. The View then proceeds with its normal rendering activity.

The following Views support layered highlights by invoking the paintLayeredHighlights method at the appropriate time:

  • Imageview (in the HTML package)

  • LabelView

  • PasswordView

  • PlainView

  • WrappedPlainView

Because the LayeredHighlighter.LayerPainter interface actually extends Highlighter HighlightPainter, it follows that a layered highlight painter must have both a paintLayer and a paint method, so it could also, theoretically, be treated as a nonlayered highlight. The default Swing highlighter is derived from LayeredHighlighter, so it supports both layered and nonlayered highlights. The default highlight painter is also a layered highlight painter. This means that the selection highlight is actually drawn as a layered highlight, which, as noted earlier, is why selection rendering still works with Views like ExtendedParagraphview that would obscure a nonlayered highlight by filling their backgrounds.

Creating a Custom Highlighter

Now let's illustrate all the theory that you've seen so far with a simple example of a custom Highlighter. A common feature of a text viewer or editor is the ability to search for a string. When the string has been located, the program usually highlights one or all the instances that it has found. This is an obvious application for a Highlighter.

There are two separate parts to the solution of this problem implementing the Highlighter and writing the code that searches the text for a string and decides which parts of the text should be highlighted. These parts are quite distinct and are connected only by the Highlighter interface that the searching code uses to add highlights where necessary. In fact, the searching code doesn't need to know which Highlighter is being used, so there is no requirement for us to implement a special Highlighter to provide a solution to this problem we do only for the purposes of illustration.

An Underlining Highlighter

As you know, the default Highlighter works by drawing a solid colored rectangle on the background of the areas that it highlights, so if you use this Highlighter, all the regions that you highlight will look just like selected text does. In the case of our word search program, if you selected some text and then performed a word search, it would be impossible to distinguish the results of the search from text that had been selected. There are four ways to address this problem:

  1. Remove any text selection before performing a search.

  2. Use the default Highlighter but create the highlights by using a different HighlightPainter that works just like the default one, but draws rectangles in a different color.

  3. Use the default Highlighter but create the highlights by using a new HighlightPainter that makes the text stand out in some other way.

  4. Use a custom Highlighter and a custom HighlightPainter.

The first solution would certainly get rid of the ambiguity problem, but it wouldn't be very user friendly. The second solution is both workable and easy to implement. Because the Highlighter addHighlight method requires that you specify a HighlightPainter (even if you are going to use the default one), using this solution is as simple as creating a variant of the default painter with your chosen color, like this:

 Highlighter.HighlightPainter painter = new            DefaultHighlighter.DefaultHighlightPainter(Color.red); 

and then using this painter when adding a new highlight:

 Highlighter highlighter = textPane.getHighlighter();  highlighter.addHighlight(startOffset, endOffset, painter); 

in which textPane is a reference to the JTextPane containing the text being highlighted. The only reason for not taking this approach is that it doesn't demonstrate how to implement a new Highlighter!

The last two solutions do, however, involve creating something new. The third solution uses a custom HighlightPainter in conjunction with whichever Highlighter is installed when the text component is created. Because the visual aspects of highlights are determined entirely by the HighlightPainter, implementing this approach would be sufficient to show you how to create highlights that are something other than a solid block of color. Creating a custom HighlightPainter does not oblige you to create a new Highlighter any HighlightPainter can be used with the default Highlighter, or with any other Highlighter, so the fourth solution is strictly overkill. Nevertheless, we choose to adopt this solution to demonstrate how to create both your own Highlighter as well as a new HighlightPainter. Another reason for taking this approach is to enable us to implement our new HighlightPainter as an inner class of its own Highlighter, which, although not at all necessary, keeps the implementation close to that of the DefaultHighlightPainter in the Swing text package.

Core Note

Do not be misled by this into assuming that you must implement a new Highlighter each time you create a new HighlightPainter. It is perfectly acceptable to implement a freestanding HighlightPainter that is not part of any Highlighter.



The new HighlightPainter that we are going to implement will underline the text that it is attached to instead of drawing a solid block of color behind the text. Although this might sound simple, as you'll see there are some assumptions that need to made about the way in which the underlying views render the text that they map. The Highlighter will primarily be a wrapper class for the new HighlightPainter; to allow it to have some added value, we implement an overloaded addHighlight method that supplies an underlining HighlightPainter, thus making it slightly easier to apply underlines if you use this Highlighter instead of the default one.

Core Note

If you look at Table 2-1 in Chapter 2, you'll see that there is a standard attribute called Underline that can be used to underline blocks of text So why create a HighlightPainter that can do the some thing? While in many cases you would use the Underline attribute to underline text as a permanent change, a Highlighter is usually used to give temporary emphasis to a block of text Using attributes for this purpose is more cumbersome, because you have to apply and remove the attributes yourself, whereas a Highlighter takes care of drawing a highlight given only the range of offsets to which it should apply. Directly manipulating the underlying attributes is more complex because you need to be careful to properly restore the previous attributes when the highlighting is removed. In the case of underlining highlights, if the affected text was already underlined, you would need to remember that the underlining attribute should not be removed if the highlight is removed. Finally, using attributes to create a temporary display effect would not be appropriate if the text component contained data loaded from a file (such as an HTML file) that might be written back to the file, because the modified attributes would then be stored as part of the permanent external representation, which is probably not a desirable side effect.



The implementation of our custom Highlighter, called UnderlineHighlighter, and the more useful painter is shown in Listing 3-5.

Listing 3-5 A Custom Highlighter
 package AdvancedSwing.Chapter3 ; import javax.swing.text.*; import java.awt.*; public class UnderlineHighlighter extends                                   DefaultHighlighter {    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);     }     public void setDrawsLayeredHighlights(boolean newValue) {        // Illegal if false -        //we only support layered highlights        if (newValue == false) {           throw new IllegalArgumentException(                "UnderlineHighlighter only draws                layered highlights");        }        super.setDrawsLayeredHighlights(true);     }     // Painter for underlined highlights     public static class UnderlineHighlightPainter extends                            LayeredHighlighter,LayerPainter {        public UnderlineHighlightPainter(Color c) {           color = c;        }        public void paint(Graphics g, int offs0,int offs1,                  Shape bounds, JTextComponent c) {           //Do nothing: this method will never be called        }     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,                            offsl, 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;        }         protected Color color; // The color for the underline     }      // Shared painter used for default highlighting      protected static final                  Highlighter.HighlightPainter sharedPainter                  = new UnderlineHighlightPainter(null);      // Painter used for this highlighter      protected Highlighter.HighlightPainter painter; } 
Implementing the UnderlineHighlighter

Let's look first at the UnderlineHighlighter class. This class extends and inherits most of its behavior from the standard Highlighter (DefaultHigh-lighter). Because of this, it automatically implements the Highlighter interface and is an instance of LayeredHighlighter, which allows it to handle both layered and nonlayered highlights without the need for any new code to be added. The only useful additional feature of this class is the overloaded addHighlight method, which allows the programmer to create highlights that use the underlining highlight painter without having to directly create an instance of that painter. In other words, the method

 public void addHighlight(int startOffset, int endOffset); 

can be used instead of the more usual

 public void addHighlight(int startOffset, int endOffset,                        Highlighter.HighlightPainter painter); 

to add underlining highlights. The implementation of this method is, as you might expect, trivial it just invokes the three argument addHighlight method of its superclass, passing through the offsets and a reference to an underlining HighlightPainter created in the constructor. In fact, as you'll see shortly, the underlining painter allows you to specify the color that it will use to draw the underline, which can be given as null to indicate that the component's selection highlight color should be used. The color to be used for underline highlights created using the UnderlineHighlighter addHighlight method is passed as an argument to the UnderlineHighlighter constructor. If this argument is null, a single painter shared by all UnderlineHighlighters that are created in this way, can be used; a private painter is created if a specific color is requested.

The only DefaultHighlighter method that UnderlineHighlighter overrides is setDrawsLayeredHighlights. As you know, if this method is called with argument false, all highlights drawn by the target Highlighter are treated as nonlayered, even if the HighlightPainters that they use are layered painters. For reasons that will shortly become clear, it is not acceptable to disable the layered painting of underlining highlights, so this method is overridden to throw a runtime exception if an attempt is made to do this by invoking it with argument false. Calling this method with argument true is harmless and does not cause an exception. Because the constructor of the class LayeredHighlighter (from which UnderlineHighlighter is descended) switches on layered highlighting by default, it is guaranteed that UnderlineHighlighter will always draw highlights configured with layered painters as layered highlights.

The UnderliningHighlightPainter

Now let's look at the highlight painter, which is the most useful and interesting part of this example. The painter is implemented as an inner class of UnderlineHighlighter called UnderlineHighlightPainter and is derived from the abstract class LayeredHighlighter.LayerPainter, which means that it will be a layered highlight painter. Because its superclass is completely abstract, UnderlineHighlightPainter does not inherit any behavior and is obliged to implement both of the abstract methods of its superclass the paintLayer method declared by LayerPainter and the paint method of the Highlighter. HighlightPainter interface that LayerPainter claims to implement. These methods should contain the code that provides, respectively, the layered and nonlayered highlighting functionality of the painter.

Let's look at the paintLayer method first. This method is called from the paint method of any View that supports layered highlights and is responsible for drawing the part of a single highlight that overlaps the space allocated to the View. If a single highlight crosses more than one view, it will be drawn in pieces as a result of separate invocations of paintLayer from each affected View. The paintLayer method is defined like this:

 public Shape paintLayer(Graphics g, int offs0, int offs1,                    Shape bounds, JTextComponent c, View view) 

and the parameters are as follows:

Graphics g A Graphics context covering the visible part of the host text component.
int offs0, offs1 The model offsets of the start and end of the part of the highlight that intersects this View.
Shape bounds The bounds of the area occupied by the View that contains the highlight being rendered.
JTextComponent c The text component that contains the highlight. This argument is usually used to get global state, such as the color that should be used to draw the highlight which, in the case of the default selection highlighter and the underlining highlighter implemented in this section, is taken from the component s selection color.
view view The View on which the highlight is being rendered.

The return value of this method is a Shape that represents the actual area of the View over which the highlight was rendered. To see how these arguments are used, lets look at a typical example.

Figure 3-15 shows a JTextPane containing a single paragraph of text split over three lines. As you know from the discussion of views earlier in this chapter, the text will be rendered by three Views of type Labelview.LabelFragment; these Views are represented in Figure 3-15 by the three boxes inside a larger box, which represents the JTextPane. The shaded area represents one contiguous highlight, which extends from model offset 13 to offset 39. Although we are implementing a highlighter that underlines the text that it is associated with, in Figure 3-15 we represent the highlight as a shaded area for clarity.

Figure 3-15. Highlights and Views.
graphics/03fig15.gif

To draw the complete highlight, the paintLayer method of the highlight painter will be invoked three times, once for each View that intersects the highlighted region. The first call draws the part of the highlight from model offset 13 to offset 18. The Graphics object for this call, as for the other two, will have its origin at the top left of the component and will cover the visible part of the JTextPane. If the component were mounted in a JScrollPane, some of the component might not be visible and the top left of the visible area might not correspond to the origin of the JTextPane or to the origin of the Graphics object. These complications are not, however, relevant to the highlight paintLayer method because all the coordinates it deals with are relative to the origin of the JTextPane, which makes them independent of the effects of scrolling.

For the first call, the parameters to paintLayer (other than the Graphics object and the JTextComponent arguments, which are the same on all three calls) are:

 offs0 = 13, offs1 = 18, Shape = (x = 3, y = 3, width = 144,                         height = 15), View = top View 

Similarly, the parameters for the other two calls are:

 offs0 = 18, offs1 = 36, Shape = (x = 3, y = 21, width = 144,                         height = 15), View = middle View offs0 = 36, offs1 = 39, Shape = (x = 3, y = 39, width = 144,                         height = 15), View = bottom View 

Notice that the offsets for all three calls are limited to the area covered by the highlight, whereas the Shape that is passed in is the bounding rectangle for the View, which will never be smaller than the part of the highlight being rendered on each call.

Now let's look at what needs to be done to draw the underline that represents the highlight. To draw a line, we need to select the correct color into the Graphics object, and then call drawLine with the coordinates of the start and end points of the line to be drawn. Selecting the correct color is simple the appropriate color is given as an argument to the constructor of the UnderlineHighlightPainter. If this value is supplied as null, the color is obtained from the host text component, a reference to which is passed to the paintLayer method. You can see the code that gets and selects the color at the beginning of the paintLayer implementation in Listing 3-5.

To determine where to draw the underline, it is necessary to convert the start and end offsets of the highlighted region to coordinate positions on the surface of the text component. This is exactly the job performed by the View modelToview method. In Listing 3-5, we use a variant of modelToView that takes two offsets and returns a Shape that represents the screen area between them. For the three Views shown in Figure 3-15, the three Shapes returned would represent the shaded areas; they would be Rectangles with the following positions and dimensions:

Top View: x = 107, y = 3, width = 40, height = 15
Middle View: x = 3, y = 21, width = 144, height = 15
Bottom View: x = 3, y = 39, width = 24, height = 15

in which, for simplicity, we have assumed the use of a monospaced font in which each character occupies 8 pixels horizontally and is 15 pixels high and that a gap of 3 pixels is left between Views. In the special case in which the highlighted area covers the entire View, there is no need to call modelToView to obtain the correct Shape we can just use the one passed to paintLayer, which represents the space allocated to the View, as shown in Listing 3-5.

The Shape thus obtained gives the x coordinates for both ends of the underline, as you can see by comparing the values shown above with the coordinates in Figure 3-15. What about the y coordinate? This is slightly more difficult. To draw a line beneath some text, you need to know where the baseline of that text is placed within its bounding box, which depends partly on the metrics of the font in use and on the code that does the drawing, which could place the baseline anywhere within the bounding box. Assuming that the height of the font and the height of the bounding box are the same, the usual location of the baseline would be a distance from the bottom of the bounding box equal to the descent value of the font in use. This is, in fact, the way in which the methods in the Swing text package that handle the drawing of text for the standard Views work. The implementation of paintLayer in Listing 3-5 assumes that this is how the baseline location is determined and draws two lines just below the assumed text baseline.

Core Note

Some of the methods that handle the drawing and placement of text for Label view and similar Views are described later in this chapter.



That's all there is to the paintLayer method, so what about paint? If you look at Listing 3-5, you'll see that the paint method has no implementation at all. This is because, as we said earlier, we have restricted the UnderlineHighlightPainter to draw only layered highlights. Because it can't legally be used to draw nonlayered highlights, it does not require a real implementation of the paint method. But why did we place this restriction in the first place?

The reason for this restriction is the fact that the paint method receives much less information about the layout of the text component than the paintLayer method does. We've just seen that to place the underline properly, it is necessary to make some assumptions about the way in which the text rendering Views draw the text that they contain. In fact, we also made the assumption that the text that needs to be underlined in the paintLayer method all appears on a single line and, as long as we use the standard Views used with JTextPane, this will be true. Now consider what happens when nonlayered highlights are being drawn. Layered highlights are rendered by Views, but nonlayered highlights are drawn directly onto the background of the text component from paint method of BasicTextUI. At this level, there is no direct visibility of any of the Views the paint method sees only the drawing space allocated to the text component, with no clue as to how the text, or other elements such as images and inline components, are laid out within that area. Suppose, then, that a highlight is added that would cover an area of a text component such as that shown as the shaded area in Figure 3-16.

Figure 3-16. Drawing a nonlayered highlight.
graphics/03fig16.gif

This diagram looks a little bare you can't see where the text is or even where the individual line of text might appear. This is intentional when the paint method of a HighlightPainter is called, this is exactly how the text component will look (except that the shading won't be there either). The fact that a highlight has been created to cover the part of the model that will be drawn within the shaded area doesn't give any information at all about how much text, if any, will appear in that region. If you had to draw the underlines that the underlining Highlighter would need to draw in Figure 3-16, where would you place them? You could assume that the initial part of the shaded area represents a line of text and draw it just below the baseline for that stretch and the same assumption could be made about the last piece. What about the rectangular section in the middle? How many lines of text appear here? To even make a guess at where the underlines should be drawn, you would need to assume that all the text lines are of equal height, which need not be true. Even assuming that there is any text in this area is not safe the whole area could be occupied by an image. If this were a layered highlight, however, the drawing would be done by the Views that cover the area. The Views reflect the actual layout of text and other objects, removing the need to guess about that, and they also know whether they should draw a highlight at all. The standard View that renders inline images (Iconview), for example, does not draw rendered highlights so, if an embedded image is surrounded by text and an underlining highlight is applied to all the text, the underline would not be drawn across the image.

This example illustrates that it is not always possible to create a highlight painter for a nonlayered highlighting effect. The problems that we have seen with the underlining painter do not exist for the default selection highlighter, which only needs to fill one, two, or three rectangular areas with a solid color and therefore does not need to concern itself with how the text component's content is organized within the highlighted area.

Using the Highlighter

Having implemented the highlighter, let's now demonstrate how to use it in the context of a simple example program. In this example, we're going to load the content of a plain text file into a JTextPane and allow the user to specify a search string. Every occurrence of this string that appears in the file will be underlined by adding a highlight drawn by the UnderlineHighlightPainter. There are several aspects of this example that are not directly related to the use of the highlighter; to avoid getting bogged down in irrelevant detail, we'll gloss over most of the implementation and focus mainly on how the highlighting is done. You can try this example using the command

 java AdvancedSwing.Chapter3.HighlightExample filename 

in which filename is the full name of the file to be read. When the program starts, you'll see a JTextPane containing the file's content with an input field just below it. If you type a word from the input file (or even a part of a word) into the input field and press RETURN, you should see all occurrences of that word underlined, as shown in Figure 3-17. The display will scroll, if necessary, to show the first instance found.

Figure 3-17. A word search program using underlining highlights.
graphics/03fig17.gif

The code for this example is shown in Listing 3-6. The only code of any interest in the main method appears toward the end, where listeners are added to both the input field and the Document in the JTextPane. The listener attached to the input field is activated when the user types a search string and presses return. It uses a package private class called WordSearcher, which we will look at shortly, to actually perform the search and apply the underline highlights. The DocumentListener is called when the content of the JTextPane changes and its job is to research the text for occurrences of the search string. This is necessary, because changing the content might add or remove occurrences of that string. For example, in the case shown in Figure 3-17, if the user started editing the displayed file content and typed the word JFrame somewhere, this listener would immediately underline it. On the other hand, if the user deleted a character from a highlighted instance of the word JFrame, that instance would no longer match the search string and the highlight should be removed. This functionality is actually all part of the WordSearcher class rather than the DocumentListener the listener simply arranges for the search to be performed from scratch whenever the Document content changes.

Listing 3-6 A Word Search Program Using a Custom Highlighter
 package AdvancedSwing.Chapter3; import javax.swing.*; import javax.swing.event.*; import javax.swing.text.*; import java.awt.*; import java.awt.event.*; import java.io.*; public class HighlightExample {    public static void main(String[] args) {       if (args.length != 1) {          System.out.println("Please supply the name                             Of a file");          System.exit(1) ;       }       JFrame f = new JFrame("Highlight example");       final JTextPane textPane = new JTextPane();       textPane.setHighlighter(highlighter);       JPanel pane = new JPanel();       pane.setLayout(new BorderLayout());       pane.add(new JLabel("Enter word: "), "West");       final JTextField tf = new JTextField();       pane.add(tf, "Center");       f.getContentPane().add(pane, "South");       f.getContentPane().add(new JScrollPane(textPane),                              "Center");       try {          textPane.read(new FileReader(args[0]), 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() {          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() {              public void insertUpdate(DocumentEvent evt) {                    searcher.search(word);              }              public void removeUpdate(DocumentEvent evt) {                    searcher.search(word);              }              public void changedUpdate(DocumentEvent evt) {              }           });        f.setSize(400, 400);        f.setVisible(true);    }    public static String word;    public static Highlighter highlighter =                       new UnderlineHighlighter(null); } //A simple class that searches for a word in //a document and highlights occurrences of that word class WordSearcher {    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;      }      // Look for the word we are given - insensitive search      String content = null;      try {         Document d = comp.getDocument();         content = d.getText(0,                             d.getLength()).toLowerCase();      } catch (BadLocationException e) {         // Cannot happen         return -1;      }      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;     }     protected JTextComponent comp;     protected Highlighter.HighlightPainter painter; } 

The relevant part of this example is in the WordSearcher class, which is created with its associated JTextComponent as the only argument to its constructor. During construction, the WordSearcher object creates an UnderlineHighlightPainter, which it will use to add highlights to ranges of characters with the text component that match its search string. Note that only one highlight painter is constructed no matter how many highlights are actually added because High-lightPainters do not hold any state that relates to individual highlights and so can be shared. In this case, the underlines will be drawn in red. The real work of the WordSearcher object is carried out in the search method, which is passed a search string. The implementation shown here is a case-insensitive search; a more useful class would add a case-sensitive search and the ability to match only on entire words and would add to the WordSearcher class properties that could be set to enable these features. Such complications would not, however, clarify the use of highlighters.

The first thing that the search method does is remove from the installed Highlighter any highlights that it added last time it was called. The quickest way to remove all highlights from a Highlighter is to use the removeAllHighlights method, but that is usually overkill because, for one thing, it would also remove the highlight corresponding to any text that is currently selected, which might not be appropriate.

Core Note

A more realistic search example would probably confine its search to the selected region if there is one and search the whole Document if there is not. Again, to avoid complicating the example with features that do not relate directly to Highlighters, this functionality is not implemented here.



To avoid losing unrelated highlights, the search method removes only those that are rendered with the UnderlineHighlightPainter, by using the Highlighter getHighlights method to get an array of the current highlights and walking down it checking the object type of the painter of each highlight in the returned array, which is obtained by calling the Highlight getPainter method.

Having cleared unrequired highlights, the text is searched for matches with the given search string. When a match is found, a new highlight is added by calling the addHighlight method of the text components installed Highlighter, passing the start and end offset of the matching character range to specify the region to be highlighted and the shared UnderlineHighlightPainter to do the highlight painting. Note that this method uses whichever Highlighter is currently installed in the text component it does not need to install an underlineHighlighter because, as we said earlier, you can use the UnderlineHighlightPainter with any Highlighter.

Notice that when you add a highlight to a Highlighter, you supply the start and end offsets as integers, not as Position objects (see Chapter 1 for a description of Positions). This means that the highlight does not automatically track changes in the document. If you allow the user to edit the document content in such a way that the start or end offset of a highlight would need to change to remain attached to its associated text, you must arrange to respond to this by changing each affected highlight appropriately. In this example, we achieve this by performing the search from scratch in response to any change in the document (see the DocumentListener in the main method), partly because this is the simplest thing to do and partly because the changes might change the number of highlights required, as described earlier.

There is actually a subtlety in the implementation of the search method that is not related to Highlighters but which is worth mentioning here. Note that the search is done by extracting the content of the text component in the form of a string, using the following code:

 String content = null; try {    Document d = comp.getDocument();    content = d.getText(0, d.getLength()).toLowerCase(); } catch (BadLocationException e) {    // Cannot happen    return -1; } 

You might wonder why you can't just use the apparently more convenient getText method provided by JTextPane, (which is inherited from JEditorPane), like this:

 String content = comp.getText(); 

In some cases, this would work. However, if the file loaded into the JTextPane used carriage return/line feed pairs as line delimiters (as is usually the case on the Windows platform), you won't get the results you expect. Consider the example of a file that contains two lines of text, as follows:

 import javax.swing.*;\r\n import javax.swing.text.*;\r\n 

When this is loaded into the JTextPane, the fact that the \r\n sequence was used as the line separator is recorded as a property of the Document and, when getText is subsequently called, both the carriage return and newline characters are written out between each pair of lines. Hence, for this file, getText would return the following String:

 import javax.swing.*;\r\nimport javax.swing.text.*;\r\n 

This is the String that is used for the search, so if a search for the word import were performed, it would be found at offsets 0 and 23. Accordingly, highlights would be applied covering model offsets 0 through 5 and 23 through 28. Unfortunately, this is not the correct result, because the Document does not contain the \r characters. As a result, the content of the model actually amounts to this:

 import javax.swing.*;\nimport javax.swing.text.*;\n 

So although the first highlight is properly placed, the second will result in the string, mport being underlined. The carriage returns and new lines are written back by the JEditorPane getText method because of the property set on the Document when its content was loaded. The Document getText method does not do this, however, so it leads to the correct results. This subtlety should be borne in mind any time you need to relate offsets in a Document to those in the file from which it was loaded.

 

 



Core Swing
Core Swing: Advanced Programming
ISBN: 0130832928
EAN: 2147483647
Year: 1999
Pages: 55
Authors: Kim Topley

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net