Author: rwhitcomb Date: Tue Apr 3 23:01:30 2018 New Revision: 1828291 URL: http://svn.apache.org/viewvc?rev=1828291&view=rev Log: PIVOT-891: Further improvements to TextPane: * Fix a problem doing delete at end of paragraph and then inserting right there again -- the last node in the sequence may not get its offset updated because the character count is decremented first; should be decremented after. * Make a bunch of additions to deal with the new CharSpan class and so we can make use of common code to do word, line selection amongst all three text input controls. * Add some debug code in TextPane to print out node trees with their offsets and counts. * Use the new CharUtils methods for moving around in and selecting text "words".
Modified: pivot/trunk/wtk/src/org/apache/pivot/wtk/TextPane.java pivot/trunk/wtk/src/org/apache/pivot/wtk/skin/TextPaneSkin.java pivot/trunk/wtk/src/org/apache/pivot/wtk/text/ComponentNode.java pivot/trunk/wtk/src/org/apache/pivot/wtk/text/Element.java pivot/trunk/wtk/src/org/apache/pivot/wtk/text/Node.java pivot/trunk/wtk/src/org/apache/pivot/wtk/text/TextNode.java Modified: pivot/trunk/wtk/src/org/apache/pivot/wtk/TextPane.java URL: http://svn.apache.org/viewvc/pivot/trunk/wtk/src/org/apache/pivot/wtk/TextPane.java?rev=1828291&r1=1828290&r2=1828291&view=diff ============================================================================== --- pivot/trunk/wtk/src/org/apache/pivot/wtk/TextPane.java (original) +++ pivot/trunk/wtk/src/org/apache/pivot/wtk/TextPane.java Tue Apr 3 23:01:30 2018 @@ -28,6 +28,7 @@ import org.apache.pivot.beans.DefaultPro import org.apache.pivot.collections.LinkedStack; import org.apache.pivot.collections.Sequence; import org.apache.pivot.text.AttributedStringCharacterIterator; +import org.apache.pivot.text.CharSpan; import org.apache.pivot.util.ListenerList; import org.apache.pivot.util.StringUtils; import org.apache.pivot.util.Utils; @@ -428,9 +429,11 @@ public class TextPane extends Container Paragraph paragraph = (Paragraph) descendant; Node node = getRightmostDescendant(paragraph); +dumpNode("insertText, paragraph", paragraph, 0); if (node instanceof TextNode) { // Insert the text into the existing node TextNode textNode = (TextNode) node; +System.out.format("textNode insert: index=%1$d, textNode.doc offset=%2$d, paragraph.doc offset=%3$d%n", index, textNode.getDocumentOffset(), paragraph.getDocumentOffset()); textNode.insertText(text, index - textNode.getDocumentOffset()); } else if (node instanceof Element) { // Append a new text node @@ -586,12 +589,11 @@ public class TextPane extends Container if (offset >= 0 && offset < document.getCharacterCount()) { Node descendant = document.getDescendantAt(offset); - +dumpNode("removeText, grandparent", descendant.getParent().getParent(), 0); // Used to be: if (selectionLength == 0 && ... if (characterCount <= 1 && descendant instanceof Paragraph) { // We are deleting a paragraph terminator Paragraph paragraph = (Paragraph) descendant; - Element parent = paragraph.getParent(); int index = parent.indexOf(paragraph); @@ -710,8 +712,9 @@ public class TextPane extends Container int start = selectionStart; int tabWidth = ((TextPane.Skin) getSkin()).getTabWidth(); Node currentNode = document.getDescendantAt(start); - while (!(currentNode instanceof Paragraph)) + while (!(currentNode instanceof Paragraph)) { currentNode = currentNode.getParent(); + } int paragraphStartOffset = start - currentNode.getDocumentOffset(); bulkOperation = true; @@ -993,6 +996,15 @@ public class TextPane extends Container } /** + * Returns a character span (start, length) representing the current selection. + * + * @return A char span with the start and length values. + */ + public CharSpan getCharSelection() { + return new CharSpan(selectionStart, selectionLength); + } + + /** * Sets the selection. The sum of the selection start and length must be * less than the length of the text input's content. * @@ -1029,6 +1041,7 @@ public class TextPane extends Container * * @param selection The new span describing the selection. * @see #setSelection(int, int) + * @throws IllegalArgumentException if the span is {@code null}. */ public final void setSelection(Span selection) { Utils.checkNull(selection, "selection"); @@ -1037,6 +1050,19 @@ public class TextPane extends Container } /** + * Sets the selection. + * + * @param selection The character span (start and length) for the selection. + * @see #setSelection(int, int) + * @throws IllegalArgumentException if the character span is {@code null}. + */ + public final void setSelection(CharSpan selection) { + Utils.checkNull(selection, "selection"); + + setSelection(selection.start, selection.length); + } + + /** * Change the selection to the given location (and length 0), * with checks to make sure the selection doesn't go out of bounds. * <p> Meant to be called from {@link #undo} (that is, internally). @@ -1169,4 +1195,18 @@ public class TextPane extends Container public ListenerList<TextPaneSelectionListener> getTextPaneSelectionListeners() { return textPaneSelectionListeners; } + + private static final String FORMAT = "%1$s%2$s: doc=%3$d, count=%4$d%n"; + + public static void dumpNode(String msg, Node node, int indent) { + if (msg != null && !msg.isEmpty()) { + System.out.println(msg + ":"); + } + System.out.format(FORMAT, StringUtils.fromNChars(' ', indent*2), node.getClass().getSimpleName(), node.getDocumentOffset(), node.getCharacterCount()); + if (node instanceof Element) { + for (Node n : (Element)node) { + dumpNode("", n, indent + 1); + } + } + } } Modified: pivot/trunk/wtk/src/org/apache/pivot/wtk/skin/TextPaneSkin.java URL: http://svn.apache.org/viewvc/pivot/trunk/wtk/src/org/apache/pivot/wtk/skin/TextPaneSkin.java?rev=1828291&r1=1828290&r2=1828291&view=diff ============================================================================== --- pivot/trunk/wtk/src/org/apache/pivot/wtk/skin/TextPaneSkin.java (original) +++ pivot/trunk/wtk/src/org/apache/pivot/wtk/skin/TextPaneSkin.java Tue Apr 3 23:01:30 2018 @@ -31,7 +31,9 @@ import java.text.AttributedCharacterIter import org.apache.pivot.collections.Dictionary; import org.apache.pivot.collections.Sequence; import org.apache.pivot.text.AttributedStringCharacterIterator; +import org.apache.pivot.text.CharSpan; import org.apache.pivot.text.CompositeIterator; +import org.apache.pivot.util.CharUtils; import org.apache.pivot.util.Utils; import org.apache.pivot.wtk.ApplicationContext; import org.apache.pivot.wtk.Bounds; @@ -43,6 +45,7 @@ import org.apache.pivot.wtk.Insets; import org.apache.pivot.wtk.Keyboard; import org.apache.pivot.wtk.Mouse; import org.apache.pivot.wtk.Platform; +import org.apache.pivot.wtk.SelectDirection; import org.apache.pivot.wtk.TextInputMethodListener; import org.apache.pivot.wtk.TextPane; import org.apache.pivot.wtk.TextPaneListener; @@ -277,6 +280,7 @@ org.apache.pivot.util.Console.logMethod( protected boolean doingCaretCalculations = false; private int anchor = -1; + private SelectDirection selectDirection = null; private TextPane.ScrollDirection scrollDirection = null; private int mouseX = -1; @@ -924,61 +928,6 @@ org.apache.pivot.util.Console.logMethod( return consumed; } - private void selectSpan(TextPane textPane, Document document, int start) { - int rowStart = getRowOffset(document, start); - int rowLength = getRowLength(document, start); - if (start - rowStart >= rowLength) { - start = rowStart + rowLength - 1; - if (start < 0) { - return; - } - char ch = document.getCharacterAt(start); - if (ch == '\r' || ch == '\n') { - start--; - } - } - if (start < 0) { - return; - } - char ch = document.getCharacterAt(start); - int selectionStart = start; - int selectionLength = 1; - if (Character.isWhitespace(ch)) { - // Move backward to beginning of whitespace block - // but not before the beginning of the line. - do { - selectionStart--; - } while (selectionStart >= rowStart - && Character.isWhitespace(document.getCharacterAt(selectionStart))); - selectionStart++; - selectionLength = start - selectionStart; - // Move forward to end of whitespace block - // but not past the end of the text or the end of line - do { - selectionLength++; - } while (selectionStart + selectionLength - rowStart < rowLength - && Character.isWhitespace(document.getCharacterAt(selectionStart + selectionLength))); - } else if (Character.isJavaIdentifierPart(ch)) { - // Move backward to beginning of identifier block - do { - selectionStart--; - } while (selectionStart >= rowStart - && Character.isJavaIdentifierPart(document.getCharacterAt(selectionStart))); - selectionStart++; - selectionLength = start - selectionStart; - // Move forward to end of identifier block - // but not past end of text - do { - selectionLength++; - } while (selectionStart + selectionLength - rowStart < rowLength - && Character.isJavaIdentifierPart(document.getCharacterAt(selectionStart - + selectionLength))); - } else { - return; - } - textPane.setSelection(selectionStart, selectionLength); - } - @Override public boolean mouseClick(Component component, Mouse.Button button, int x, int y, int count) { boolean consumed = super.mouseClick(component, button, x, y, count); @@ -990,7 +939,10 @@ org.apache.pivot.util.Console.logMethod( int index = getInsertionPoint(x, y); if (index != -1) { if (count == 2) { - selectSpan(textPane, document, index); + CharSpan charSpan = CharUtils.selectWord(getRowCharacters(document, index), index); + if (charSpan != null) { + textPane.setSelection(charSpan); + } } else if (count == 3) { textPane.setSelection(getRowOffset(document, index), getRowLength(document, index)); } @@ -1022,35 +974,45 @@ org.apache.pivot.util.Console.logMethod( return consumed; } - private int getRowOffset(Document document, int index) { + private Node getParagraphAt(Document document, int index) { if (document != null) { Node node = document.getDescendantAt(index); while (node != null && !(node instanceof Paragraph)) { node = node.getParent(); } - // TODO: doesn't take into account the line wrapping within a paragraph - if (node != null) { - return node.getDocumentOffset(); - } + return node; + } + return null; + } + + private int getRowOffset(Document document, int index) { + Node node = getParagraphAt(document, index); + // TODO: doesn't take into account the line wrapping within a paragraph + if (node != null) { + return node.getDocumentOffset(); } return 0; } private int getRowLength(Document document, int index) { - if (document != null) { - Node node = document.getDescendantAt(index); - while (node != null && !(node instanceof Paragraph)) { - node = node.getParent(); - } - // TODO: doesn't take into account the line wrapping within a paragraph - // Assuming the node is a Paragraph, the count includes the trailing \n, so discount it - if (node != null) { - return node.getCharacterCount() - 1; - } + Node node = getParagraphAt(document, index); + // TODO: doesn't take into account the line wrapping within a paragraph + // Assuming the node is a Paragraph, the count includes the trailing \n, so discount it + if (node != null) { + return node.getCharacterCount() - 1; } return 0; } + private CharSequence getRowCharacters(Document document, int index) { + Node node = getParagraphAt(document, index); + // TODO: doesn't take into account the line wrapping within a paragraph + if (node != null) { + return node.getCharacters(); + } + return null; + } + @Override public boolean keyPressed(final Component component, int keyCode, Keyboard.KeyLocation keyLocation) { @@ -1062,11 +1024,9 @@ org.apache.pivot.util.Console.logMethod( int selectionStart = textPane.getSelectionStart(); int selectionLength = textPane.getSelectionLength(); - Keyboard.Modifier commandModifier = Platform.getCommandModifier(); - boolean commandPressed = Keyboard.isPressed(commandModifier); + boolean commandPressed = Keyboard.isPressed(Platform.getCommandModifier()); boolean wordNavPressed = Keyboard.isPressed(Platform.getWordNavigationModifier()); boolean shiftPressed = Keyboard.isPressed(Keyboard.Modifier.SHIFT); - // boolean ctrlPressed = Keyboard.isPressed(Keyboard.Modifier.CTRL); boolean metaPressed = Keyboard.isPressed(Keyboard.Modifier.META); boolean isEditable = textPane.isEditable(); @@ -1095,12 +1055,15 @@ org.apache.pivot.util.Console.logMethod( } if (shiftPressed) { + + // TODO: if last direction was left, then extend further left + // but if right, then reverse selection from the pivot point selectionLength += selectionStart - start; } else { selectionLength = 0; } - if (selectionStart >= 0) { + if (start >= 0) { textPane.setSelection(start, selectionLength); scrollCharacterToVisible(start); @@ -1122,6 +1085,8 @@ org.apache.pivot.util.Console.logMethod( } if (shiftPressed) { + // TODO: if last direction was right, then extend further right + // but if left, then reverse selection from the pivot point selectionLength += end - index; } else { selectionStart = end; @@ -1135,16 +1100,11 @@ org.apache.pivot.util.Console.logMethod( consumed = true; } } else if (keyCode == Keyboard.KeyCode.LEFT) { - if (shiftPressed) { - // TODO: undo last right select depending... see TextInput skin - // Add the previous character to the selection - if (selectionStart > 0) { - selectionStart--; - selectionLength++; - } - } else if (wordNavPressed) { + if (wordNavPressed) { // Move the caret to the start of the next word to our left if (selectionStart > 0) { + int originalStart = selectionStart; + // TODO: what if last select direction was to the right? // first, skip over any space immediately to our left while (selectionStart > 0 && Character.isWhitespace(document.getCharacterAt(selectionStart - 1))) { @@ -1156,7 +1116,18 @@ org.apache.pivot.util.Console.logMethod( selectionStart--; } - selectionLength = 0; + if (shiftPressed) { + selectionLength += (originalStart - selectionStart); + } else { + selectionLength = 0; + } + } + } else if (shiftPressed) { + // TODO: undo last right select depending... see TextInput skin + // Add the previous character to the selection + if (selectionStart > 0) { + selectionStart--; + selectionLength++; } } else { // Clear the selection and move the caret back by one character Modified: pivot/trunk/wtk/src/org/apache/pivot/wtk/text/ComponentNode.java URL: http://svn.apache.org/viewvc/pivot/trunk/wtk/src/org/apache/pivot/wtk/text/ComponentNode.java?rev=1828291&r1=1828290&r2=1828291&view=diff ============================================================================== --- pivot/trunk/wtk/src/org/apache/pivot/wtk/text/ComponentNode.java (original) +++ pivot/trunk/wtk/src/org/apache/pivot/wtk/text/ComponentNode.java Tue Apr 3 23:01:30 2018 @@ -16,7 +16,9 @@ */ package org.apache.pivot.wtk.text; +import org.apache.pivot.text.CharSpan; import org.apache.pivot.util.ListenerList; +import org.apache.pivot.util.Utils; import org.apache.pivot.wtk.Button; import org.apache.pivot.wtk.Component; import org.apache.pivot.wtk.Container; @@ -64,57 +66,76 @@ public class ComponentNode extends Block } private String getText(Component comp) { + return getCharacters(comp).toString(); + } + + public String getSubstring(Span range) { + Utils.checkNull(range, "range"); + return getText(this.component).substring(range.start, range.end + 1); + } + + public String getSubstring(int start, int end) { + return getText(this.component).substring(start, end); + } + + public String getSubstring(CharSpan charSpan) { + Utils.checkNull(charSpan, "charSpan"); + return getText(this.component).substring(charSpan.start, charSpan.start + charSpan.length); + } + + public CharSequence getCharacters(Component comp) { if (comp instanceof TextInput) { - return ((TextInput)comp).getText(); + return ((TextInput)comp).getCharacters(); } else if (comp instanceof TextArea) { - return ((TextArea)comp).getText(); + return ((TextArea)comp).getCharacters(); } else if (comp instanceof TextPane) { - return ((TextPane)comp).getText(); + return ((TextPane)comp).getText(); // TODO: use getCharacters when it is available } else if (comp instanceof Label) { return ((Label)comp).getText(); } else if (comp instanceof Button) { Object buttonData = ((Button)comp).getButtonData(); if (buttonData instanceof BaseContent) { return ((BaseContent)buttonData).getText(); - } else if (buttonData instanceof String) { - return (String)buttonData; + } else if (buttonData instanceof CharSequence) { + return (CharSequence)buttonData; } else { return buttonData.toString(); } } else if (comp instanceof Container) { StringBuilder buf = new StringBuilder(); for (Component child : (Container)comp) { - buf.append(getText(child)); + buf.append(getCharacters(child)); } - return buf.toString(); + return buf; } return ""; } - public String getSubstring(Span range) { - return getText(this.component).substring(range.start, range.end + 1); - } - - public String getSubstring(int start, int end) { - return getText(this.component).substring(start, end); + public CharSequence getCharacters() { + return getCharacters(this.component); } public CharSequence getCharacters(Span range) { - return getText(this.component).subSequence(range.start, range.end + 1); + Utils.checkNull(range, "range"); + return getCharacters(this.component).subSequence(range.start, range.end + 1); } public CharSequence getCharacters(int start, int end) { - return getText(this.component).subSequence(start, end); + return getCharacters(this.component).subSequence(start, end); + } + + public CharSequence getCharacters(CharSpan charSpan) { + return getCharacters(this.component).subSequence(charSpan.start, charSpan.start + charSpan.length); } @Override public char getCharacterAt(int offset) { - return getText(this.component).charAt(offset); + return getCharacters(this.component).charAt(offset); } @Override public int getCharacterCount() { - return getText(this.component).length(); + return getCharacters(this.component).length(); } @Override Modified: pivot/trunk/wtk/src/org/apache/pivot/wtk/text/Element.java URL: http://svn.apache.org/viewvc/pivot/trunk/wtk/src/org/apache/pivot/wtk/text/Element.java?rev=1828291&r1=1828290&r2=1828291&view=diff ============================================================================== --- pivot/trunk/wtk/src/org/apache/pivot/wtk/text/Element.java (original) +++ pivot/trunk/wtk/src/org/apache/pivot/wtk/text/Element.java Tue Apr 3 23:01:30 2018 @@ -18,6 +18,7 @@ package org.apache.pivot.wtk.text; import java.awt.Color; import java.awt.Font; +import java.io.IOException; import java.util.Iterator; import org.apache.pivot.collections.ArrayList; @@ -111,6 +112,14 @@ public abstract class Element extends No } } + private Paragraph getParagraphParent(Node node) { + Element parent = node.getParent(); + while ((parent != null) && !(parent instanceof Paragraph)) { + parent = parent.getParent(); + } + return (Paragraph)parent; + } + @Override public Node removeRange(int offset, int charCount) { Utils.checkIndexBounds(offset, charCount, 0, characterCount); @@ -121,8 +130,9 @@ public abstract class Element extends No if (charCount > 0) { Element element = (Element) range; + int endOffset = offset + charCount - 1; int start = getNodeAt(offset); - int end = getNodeAt(offset + charCount - 1); + int end = (endOffset == offset) ? start : getNodeAt(endOffset); if (start == end) { // The range is entirely contained by one child node @@ -136,8 +146,9 @@ public abstract class Element extends No // underneath a paragraph to allow for composed text on an // empty line, so check that condition and don't remove the // entire node. - if (node instanceof TextNode && node.getParent() instanceof Paragraph && - node.getParent().getLength() == 1) { + Paragraph paragraph; + if (node instanceof TextNode && (paragraph = getParagraphParent(node)) != null && + paragraph.getLength() == 1) { segment = node.removeRange(0, charCount); } else { // Remove the entire node @@ -205,8 +216,9 @@ public abstract class Element extends No if (charCount > 0) { + int endOffset = offset + charCount - 1; int start = getNodeAt(offset); - int end = getNodeAt(offset + charCount - 1); + int end = (endOffset == offset) ? start : getNodeAt(endOffset); if (start == end) { // The range is entirely contained by one child node @@ -274,27 +286,31 @@ public abstract class Element extends No return characterCount; } - private void addText(StringBuilder buf, Element element) { - for (Node child : element.nodes) { - if (child instanceof TextNode) { - TextNode textNode = (TextNode)child; - buf.append(textNode.getText()); - } else if (child instanceof ComponentNode) { - ComponentNode compNode = (ComponentNode)child; - buf.append(compNode.getText()); - } else if (child instanceof Paragraph) { - addText(buf, (Element)child); + private void addCharacters(Appendable buf, Element element) { + try { + for (Node child : element.nodes) { + if (child instanceof Element) { + addCharacters(buf, (Element)child); + } else { + buf.append(child.getCharacters()); + } + } + if (element instanceof Paragraph) { buf.append('\n'); - } else if (child instanceof Element) { - addText(buf, (Element)child); } + } catch (IOException ioe) { + throw new RuntimeException(ioe); } } public String getText() { + return getCharacters().toString(); + } + + public CharSequence getCharacters() { StringBuilder buf = new StringBuilder(characterCount); - addText(buf, this); - return buf.toString(); + addCharacters(buf, this); + return buf; } @Override @@ -511,8 +527,6 @@ public abstract class Element extends No @Override protected void rangeRemoved(Node originalNode, int offset, int charCount, CharSequence removedChars) { - this.characterCount -= charCount; - // Update the offsets of consecutive nodes, if any if (offset < this.characterCount) { int index = getNodeAt(offset); @@ -522,6 +536,8 @@ public abstract class Element extends No node.setOffset(node.getOffset() - charCount); } } + // Adjust our count last, so we make sure to get all our children updated + this.characterCount -= charCount; super.rangeRemoved(originalNode, offset, charCount, removedChars); } Modified: pivot/trunk/wtk/src/org/apache/pivot/wtk/text/Node.java URL: http://svn.apache.org/viewvc/pivot/trunk/wtk/src/org/apache/pivot/wtk/text/Node.java?rev=1828291&r1=1828290&r2=1828291&view=diff ============================================================================== --- pivot/trunk/wtk/src/org/apache/pivot/wtk/text/Node.java (original) +++ pivot/trunk/wtk/src/org/apache/pivot/wtk/text/Node.java Tue Apr 3 23:01:30 2018 @@ -78,7 +78,7 @@ public abstract class Node { * offset of our parent (if any) added to our own offset. */ public int getDocumentOffset() { - return (parent == null) ? 0 : parent.getDocumentOffset() + offset; + return ((parent == null) ? 0 : parent.getDocumentOffset()) + offset; } /** @@ -150,6 +150,11 @@ public abstract class Node { public abstract int getCharacterCount(); /** + * @return The character sequence in this node. + */ + public abstract CharSequence getCharacters(); + + /** * Creates a copy of this node. * * @param recursive Whether to duplicate the children also. Modified: pivot/trunk/wtk/src/org/apache/pivot/wtk/text/TextNode.java URL: http://svn.apache.org/viewvc/pivot/trunk/wtk/src/org/apache/pivot/wtk/text/TextNode.java?rev=1828291&r1=1828290&r2=1828291&view=diff ============================================================================== --- pivot/trunk/wtk/src/org/apache/pivot/wtk/text/TextNode.java (original) +++ pivot/trunk/wtk/src/org/apache/pivot/wtk/text/TextNode.java Tue Apr 3 23:01:30 2018 @@ -16,6 +16,7 @@ */ package org.apache.pivot.wtk.text; +import org.apache.pivot.text.CharSpan; import org.apache.pivot.util.ListenerList; import org.apache.pivot.util.Utils; import org.apache.pivot.wtk.Span; @@ -49,6 +50,18 @@ public final class TextNode extends Node return characters.substring(beginIndex, endIndex); } + public String getText(Span span) { + Utils.checkNull(span, "span"); + + return characters.substring(span.normalStart(), span.normalEnd() + 1); + } + + public String getText(CharSpan charSpan) { + Utils.checkNull(charSpan, "charSpan"); + + return characters.substring(charSpan.start, charSpan.start + charSpan.length); + } + public void setText(String text) { Utils.checkNull(text, "text"); @@ -94,6 +107,7 @@ public final class TextNode extends Node } public String getSubstring(Span range) { + Utils.checkNull(range, "range"); return characters.substring(range.start, range.end + 1); } @@ -101,6 +115,11 @@ public final class TextNode extends Node return characters.substring(start, end); } + public String getSubstring(CharSpan charSpan) { + Utils.checkNull(charSpan, "charSpan"); + return characters.substring(charSpan.start, charSpan.start + charSpan.length); + } + public CharSequence getCharacters() { return characters; } @@ -110,9 +129,15 @@ public final class TextNode extends Node } public CharSequence getCharacters(Span range) { + Utils.checkNull(range, "range"); return characters.subSequence(range.start, range.end + 1); } + public CharSequence getCharacters(CharSpan charSpan) { + Utils.checkNull(charSpan, "charSpan"); + return characters.subSequence(charSpan.start, charSpan.start + charSpan.length); + } + @Override public char getCharacterAt(int index) { return characters.charAt(index);