Table of Contents
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.
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.
App is the abstraction for a 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.)
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 three possible GUI specifications: common.xxe_gui, Professional.xxe_gui and NoToolTabs.xxe_gui.
AppPart is an interface:
| Method | Description |
|---|---|
| activeEditorChanged | Invoked after the active editor has changed or when there is no active editor at all (generally because all documents have been closed). |
| isEditingContextSensitive | This method must return true if the part is intrinsically context sensitive and it must return false if this part is intrinsically not context sensitive. |
| editingContextChanged | Invoked 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. |
| validityStateChanged | Invoked after active document has been checked for validity. |
| saveStateChanged | Invoked after active document has been saved or, on the contrary, when its has been modified and thus needs to be saved. |
| namespacePrefixesChanged | Invoked after the namespace/prefix map has been modified for the active document. |
| undoStateChanged | Invoked 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. |
| applyPreferences | If this part supports user preferences, this part should update its state after reading its settings from the object returned by App.getPreferences. |
| flushPreferences | If 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: PreferencesSheets. 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 PreferencesSheet.
This chapter will not attempt to describe two important ways to extend XMLmind XML Editor: OpenDocumentHook and EditorListener. These observers deserve a chapter of their own.
Several interfaces extends the AppPart interface:
Several abstract classes implements the AppPart interface (they are not all listed here):
A javax.swing.AbstractAction which implements the AppPart interface.
An AppAction which is expected to take a long time to run.
An AppAction which is expected to take a long time to run and which can be canceled during its execution.
An AppAction which is a wrapper for a Command.
A dynamic set of menu items. For example, this is used to implement configuration specific menu items.
A dynamic set of tool bar buttons. For example, this is used to implement configuration specific tool bar buttons.
Executing ant (see build.xml) in samples/custom_parts/
will compile the following custom parts:
AboutAction.java,
CountWordsTool.java,
CountWordsOptions.java;
will create custom_parts.jar containing the code of the 3 custom parts and their resources (contained in icons/).
Deploying the custom GUI making use of the above custom parts is described in Chapter 2, Tutorial in XMLmind XML Editor - Customizing the User Interface.
The idea is to replace the standard "" action found in the menu (this action is called "aboutAction" — see common.xxe_gui) by an action of our own. Our custom action will display our custom dialog box.
public class AboutAction extends AppAction {
private ImageIcon icon;
public void doIt() {
if (icon == null) {
icon = new ImageIcon(AboutAction.class.getResource(
"icons/aboutAction.png"));
}
JOptionPane.showMessageDialog(app.getDialogParent(),
"A Customized XMLmind XML Editor.",
getLabel(),
JOptionPane.PLAIN_MESSAGE,
icon);
}
public void updateEnabled() {
// Always enabled.
}
}All AppActions must implement doIt and updateEnabled. | |
All dialog boxes opened by actions must use App.getDialogParent as their parent (also called ``their owner''). | |
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);
} |
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 even describe how words are actually counted[12].
There are many ways to implement word counting in XMLmind XML Editor, from simplest to hardest:
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).
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.
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.

...
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() {
Preferences prefs = app.getPreferences();
activated = prefs.getBoolean("countWords", false);
minCharCount = prefs.getInt("countedWordMinChars", 1, 1000, 1);
toggle.setSelected(activated);
activationChanged();
}
public void flushPreferences() {
Preferences prefs = app.getPreferences();
prefs.putBoolean("countWords", toggle.isSelected());
prefs.putInt("countedWordMinChars", minCharCount);
}
...The word counter supports two user preferences:
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. | |
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() {
if (!activated)
return;
WordCount wc = null;
OpenedDocument openedDoc = app.getActiveOpenedDocument();
if (openedDoc != null) {
wc = (WordCount) openedDoc.getProperty(WORD_COUNT_PROPERTY);
if (wc == null) {
wc = new WordCount();
openedDoc.putProperty(WORD_COUNT_PROPERTY, wc);
}
}
updateField(wc);
}
public void saveStateChanged() {
if (!activated)
return;
OpenedDocument openedDoc = app.getActiveOpenedDocument();
if (openedDoc.isSaveNeeded()) {
WordCount wc =
(WordCount) openedDoc.getProperty(WORD_COUNT_PROPERTY);
wc.needUpdate = true;
updateField(wc);
} else {
updateWordCount(openedDoc);
}
}
public void namespacePrefixesChanged() {}
public void validityStateChanged() {}
public void undoStateChanged() {}
public void editingContextChanged() {}
public boolean isEditingContextSensitive() { return false; }
...
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) {
...
}
...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. | |
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. | |
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. | |
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. | |
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.gif")));
DialogUtil.setIconic(toggle);
toggle.setToolTipText("Turns word counting on and off");
toggle.setFocusable(false);
add(toggle);
toggle.addActionListener(new ActionListener() {
public void actionPerformed(ActionEvent event) {
activated = toggle.isSelected();
activationChanged();
}
});
field = new JTextField(10);
field.setEditable(false);
field.setFont(new Font("SansSerif", Font.PLAIN, 10));
Color bg = field.getBackground();
field.setBorder(new ThinBorder(bg, /*raised*/ false));
field.setToolTipText("Word count (click on it to update it)");
field.setFocusable(false);
add(field);
needUpdateColor = bg.darker();
upToDateColor = field.getForeground();
field.addMouseListener(new MouseAdapter() {
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:
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[13] 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 common.xxe_gui) creates such editor.
There are two kinds of preferences sheet:
``Plain'' preferences sheets: PreferencesSheet.
Example: PrintOptions.
Preferences sheets that need to access the App that contains them: AppPreferencesSheet.
Examples: OpenOptions, SaveOptions.
The custom preferences sheet we are going to describe here belong to this second kind.
public class CountWordsOptions extends AppPreferencesSheet {
private SpinnerNumberModel countedWordMinCharsModel;
private JSpinner countedWordMinChars;
public CountWordsOptions() {
super("countWords", "Count Words");
}
protected PreferencesSheetPane createPane() {
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() {
countedWordMinChars.requestFocus();
}
public void fillPane(Preferences prefs) {
int count = prefs.getInt("countedWordMinChars", 1, 1000, 1);
countedWordMinCharsModel.setValue(new Integer(count));
}
public boolean validatePane(Preferences prefs) {
int count = countedWordMinCharsModel.getNumber().intValue();
prefs.putInt("countedWordMinChars", count);
return true;
}
public void applyPreferences(Preferences prefs) {
AppPart part = app.getPart("countWordsTool");
if (part != null)
part.applyPreferences();
}
}A preferences sheet has a sheet ID: " | |
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. | |
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. | |
PreferencesSheet.fillPane fills the form using values read from the Preferences object which is the argument of this method. | |
PreferencesSheet.validatePane stores in the Preferences argument all the values specified by the user in the form. | |
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. |
[12] For the curious reader, let say that the implementation is based on ElementCharSequence. What follows works great with most content models, but is puzzled by content models such as the content models of XHTML div, td, etc (``flows'').
ElementCharSequence chars =
new ElementCharSequence(doc.getRootElement());This alternate approach is less smart but at least, is not ridiculous when faced to ``flows''.
ElementCharSequence chars = new ElementCharSequence(
doc.getRootElement(), null, null,
ElementCharSequence.DEFAULT_EMPTY_TEXT_CONTENT,
ElementCharSequence.MARK_ALL,
ElementCharSequence.DEFAULT_PARAGRAPH_MARK);[13] More information about supported preferences keys in the Section 6.6, “The "Options" dialog box” in XMLmind XML Editor - Online Help.