1. Sample command ConvertCaseCmd

This sample command is a slightly simplified version of command convertCase in XMLmind XML Editor - Commands.

1.1. First step: prepare

Excerpts from ConvertCaseCmd.java:

public class ConvertCaseCmd extends CommandBase {
    private enum Op {
        LOWER,
        UPPER,
        CAPITAL
    }

    private Op op;
    private TextNode beginText;
    private int beginOffset;
    private TextNode endText;
    private int endOffset;

    public ConvertCaseCmd() {
        super(/*repeatable*/ false, /*recordable*/ true);
    }

    public boolean prepare(DocumentView docView, 
                           String parameter, int x, int y) {
        op = null;1
        beginText = endText = null;
        beginOffset = endOffset = -1;

        MarkManager markManager = docView.getMarkManager();2
        if (markManager == null) {
            return false;
        }

        if ("lower".equals(parameter)) {3
            op = Op.LOWER;
        } else if ("upper".equals(parameter)) {
            op = Op.UPPER;
        } else if ("capital".equals(parameter)) {
            op = Op.CAPITAL;
        } else {
            return false;
        }

        NodeMark selected = markManager.getSelected();
        if (selected != null) {4
            Node selectedNode = selected.getNode();

            NodeMark selected2 = markManager.getSelected2();
            if (selected2 != null && selected2.getNode() != selectedNode) {
                // Not implemented.
                return false;
            }

            beginText = 
                (TextNode) Traversal.traverse(selectedNode, 
                                              Traversal.textNodeFinder);
            if (beginText == null) {
                // Does not contain text.
                return false;
            }

            beginOffset = 0;

            endText = 
              (TextNode) Traversal.traverseBackwards(selectedNode, 
                                                     Traversal.textNodeFinder);
            // endText cannot be null because beginText is not null.
            endOffset = endText.getTextLength();

            if (beginText == endText && beginOffset == endOffset) {
                // Single empty text node: nothing to convert.
                return false;
            }
        } else {
            TextLocation dot = markManager.getDot();
            if (dot == null) {
                // Document does not contain text.
                return false;
            }

            beginText = dot.getTextNode();
            beginOffset = dot.getOffset();

            TextLocation mark = markManager.getMark();
            if (mark != null) {5
                endText = mark.getTextNode();
                endOffset = mark.getOffset();
            } else {
                endText = beginText;
                endOffset = beginOffset;
            }

            if (beginText == endText) {
                if (beginOffset == endOffset) {6
                    // Not text selection: convert to end of word.

                    int count = beginText.getTextLength();
                    while (endOffset < count) {
                        if (XMLText.isXMLSpace(
                                beginText.getTextChar(endOffset))) {
                            break;
                        }
                        ++endOffset;
                    }

                    if (endOffset == beginOffset)  {
                        // Nothing to convert (empty text node or caret at end
                        // of word).
                        return false;
                    }
                } else if (beginOffset > endOffset) {
                    int swap;
                    swap = beginOffset; 
                    beginOffset = endOffset; 
                    endOffset = swap;
                }
            }
        }

        return true;
    }

1

Initializes the instance variables of ConvertCaseCmd. If prepare is successful, these instance variables will be assigned new values to prepare the job of doExecute.

2

If a DocumentView has no MarkManager, this means that no Document is being displayed. In such case, ConvertCaseCmd cannot be executed and prepare immediately returns false.

3

Parameter is parsed and parsed value is assigned to instance variable conversion. Note that when parameter is syntactically incorrect, prepare just returns false without reporting an error message.

4

Case where nodes are selected.

Selection of a node range is not supported by ConvertCaseCmd. In such case, prepare simply returns false.

If selected node does not contain textual nodes or if selected element only contains a single empty textual node, prepare also returns false. (The case where selected element only contains multiple empty textual nodes is not detected.)

5

Case where there is a text selection.

6

Case where there is no text selection (or when mark is located at the same place than dot). Case conversion will be performed from dot position to end of word.

1.2. Second step: doExecute

    public CommandResult doExecute(DocumentView docView, 
                                   String parameter, int x, int y) {
        MarkManager markManager = docView.getMarkManager();
        markManager.beginMark();1

        boolean swapped = false;

        if (beginText == endText) {2
            convertCase(beginText, beginOffset, endOffset - beginOffset);
        } else {
            Document doc = docView.getDocument();
            doc.beginEdit();3

            TextRangeList rangeList = new TextRangeList();

            // Note that TextCollector handles the case where beginText is
            // after endText.
            TextCollector.Status status = 
                (new TextCollector()).collect(beginText, beginOffset, 
                                              endText, endOffset, 
                                              rangeList);4
            swapped = (status == TextCollector.Status.COLLECTED_AFTER_SWAP);

            TextRange[] list = rangeList.list;
            int count = rangeList.size;

            for (int i = 0; i < count; ++i) {5
                TextRange range = list[i];
                convertCase(range.text, range.offset, range.count);
            }

            doc.endEdit();
        }

        docView.describeUndo("CONVERT TO " + op.toString() + "CASE");6

        markManager.remove(Mark.SELECTED);7
        markManager.remove(Mark.SELECTED2);
        markManager.remove(Mark.MARK);
        if (swapped) {
            markManager.getDot().moveTo(beginText, beginOffset);
        } else {
            markManager.getDot().moveTo(endText, endOffset);
        }
        markManager.endMark();

        op = null;8
        beginText = endText = null;
        beginOffset = endOffset = -1;

        return CommandResult.DONE;
    }

    private void convertCase(TextNode text, int offset, int count) {
        char[] chars = text.getTextChars();

        switch (op) {
        case LOWER:
            text.replaceText(offset, count, 
                             (new String(chars, offset, count)).toLowerCase());
            break;
        case UPPER:
            text.replaceText(offset, count, 
                             (new String(chars, offset, count)).toUpperCase());
            break;
        default:
            {
                char[] replacement = new char[count];
                System.arraycopy(chars, offset, replacement, 0, count);

                boolean firstCharOfWord = true;
                for (int i = 0; i < count; ++i) {
                    char c = replacement[i];

                    if (XMLText.isXMLSpace(c)) {
                        firstCharOfWord = true;
                    } else {
                        if (firstCharOfWord) {
                            firstCharOfWord = false;
                            replacement[i] = Character.toUpperCase(c);
                        } else {
                            replacement[i] = Character.toLowerCase(c);
                        }
                    }
                }

                text.replaceText(offset, count, new String(replacement));
            }
        }
    }

1

Using beginMark/endMark prevents the MarkManager to report several editing context changes when several marks are added, moved or removed in a batch.

When the editing context changes, some commands can no longer be executed and other commands which were disabled, now become executable. This is the definition and the sole purpose of the ContextChangeEvent.

An application such as XXE invokes the prepare methods of all commands used in its GUI and updates all its menus and toolbars each time the MarkManager of the document being edited reports a change for the editing context. This type of update is not quick and should occur when really needed.

2

Optimized case: case conversion from caret position to end of word. Note that the general case could have handled this simple case perfectly well.

3

BeginEdit/endEdit is used to mark a sequence of low-level editing operations as being a single high-level editing command .

The UndoManager uses this feature to undo the whole side effect of a command, whatever does this command, rather than to undo multiple low-level operations acting on Document nodes (by the way, such atomic operations generally mean nothing at all for the user of the XML editor).

4

TextCollector is one of the many available implementations of Traversal.Handler. It is used to collect the textual ranges (in the form of TextRange objects) contained in the area on which ConvertCaseCmd must operate.

5

General case: case conversion of all text ranges found by TextCollector.

6

The describeUndo convenience method of DocumentView is used to tag the new undo action created by the UndoManager in response to document modification by ConvertCaseCmd. Without this user-friendly label, the user of the XML editor would just see "Undo" as the “tool tip” of the Undo button of the main tool bar. Using this method allows the user to see "Undo CONVERT TO UPPERCASE".

7

An editing command always updates the marks once it has finished its job. There is no general rule: a command must try to update the marks in a way which clearly indicates to the user what has been done.

8

It is generally a good idea to reinitialize the instance variables at the end of doExecute. Not keeping references to document nodes helps the Java™ garbage collector to do its job.