Table of Contents
In the previous chapter we have learned:
What is the Command interface (and the more convenient base class CommandBase).
What are the selection Marks.
Batching low-level editing operations for the UndoManager.
Batching selection operations to spare ContextChangeEvents.
Registering custom commands with XXE.
Specifying custom bindings programmatically.
But reading the code of a single command is not sufficient to have a clear idea of what is a command and how to write a custom one.
In this chapter, we will study three very different commands:
When a character such as ')' is typed, inserts this character at caret position and then highlights matching '(' for half a second. (Identical to the standard showMatchingChar command. See Section 77, “showMatchingChar” in XMLmind XML Editor - Commands.)
Wraps selected element in a new parent element which is chosen interactively using the element chooser dialog box. (Same behavior than the standard wrap command in the case of single element selection. See Section 87, “wrap” in XMLmind XML Editor - Commands.)
Converts text lines contained in the clipboard to a sequence of paragraphs. (Identical to the standard makeParagraphs command. See Section 43, “makeParagraphs” in XMLmind XML Editor - Commands.)
When a character such as ')' is typed, ShowMatchingCharCmd inserts this character at caret position and then highlights matching '(' for half a second.
When writing such command, a developer may be tempted to find the bounding box of the matching '(' using methods such as DocumentView.modelToView and to directly draw the highlight on the DocumentPane containing the DocumentView.
Commands should never do this. Commands should only deal with nodes and selection marks.
Nodes which are contained in a Document are rendered graphically using NodeViews created by a ViewFactory. Similarly, Marks which are attached to Nodes and which managed by a MarkManager are rendered graphically using Highlights created by a HighlightFactory.
Dot, mark, selected, selected2 are special marks only because standard commands operate on them. An application can create its own marks for any purpose.
A custom mark has an ID (any Object which implements hashCode() and equals() will do) and is attached to a Node. It is created using MarkManager.set(Object id, Node node) for NodeMarks and MarkManager.set(Object id, TextNode text, int offset) for TextLocations.
Standard commands completely ignore these custom marks. For example, standard command CancelSelection will leave these marks intact. The only way to get rid of them is either to remove them using MarkManager.remove or to delete the Nodes they are attached to.
Custom marks are rendered graphically using Highlights created by the HighlightFactory of the DocumentView. By default, the HighlightFactory of a DocumentView is a BasicHighlightFactory.
A BasicHighlightFactory creates Highlights which are:
Similar to the caret for a TextLocation with a String ID which starts with "bookmark.".
For example, it will create a colored vertical bar after a text location marked "bookmark.foo".
Similar to the text selection for two TextLocations with String IDs which start with "highlight." and "highlight2.".
For example, it will create a colored background behind the area between a text location marked "highlight.bar" and another text location marked "highlight2.bar".
Similar to the node selection for two NodeMarks with String IDs which start with "highlight." and "highlight2.".
For example, it will create a colored box around the area between a node marked "highlight.wiz" and its sibling node marked "highlight2.wiz".
Therefore, in order to highlight the matching '(', ShowMatchingCharCmd creates a TextLocation named "highlight.matchingChar" before the matching N'(' and a TextLocation named "highlight2.matchingChar" after the matching N'('. After half a second, ShowMatchingCharCmd automatically removes these marks.
public class ShowMatchingCharCmd extends CommandBase
implements Traversal.Handler {
private int highlightId = 0;
private char matchingChar;
private char insertedChar;
private int charCount;
public boolean prepare(DocumentView docView,
String parameter, int x, int y) {
if (parameter == null || parameter.length() != 1)
return false;
return docView.canInsertString();
}
...ShowMatchingCharCmd can be executed if:
The document view is not empty.
The document loaded in the document view contains some text. In such case, the caret shows where to insert ')', ']' or '}'.
The text node containing the caret has not been marked as being read-only.
All the above conditions are tested by DocumentView.canInsertString.
...
public Object execute(DocumentView docView,
String parameter, int x, int y) {
String s = parameter.substring(0, 1);
docView.insertString(s);
insertedChar = s.charAt(0);
switch (insertedChar) {
case '}':
matchingChar = '{';
break;
case ')':
matchingChar = '(';
break;
case ']':
matchingChar = '[';
break;
default:
matchingChar = '\0';
}
if (matchingChar == '\0') {
docView.getToolkit().beep();
return null;
}
final MarkManager markManager = docView.getMarkManager();
TextLocation dot = markManager.getDot();
charCount = 0;
TextOffset match = null;
if (dot.getOffset() > 0)
match = processTextNode(dot.getTextNode(), dot.getOffset() - 1);
if (match == null)
match = (TextOffset) Traversal.traverseBefore(dot.getTextNode(),
this);
if (match == null) {
docView.getToolkit().beep();
return null;
}
Rectangle r1 = docView.modelToView(match.text, match.offset);
Rectangle r2 = docView.getVisibleRectangle();
// r1 is null if the matching char is contained in a collapsed section.
if (r1 == null || r1.height <= 0 ||
r2.height <= 0 || !r1.intersects(r2)) {
char[] chars = match.text.getTextChars();
int count = 1;
int boundary = match.offset - 1;
loop: while (boundary >= 0) {
switch (chars[boundary]) {
case '\n': case '\r':
break loop;
}
++count;
if (count == 80)
break;
--boundary;
}
String line = new String(chars, boundary+1, match.offset-boundary);
line = XMLText.collapseWhiteSpace(line);
if (boundary >= 0 && count == 80)
line = "..." + line;
ShowStatus.showStatus("Matches '" + line + "'");
return null;
}
final String id1 = "highlight.matchingChar" + highlightId;
final String id2 = "highlight2.matchingChar" + highlightId;
++highlightId;
markManager.add(id2, match.text, match.offset+1);
markManager.add(id1, match.text, match.offset);
Timer timer =
new Timer(500 /*ms*/, new ActionListener() {
public void actionPerformed(ActionEvent e) {
markManager.remove(id1);
markManager.remove(id2);
}
});
timer.setRepeats(false);
timer.start();
return null;
}
...DocumentView.insertString inserts specified string before caret. If there is a text selection, insertString deletes the nodes specified by the selection and then inserts its argument. | |
Find character corresponding to inserted character. | |
If the caret is not located a the very beginning of a text node, search matching character before the caret. | |
If matching character has not been found, search it in nodes which precede the node containing the caret. | |
Matching character is found. ShowMatchingCharCmd doesn't need to highlight it if it is out of sight. DocumentView.modelToView and getVisibleRectangle are used to determine whether the matching character is out of sight. | |
When matching character is out of sight, ShowMatchingCharCmd simply prints a message containing the text line which precedes this character. | |
TextLocations with IDs " | |
After 500ms, a Timer removes TextLocations with IDs " |
The following Traversal.Handler is used to find the matching '(' in nodes which precede the node where ')':
...
private TextOffset processTextNode(TextNode textNode, int from) {
char[] chars = textNode.getTextChars();
if (from < 0)
from = chars.length - 1;
for (int i = from; i >= 0; --i) {
char c = chars[i];
if (c == insertedChar) {
++charCount;
} else if (c == matchingChar) {
--charCount;
if (charCount == 0)
return new TextOffset(textNode, i);
}
}
return null;
}
// ---------------------------------------
// Traversal.Handler
// ---------------------------------------
public Object processText(Text text) {
return processTextNode(text, -1);
}
public Object processPI(ProcessingInstruction pi) {
return processTextNode(pi, -1);
}
public Object processComment(Comment comment) {
return processTextNode(comment, -1);
}
public Object enterElement(Element element) {
return null;
}
public Object leaveElement(Element element) {
return null;
}
}WrapElementCmd wraps explicitly or implicitly selected element in a new parent element.
Example:
"<blockquote><para>the <emphasis>little</emphasis> girl.</para></blockquote>"
In the above blockquote, para is implicitly selected because the caret is before word "girl". User chooses to wrap it in a note element, which gives:
"<blockquote><note><para>the <emphasis>little</emphasis> girl.</para></note><blockquote>".
WrapElementCmd is an example of a validating command, that is, a command which cannot make the document invalid by inserting, deleting or replacing nodes.
This kind of command often requires the user to specify which element is to be inserted in the document. In such case, the chooser dialog box should only display elements which, when inserted in the document, cannot make it invalid.
This kind of command always uses an ElementEditor.
An ElementEditor is a validating editor which operates on the child nodes of a given element.
In order to use an ElementEditor, you need to create it and then to specify which element is being edited:
ElementEditor myElementEditor = new ElementEditor(myDocumentType); myElementEditor.setEditedElement(myElement);
It implements a number of editing operations: delete, insert, replace, convert, wrap, cut, paste, etc.
For each operation, there are 3 methods:
can which answers the question "can I do OP(node range)OP here without making the document invalid".
Example: canWrap(Node child1, Node child2).
can which answers the question "how can I do OP(node range, list of suggestions)OP here without making the document invalid".
Example: canWrap(Node child1, Node child2, List<Field> which).
which actually performs operation OP(node range, argument)OP.
Example: wrap(Node child1, Node child2, Field childField, Name childName).
The concept of Field is a complex one and is beyond the scope of this tutorial. To make it simple, let's say that a child element Field and a child element Name are used to specify a child element type.
Validating commands operating on the text selection rather than on the node selection use a TextEditor rather than an ElementEditor.
public class WrapElementCmd extends CommandBase {
private Element element;
public boolean prepare(DocumentView docView,
String parameter, int x, int y) {
element = docView.getSelectedElement(/*implicit*/ true);
if (element == null)
return false;
Element editedElement = element.getParentElement();
if (editedElement == null)
return false;
ElementEditor elementEditor = docView.getElementEditor();
elementEditor.editElement(editedElement);
return elementEditor.canWrap(element, element);
}
...WrapElementCmd can be executed only if a single element is implicitly or explicitly selected. getSelectedElement(true) returns this implicitly or explicitly selected element. It returns | |
The element being edited is the parent of the selected element. | |
Commands do not need to create ElementEditors or TextEditors. The DocumentView has ready-to-use validating editors. Such editors can be accessed using getElementEditor and getTextEditor. | |
editElement is more efficient than setEditedElement but can only be used with the ElementEditor owned by the DocumentView. | |
Tests if there is at least one parent element which can be used to wrap selected element. |
...
public Object execute(DocumentView docView,
String parameter, int x, int y) {
ElementEditor elementEditor = docView.getElementEditor();
Element editedElement = element.getParentElement();
ArrayList<Field> fieldList = new ArrayList<Field>();
elementEditor.canWrap(element, element, fieldList);
Field[] fields = new Field[fieldList.size()];
fieldList.toArray(fields);
String title = "Wrap Element";
FieldChooserDialog dialog =
new FieldChooserDialog(docView.getPanel(), title);
FieldChoice[] choices =
FieldChoice.list(fields, /*includingText*/ false,
/*includingWildcards*/ true,
/*includingTemplates*/ false, editedElement);
FieldChoice choice = dialog.chooseField(choices, editedElement);
if (choice == null)
return null;
MarkManager markManager = docView.getMarkManager();
markManager.beginMark();
Element wrapper = elementEditor.wrap(element, element,
choice.field, choice.name);
docView.describeUndo(title);
markManager.remove(Mark.MARK);
markManager.remove(Mark.SELECTED2);
markManager.set(Mark.SELECTED, wrapper);
docView.moveDotInto(wrapper);
markManager.endMark();
return null;
}
}There is no need to call | |
canWrap fills ArrayList fieldList with all possible Fields. | |
FieldChoice.list is used to create a properly labeled list of choices out of a list of Fields and a context ( | |
FieldChooserDialog is displayed to let the user specify which element he wants to use to wrap selected element. This dialog box returns a FieldChoice describing user's choice or | |
Wraps selected element in a new parent element. | |
Selects the newly created parent element. |
Authors often need to paste in their XML documents text blocks copied from a word processor.
Without a special command, doing so is tedious because:
The author must create a blank paragraph in the XML document.
The author must paste text copied from the clipboard inside this paragraph.
If multiple text blocks have been copied, the author must remove open lines and split the paragraph in multiple pieces.
A special command can do all this automatically. Such a command is a validating one because the author must not be allowed to insert captured paragraphs in places which would make the document invalid.
The Paste command can be passed plain text or an XML string (a string which starts with "<?xml ") as its parameter (see Section 48, “paste” in XMLmind XML Editor - Commands). So why not use this standard validating command to do the paragraph insertion?
The idea here is to write a simple command, MakeParagraphsCmd, which returns an XML string containing one or several paragraphs built using clipboard content. This string is then passed to the Paste command which inserts the paragraphs (if the XML Schema or the DTD constraining the document allows to do so).
Note that MakeParagraphsCmd cannot be used alone. It must be part of a macro-command such as the following one (DocBook example):
<command name="insertAfterAsSimpara">
<macro>
<sequence>
<command name="MakeParagraphsCmd" parameter="simpara" />
<command name="paste" parameter="after[implicitElement] %_" />
</sequence>
</macro>
</command>public class MakeParagraphsCmd extends CommandBase {
private Name elementName;
private boolean systemSelection;
private boolean blocks;
private String pastable;
public boolean prepare(DocumentView docView,
String parameter, int x, int y) {
elementName = null;
pastable = null;
if (docView.getDocument() == null)
return false;
if (parameter == null || parameter.length() == 0)
return false;
String name = null;
int pos = parameter.lastIndexOf(']');
if (pos < 0)
name = parameter;
else if (pos+1 < parameter.length())
name = parameter.substring(pos+1);
if (name != null)
elementName = Name.fromString(name.trim());
if (elementName == null)
return false;
systemSelection = (parameter.indexOf("[systemSelection]") >= 0);
blocks = (parameter.indexOf("[blocks]") >= 0);
if (systemSelection)
pastable = SystemSelection.get(docView.getPanel());
else
pastable = Clipboard.get(docView.getPanel());
if (pastable == null || pastable.length() == 0 ||
pastable.startsWith("<?xml "))
return false;
return true;
}
...This command must be passed a parameter. | |
After options DocBook 4 examples: XHTML example: {http://www.w3.org/1999/xhtml} Email example (see next chapter): | |
If the | |
If the | |
MakeParagraphsCmd cannot be executed if the clipboard is empty or if it contains XML. |
...
public Object execute(DocumentView docView,
String parameter, int x, int y) {
ArrayList<String> textList = new ArrayList<String>();
StringBuilder block = new StringBuilder();
LineNumberReader lines =
new LineNumberReader(new StringReader(pastable));
String line;
while ((line = readLine(lines)) != null) {
line = XMLText.collapseWhiteSpace(line);
if (line.length() == 0) {
// Open line.
if (blocks && block.length() > 0) {
textList.add(block.toString());
block = new StringBuilder();
}
continue;
}
line = XMLText.filterText(line);
if (blocks) {
if (block.length() > 0)
block.append(' ');
block.append(line);
} else {
textList.add(line);
}
}
if (blocks && block.length() > 0)
textList.add(block.toString());
int count = textList.size();
if (count == 0)
return EXECUTION_FAILED;
// The content of the clipboard is fetched as a *Java String*.
// Therefore no need to specify encoding in the XML declaration.
StringBuilder result = new StringBuilder();
result.append("<?xml version='1.0'?>");
openTag(ClipboardFormat.ENVELOPE_NAME, result);
for (int i = 0; i < count; ++i) {
String text = (String) textList.get(i);
openTag(elementName, result);
XMLText.escapeXML(text, result);
closeTag(elementName, result);
}
closeTag(ClipboardFormat.ENVELOPE_NAME, result);
return result.toString();
}
...This loop reads the content of the clipboard line by line and, for each paragraph that needs to be created, adds its textual content to ArrayList textList. | |
Whitespaces contained in the textual content of a future paragraph are collapsed using utility XMLText.collapseWhiteSpace. | |
Non-XML characters contained in the textual content of a future paragraph are discarded using utility XMLText.filterText. | |
The content of the clipboard is fetched as a Java™ String. Therefore there is no need to specify an encoding in the XML declaration. | |
XXE only supports text inside the clipboard. This text is considered to be XML (that is, not plain text) if it starts with " As expected, the following text is parsed as a single <?xml version="1.0"?><p>Paragraph #1.</p> Using special wrapper element <?xml version="1.0"?> <ns:clipboard xmlns:ns="http://www.xmlmind.com/xmleditor/namespace/clipboard"> <p>Paragraph #1.</p> <p>Paragraph #2.</p> </ns:clipboard> |
Execute uses the following helper methods:
...
private static final String readLine(LineNumberReader lines) {
try {
return lines.readLine();
} catch (IOException cannotHappen) {
cannotHappen.printStackTrace();
return null;
}
}
private static final void openTag(Name name, StringBuilder buffer) {
buffer.append('<');
buffer.append(name.localPart);
buffer.append(" xmlns='");
buffer.append(name.namespace.uri);
buffer.append("'>");
}
private static final void closeTag(Name name, StringBuilder buffer) {
buffer.append("</");
buffer.append(name.localPart);
buffer.append('>');
}
}
All the built-in commands of XMLmind XML Editor are recordable. That is, the user can automatically create a macro-command by recording the execution of a sequence of commands ( → → , ) and then replay the recorded macro-command ( → → ).
For simplicity, none of the commands described in the previous chapters have been made recordable.
The easiest way to make a command recordable is to extend the RecordableCommand class rather than implement the Command interface or extend the CommandBase base class.
Example:
public class ConvertCaseCmd extends CommandBase {
.
.
.
public Object execute(DocumentView docView,
String parameter, int x, int y) {
Some code
.
.
.
}
}becomes:
public class ConvertCaseCmd extends RecordableCommand {
.
.
.
protected Object doExecute(DocumentView docView,
String parameter, int x, int y) {
Exactly the same code as above
.
.
.
}
}Extending the RecordableCommand is not the only way to make a command recordable. In order to explain the other, lower-level, method, we'll look at the way the RecordableCommand abstract class is implemented:
public abstract class RecordableCommand extends CommandBase {
public Object execute(DocumentView docView,
String parameter, int x, int y) {
.
.
.
if (!docView.notifyCommandListeners(this, parameter, x, y,
CommandEvent.Status.UNKNOWN, null))
// Vetoed by a listener: give up.
return EXECUTION_FAILED;
CommandEvent.Status status;
Object result;
try {
result = doExecute(docView, parameter, x, y);
status = CommandEvent.Status.EXECUTED;
} catch (Exception ex) {
Report the error on the console
result = EXECUTION_FAILED;
status = CommandEvent.Status.CRASHED;
}
docView.notifyCommandListeners(this, parameter, x, y, status, result);
return result;
}
protected abstract Object doExecute(DocumentView docView,
String parameter, int x, int y);
}Notify CommandListeners that we are about the execute a command. This is done using DocumentView.notifyCommandListeners. CommandEvent.Status.UNKNOWN implies that the command has not yet been executed. CommandListeners such as the facility used to record macros (command RecordMacro) register themselves with a DocumentView using DocumentView.addCommandListener. Such listeners will receive
| |
CommandListeners may veto the execution of a command[6]. In which case the vetoed command must immediately return value EXECUTION_FAILED. | |
After the command has been executed, successfully or not, notify the listeners one more time. This time, a status different from CommandEvent.Status.UNKNOWN (Status.EXECUTED or Status.CRASHED) means that the command has been executed. |