When a character such as ')'
is typed, ShowMatchingCharCmd
inserts this character at caret position and then highlights matching '('
for half a second. This is a slightly simplified version of command showMatchingChar
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.
Node
s which are contained in a Document
are rendered graphically using NodeView
s created by a ViewFactory
. Similarly, Mark
s which are attached to Nodes and which managed by a MarkManager
are rendered graphically using Highlight
s created by a HighlightFactory
.
Highlighting text involves marks than dot
, mark
, selected
and selected2
. 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 NodeMark
s and MarkManager.set(Object id, TextNode text, int offset)
for TextLocation
s.
Standard commands completely ignore these custom marks. For example, standard command cancelSelection
in XMLmind XML Editor - Commands leaves these marks intact. The only way to get rid of them is either to remove them using MarkManager.remove
or to delete the Node
s they are attached to.
Custom marks are rendered graphically using Highlight
s created by the HighlightFactory
of the DocumentView
. By default, the HighlightFactory
of a DocumentView
is a BasicHighlightFactory
.
A BasicHighlightFactory
creates Highlight
s 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 TextLocation
s 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 NodeMark
s 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 another TextLocation
named "highlight2.matchingChar
" after the matching N
'('
. After half a second, ShowMatchingCharCmd
automatically removes these marks.
Excerpts from ShowMatchingCharCmd.java
:
public class ShowMatchingCharCmd extends CommandBase implements Traversal.Handler { private int highlightId; private char matchingChar; private char insertedChar; private int charCount; public ShowMatchingCharCmd() { super(/*repeatable*/ false, /*recordable*/ true); highlightId = 0; } 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 CommandResult doExecute(DocumentView docView, String parameter, int x, int y) { String s = parameter.substring(0, 1); docView.insertString(s, docView.getOverwriteMode()); 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 CommandResult.DONE; } 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 CommandResult.DONE; } 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; } docView.showStatus("IT MATCHES '" + line + "'"); return CommandResult.DONE; } final String id1 = "highlight.matchingChar" + highlightId; final String id2 = "highlight2.matchingChar" + highlightId; ++highlightId; markManager.beginMark(); markManager.add(id2, match.text, match.offset+1); markManager.add(id1, match.text, match.offset); markManager.endMark(); Timer timer = new Timer(500 /*ms*/, new ActionListener() { public void actionPerformed(ActionEvent e) { markManager.beginMark(); markManager.remove(id1); markManager.remove(id2); markManager.endMark(); } }); timer.setRepeats(false); timer.start(); return CommandResult.DONE; }
| |
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. | |
When matching character is out of sight, | |
| |
After 500ms, a |
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; }