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 —any class derived from abstract class ViewFactoryBase— as being a stylesheet extension.

@extension "StyleSheetExtension navy white";

More precisely:

  • When a CSS stylesheet 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 stylesheet is in use.

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

    class_name(String[] args, ViewFactoryBase 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, 
                           ViewFactoryBase 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 (excerpts from StyleSheetExtension.java):

    public StyleSheetExtension(String[] args1, ViewFactoryBase 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 stylesheet 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 ViewFactoryBase.addDependency.

2.1.2. The localize method

The localize method is implemented as follows (excerpts from StyleSheetExtension.java):

    public StyleValue localize(StyleValue[] args, Node contextNode,
                               ViewFactoryBase 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

Stylesheet 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 ViewFactoryBase as being a set of intrinsic style specifications.

Excerpt from the StyleSheetExtension 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(
            new EmphasisStyleSpecs(highlightBackground, highlightForeground));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 creates a EmphasisStyleSpecs and registers it with XXE style engine as being an implementation of StyleSpecs using ViewFactoryBase.addIntrinsicStyleSpecs.

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 ViewFactoryBase.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 stylesheet.

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 HTML tables[4] 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.

Excerpts from EmphasisStyleSpecs.java:

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

    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);

    ...

    @Override
    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 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 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

findStyleSpec 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 (excerpts from StyleSheetExtension.java):

    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 (excerpts from samples/email/StyleSheetExtension.java)

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 ViewFactoryBase 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 (excerpts from samples/email/StyleSheetExtension.java):

        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 [5]are changed.

2.3.2. The implementation of interface BasicElementObserver

Class OrderedListObserver is implemented as follows (excerpts from OrderedListObserver.java):

    public class OrderedListObserver 
                  mplements CustomViewManager.BasicElementObserver {
        private DocumentView docView;
        private ArrayList<Element> orderedLists;

        ...

        public OrderedListObserver(StyledViewFactory viewFactory) {
            docView = viewFactory.getDocumentView();
            orderedLists = new ArrayList<Element>();
        }

        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 stylesheet, 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 [6]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 stylesheet email_passive_smiley.css contains:

@import url(email.css);

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

Class PassiveSmiley is a very simplified version of Smiley:

PassiveSmileySmiley
Represents a smiley element as a (borderless) JButton.Represents a smiley element as a JComboBox.
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.

Excerpts from PassiveSmiley.java:

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);

        ComponentUtil.addFocusGainedListener(smileyLabel, element,
                                             viewFactory);

        return smileyLabel;
    }

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

        public SmileyLabel(Element element) {
            // A borderless JButton looks like a JLabel but is focusable.
            setBorderPainted(false);
            setContentAreaFilled(false);

            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 Icon icon;
    private String asciiArt;

    public SmileyInfo(String emotion, Icon 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. Excerpts from Smiley.java:

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 (a class derived from abstract class ViewFactoryBase).

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 (a class derived from abstract class ViewFactoryBase). 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

Excerpts from Smiley.java:

    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.)



[4] 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.

[5] Like body, listitems can contain orderedlists.

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