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

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

2.2. Best strategy

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

  1. Write a CommandBase which, when invoked, counts the words found in the selection and then, prints this word count using DocumentView.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.

2.3. The word counter tool

Excerpts from 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,
                               Math.max(10, font.getSize()-2)));
        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.