Chapter 8. Writing style sheet extensions

Table of Contents

1. The problem
2. The solution
2.1. Solution of problem #1: invoke a custom method computing a CSS property value
2.1.1. The StyleSheetExtension class
2.1.2. The localize method
2.2. Solution of problem #2: implement a StyleSpecs which knows how to style nested emphasis elements
2.2.1. The implementation of interface StyleSpecs
2.3. Solution of problem #3: invoke a custom method computing the number of a listitem and use a BasicElementObserver to update orderedlists when needed to
2.3.1. Interface BasicElementObserver
2.3.2. The implementation of interface BasicElementObserver
2.4. Solution of problem #4: implement an AttributeValueEditor
2.4.1. Passive custom views
2.4.2. Active custom views: specialized editors embedded in the DocumentView
3. Compiling and testing the sample style sheet extensions

What to do when cascading style sheets (CSS) are not powerful enough to style XML elements exactly like you want? Answer: write custom style sheet extensions in Java™ using the APIs described in this chapter.

1. The problem

In this chapter, we will use a custom XML schema: email.xsd. This schema is used to model a simple email message:

  • Root element message contains:

    • Required from, to elements,

    • Optional replyTo, cc, bcc elements,

    • Required subject, body elements,

    • Optional signature, attachments elements.

  • A body contains at least one para, literallayout, itemizedlist or orderedlist element (similar to their DocBook counterparts).

  • A para contains text interspersed with any number of email, ulink, emphasis, inlinegraphic or smiley elements.

  • An emphasis element has a role attribute with 2 values properly styled by the CSS style sheet: bold and highlight.

  • A smiley is an empty element having an emotion attribute with many possible values: happy, wink, vicious, etc.

Using a CSS without custom extensions to style an email message gives good results, but here we want excellent results. And CSS alone cannot solve the following problems:

Problem #1

A message has From:, To:, Subject:, etc, headers. We would like to see the name of these headers displayed in French (De:, À:, Objet:, etc) if the user adds the attribute xml:lang=fr to the root message element.

Figure 8.1. A message having a xml:lang="fr" attribute

A message having a xml:lang="fr" attribute

Problem #2

An emphasis element can contain another emphasis element and this, at any nesting level. We would like emphasis elements having an even number of emphasis ancestors to be displayed using an italic font. We would like emphasis elements having an odd number of emphasis ancestors to be displayed using a plain (non-italic) font.

Figure 8.2. In the three following paragraphs, nested emphasis elements (containing words "nested emphasis text") are displayed using a non-italic font

In the three following paragraphs, nested emphasis elements (containing words "nested emphasis text") are displayed using a non-italic font

Problem #3

Like in DocBook, orderedlist elements have a continuation attribute. This attribute has two possible values:

restarts

This is the default value of the continuation attribute.

If a message body contains an orderedlist having 2 listitems (therefore numbered 1 and 2), followed by another orderedlist having 2 listitems and if the second orderedlist has continuation=restarts, its listitems are numbered 1 and 2.

continues

If a message body contains an orderedlist having 2 listitems (therefore numbered 1 and 2), followed by another orderedlist having 2 listitems and if the second orderedlist has continuation=continues, its listitems are numbered 3 and 4.

Figure 8.3. Two orderedlists, the second one having a continuation="continues" attribute

Two orderedlists, the second one having a continuation="continues" attribute

Problem #4

The text of a message can be interspersed with smileys expressing emotions: happy, sad, tired, etc. Not only we would like these smiley elements to be represented graphically (, , , etc) but we also would like to use a combobox embedded in the document view to directly edit the emotion attribute of a smiley element.

Figure 8.4. Four smiley elements represented by four comboboxes

Four smiley elements represented by four comboboxes

2. The solution

2.1. Solution of problem #1: invoke a custom method computing a CSS property value

The from header, for example, is normally styled using these CSS rules:

from {
    display: block;
    margin-left: 15ex;
}

from:before {
    display: marker;
    color: #004080;
    font-weight: bold;
    content: "From:";
}

To solve our problem, we need to replace:

content: "From:";

by:

content: invoke("localize", "from") ":";

where localize is a custom method, which, given a key ("from" in the above example), returns a localized string.

Localize is a method defined in a class named StyleSheetExtension (see samples/email/StyleSheetExtension.java). The following at-rule, added at the top of email.css, registers this class with XXE style engine (StyledViewFactory) as being a style sheet extension.

@extension "StyleSheetExtension navy white";

More precisely:

  • When a CSS style sheet is loaded, the style engine searches @extension "arg0 arg1 ... argN" in it.

    If such at-rule is found, the style engine creates an instance of the class with fully qualified name arg0 and keeps this instance as long as the style sheet is in use.

    This class must have a public constructor with the following signature:

    class_name(String[] args, StyledViewFactory viewFactory)
  • When a property value contains a pseudo-function call like invoke(arg0arg1, ..., argN), the style engine attempts to find a public method named arg0 in the class described above.

    This method must have the following signature:

    StyleValue method_name(StyleValue[] args, Node contextNode, 
                           StyledViewFactory viewFactory)

    If such method is found, it is invoked and the returned result is used to specify the property value.

2.1.1. The StyleSheetExtension class

The constructor of StyleSheetExtension is:

    public StyleSheetExtension(String[] args1, StyledViewFactory viewFactory) {
        viewFactory.addDependency(EMAIL_NAMESPACE2, "message", 
                                  Namespace.XML, "lang");3
            .
            .
            .
    }

1

The args array contains strings "navy" and "white". We will study their use in the solution of problem #2.

2

EMAIL_NAMESPACE is simply a constant containing Namespace.get("http://www.xmlmind.com/xmleditor/schema/email").

3

When a CSS style sheet contains a rule such as:

p[align=left] {
    text-align: left;
}

the style engine knows that the view of a p element having an align attribute needs to be rebuilt each time the align attribute is changed.

In email.css, there is no rule which explicitly instructs the style engine that the view of a message element depends on the value of its xml:lang attribute. That's why we need to add this dependency programmatically using StyledViewFactory.addDependency.

2.1.2. The localize method

The localize method is implemented as follows:

    public StyleValue localize(StyleValue[] args, Node contextNode,
                               StyledViewFactory viewFactory) {
        String text;
        if (args.length != 1 || (text = args[0].stringValue()) == null) {
            System.err.println("usage: content: invoke('localize', text);");
            return null;
        }

        Element root = contextNode.getDocument().getRootElement();1
        String lang = root.getTokenAttribute(Name.XML_LANG, "en");2

        ResourceBundle messages = getMessages(lang);
        if (messages == null && !"en".equals(lang)) 
            messages = getMessages("en");

        String localizedText = null;
        if (messages != null) {
            try {
                localizedText = messages.getString(text);
            } catch (Exception ignored) {}
        }

        if (localizedText == null)
            return args[0];
        else
            return StyleValue.createString(localizedText);3
    }

    private static HashMap<String, ResourceBundle> langToMessages = 
        new HashMap<String, ResourceBundle>(5);

    private static ResourceBundle getMessages(String lang) {
        ResourceBundle messages = langToMessages.get(lang);
        if (messages == null) {
            try {
                messages = ResourceBundle.getBundle("messages/Messages", 
                                                    new Locale(lang));
            } catch (MissingResourceException ignored) {}

            if (messages != null)
                langToMessages.put(lang, messages);
        }
        return messages;
    }

The implementation of the localize method is straightforward:

  1. Get the xml:lang attribute of the root message element, if any. The value of this attribute specifies the language to use.

  2. Try to load a ResourceBundle containing messages localized to this language.

  3. Use the key passed as an argument to read the localized text from the ResourceBundle.

About the implementation:

1

contextNode is the target of current CSS rule, almost always an Element but because XXE style engine can be used to style comments and processing instructions, the type of contextNode is Node and not Element.

2

getTokenAttribute is one of the many convenience methods which return an attribute value. Unlike getAttribute, getTokenAttribute properly strips whitespaces from the attribute value.

3

StyleValue represents a parsed CSS property value. It is a simple data structure which contains a bunch of public fields. Examples:

public Type type;
public Keyword keyword;
public double number;
public String string; 
public Name name;
public StyleValue[] list;
public Color color;
public StringExpr xpath;

The type field must be used to determine which fields: number, string, name, list, color, etc, have been initialized.

2.2. Solution of problem #2: implement a StyleSpecs which knows how to style nested emphasis elements

Style sheet email.css could have contained:

emphasis {
    display: inline;
    font-style: italic;
}

emphasis[role=bold] {
    font-style: normal;
    font-weight: bold;
}

emphasis[role=hightlight] {
    font-style: normal;
    background-color: navy;
    color: white;
}

But email.css does not contain any rule to style emphasis elements. Instead, class StyleSheetExtension extends class StyleSpecsBase (which is an adapter class for interface StyleSpecs) and its constructor registers the StyleSheetExtension instance with the StyledViewFactory as being a set of intrinsic style specifications.

Excerpt of the constructor:

        switch (args.length) {
        case 2:
            highlightForeground = StyleValue.parseColor(args[1]);
            /*FALLTHROUGH*/
        case 1:
            highlightBackground = StyleValue.parseColor(args[0]);
            break;
        }
        if (highlightBackground == null)
            highlightBackground = Color.yellow;
        if (highlightForeground == null)
            highlightForeground = Color.red;1

        viewFactory.addIntrinsicStyleSpecs(this);2

        viewFactory.addDependency(EMAIL_NAMESPACE, "emphasis", 
                                  Namespace.NONE, "role");3

1

The constructor is passed two strings "navy" and "white" in its args argument. These strings, which are parsed as CSS colors, specify the foreground and background color of emphasis elements with an attribute role=highlight.

2

The newly constructed StyleSheetExtension instance registers itself with XXE style engine as being an implementation of StyleSpecs.

3

In email.css, there is no rule which tells the style engine to rebuild the view of an emphasis element when its role attribute is changed. Invoking StyledViewFactory.addDependency to do so is therefore needed.

2.2.1. The implementation of interface StyleSpecs

Interface StyleSpecs specifies the services expected by XXE style engine from a style sheet.

An actual StyleSheet of course implements StyleSpecs. Custom code could also implement this interface to style a few, otherwise hard to style, elements. For example, such custom code is used to style XHTML (HTML 4) tables[7] and another custom code is used to style DocBook (CALS) tables.

If an implementation of StyleSpecs has been registered, the style engine first uses it to find styles for the target of current CSS rule, then it uses the regular StyleSheet to find more styles. Therefore the styles returned by the StyleSheet may override those returned by the StyleSpecs.

An implementation of StyleSpecs generally just defines the following method:

int findStyleSpec(Element element, StyleSpec[] specs);

The style engine passes to the StyleSpecs the element which is the target of current CSS rule and an array of 3 pre-created StyleSpec data structures.

  • specs[StyleSpecs.ELEMENT] must be filled with the styles of the element itself.

  • specs[StyleSpecs.BEFORE_ELEMENT] must be filled with the styles of the content generated before the element.

  • specs[StyleSpecs.AFTER_ELEMENT] must be filled with the styles of the content generated after the element.

The returned value is a mask specifying which one, of the 3 StyleSpec data structures, has been filled with styles.

    public int findStyleSpec(Element element, StyleSpec[] specs) {
        if (element.getName() == EMPHASIS) {1
            styleEmphasis(element, specs[StyleSpecsBase.ELEMENT]);
            return StyleSpecsBase.ELEMENT_MASK;
        }

        return 0x0;
    }

    private void styleEmphasis(Element element, StyleSpec styleSpec) {
        int role = getRole(element);

        int nesting = 0;
        Element ancestor = element.getParentElement();
        while (ancestor != null) {2
            if (ancestor.getName() != EMPHASIS ||
                getRole(ancestor) != role)
                break;

            ++nesting;
            ancestor = ancestor.getParentElement();
        }

        switch (role) {
        case BOLD:
            if ((nesting % 2) == 0) {3
                setFontWeight(styleSpec, StyleValue.Keyword.BOLD);
            } else {
                setFontWeight(styleSpec, StyleValue.Keyword.NORMAL);
            }
            break;
        case HIGHLIGHT:
            if ((nesting % 2) == 0) {
                setBackgroundColor(styleSpec, highlightBackground);
                setColor(styleSpec, highlightForeground);
            } else {
                setBackgroundColor(styleSpec, Color.white);
                setColor(styleSpec, Color.black);
            }
            break;
        default:
            if ((nesting % 2) == 0) {
                setFontStyle(styleSpec, StyleValue.Keyword.ITALIC);
            } else {
                setFontStyle(styleSpec, StyleValue.Keyword.NORMAL);
            }
        }
    }

    private static final int ITALIC = 0;
    private static final int BOLD = 1;
    private static final int HIGHLIGHT = 2;

    private static int getRole(Element element) {
        String role = element.getTokenAttribute(ROLE, null);
        if ("bold".equals(role))
            return BOLD;
        else if ("highlight".equals(role))
            return HIGHLIGHT;
        else
            return ITALIC;
    }

    private StyleValue fontStyleValue = 
        StyleValue.createIdentifier(StyleValue.Keyword.NORMAL);
    private StyleValue fontWeightValue = 
        StyleValue.createIdentifier(StyleValue.Keyword.NORMAL);
    private StyleValue backgroundColorValue = 
        StyleValue.createColor(Color.white);
    private StyleValue colorValue = 
        StyleValue.createColor(Color.black);

    private void setFontStyle(StyleSpec styleSpec, 
                              StyleValue.Keyword fontStyle) {
        fontStyleValue.initIdentifier(fontStyle);4
        styleSpec.fontStyle = fontStyleValue;5
    }

    private void setFontWeight(StyleSpec styleSpec, 
                               StyleValue.Keyword fontWeight) {
        fontWeightValue.initIdentifier(fontWeight);
        styleSpec.fontWeight = fontWeightValue;
    }

    private void setBackgroundColor(StyleSpec styleSpec, Color color) {
        backgroundColorValue.color = color;
        styleSpec.backgroundColor = backgroundColorValue;
    }

    private void setColor(StyleSpec styleSpec, Color color) {
        colorValue.color = color;
        styleSpec.color = colorValue;
    }

1

findStyleSpecs is called very often. An implementation of such method must decide very quickly whether it can return styles for the target element or not.

2

The logic of styleEmphasis is simple: count the emphasis ancestors of current emphasis elements. Treat emphasis with different roles as being different elements.

3

If the number of emphasis ancestors with the same role is even, use a special style, otherwise use a plain style.

4

The way XXE style engine is written allows to reuse pre-created StyleValues.

5

A StyleSpec is a simple data structure which contains one StyleValue field per CSS property supported by XXE. Examples:

    public StyleValue marginTop = null;
    public StyleValue marginRight = null;
    public StyleValue marginBottom = null;
    public StyleValue marginLeft = null;
    public StyleValue paddingTop = null;
    public StyleValue paddingRight = null;
    public StyleValue paddingBottom = null;
    public StyleValue paddingLeft = null;
    public StyleValue borderStyle = null;
    public StyleValue borderWidth = null;
    public StyleValue borderTopColor = null;
        .
        .
        .

2.3. Solution of problem #3: invoke a custom method computing the number of a listitem and use a BasicElementObserver to update orderedlists when needed to

A listitem contained in an orderedlist could be styled using the following rules:

listitem {
    display: block;
}

orderedlist > listitem {
    margin-left: 6ex; 
}

orderedlist > listitem:before {
    display: marker; 
    content: counter(n, decimal) ".";
    font-weight: bold; 
    color: #004080;
}

With the above rules, orderedlists with continuation=continues are not properly styled. Therefore, in email.css, last rule has been replaced by:

orderedlist > listitem:before {
    display: marker; 
    content: invoke("listItemCounter"); 
    font-weight: bold; 
    color: #004080;
}

Custom method listItemCounter is implemented as follows:

    public StyleValue listItemCounter(StyleValue[] args, Node contextNode,
                                      StyledViewFactory viewFactory) {
        int index = indexOfListItem((Element) contextNode);
        return StyleValue.createString(Integer.toString(1 + index) + '.');
    }

    private static int indexOfListItem(Element listItem) {
        Element orderedList = listItem.getParentElement();
        if (orderedList == null || orderedList.getName() != ORDEREDLIST)
            return -1;

        int index = orderedList.indexOfChildElement(listItem);
        int offset = 0;

        String continuation = orderedList.getNmtokenAttribute(CONTINUATION, 
                                                              "restarts");
        if ("continues".equals(continuation)) {
            Element prevOrderedList = null;

            if (orderedList.getParentElement() != null) {
                Node node = orderedList.getPreviousSibling();
                while (node != null) {
                    if ((node instanceof Element) &&
                        ((Element) node).getName() == ORDEREDLIST) {
                        prevOrderedList = (Element) node;
                        break;
                    }

                    node = node.getPreviousSibling();
                }
            } // Otherwise, orderedList is the root element.

            if (prevOrderedList != null) {
                Element last = prevOrderedList.getLastChildElement();
                if (last != null) {
                    offset = indexOfListItem(last) + 1;
                } // Otherwise, prevOrderedList is invalid.
            }
        }

        return (offset + index);
    }

2.3.1. Interface BasicElementObserver

In the solution of problem #1, we have already explained all the concepts behind a custom method such as StyleSheetExtension.listItemCounter. So what is new in problem #3?

With the above code, listitems contained in orderedlists with continuation=continues are properly styled. But if you insert or delete orderedlists in an email message, other orderedlists with continuation=continues are not properly updated.

Just invoking

viewFactory.addDependency(EMAIL_NAMESPACE, "orderedlist", 
                          Namespace.NONE, "continuation");

in the constructor of StyleSheetExtension to declare a dependency between orderedlist and its continuation attribute is obviously not sufficient.

Here the idea is to write some custom code which observes modifications made to the email message and which rebuilds the views of orderedlists with continuation=continues when needed to.

This custom code is an implementation of interface BasicElementObserver. A BasicElementObserver must implement:

void elementChanged(DocumentEvent[] events);

This method is invoked by the CustomViewManager of the StyledViewFactory each time the structure or the attributes (but not the text contained in mixed elements) of elements of interest have been modified. (More about CustomViewManagers in next section.)

The implementation of BasicElementObserver is created and registered with XXE style engine in the constructor of StyleSheetExtension:

        CustomViewManager.NamePattern[] observed = {
            new CustomViewManager.NamePattern(EMAIL_NAMESPACE, "body"),1
            new CustomViewManager.NamePattern(EMAIL_NAMESPACE, "listitem"),
            new CustomViewManager.NamePattern(EMAIL_NAMESPACE, "orderedlist")
        };
        viewFactory.getCustomViewManager().add(
            new OrderedListObserver(viewFactory), observed);2

1

A NamePattern specifies a set of qualified names. Unlike Name, it supports wildcards like "any qualified name with a given local part" or like "any qualified name in a given namespace".

2

This statement means: invoke OrderedListObserver.elementChanged each time the structure or the attributes of an orderedlist, body or listitem [8]are changed.

2.3.2. The implementation of interface BasicElementObserver

Class OrderedListObserver is implemented as follows:

    private static class OrderedListObserver 
                   implements CustomViewManager.BasicElementObserver {
        private DocumentView docView;
        private ArrayList<Element> orderedLists = new ArrayList<Element>();

        public OrderedListObserver(StyledViewFactory viewFactory) {
            docView = viewFactory.getDocumentView();
        }

        public void customViewAdded() {}
        public void customViewRemoved() {}

        public void elementChanged(DocumentEvent[] events) {
            orderedLists.clear();

            for (int i = 0; i < events.length; ++i) {1
                DocumentEvent event = events[i];

                switch (event.getType()) {
                case CHILD_ADDED:
                case CHILD_REPLACED:
                case CHILD_REMOVED:
                    {
                        TreeEvent e = (TreeEvent) event;

                        Element element = e.getElementSource();
                        if (element == null)
                            break;

                        if (element.getName() == ORDEREDLIST) {2
                            add(orderedLists, element);
                        } else {
                            if (isOrderedList(e.getOldChild()) ||
                                isOrderedList(e.getNewChild())) {3
                                // Add first child orderedlist (if any).

                                Node node = element.getFirstChild();
                                while (node != null) {
                                    if ((node instanceof Element) &&
                                        ((Element) node).getName() == 
                                        ORDEREDLIST) {
                                        add(orderedLists, (Element) node);
                                        break;
                                    }

                                    node = node.getNextSibling();
                                }
                            }
                        }
                    }
                    break;

                // ------------------------------------------------------
                // Left as an exercise, treat INCLUSION_UPDATED similarly 
                // to CHILD_REPLACED
                // ------------------------------------------------------

                case ATTRIBUTE_ADDED:
                case ATTRIBUTE_CHANGED:
                case ATTRIBUTE_REMOVED:
                    {
                        Element element = 
                            ((AttributeEvent) event).getElementSource();
                        if (element.getName() == ORDEREDLIST) {4
                            add(orderedLists, element);
                        }
                    }
                    break;
                }
            }

            int count = orderedLists.size();
            if (count > 0) {5
                for (int i = 0; i < count; ++i) {
                    Element orderedList = (Element) orderedLists.get(i);

                    if (orderedList.getParentElement() != null) {
                        // Add all following orderedlist siblings (if any).

                        Node node = orderedList.getNextSibling();
                        while (node != null) {
                            if ((node instanceof Element) &&
                                ((Element)node).getName() == ORDEREDLIST) {
                                add(orderedLists, (Element) node);
                            }

                            node = node.getNextSibling();
                        }
                    } // Otherwise, orderedList is the root element.
                }

                count = orderedLists.size();
                for (int i = 0; i < count; ++i) {6
                    docView.rebuildView((Element) orderedLists.get(i));
                }

                orderedLists.clear();
            }
        }

        private static final void add(ArrayList<Element> orderedLists,
                                      Element orderedList) {
            if (!orderedLists.contains(orderedList))
                orderedLists.add(orderedList);
        }

        private static final boolean isOrderedList(Node node) {
            if (node == null || !(node instanceof Element))
                return false;
            else
                return (((Element) node).getName() == ORDEREDLIST);
        }
    }

The above implementation is simple but not very efficient:

1

First pass: add to set orderedLists all the orderedlists possibly impacted by the document modification.

Document modifications are reported as DocumentEvents. A BasicElementObserver can only receive TreeEvents, InclusionUpdatedEvents and AttributeEvents.

2

Add to the set the orderedlist in which a listitem been added or deleted.

3

Add to the set all the orderedlists contained in a body or a listitem in which an orderedlist has been added or deleted.

4

Add to the set the orderedlist in which an attribute has been modified.

5

Second pass: For each orderedlist collected during first pass, add to the set all the orderedlists following it in its parent element.

6

Third pass: rebuild the views of all the orderedlists collected during first and second pass using DocumentView.rebuildView.

2.4. Solution of problem #4: implement an AttributeValueEditor

A smiley element is styled as follows:

smiley {
    content: component("Smiley");
    font: normal normal small sans-serif;
    /* Needed to display the red border of the selection */
    display: inline-block;
    padding: 1px;
}

Here, normal content has been replaced by custom content: component("Smiley"). When XXE style engine finds the component pseudo-function in a style sheet, it creates an instance of the class whose fully qualified name has been passed as the argument of component.

This class must implement interface ComponentFactory.

Component createComponent(Element element, 
                          Style style, StyleValue[] parameters, 
                          StyledViewFactory viewFactory,
                          boolean[] stretch)
  • The factory is used to create a custom view of Element element.

  • This custom view may use the Style of this element. (Our Smiley example will use the font specified in the CSS rule: sans-serif, small.)

  • Passing extra parameters to component after the fully qualified name of the factory class is possible. Theses parameters, which are StyleValues, that is parsed CSS property values, are passed in the parameters array.

  • The factory can set stretch[0] to true if it wants the custom view to be enlarged or shrunken when the document view is itself enlarged or shrunken. stretch[0] specifies this option for the width of the custom view. stretch[1], which specifies this option for the height of the custom view, is currently ignored.

  • The returned custom view is simply a newly created AWT component [9]which has been properly configured to render graphically its model: part or all of Element element.

2.4.1. Passive custom views

Before really solving problem #4, we will explain here how to write the simplest custom views.

Alternate style sheet email_passive_smiley.css contains:

@import url(email.css);

smiley {
    content: component("PassiveSmiley");
}

Class PassiveSmiley is a very simplified version of Smiley:

PassiveSmileySmiley
Represents smiley elements a JLabels.Represents smiley elements as JComboBoxes.
The value of the emotion attribute must be changed using the Attribute tool.The JComboBox can be used to change the value of the emotion attribute, directly from the document view.
public class PassiveSmiley implements ComponentFactory {
    private static final Name EMOTION = Name.get("emotion");

    public Component createComponent(Element element, 
                                     Style style, StyleValue[] parameters, 
                                     StyledViewFactory viewFactory,
                                     boolean[] stretch) {1
        SmileyLabel smileyLabel = new SmileyLabel(element);
        smileyLabel.setFont(style.font);

        return smileyLabel;
    }

    private static class SmileyLabel extends JLabel implements TextLines2 {
        private Element element;

        public SmileyLabel(Element element) {
            String emotion = element.getNmtokenAttribute(EMOTION, "happy");

            SmileyInfo smiley = null;

            SmileyInfo[] smileys = SmileyInfo.getKnownSmileys();3
            for (int i = 0; i < smileys.length; ++i) {
                if (smileys[i].getEmotion().equals(emotion)) {
                    smiley = smileys[i];
                    break;
                }
            }

            if (smiley == null)
                // Invalid emotion. Show it anyway.
                smiley = new SmileyInfo(emotion, null, "???");

            if (smiley.getIcon() != null)
                setIcon(smiley.getIcon());
            setText(smiley.toString());
        }

        public int getFirstBaseLine() {4
            return ComponentUtil.getBaseLine(this);
        }

        public int getLastBaseLine() {
            return getFirstBaseLine();
        }
    }
}

1

The implementation of the ComponentFactory interface consists just in creating a properly configured JLabel.

2 4

Implementing interface TextLines is a refinement which allows to specify the baseline of a custom view. The implementation uses utility ComponentUtil.getBaseLine.

3

Static method SmileyInfo.getKnownSmileys returns an array of information about the known smileys (see samples/email/SmileyInfo.java).

public class SmileyInfo {
    private String emotion;
    private ImageIcon icon;
    private String asciiArt;

    public SmileyInfo(String emotion, ImageIcon icon, String asciiArt) {
        this.emotion = emotion;
        this.icon = icon;
        this.asciiArt = asciiArt;
    }
        .
        .
        .
    public static SmileyInfo[] getKnownSmileys() {
        .
        .
        .
    }
}

Known smileys are listed in the samples/email/smileys/smileys.properties property file. This property file and all the smiley icons are resources contained in email.jar.

PassiveSmiley would not work very well without explicitly declaring a dependency between the smiley element and its emotion attribute in the constructor of StyleSheetExtension:

viewFactory.addDependency(EMAIL_NAMESPACE, "smiley", 
                          Namespace.NONE, "emotion");

2.4.2. Active custom views: specialized editors embedded in the DocumentView

Like class PassiveSmiley, class Smiley also implements interface ComponentFactory.

public class Smiley implements ComponentFactory {
    private static final Name EMOTION = Name.get("emotion");

    public Component createComponent(Element element, 
                                     Style style, StyleValue[] parameters, 
                                     StyledViewFactory viewFactory,
                                     boolean[] stretch) {
        SmileyComboBox smileyCombo = new SmileyComboBox(element);1
        smileyCombo.setFont(style.font);

        viewFactory.getCustomViewManager().add(smileyCombo, element, EMOTION);2

        ComponentUtil.addFocusGainedListener(smileyCombo, element,
                                             viewFactory);3

        return smileyCombo;
    }        
        .
        .
        .
}

1

Smiley creates and returns a properly configured JComboBox.

2

Unlike the JLabel created by PassiveSmiley, the JComboBox created by Smiley implements interface AttributeValueEditor.

void attributeValueChanged(DocumentEvent[] events);

The above add statement registers the JComboBox as an AttributeValueEditor with the CustomViewManager of the StyledViewFactory.

It means: invoke the attributeValueChanged method of smileyCombo each time the emotion attribute of specified element is modified.

3

ComponentUtil.addFocusGainedListener is a refinement which automatically selects the element (and therefore draws a red box around the JComboBox) for which the JComboBox is a custom view, when this JComboBox receives keyboard focus.

2.4.2.1. What is the CustomViewManager?

A CustomViewManager is a helper object owned by the StyledViewFactory. This object is a registry for custom views created by applying certain CSS rules to elements. This registry is cleared each time the StyleSheet is changed using setStyleSheet.

A rule like the following creates a custom view.

smiley {
    content: component("Smiley");
    font: normal normal small sans-serif;
    /* Needed to display the red border of the selection */
    display: inline-block;
    padding: 1px;
}

Immediately after the creation of an active custom view, the factory that created it registers it with the CustomViewManager.

The CustomViewManager, which is a DocumentEventListener, mainly forwards DocumentEvents to its registered custom views.

When and which DocumentEvents are sent to a custom view depend on the interface implemented by the custom view. We have already studied BasicElementObserver and AttributeValueEditor, but there are many other kinds of custom views: SimpleCounter, BasicElementEditor, SimpleElementEditor, ElementEditor, ElementValueEditor.

An alternative to CustomViewManager would be to have custom views directly implement interface DocumentEventListener. In such case, all custom views (possibly thousands) would receive all document events. Not only this would make custom views tedious to write (because they would have to filter themselves uninteresting events) but this would also be extremely inefficient.

CustomViewManager also notifies its registered custom views:

  • when a custom view is actually registered, by invoking method customViewAdded;

  • when a custom view is no longer in use (because its model has been removed from the document), by invoking method customViewRemoved;

  • when a custom view needs to update its model, by invoking method commitChanges.

    Imagine a JTextField used to implement an attribute editor. User has typed a value in the JTextField and then has typed Ctrl+S to save its document. Before the document is saved, commitChanges instructs the JTextField that the new value needs to be assigned to the attribute.

2.4.2.2. Implementation of SmileyComboBox
    private static class SmileyComboBox extends JComboBox 
                   implements CustomViewManager.AttributeValueEditor, 
                              TextLines {
        private Element element;
        private boolean performingAction = false;
        private boolean selectingItem = false;

        public SmileyComboBox(Element element) {
            super(SmileyInfo.getKnownSmileys());

            setEditable(false);
            setRenderer(new SmileyRenderer());

            this.element = element;
            updateSelectedItem();

            addActionListener(new ActionListener() {
                public void actionPerformed(ActionEvent event) {
                    if (selectingItem)1
                        return;

                    SmileyInfo smiley = (SmileyInfo) getSelectedItem();
                    if (smiley != null) {
                        performingAction = true;
                        putAttribute(SmileyComboBox.this.element,
                                     EMOTION, smiley.getEmotion());2
                        performingAction = false;3
                    }
                }
            }); 
        }

        public void customViewAdded() {}
        public void customViewRemoved() {}
        public void commitChanges() {}

        public void attributeValueChanged(DocumentEvent[] events) {
            if (performingAction)4
                return;

            updateSelectedItem();5
        }

        private void updateSelectedItem() {
            String emotion = element.getNmtokenAttribute(EMOTION, "happy");

            SmileyInfo smiley = null;

            SmileyInfo[] smileys = SmileyInfo.getKnownSmileys();
            for (int i = 0; i < smileys.length; ++i) {
                if (smileys[i].getEmotion().equals(emotion)) {
                    smiley = smileys[i];
                    break;
                }
            }

            if (smiley == null)
                // Invalid emotion. Show it anyway.
                smiley = new SmileyInfo(emotion, null, "???");

            selectingItem = true;
            // setSelectedItem triggers actionPerformed.
            setSelectedItem(smiley);
            selectingItem = false;6
        }

        public int getFirstBaseLine() {
            return ComponentUtil.getBaseLine(this);
        }

        public int getLastBaseLine() {
            return getFirstBaseLine();
        }
    }

2

Interactively selecting an item using the JComboBox assigns the value of this item to the emotion attribute of the element which is the model of the custom view.

4 3

The performingAction flag is used to prevent the invocation of attributeValueChanged, automatically triggered by putAttribute (see below), from updating the JComboBox.

    private static final String putAttribute(Element element, 
                                             Name name, String value) {
        if (element.isEditable())
            return element.putAttribute(name, value);
        else
            return null;
    }

5

When the emotion attribute is modified (for example, using the Attribute tool or using another document view), the JComboBox has to update its selected item to reflect this change.

1 6

The selectingItem flag is used to prevent the ActionListener from updating the value of the emotion attribute. (Even if this is non-intuitive, the setSelectedItem method of a JComboBox notifies all its ActionListeners, like when the user uses the JComboBox interactively.)

3. Compiling and testing the sample style sheet extensions

  1. Compile StyleSheetExtension.java, SmileyInfo.java, PassiveSmiley.java and Smiley.java by running ant in the samples/email/ directory.

    This command will also archive the compiled code in samples/email/email_config/email.jar.

  2. Directory samples/email/email_config/ contains an XXE configuration for the email document class.

    This directory contains:

    email.xxe

    The XXE configuration.

    email.xsd

    The XML schema used to model an email message.

    email.css, attachment.gif

    The CSS style sheet (and its resources) used to style an email message.

    email_passive_smiley.css

    An alternate CSS style sheet.

    template.email

    A template for creating new email messages.

    email.jar

    Custom code. In order to tell XXE to load this jar file, simply copy it to one of the directories scanned by XXE at startup time (e.g. XXE_user_preferences_dir/addon/)

    Copy the whole directory to XXE_user_preferences_dir/addon/. XXE user preferences directory is:

    • $HOME/.xxe7/ on Linux.

    • $HOME/Library/Application Support/XMLmind/XMLEditor7/ on the Mac.

    • %APPDATA%\XMLmind\XMLEditor7\ on Windows XP, Vista, 7 and 8.

      Example: C:\Documents and Settings\john\Application Data\XMLmind\XMLEditor7\ on Windows XP. C:\Users\john\AppData\Roaming\XMLmind\XMLEditor7\ on Windows Vista, 7 and 8.

      If you cannot see the "Application Data" directory using Microsoft Windows File Manager, turn on Tools>Folder Options>View>File and Folders>Show hidden files and folders.

  3. Restart XXE.

  4. Clear the Quick Start cache (OptionsPreferences, Advanced|Cached Data section in XMLmind XML Editor - Online Help), then restart XXE one more time. If you forget to do that, XXE will fail to see your extension.

  5. Use FileNew and select Email Message/From me to you to create a new email message.

    Or open the sample email message contained in samples/tests/in/sample.email.



[7] Even if this code has been written by XMLmind staff, it is technically custom code. That is,

  • this code could have been written by third-party programmers using documented APIs;

  • this code is not contained in xxe.jar;

  • instead this code is dynamically discovered by XXE at startup time.

[8] Like body, listitems can contain orderedlists.

[9] Note that a Swing JComponent is also an AWT Component.