Chapter 9. Extending the GUI of XMLmind XML Editor

Table of Contents

1. A framework for creating XML editors
2. High-level building blocks
3. Compiling and running the code sample
4. A custom About dialog box
5. A custom tool which counts the words found in the active document
5.1. How to count words in an XML document?
5.2. Best strategy
5.3. The word counter tool
6. A custom preferences sheet which parametrizes the word counter

In this chapter, we'll learn how to write high-level building blocks, called parts. Parts are used to create custom GUIs for XMLmind XML Editor.

Prerequisite: the custom parts we are going to write in this lesson are those needed to create the custom GUI described in the tutorial part of Chapter 2, Tutorial in XMLmind XML Editor - Customizing the User Interface. Therefore you need to read this tutorial before studying this lesson.

1. A framework for creating XML editors

App is the abstraction for a possibly multi-document, multi-view per document, XML editor. It is an abstraction because it does not force you to create a multi-document, multi-view XML editor and because it makes very few assumptions about the type of GUI (single frame, several frames, MDI, stand alone, embedded in a larger application, etc) the XML editor has.

A document opened in an App is represented by an OpenedDocument object. An OpenedDocument is a wrapper around the actual XML document: the Document object. (OpenedDocument.getDocument returns this Document object.)

An OpenedDocument is displayed and can be edited using an Editor. In the case of a multi-view XML editor, an OpenedDocument may have several views, that is, several Editors.

An Editor is basically a JScrollPane containing a StyledDocumentPane which in turn contains a StyledDocumentView; the important object here being the StyledDocumentView.

App assumes that a single Editor is active at a time. The active Editor is the Editor having the keyboard focus. The whole GUI of the XML editor is expected to act on this active Editor and to reflect its state. The active Editor is obtained using App.getActiveEditor and the OpenedDocument displayed by an Editor is obtained using Editor.getOpenedDocument. Therefore, the OpenedDocument displayed by the active Editor is called the active OpenedDocument. (Convenience method App.getActiveOpenedDocument allows to directly get it.)

2. High-level building blocks

An App is always an assembly of AppParts. How this assembly is specified and how the GUI is built are specific to the class derived from App. In the case of XMLmind XML Editor, this assembly is specified in .xxe_gui XML files and the GUI is automatically built using an engine (GUISpec) which interprets this specification. See for example the following two, quite different, GUI specifications: DesktopApp.xxe_gui (desktop application GUI) and SingleDocApp.xxe_gui (single document, single view GUI).

AppPart is an interface:

MethodDescription
activeEditorChangedInvoked after the active editor has changed or when there is no active editor at all (generally because all documents have been closed).
isEditingContextSensitiveThis method must return true if the part is intrinsically context sensitive and it must return false if this part is intrinsically not context sensitive.
editingContextChangedInvoked when the editing context (text node containing caret, node selection, etc) changes in active editor. This method is never invoked if isEditingContextSensitive returned false when the App has registered the part.
validityStateChangedInvoked after active document has been checked for validity.
saveStateChangedInvoked after active document has been saved or, on the contrary, when its has been modified and thus needs to be saved.
namespacePrefixesChangedInvoked after the namespace/prefix map has been modified for the active document.
undoStateChangedInvoked after it becomes possible to undo or redo a command in active document or, on the contrary, when it becomes impossible to undo or redo a command.
applyPreferencesIf this part supports user preferences, this part should update its state after reading its settings from the object returned by App.getPreferences.
flushPreferencesIf this part supports user preferences, this part should store its current settings in the object returned by App.getPreferences.

Visual objects may implement this interface (EditAttributePane, OpenAction, etc) as well as non-visual objects (AutoSavePart, SpellOptionsPart, etc).

There are building blocks other than AppParts: AppPreferencesSheets. These objects are somewhat simpler than AppParts and much less related to the App than AppParts. For now, suffice to say that next section will describe how to write a simple AppPreferencesSheet.

This chapter will not attempt to describe another way to extend XMLmind XML Editor: OpenDocumentHook.

Several interfaces extends the AppPart interface:

AppTool

Interface implemented by a java.awt.Component designed to be included in an horizontal tool bar or in a status bar.

AppPane

Interface implemented by a java.awt.Component designed to be included in the ``tool area'' found at the left and/or at the right of the document views.

Several abstract classes implements the AppPart interface (they are not all listed here):

AppAction

A javax.swing.AbstractAction which implements the AppPart interface.

LengthyAction

An AppAction which is expected to take a long time to run.

CancelableAction

An AppAction which is expected to take a long time to run and which can be canceled during its execution.

EditAction

An AppAction which is a wrapper for a Command.

AppMenuItems

A dynamic set of menu items. For example, this is used to implement configuration specific menu items.

AppToolBarItems

A dynamic set of tool bar buttons. For example, this is used to implement configuration specific tool bar buttons.

AppPartBase

A “worker” part, having no GUI. For example, the auto-save feature is implemented this way.

3. Compiling and running the code sample

Executing ant (see build.xml) in samples/custom_parts/

  1. will compile the following custom parts:

    • AboutAction.java,

    • CountWordsTool.java,

    • CountWordsOptions.java;

  2. will create custom_parts.jar containing the code of the 3 custom parts and their resources (contained in icons/).

How to deploy the custom GUI making use of the above custom parts is explained in Chapter 2, Tutorial in XMLmind XML Editor - Customizing the User Interface.

4. A custom About dialog box

The idea is to replace the standard "About..." action found in the Help menu (this action is called "aboutAction" — see DesktopApp.xxe_gui) by an action of our own. Our custom action will display our custom dialog box.

AboutAction.java:

public class AboutAction extends AppAction {
    private ImageIcon icon;

    public void doIt() {1
        if (icon == null) {
            icon = new ImageIcon(AboutAction.class.getResource(
                                     "icons/aboutAction.png"));
        }

        JOptionPane.showMessageDialog(app.getHostComponent(),2
                                      "A Customized XMLmind XML Editor.",
                                      getLabel(),
                                      JOptionPane.PLAIN_MESSAGE,
                                      icon);
    }

    public void updateEnabled() {3
        // Always enabled.
    }
}

1

All AppActions must implement doIt and updateEnabled.

2

All dialog boxes opened by actions must use App.getHostComponent as their parent (also called ``their owner'').

3

This implementation of updateEnabled is trivial. Another very simple and very common implementation of updateEnabled is (example: PrintAction):

    public void updateEnabled() {
        setEnabled(app.getActiveEditor() != null);
    }

5. A custom tool which counts the words found in the active document

5.1. How to count words in an XML document?

We'll not discuss what is a word in this lesson. Let's suppose a word is simply a contiguous sequence of non-space characters.

We'll not describe how words are actually counted.

5.2. Best strategy

There are many ways to implement word counting in XMLmind XML Editor, from simplest to hardest:

  1. Write a Command which, when invoked, counts the words found in the selection and then, prints this word count using ShowStatus.showStatus. After that:

    • declare the command in the .xxe_gui file;

    • reference this command in the hidden child of the layout element of the .xxe_gui file;

    • declare the action making use of this command;

    • reference this action somewhere in the .xxe_gui file (e.g. in a menu or toolBar element itself referenced in the layout element of the .xxe_gui file).

  2. Write a AppTool, a very simple extension of interface AppPart, which will be added to the status bar. The word count will be automatically updated each time the active document is saved. If the user wants to count words without saving the document, she will have to click on the AppTool.

  3. Write a AppTool which will be added to the status bar. The word count will be automatically updated each time the editing context is changed in the active document (i.e. caret moved to another text node, nodes are explicitly selected, etc). This AppTool would need to return true in AppPart.isEditingContextSensitive and would need to do most of its job in AppPart.editingContextChanged.

We will not implement [a] because we have already explained how to write a custom Command.

We will not implement [c] because this would be too hard. Any context-sensitive part needs to be fast. This would mean implementing an incremental method for counting of words.

5.3. The word counter tool

CountWordsTool.java:

...
public class CountWordsTool extends JPanel implements AppTool {
    private App app;
    private String id;
    private String helpId;
    ...

    public CountWordsTool() {
        ...  
    }

    public void initApp(App app, String id) {
        this.app = app;
        this.id = id;
    }

    public App getApp() {
        return app;
    }

    public String getId() {
        return id;
    }

    public void setHelpId(String helpId) {
        this.helpId = helpId;
    }

    public String getHelpId() {
        return helpId;
    }
    ...

These methods belows can always be implemented mechanically as shown above:

    ...
    private boolean activated;
    private int minCharCount;
    ...

    public void applyPreferences() {1
        Preferences prefs = app.getPreferences();
        activated = prefs.getBoolean("countWords", false);
        minCharCount = prefs.getInt("countedWordMinChars", 1, 1000, 1);

        toggle.setSelected(activated);
        activationChanged();
    }

    public void flushPreferences() {2
        Preferences prefs = app.getPreferences();
        prefs.putBoolean("countWords", toggle.isSelected());
        prefs.putInt("countedWordMinChars", minCharCount);
    }

    ...

1

The word counter supports two user preferences:

countWords

a boolean which specifies whether word counting is enabled or not.

countedWordMinChars

an integer which specifies the minimum length for a word to be counted.

App invokes the applyPreferences method of all its parts at a certain stage of its own initialization. This allows the counter to read its settings from the Preferences object returned by App.getPreferences.

2

App invokes the flushPreferences method of all its parts at a certain stage of its own destruction. This allows the counter to store its settings in the Preferences object returned by App.getPreferences.

    ...
     private static final class WordCount {
        public int wordCount;
        public boolean needUpdate;

        public WordCount() {
            wordCount = 0;
            needUpdate = true;
        }
    }

    private static final String WORD_COUNT_PROPERTY = 
        "CountWordsTool.WordCount";
    ...
    public void activeEditorChanged() {1
        if (!activated) {
            return;
        }

        WordCount wc = null;

        OpenedDocument openedDoc = app.getActiveOpenedDocument();
        if (openedDoc != null) {
            wc = (WordCount) openedDoc.getProperty(WORD_COUNT_PROPERTY);2
            if (wc == null) {
                wc = new WordCount();
                openedDoc.putProperty(WORD_COUNT_PROPERTY, wc);
            }
        }

        updateField(wc);
    }

    public void saveStateChanged() {3
        if (!activated) {
            return;
        }

        OpenedDocument openedDoc = app.getActiveOpenedDocument();
        if (openedDoc.isSaveNeeded()) {
            WordCount wc = 
                (WordCount) openedDoc.getProperty(WORD_COUNT_PROPERTY);
            wc.needUpdate = true;4
            updateField(wc);
        } else {
            updateWordCount(openedDoc);5
        }
    }
    ...
    private void updateWordCount(OpenedDocument openedDoc) {
        WordCount wc = (WordCount) openedDoc.getProperty(WORD_COUNT_PROPERTY);
        wc.wordCount = countWords(openedDoc.getDocument());
        wc.needUpdate = false;

        updateField(wc);
    }

    private int countWords(Document doc) {
        ...
    }
    ...

1

Each time the active Editor changes, the counter should show in its JTextField the last word count computed for the active OpenedDocument.

In order to do that, the counter adds a client property to each OpenedDocument using PropertySet.putProperty (an OpenedDocument is a PropertySet). The name of this client property is WORD_COUNT_PROPERTY and the value of this property is a WordCount object.

2

The active OpenedDocument is obtained using App.getActiveOpenedDocument. If the active OpenedDocument already has a WORD_COUNT_PROPERTY client property, the value of this property is displayed in the JTextField. Otherwise, a blank WordCount is added to the active OpenedDocument.

Note that App.getActiveOpenedDocument will return null if all documents have been closed. In such case, activeEditorChanged is invoked to signal that there is no active Editor.

3

The counter needs to recompute the word count of a document each time this document is saved. Therefore, the counter needs to implement the saveStateChanged method.

4

Here saveStateChanged is invoked to notify that the active OpenedDocument needs to be saved: OpenedDocument.isSaveNeeded returns true. In such case, the word count displayed in the JTextField is probably wrong. Tell this to the user by marking the WordCount client property as ``dirty'' and thus, by displaying the word count in gray.

5

Here saveStateChanged is invoked to notify that the active OpenedDocument has been saved to disk: OpenedDocument.isSaveNeeded returns false. Recompute the word count, update the WordCount client property and refresh the JTextField accordingly.

    ...
    private JToggleButton toggle;
    private JTextField field;
    private Color needUpdateColor;
    private Color upToDateColor;
    ...
    public CountWordsTool() {
        setLayout(new FlowLayout(FlowLayout.LEFT, 2, 0));

        toggle = new JToggleButton(
            new ImageIcon(CountWordsTool.class.getResource(
                              "icons/wordCountTool.png")));
        DialogUtil.setIconic(toggle);
        toggle.setToolTipText("Turns word counting on and off");
        toggle.setFocusable(false);
        add(toggle);

        toggle.addActionListener(new ActionListener() {1
            public void actionPerformed(ActionEvent event) {
                activated = toggle.isSelected();
                activationChanged();
            }
        });

        field = new JTextField(10);
        field.setFont(new Font("SansSerif", Font.PLAIN, 10));
        field.setToolTipText("Word count (click on it to update it)");
        InfoBorder.configureField(field);
        add(field);

        needUpdateColor = field.getBackground().darker();
        upToDateColor = field.getForeground();

        field.addMouseListener(new MouseAdapter() {2
            public void mouseClicked(MouseEvent event) {
                if (!activated) {
                    return;
                }

                OpenedDocument openedDoc = app.getActiveOpenedDocument();
                if (openedDoc != null)
                    updateWordCount(openedDoc);
            }
        });
    }

    private void activationChanged() {
        WordCount wc = null;

        OpenedDocument[] openedDocs = app.getOpenedDocuments();
        for (int i = 0; i < openedDocs.length; ++i) {
            if (!activated) {
                openedDocs[i].removeProperty(WORD_COUNT_PROPERTY);
            } else {
                wc = new WordCount();
                openedDocs[i].putProperty(WORD_COUNT_PROPERTY, wc);
            }
        }

        updateField(wc);
    }

    private void updateField(WordCount wc) {
        if (!activated || wc == null) {
            field.setText("");
        } else {
            field.setText(
                NumberFormat.getIntegerInstance().format(wc.wordCount));
            field.setForeground(wc.needUpdate? 
                                needUpdateColor : upToDateColor);
        }
    }
    ...

The rest of the implementation has nothing specific to an AppPart/AppTool and therefore, will not be described. Just notice how:

1

clicking on the JToggleButton enables or disables word counting;

2

clicking on the JTextField can be used at any time to recompute the word count.

6. A custom preferences sheet which parametrizes the word counter

A preferences sheet does not have to implement the AppPart interface. A preferences sheet is simply a component of a specialized editor for Preferences objects.

In XMLmind XML Editor, user preferences such as encoding, lastOpenedFiles, etc[10] are stored in a Preferences object. This object is accessed using App.getPreferences.

The specialized editor for Preferences objects is a PreferencesEditor. Action EditPreferencesAction (declared as "editPreferencesAction" in Professional.xxe_gui) creates such editor.

There are two kinds of preferences sheet:

CountWordsOptions.java:

public class CountWordsOptions extends AppPreferencesSheet {
    private SpinnerNumberModel countedWordMinCharsModel;
    private JSpinner countedWordMinChars;
    
    public CountWordsOptions() {
        super("countWords", "Count Words");1
    }

    protected PreferencesSheetPane createPane() {2
        PreferencesSheetPane form = 
            new PreferencesSheetPane(new FlowLayout(FlowLayout.LEFT, 5, 2));

        JLabel label = new JLabel("Do not count words having less than");
        form.add(label);

        countedWordMinCharsModel = new SpinnerNumberModel(1, 1, 1000, 1);
        countedWordMinChars = new JSpinner(countedWordMinCharsModel);
        form.add(countedWordMinChars);

        label = new JLabel("characters");
        form.add(label);

        return form;
    }

    public void focusPane() {3
        countedWordMinChars.requestFocus();
    }

    public void fillPane(Preferences prefs) {4
        int count = prefs.getInt("countedWordMinChars", 1, 1000, 1);
        countedWordMinCharsModel.setValue(new Integer(count));
    }

    public boolean validatePane(Preferences prefs) {5
        int count = countedWordMinCharsModel.getNumber().intValue();
        prefs.putInt("countedWordMinChars", count);

        return true;
    }

    public void applyPreferences(Preferences prefs) {6
        AppPart part = app.getPart("countWordsTool");
        if (part != null) {
            part.applyPreferences();
        }
    }
}

1

A preferences sheet has a sheet ID: "countWords" and a label: "Count Words". The label is displayed by the GUI of the preferences editor. The sheet ID is needed, for example, to specify that sheet A is a ``child'' of sheet B. Example: in XMLmind XML Editor, sheet "featuresOptions" is the child of sheet "generalOptions" (see DesktopApp.xxe_gui).

2

PreferencesSheet.createPane creates the GUI of the preferences sheet. It must create and return a PreferencesSheetPane, which is simply a JPanel with a ``special connection'' to its JScrollPane parent.

3

PreferencesSheet.focusPane is invoked when the sheet is selected. It should give the keyboard focus to the first component of the form it has created in PreferencesSheet.createPane.

4

PreferencesSheet.fillPane fills the form using values read from the Preferences object which is the argument of this method.

5

PreferencesSheet.validatePane stores in the Preferences argument all the values specified by the user in the form.

6

PreferencesSheet.applyPreferences delegates to the CountWordsTool the task of actually applying to itself the user preferences specified in the Preferences argument. This works because, in the case of PreferencesSheet.applyPreferences, the Preferences argument is identical to the Preferences object returned by App.getPreferences.

The instance of CountWordsTool which is part of the App is obtained using App.getPart. This is why CountWordsOptions is an AppPreferencesSheet and not a ``plain'' PreferencesSheet.



[10] More information about supported preferences keys in the Section 7, “The "Preferences" dialog box” in XMLmind XML Editor - Online Help.