You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@pivot.apache.org by rw...@apache.org on 2013/12/17 21:17:49 UTC

svn commit: r1551679 - in /pivot/trunk/wtk/src/org/apache/pivot/wtk: ./ skin/ text/

Author: rwhitcomb
Date: Tue Dec 17 20:17:48 2013
New Revision: 1551679

URL: http://svn.apache.org/r1551679
Log:
PIVOT-696: Support expansion of tabs in TextPane in the same ways as
with TextArea.

Add new style: tabWidth and property expandTabs.

This also required changing the way we get a hold of the TextPane.Skin
from within the skin view classes.  Also PlainTextSerializer needs the
tab settings.

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/skin/TextPaneSkinBlockView.java
    pivot/trunk/wtk/src/org/apache/pivot/wtk/skin/TextPaneSkinBulletedListView.java
    pivot/trunk/wtk/src/org/apache/pivot/wtk/skin/TextPaneSkinComponentNodeView.java
    pivot/trunk/wtk/src/org/apache/pivot/wtk/skin/TextPaneSkinDocumentView.java
    pivot/trunk/wtk/src/org/apache/pivot/wtk/skin/TextPaneSkinElementView.java
    pivot/trunk/wtk/src/org/apache/pivot/wtk/skin/TextPaneSkinImageNodeView.java
    pivot/trunk/wtk/src/org/apache/pivot/wtk/skin/TextPaneSkinListItemView.java
    pivot/trunk/wtk/src/org/apache/pivot/wtk/skin/TextPaneSkinListView.java
    pivot/trunk/wtk/src/org/apache/pivot/wtk/skin/TextPaneSkinNodeView.java
    pivot/trunk/wtk/src/org/apache/pivot/wtk/skin/TextPaneSkinNumberedListView.java
    pivot/trunk/wtk/src/org/apache/pivot/wtk/skin/TextPaneSkinParagraphView.java
    pivot/trunk/wtk/src/org/apache/pivot/wtk/skin/TextPaneSkinSpanView.java
    pivot/trunk/wtk/src/org/apache/pivot/wtk/skin/TextPaneSkinTextNodeView.java
    pivot/trunk/wtk/src/org/apache/pivot/wtk/skin/TextPaneSkinVerticalElementView.java
    pivot/trunk/wtk/src/org/apache/pivot/wtk/text/PlainTextSerializer.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=1551679&r1=1551678&r2=1551679&view=diff
==============================================================================
--- pivot/trunk/wtk/src/org/apache/pivot/wtk/TextPane.java (original)
+++ pivot/trunk/wtk/src/org/apache/pivot/wtk/TextPane.java Tue Dec 17 20:17:48 2013
@@ -17,8 +17,12 @@
 package org.apache.pivot.wtk;
 
 import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.Reader;
 import java.io.StringReader;
 import java.io.StringWriter;
+import java.net.URL;
 
 import org.apache.pivot.beans.DefaultProperty;
 import org.apache.pivot.collections.LinkedList;
@@ -96,6 +100,12 @@ public class TextPane extends Container 
          * @return The bounds of the character at the given offset.
          */
         public Bounds getCharacterBounds(int offset);
+
+        /**
+         * Returns the current setting of the "tabWidth" style (so "setText"
+         * uses the same value as Ctrl-Tab from user).
+         */
+        public int getTabWidth();
     }
 
     private interface Edit {
@@ -199,6 +209,8 @@ public class TextPane extends Container 
     private int selectionStart = 0;
     private int selectionLength = 0;
 
+    private boolean expandTabs = false;
+
     private boolean editable = true;
     private boolean undoingHistory = false;
     private boolean bulkOperation = false;
@@ -643,6 +655,8 @@ public class TextPane extends Container 
                 try {
                     PlainTextSerializer serializer = new PlainTextSerializer();
                     StringReader reader = new StringReader(text);
+                    serializer.setExpandTabs(this.expandTabs);
+                    serializer.setTabWidth(((TextPane.Skin) getSkin()).getTabWidth());
                     documentLocal = serializer.readObject(reader);
                     n = documentLocal.getCharacterCount();
 
@@ -716,14 +730,78 @@ public class TextPane extends Container 
     /**
      * Convenience method to create a text-only document consisting of one
      * paragraph per line of the given text.
+     *
+     * @param text
      */
     public void setText(String text) {
+        if (text == null) {
+            throw new IllegalArgumentException();
+        }
+
+        try {
+            setText(new StringReader(text));
+        } catch (IOException exception) {
+            throw new RuntimeException(exception);
+        }
+    }
+
+    public void setText(URL textURL) throws IOException {
+        if (textURL == null) {
+            throw new IllegalArgumentException();
+        }
+
+        InputStream inputStream = null;
+        try {
+            inputStream = textURL.openStream();
+            setText(new InputStreamReader(inputStream));
+        } finally {
+            if (inputStream != null) {
+                inputStream.close();
+            }
+        }
+    }
+
+    public void setText(Reader textReader) throws IOException {
+        if (textReader == null) {
+            throw new IllegalArgumentException();
+        }
+
+        int tabPosition = 0;
+        int tabWidth = ((TextPane.Skin) getSkin()).getTabWidth();
+
         Document doc = new Document();
-        String[] lines = text.split("\r?\n");
-        for (int i = 0; i < lines.length; i++) {
-            Paragraph paragraph = new Paragraph(lines[i]);
+        StringBuilder text = new StringBuilder();
+
+        int c = textReader.read();
+        while (c != -1) {
+            if (c == '\n') {
+                Paragraph paragraph = new Paragraph(text.toString());
+                doc.add(paragraph);
+                text.setLength(0);
+                tabPosition = 0;
+            } else if (c == '\t') {
+                if (expandTabs) {
+                    int spaces = tabWidth - (tabPosition % tabWidth);
+                    for (int i = 0; i < spaces; i++) {
+                        text.append(' ');
+                    }
+                    tabPosition += spaces;
+                } else {
+                    text.append('\t');
+                }
+            } else {
+                text.append((char) c);
+                tabPosition++;
+            }
+
+            c = textReader.read();
+        }
+
+        if (text.length() != 0) {
+            Paragraph paragraph = new Paragraph(text.toString());
             doc.add(paragraph);
         }
+
         setDocument(doc);
     }
 
@@ -876,6 +954,27 @@ public class TextPane extends Container 
         }
     }
 
+    public boolean getExpandTabs() {
+        return expandTabs;
+    }
+
+    /**
+     * Sets whether tab characters (<code>\t</code>) are expanded to an
+     * appropriate number of spaces during {@link #setText} and
+     * {@link #paste} operations.  Note: doing this for keyboard input
+     * is handled in the skin.
+     *
+     * @param expandTabs <code>true</code> to replace tab characters with space
+     * characters (depending on the setting of the
+     * {@link TextPane.Skin#getTabWidth} value) or <code>false</code> to leave
+     * tabs alone. Note: this only affects tabs encountered during program
+     * operations; tabs entered via the keyboard by the user are always
+     * expanded, regardless of this setting.
+     */
+    public void setExpandTabs(boolean expandTabs) {
+        this.expandTabs = expandTabs;
+    }
+
     public int getInsertionPoint(int x, int y) {
         TextPane.Skin textPaneSkin = (TextPane.Skin) getSkin();
         return textPaneSkin.getInsertionPoint(x, y);

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=1551679&r1=1551678&r2=1551679&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 Dec 17 20:17:48 2013
@@ -39,16 +39,9 @@ import org.apache.pivot.wtk.TextPane;
 import org.apache.pivot.wtk.TextPaneListener;
 import org.apache.pivot.wtk.TextPaneSelectionListener;
 import org.apache.pivot.wtk.Theme;
-import org.apache.pivot.wtk.text.BulletedList;
-import org.apache.pivot.wtk.text.ComponentNode;
 import org.apache.pivot.wtk.text.Document;
-import org.apache.pivot.wtk.text.ImageNode;
-import org.apache.pivot.wtk.text.List;
 import org.apache.pivot.wtk.text.Node;
-import org.apache.pivot.wtk.text.NumberedList;
 import org.apache.pivot.wtk.text.Paragraph;
-import org.apache.pivot.wtk.text.TextNode;
-import org.apache.pivot.wtk.text.TextSpan;
 
 /**
  * Text pane skin.
@@ -145,6 +138,8 @@ public class TextPaneSkin extends Contai
     private Insets margin = new Insets(4);
 
     private boolean wrapText = true;
+    private int tabWidth = 4;
+    private boolean acceptsTab = false;
 
     private static final int SCROLL_RATE = 30;
 
@@ -171,7 +166,7 @@ public class TextPaneSkin extends Contai
 
         Document document = textPane.getDocument();
         if (document != null) {
-            documentView = (TextPaneSkinDocumentView) createNodeView(document);
+            documentView = (TextPaneSkinDocumentView) TextPaneSkinNodeView.createNodeView(this, document);
             documentView.attach();
             updateSelection();
         }
@@ -386,6 +381,45 @@ public class TextPaneSkin extends Contai
         return characterBounds;
     }
 
+    /**
+     * Gets current value of style that determines the behavior of <tt>TAB</tt>
+     * and <tt>Ctrl-TAB</tt> characters.
+     *
+     * @return <tt>true</tt> if <tt>TAB</tt> inserts an appropriate number of
+     * spaces, while <tt>Ctrl-TAB</tt> shifts focus to next component.
+     * <tt>false</tt> (default) means <tt>TAB</tt> shifts focus and
+     * <tt>Ctrl-TAB</tt> inserts spaces.
+     */
+    public boolean getAcceptsTab() {
+        return acceptsTab;
+    }
+
+    /**
+     * Sets current value of style that determines the behavior of <tt>TAB</tt>
+     * and <tt>Ctrl-TAB</tt> characters.
+     *
+     * @param acceptsTab <tt>true</tt> if <tt>TAB</tt> inserts an appropriate
+     * number of spaces, while <tt>Ctrl-TAB</tt> shifts focus to next component.
+     * <tt>false</tt> (default) means <tt>TAB</tt> shifts focus and
+     * <tt>Ctrl-TAB</tt> inserts spaces.
+     */
+    public void setAcceptsTab(boolean acceptsTab) {
+        this.acceptsTab = acceptsTab;
+    }
+
+    @Override
+    public int getTabWidth() {
+        return tabWidth;
+    }
+
+    public void setTabWidth(int tabWidth) {
+        if (tabWidth < 0) {
+            throw new IllegalArgumentException("tabWidth is negative.");
+        }
+
+        this.tabWidth = tabWidth;
+    }
+
     private void scrollCharacterToVisible(int offset) {
         TextPane textPane = (TextPane) getComponent();
         Bounds characterBounds = getCharacterBounds(offset);
@@ -797,6 +831,35 @@ public class TextPaneSkin extends Contai
         return consumed;
     }
 
+    private int getRowOffset(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 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;
+            }
+        }
+        return 0;
+    }
+
     @Override
     public boolean keyPressed(final Component component, int keyCode,
         Keyboard.KeyLocation keyLocation) {
@@ -805,25 +868,88 @@ public class TextPaneSkin extends Contai
         final TextPane textPane = (TextPane) getComponent();
         Document document = textPane.getDocument();
 
+        int selectionStart = textPane.getSelectionStart();
+        int selectionLength = textPane.getSelectionLength();
+
         Keyboard.Modifier commandModifier = Platform.getCommandModifier();
+        boolean commandPressed = Keyboard.isPressed(commandModifier);
+        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();
+
         if (document != null) {
-            if (keyCode == Keyboard.KeyCode.ENTER && textPane.isEditable()) {
+            if (keyCode == Keyboard.KeyCode.ENTER && isEditable) {
                 textPane.insertParagraph();
 
                 consumed = true;
-            } else if (keyCode == Keyboard.KeyCode.DELETE && textPane.isEditable()) {
+            } else if (keyCode == Keyboard.KeyCode.DELETE && isEditable) {
                 textPane.delete(false);
 
                 consumed = true;
-            } else if (keyCode == Keyboard.KeyCode.BACKSPACE && textPane.isEditable()) {
+            } else if (keyCode == Keyboard.KeyCode.BACKSPACE && isEditable) {
                 textPane.delete(true);
 
                 consumed = true;
-            } else if (keyCode == Keyboard.KeyCode.LEFT) {
-                int selectionStart = textPane.getSelectionStart();
-                int selectionLength = textPane.getSelectionLength();
+            } else if (keyCode == Keyboard.KeyCode.HOME
+                   || (keyCode == Keyboard.KeyCode.LEFT && metaPressed)) {
+                int start;
+                if (commandPressed) {
+                    // Move the caret to the beginning of the text
+                    start = 0;
+                } else {
+                    // Move the caret to the beginning of the line
+                    start = getRowOffset(document, selectionStart);
+                }
 
-                if (Keyboard.isPressed(Keyboard.Modifier.SHIFT)) {
+                if (shiftPressed) {
+                    selectionLength += selectionStart - start;
+                } else {
+                    selectionLength = 0;
+                }
+
+                if (selectionStart >= 0) {
+                    textPane.setSelection(start, selectionLength);
+                    scrollCharacterToVisible(start);
+
+                    consumed = true;
+                }
+            } else if (keyCode == Keyboard.KeyCode.END
+                   || (keyCode == Keyboard.KeyCode.RIGHT && metaPressed)) {
+                int end;
+                int index = selectionStart + selectionLength;
+
+                if (commandPressed) {
+                    // Move the caret to end of the text
+                    end = textPane.getCharacterCount() - 1;
+                } else {
+                    // Move the caret to the end of the line
+                    int rowOffset = getRowOffset(document, index);
+                    int rowLength = getRowLength(document, index);
+                    end = rowOffset + rowLength;
+                }
+
+                if (shiftPressed) {
+                    selectionLength += end - index;
+                } else {
+                    selectionStart = end;
+                    if (selectionStart < textPane.getCharacterCount()
+                        && document.getCharacterAt(selectionStart) == '\n') {
+                        selectionStart--;
+                    }
+
+                    selectionLength = 0;
+                }
+
+                if (selectionStart + selectionLength <= textPane.getCharacterCount()) {
+                    textPane.setSelection(selectionStart, selectionLength);
+                    scrollCharacterToVisible(selectionStart + selectionLength);
+
+                    consumed = true;
+                }
+            } else if (keyCode == Keyboard.KeyCode.LEFT) {
+                if (shiftPressed) {
                     // Add the previous character to the selection
                     if (selectionStart > 0) {
                         selectionStart--;
@@ -862,10 +988,7 @@ public class TextPaneSkin extends Contai
 
                 consumed = true;
             } else if (keyCode == Keyboard.KeyCode.RIGHT) {
-                int selectionStart = textPane.getSelectionStart();
-                int selectionLength = textPane.getSelectionLength();
-
-                if (Keyboard.isPressed(Keyboard.Modifier.SHIFT)) {
+                if (shiftPressed) {
                     // Add the next character to the selection
                     if (selectionStart + selectionLength < document.getCharacterCount()) {
                         selectionLength++;
@@ -911,8 +1034,6 @@ public class TextPaneSkin extends Contai
 
                 consumed = true;
             } else if (keyCode == Keyboard.KeyCode.UP) {
-                int selectionStart = textPane.getSelectionStart();
-
                 int offset = getNextInsertionPoint(caretX, selectionStart,
                     TextPane.ScrollDirection.UP);
 
@@ -920,10 +1041,8 @@ public class TextPaneSkin extends Contai
                     offset = 0;
                 }
 
-                int selectionLength;
-                if (Keyboard.isPressed(Keyboard.Modifier.SHIFT)) {
-                    int selectionEnd = selectionStart + textPane.getSelectionLength() - 1;
-                    selectionLength = selectionEnd - offset + 1;
+                if (shiftPressed) {
+                    selectionLength = selectionStart + selectionLength - offset;
                 } else {
                     selectionLength = 0;
                 }
@@ -933,10 +1052,8 @@ public class TextPaneSkin extends Contai
 
                 consumed = true;
             } else if (keyCode == Keyboard.KeyCode.DOWN) {
-                int selectionStart = textPane.getSelectionStart();
-                int selectionLength = textPane.getSelectionLength();
 
-                if (Keyboard.isPressed(Keyboard.Modifier.SHIFT)) {
+                if (shiftPressed) {
                     int from;
                     int x;
                     if (selectionLength == 0) {
@@ -993,27 +1110,42 @@ public class TextPaneSkin extends Contai
                 }
 
                 consumed = true;
-            } else if (Keyboard.isPressed(commandModifier) && keyCode == Keyboard.KeyCode.TAB
-                && textPane.isEditable()) {
-                textPane.insert("\t");
+            } else if (keyCode == Keyboard.KeyCode.TAB
+                && (acceptsTab != Keyboard.isPressed(Keyboard.Modifier.CTRL))
+                && isEditable) {
+                if (textPane.getExpandTabs()) {
+                    int linePos = selectionStart - getRowOffset(document, selectionStart);
+                    StringBuilder tabBuilder = new StringBuilder(tabWidth);
+                    for (int i = 0; i < tabWidth - (linePos % tabWidth); i++) {
+                        tabBuilder.append(" ");
+                    }
+                    textPane.insert(tabBuilder.toString());
+                } else {
+                    textPane.insert("\t");
+                }
                 showCaret(true);
 
                 consumed = true;
-            } else if (Keyboard.isPressed(commandModifier)) {
+            } else if (keyCode == Keyboard.KeyCode.INSERT) {
+                if (shiftPressed && isEditable) {
+                    textPane.paste();
+                    consumed = true;
+                }
+            } else if (commandPressed) {
                 if (keyCode == Keyboard.KeyCode.A) {
                     textPane.setSelection(0, document.getCharacterCount());
                     consumed = true;
-                } else if (keyCode == Keyboard.KeyCode.X && textPane.isEditable()) {
+                } else if (keyCode == Keyboard.KeyCode.X && isEditable) {
                     textPane.cut();
                     consumed = true;
                 } else if (keyCode == Keyboard.KeyCode.C) {
                     textPane.copy();
                     consumed = true;
-                } else if (keyCode == Keyboard.KeyCode.V && textPane.isEditable()) {
+                } else if (keyCode == Keyboard.KeyCode.V && isEditable) {
                     textPane.paste();
                     consumed = true;
-                } else if (keyCode == Keyboard.KeyCode.Z && textPane.isEditable()) {
-                    if (Keyboard.isPressed(Keyboard.Modifier.SHIFT)) {
+                } else if (keyCode == Keyboard.KeyCode.Z && isEditable) {
+                    if (shiftPressed) {
                         textPane.redo();
                     } else {
                         textPane.undo();
@@ -1021,33 +1153,6 @@ public class TextPaneSkin extends Contai
 
                     consumed = true;
                 }
-            } else if (keyCode == Keyboard.KeyCode.HOME) {
-                // Move the caret to the beginning of the text
-                if (Keyboard.isPressed(Keyboard.Modifier.SHIFT)) {
-                    textPane.setSelection(0, textPane.getSelectionStart());
-                } else {
-                    textPane.setSelection(0, 0);
-                }
-                scrollCharacterToVisible(0);
-
-                consumed = true;
-            } else if (keyCode == Keyboard.KeyCode.END) {
-                // Move the caret to the end of the text
-                if (Keyboard.isPressed(Keyboard.Modifier.SHIFT)) {
-                    int selectionStart = textPane.getSelectionStart();
-                    textPane.setSelection(selectionStart, textPane.getCharacterCount()
-                        - selectionStart);
-                } else {
-                    textPane.setSelection(textPane.getCharacterCount() - 1, 0);
-                }
-                scrollCharacterToVisible(textPane.getCharacterCount() - 1);
-
-                consumed = true;
-            } else if (keyCode == Keyboard.KeyCode.INSERT) {
-                if (Keyboard.isPressed(Keyboard.Modifier.SHIFT) && textPane.isEditable()) {
-                    textPane.paste();
-                    consumed = true;
-                }
             } else {
                 consumed = super.keyPressed(component, keyCode, keyLocation);
             }
@@ -1089,7 +1194,7 @@ public class TextPaneSkin extends Contai
 
         Document document = textPane.getDocument();
         if (document != null) {
-            documentView = (TextPaneSkinDocumentView) createNodeView(document);
+            documentView = (TextPaneSkinDocumentView) TextPaneSkinNodeView.createNodeView(this, document);
             documentView.attach();
         }
 
@@ -1132,35 +1237,6 @@ public class TextPaneSkin extends Contai
         }
     }
 
-    TextPaneSkinNodeView createNodeView(Node node) {
-        TextPaneSkinNodeView nodeView = null;
-
-        if (node instanceof Document) {
-            nodeView = new TextPaneSkinDocumentView(this, (Document) node);
-        } else if (node instanceof Paragraph) {
-            nodeView = new TextPaneSkinParagraphView((Paragraph) node);
-        } else if (node instanceof TextNode) {
-            nodeView = new TextPaneSkinTextNodeView((TextNode) node);
-        } else if (node instanceof ImageNode) {
-            nodeView = new TextPaneSkinImageNodeView((ImageNode) node);
-        } else if (node instanceof ComponentNode) {
-            nodeView = new TextPaneSkinComponentNodeView((ComponentNode) node);
-        } else if (node instanceof TextSpan) {
-            nodeView = new TextPaneSkinSpanView((TextSpan) node);
-        } else if (node instanceof NumberedList) {
-            nodeView = new TextPaneSkinNumberedListView((NumberedList) node);
-        } else if (node instanceof BulletedList) {
-            nodeView = new TextPaneSkinBulletedListView((BulletedList) node);
-        } else if (node instanceof List.Item) {
-            nodeView = new TextPaneSkinListItemView((List.Item) node);
-        } else {
-            throw new IllegalArgumentException("Unsupported node type: "
-                + node.getClass().getName());
-        }
-
-        return nodeView;
-    }
-
     private void updateSelection() {
         if (documentView.getCharacterCount() > 0) {
             TextPane textPane = (TextPane) getComponent();

Modified: pivot/trunk/wtk/src/org/apache/pivot/wtk/skin/TextPaneSkinBlockView.java
URL: http://svn.apache.org/viewvc/pivot/trunk/wtk/src/org/apache/pivot/wtk/skin/TextPaneSkinBlockView.java?rev=1551679&r1=1551678&r2=1551679&view=diff
==============================================================================
--- pivot/trunk/wtk/src/org/apache/pivot/wtk/skin/TextPaneSkinBlockView.java (original)
+++ pivot/trunk/wtk/src/org/apache/pivot/wtk/skin/TextPaneSkinBlockView.java Tue Dec 17 20:17:48 2013
@@ -22,8 +22,8 @@ import org.apache.pivot.wtk.text.BlockLi
 
 abstract class TextPaneSkinBlockView extends TextPaneSkinElementView implements BlockListener {
 
-    public TextPaneSkinBlockView(Block block) {
-        super(block);
+    public TextPaneSkinBlockView(TextPaneSkin textPaneSkin, Block block) {
+        super(textPaneSkin, block);
     }
 
     @Override

Modified: pivot/trunk/wtk/src/org/apache/pivot/wtk/skin/TextPaneSkinBulletedListView.java
URL: http://svn.apache.org/viewvc/pivot/trunk/wtk/src/org/apache/pivot/wtk/skin/TextPaneSkinBulletedListView.java?rev=1551679&r1=1551678&r2=1551679&view=diff
==============================================================================
--- pivot/trunk/wtk/src/org/apache/pivot/wtk/skin/TextPaneSkinBulletedListView.java (original)
+++ pivot/trunk/wtk/src/org/apache/pivot/wtk/skin/TextPaneSkinBulletedListView.java Tue Dec 17 20:17:48 2013
@@ -21,8 +21,8 @@ import org.apache.pivot.wtk.text.Bullete
 
 class TextPaneSkinBulletedListView extends TextPaneSkinListView implements BulletedListListener {
 
-    public TextPaneSkinBulletedListView(BulletedList bulletedList) {
-        super(bulletedList);
+    public TextPaneSkinBulletedListView(TextPaneSkin textPaneSkin, BulletedList bulletedList) {
+        super(textPaneSkin, bulletedList);
     }
 
     @Override

Modified: pivot/trunk/wtk/src/org/apache/pivot/wtk/skin/TextPaneSkinComponentNodeView.java
URL: http://svn.apache.org/viewvc/pivot/trunk/wtk/src/org/apache/pivot/wtk/skin/TextPaneSkinComponentNodeView.java?rev=1551679&r1=1551678&r2=1551679&view=diff
==============================================================================
--- pivot/trunk/wtk/src/org/apache/pivot/wtk/skin/TextPaneSkinComponentNodeView.java (original)
+++ pivot/trunk/wtk/src/org/apache/pivot/wtk/skin/TextPaneSkinComponentNodeView.java Tue Dec 17 20:17:48 2013
@@ -35,8 +35,8 @@ class TextPaneSkinComponentNodeView exte
         }
     };
 
-    public TextPaneSkinComponentNodeView(ComponentNode componentNode) {
-        super(componentNode);
+    public TextPaneSkinComponentNodeView(TextPaneSkin textPaneSkin, ComponentNode componentNode) {
+        super(textPaneSkin, componentNode);
     }
 
     @Override

Modified: pivot/trunk/wtk/src/org/apache/pivot/wtk/skin/TextPaneSkinDocumentView.java
URL: http://svn.apache.org/viewvc/pivot/trunk/wtk/src/org/apache/pivot/wtk/skin/TextPaneSkinDocumentView.java?rev=1551679&r1=1551678&r2=1551679&view=diff
==============================================================================
--- pivot/trunk/wtk/src/org/apache/pivot/wtk/skin/TextPaneSkinDocumentView.java (original)
+++ pivot/trunk/wtk/src/org/apache/pivot/wtk/skin/TextPaneSkinDocumentView.java Tue Dec 17 20:17:48 2013
@@ -23,11 +23,8 @@ import org.apache.pivot.wtk.text.Documen
  */
 class TextPaneSkinDocumentView extends TextPaneSkinVerticalElementView {
 
-    protected final TextPaneSkin textPaneSkin;
-
     public TextPaneSkinDocumentView(TextPaneSkin textPaneSkin, Document document) {
-        super(document);
-        this.textPaneSkin = textPaneSkin;
+        super(textPaneSkin, document);
     }
 
     @Override
@@ -43,8 +40,4 @@ class TextPaneSkinDocumentView extends T
         textPaneSkin.invalidateComponent();
     }
 
-    @Override
-    public TextPaneSkin getTextPaneSkin() {
-        return textPaneSkin;
-    }
 }
\ No newline at end of file

Modified: pivot/trunk/wtk/src/org/apache/pivot/wtk/skin/TextPaneSkinElementView.java
URL: http://svn.apache.org/viewvc/pivot/trunk/wtk/src/org/apache/pivot/wtk/skin/TextPaneSkinElementView.java?rev=1551679&r1=1551678&r2=1551679&view=diff
==============================================================================
--- pivot/trunk/wtk/src/org/apache/pivot/wtk/skin/TextPaneSkinElementView.java (original)
+++ pivot/trunk/wtk/src/org/apache/pivot/wtk/skin/TextPaneSkinElementView.java Tue Dec 17 20:17:48 2013
@@ -41,8 +41,8 @@ abstract class TextPaneSkinElementView e
     private int skinX = 0;
     private int skinY = 0;
 
-    public TextPaneSkinElementView(Element element) {
-        super(element);
+    public TextPaneSkinElementView(TextPaneSkin textPaneSkin, Element element) {
+        super(textPaneSkin, element);
     }
 
     @Override
@@ -54,7 +54,7 @@ abstract class TextPaneSkinElementView e
 
         // Attach child node views
         for (Node node : element) {
-            add(getTextPaneSkin().createNodeView(node));
+            add(createNodeView(getTextPaneSkin(), node));
         }
     }
 
@@ -235,7 +235,7 @@ abstract class TextPaneSkinElementView e
             int nodeViewOffset = nodeView.getOffset();
             int characterCount = nodeView.getCharacterCount();
 
-            if (offset >= nodeViewOffset && offset < nodeViewOffset + characterCount) {
+            if (offset >= nodeViewOffset && offset <= nodeViewOffset + characterCount) {
                 characterBounds = nodeView.getCharacterBounds(offset - nodeViewOffset);
 
                 if (characterBounds != null) {
@@ -255,7 +255,7 @@ abstract class TextPaneSkinElementView e
 
     @Override
     public void nodeInserted(Element element, int index) {
-        insert(getTextPaneSkin().createNodeView(element.get(index)), index);
+        insert(createNodeView(getTextPaneSkin(), element.get(index)), index);
         invalidateUpTree();
     }
 

Modified: pivot/trunk/wtk/src/org/apache/pivot/wtk/skin/TextPaneSkinImageNodeView.java
URL: http://svn.apache.org/viewvc/pivot/trunk/wtk/src/org/apache/pivot/wtk/skin/TextPaneSkinImageNodeView.java?rev=1551679&r1=1551678&r2=1551679&view=diff
==============================================================================
--- pivot/trunk/wtk/src/org/apache/pivot/wtk/skin/TextPaneSkinImageNodeView.java (original)
+++ pivot/trunk/wtk/src/org/apache/pivot/wtk/skin/TextPaneSkinImageNodeView.java Tue Dec 17 20:17:48 2013
@@ -28,8 +28,8 @@ import org.apache.pivot.wtk.text.ImageNo
 
 class TextPaneSkinImageNodeView extends TextPaneSkinNodeView implements ImageNodeListener,
     ImageListener {
-    public TextPaneSkinImageNodeView(ImageNode imageNode) {
-        super(imageNode);
+    public TextPaneSkinImageNodeView(TextPaneSkin textPaneSkin, ImageNode imageNode) {
+        super(textPaneSkin, imageNode);
     }
 
     @Override

Modified: pivot/trunk/wtk/src/org/apache/pivot/wtk/skin/TextPaneSkinListItemView.java
URL: http://svn.apache.org/viewvc/pivot/trunk/wtk/src/org/apache/pivot/wtk/skin/TextPaneSkinListItemView.java?rev=1551679&r1=1551678&r2=1551679&view=diff
==============================================================================
--- pivot/trunk/wtk/src/org/apache/pivot/wtk/skin/TextPaneSkinListItemView.java (original)
+++ pivot/trunk/wtk/src/org/apache/pivot/wtk/skin/TextPaneSkinListItemView.java Tue Dec 17 20:17:48 2013
@@ -18,6 +18,7 @@ package org.apache.pivot.wtk.skin;
 
 import java.util.Iterator;
 
+import org.apache.pivot.wtk.text.List;
 import org.apache.pivot.wtk.text.TextNode;
 
 class TextPaneSkinListItemView extends TextPaneSkinVerticalElementView {
@@ -25,8 +26,8 @@ class TextPaneSkinListItemView extends T
     private TextNode indexTextNode;
     private TextPaneSkinTextNodeView indexTextNodeView;
 
-    public TextPaneSkinListItemView(org.apache.pivot.wtk.text.List.Item listItem) {
-        super(listItem);
+    public TextPaneSkinListItemView(TextPaneSkin textPaneSkin, List.Item listItem) {
+        super(textPaneSkin, listItem);
 
         this.indexTextNode = new TextNode("");
     }
@@ -36,7 +37,7 @@ class TextPaneSkinListItemView extends T
         super.attach();
 
         // add an extra TextNodeView to render the index text
-        indexTextNodeView = new TextPaneSkinTextNodeView(indexTextNode);
+        indexTextNodeView = new TextPaneSkinTextNodeView(getTextPaneSkin(), indexTextNode);
         indexTextNodeView.setLocation(0, 0);
         insert(indexTextNodeView, 0);
     }

Modified: pivot/trunk/wtk/src/org/apache/pivot/wtk/skin/TextPaneSkinListView.java
URL: http://svn.apache.org/viewvc/pivot/trunk/wtk/src/org/apache/pivot/wtk/skin/TextPaneSkinListView.java?rev=1551679&r1=1551678&r2=1551679&view=diff
==============================================================================
--- pivot/trunk/wtk/src/org/apache/pivot/wtk/skin/TextPaneSkinListView.java (original)
+++ pivot/trunk/wtk/src/org/apache/pivot/wtk/skin/TextPaneSkinListView.java Tue Dec 17 20:17:48 2013
@@ -22,8 +22,8 @@ class TextPaneSkinListView extends TextP
 
     protected int maxIndexTextWidth;
 
-    public TextPaneSkinListView(List list) {
-        super(list);
+    public TextPaneSkinListView(TextPaneSkin textPaneSkin, List list) {
+        super(textPaneSkin, list);
     }
 
     public int getMaxIndexTextWidth() {

Modified: pivot/trunk/wtk/src/org/apache/pivot/wtk/skin/TextPaneSkinNodeView.java
URL: http://svn.apache.org/viewvc/pivot/trunk/wtk/src/org/apache/pivot/wtk/skin/TextPaneSkinNodeView.java?rev=1551679&r1=1551678&r2=1551679&view=diff
==============================================================================
--- pivot/trunk/wtk/src/org/apache/pivot/wtk/skin/TextPaneSkinNodeView.java (original)
+++ pivot/trunk/wtk/src/org/apache/pivot/wtk/skin/TextPaneSkinNodeView.java Tue Dec 17 20:17:48 2013
@@ -18,19 +18,33 @@ package org.apache.pivot.wtk.skin;
 
 import java.awt.Graphics2D;
 
+import java.lang.reflect.Constructor;
+import java.lang.reflect.InvocationTargetException;
+
+import org.apache.pivot.collections.HashMap;
 import org.apache.pivot.collections.Sequence;
 import org.apache.pivot.wtk.Bounds;
 import org.apache.pivot.wtk.Dimensions;
 import org.apache.pivot.wtk.Point;
 import org.apache.pivot.wtk.TextPane;
+import org.apache.pivot.wtk.text.BulletedList;
+import org.apache.pivot.wtk.text.ComponentNode;
+import org.apache.pivot.wtk.text.Document;
 import org.apache.pivot.wtk.text.Element;
+import org.apache.pivot.wtk.text.ImageNode;
+import org.apache.pivot.wtk.text.List;
 import org.apache.pivot.wtk.text.Node;
 import org.apache.pivot.wtk.text.NodeListener;
+import org.apache.pivot.wtk.text.NumberedList;
+import org.apache.pivot.wtk.text.Paragraph;
+import org.apache.pivot.wtk.text.TextNode;
+import org.apache.pivot.wtk.text.TextSpan;
 
 /**
  * Abstract base class for node views.
  */
 abstract class TextPaneSkinNodeView implements NodeListener {
+    protected final TextPaneSkin textPaneSkin;
     private Node node = null;
     private TextPaneSkinElementView parent = null;
 
@@ -42,7 +56,8 @@ abstract class TextPaneSkinNodeView impl
 
     private boolean valid = false;
 
-    public TextPaneSkinNodeView(Node node) {
+    public TextPaneSkinNodeView(TextPaneSkin textPaneSkin, Node node) {
+        this.textPaneSkin = textPaneSkin;
         this.node = node;
     }
 
@@ -59,7 +74,7 @@ abstract class TextPaneSkinNodeView impl
     }
 
     protected TextPaneSkin getTextPaneSkin() {
-        return getParent().getTextPaneSkin();
+        return textPaneSkin;
     }
 
     protected void attach() {
@@ -232,4 +247,41 @@ abstract class TextPaneSkinNodeView impl
         // No-op
     }
 
+    private static HashMap<Class<? extends Node>, Class<? extends TextPaneSkinNodeView>>
+            nodeViewSkinMap = new HashMap<>();
+    static {
+        nodeViewSkinMap.put(Document.class, TextPaneSkinDocumentView.class);
+        nodeViewSkinMap.put(Paragraph.class, TextPaneSkinParagraphView.class);
+        nodeViewSkinMap.put(TextNode.class, TextPaneSkinTextNodeView.class);
+        nodeViewSkinMap.put(ImageNode.class, TextPaneSkinImageNodeView.class);
+        nodeViewSkinMap.put(ComponentNode.class, TextPaneSkinComponentNodeView.class);
+        nodeViewSkinMap.put(TextSpan.class, TextPaneSkinSpanView.class);
+        nodeViewSkinMap.put(NumberedList.class, TextPaneSkinNumberedListView.class);
+        nodeViewSkinMap.put(BulletedList.class, TextPaneSkinBulletedListView.class);
+        nodeViewSkinMap.put(List.Item.class, TextPaneSkinListItemView.class);
+    }
+
+    public static TextPaneSkinNodeView createNodeView(TextPaneSkin textPaneSkin, Node node) {
+        TextPaneSkinNodeView nodeView = null;
+
+        Class<? extends Node> nodeClass = node.getClass();
+        Class<? extends TextPaneSkinNodeView> skinClass = nodeViewSkinMap.get(nodeClass);
+        if (skinClass != null) {
+            try {
+                Constructor<?> constructor = skinClass.getConstructor(TextPaneSkin.class, nodeClass);
+                nodeView = (TextPaneSkinNodeView)constructor.newInstance(textPaneSkin, node);
+            } catch (NoSuchMethodException | InstantiationException
+                     | IllegalAccessException | InvocationTargetException ex) {
+                throw new RuntimeException("Error instantiating node view for "
+                    + nodeClass.getName(), ex);
+            }
+        }
+        if (nodeView == null) {
+            throw new IllegalArgumentException("Unsupported node type: "
+                + nodeClass.getName());
+        }
+
+        return nodeView;
+    }
+
 }

Modified: pivot/trunk/wtk/src/org/apache/pivot/wtk/skin/TextPaneSkinNumberedListView.java
URL: http://svn.apache.org/viewvc/pivot/trunk/wtk/src/org/apache/pivot/wtk/skin/TextPaneSkinNumberedListView.java?rev=1551679&r1=1551678&r2=1551679&view=diff
==============================================================================
--- pivot/trunk/wtk/src/org/apache/pivot/wtk/skin/TextPaneSkinNumberedListView.java (original)
+++ pivot/trunk/wtk/src/org/apache/pivot/wtk/skin/TextPaneSkinNumberedListView.java Tue Dec 17 20:17:48 2013
@@ -59,8 +59,8 @@ class TextPaneSkinNumberedListView exten
         return (char) ('A' + n - 1) + "";
     }
 
-    public TextPaneSkinNumberedListView(NumberedList numberedList) {
-        super(numberedList);
+    public TextPaneSkinNumberedListView(TextPaneSkin textPaneSkin, NumberedList numberedList) {
+        super(textPaneSkin, numberedList);
     }
 
     @Override

Modified: pivot/trunk/wtk/src/org/apache/pivot/wtk/skin/TextPaneSkinParagraphView.java
URL: http://svn.apache.org/viewvc/pivot/trunk/wtk/src/org/apache/pivot/wtk/skin/TextPaneSkinParagraphView.java?rev=1551679&r1=1551678&r2=1551679&view=diff
==============================================================================
--- pivot/trunk/wtk/src/org/apache/pivot/wtk/skin/TextPaneSkinParagraphView.java (original)
+++ pivot/trunk/wtk/src/org/apache/pivot/wtk/skin/TextPaneSkinParagraphView.java Tue Dec 17 20:17:48 2013
@@ -54,8 +54,8 @@ class TextPaneSkinParagraphView extends 
     private ArrayList<Row> rows = null;
     private Bounds terminatorBounds = new Bounds(0, 0, 0, 0);
 
-    public TextPaneSkinParagraphView(Paragraph paragraph) {
-        super(paragraph);
+    public TextPaneSkinParagraphView(TextPaneSkin textPaneSkin, Paragraph paragraph) {
+        super(textPaneSkin, paragraph);
     }
 
     @Override

Modified: pivot/trunk/wtk/src/org/apache/pivot/wtk/skin/TextPaneSkinSpanView.java
URL: http://svn.apache.org/viewvc/pivot/trunk/wtk/src/org/apache/pivot/wtk/skin/TextPaneSkinSpanView.java?rev=1551679&r1=1551678&r2=1551679&view=diff
==============================================================================
--- pivot/trunk/wtk/src/org/apache/pivot/wtk/skin/TextPaneSkinSpanView.java (original)
+++ pivot/trunk/wtk/src/org/apache/pivot/wtk/skin/TextPaneSkinSpanView.java Tue Dec 17 20:17:48 2013
@@ -25,8 +25,8 @@ import org.apache.pivot.wtk.text.TextSpa
  */
 class TextPaneSkinSpanView extends TextPaneSkinElementView {
 
-    public TextPaneSkinSpanView(TextSpan span) {
-        super(span);
+    public TextPaneSkinSpanView(TextPaneSkin textPaneSkin, TextSpan span) {
+        super(textPaneSkin, span);
     }
 
     @Override

Modified: pivot/trunk/wtk/src/org/apache/pivot/wtk/skin/TextPaneSkinTextNodeView.java
URL: http://svn.apache.org/viewvc/pivot/trunk/wtk/src/org/apache/pivot/wtk/skin/TextPaneSkinTextNodeView.java?rev=1551679&r1=1551678&r2=1551679&view=diff
==============================================================================
--- pivot/trunk/wtk/src/org/apache/pivot/wtk/skin/TextPaneSkinTextNodeView.java (original)
+++ pivot/trunk/wtk/src/org/apache/pivot/wtk/skin/TextPaneSkinTextNodeView.java Tue Dec 17 20:17:48 2013
@@ -47,12 +47,12 @@ class TextPaneSkinTextNodeView extends T
     private GlyphVector glyphVector = null;
     private TextPaneSkinTextNodeView next = null;
 
-    public TextPaneSkinTextNodeView(TextNode textNode) {
-        this(textNode, 0);
+    public TextPaneSkinTextNodeView(TextPaneSkin textPaneSkin, TextNode textNode) {
+        this(textPaneSkin, textNode, 0);
     }
 
-    public TextPaneSkinTextNodeView(TextNode textNode, int start) {
-        super(textNode);
+    public TextPaneSkinTextNodeView(TextPaneSkin textPaneSkin, TextNode textNode, int start) {
+        super(textPaneSkin, textNode);
         this.start = start;
     }
 
@@ -134,7 +134,7 @@ class TextPaneSkinTextNodeView extends T
 
         if (end < ci.getEndIndex()) {
             length = end - start;
-            next = new TextPaneSkinTextNodeView(textNode, end);
+            next = new TextPaneSkinTextNodeView(getTextPaneSkin(), textNode, end);
             next.setParent(getParent());
         } else {
             length = ci.getEndIndex() - start;
@@ -473,11 +473,18 @@ class TextPaneSkinTextNodeView extends T
 
     @Override
     public Bounds getCharacterBounds(int offset) {
-        Shape glyphBounds = glyphVector.getGlyphLogicalBounds(offset);
+        // If the offest == length, then use the right hand edge of the previous
+        // offset instead -- this is for positioning the caret at the end of the text
+        Shape glyphBounds = glyphVector.getGlyphLogicalBounds(offset == length ? offset - 1 : offset);
         Rectangle2D glyphBounds2D = glyphBounds.getBounds2D();
 
-        return new Bounds((int) Math.floor(glyphBounds2D.getX()), 0,
-            (int) Math.ceil(glyphBounds2D.getWidth()), getHeight());
+        if (offset == length) {
+            return new Bounds((int) Math.ceil(glyphBounds2D.getX() + glyphBounds2D.getWidth()), 0,
+                1, getHeight());
+        } else {
+            return new Bounds((int) Math.floor(glyphBounds2D.getX()), 0,
+                (int) Math.ceil(glyphBounds2D.getWidth()), getHeight());
+        }
     }
 
     @Override

Modified: pivot/trunk/wtk/src/org/apache/pivot/wtk/skin/TextPaneSkinVerticalElementView.java
URL: http://svn.apache.org/viewvc/pivot/trunk/wtk/src/org/apache/pivot/wtk/skin/TextPaneSkinVerticalElementView.java?rev=1551679&r1=1551678&r2=1551679&view=diff
==============================================================================
--- pivot/trunk/wtk/src/org/apache/pivot/wtk/skin/TextPaneSkinVerticalElementView.java (original)
+++ pivot/trunk/wtk/src/org/apache/pivot/wtk/skin/TextPaneSkinVerticalElementView.java Tue Dec 17 20:17:48 2013
@@ -27,8 +27,8 @@ import org.apache.pivot.wtk.text.Element
  */
 abstract class TextPaneSkinVerticalElementView extends TextPaneSkinElementView {
 
-    public TextPaneSkinVerticalElementView(Element element) {
-        super(element);
+    public TextPaneSkinVerticalElementView(TextPaneSkin textPaneSkin, Element element) {
+        super(textPaneSkin, element);
     }
 
     @Override

Modified: pivot/trunk/wtk/src/org/apache/pivot/wtk/text/PlainTextSerializer.java
URL: http://svn.apache.org/viewvc/pivot/trunk/wtk/src/org/apache/pivot/wtk/text/PlainTextSerializer.java?rev=1551679&r1=1551678&r2=1551679&view=diff
==============================================================================
--- pivot/trunk/wtk/src/org/apache/pivot/wtk/text/PlainTextSerializer.java (original)
+++ pivot/trunk/wtk/src/org/apache/pivot/wtk/text/PlainTextSerializer.java Tue Dec 17 20:17:48 2013
@@ -39,6 +39,9 @@ public class PlainTextSerializer impleme
     public static final String MIME_TYPE = "text/plain";
     public static final int BUFFER_SIZE = 2048;
 
+    private boolean expandTabs = false;
+    private int tabWidth = 4;
+
     public PlainTextSerializer() {
         this(Charset.defaultCharset());
     }
@@ -55,6 +58,35 @@ public class PlainTextSerializer impleme
         this.charset = charset;
     }
 
+    public int getTabWidth() {
+        return tabWidth;
+    }
+
+    public void setTabWidth(int tabWidth) {
+        if (tabWidth < 0) {
+            throw new IllegalArgumentException("tabWidth is negative.");
+        }
+
+        this.tabWidth = tabWidth;
+    }
+
+    public boolean getExpandTabs() {
+        return expandTabs;
+    }
+
+    /**
+     * Sets whether tab characters (<code>\t</code>) are expanded to an
+     * appropriate number of spaces while reading the text.
+     *
+     * @param expandTabs <code>true</code> to replace tab characters with space
+     * characters (depending on the setting of the {@link #getTabWidth} value)
+     * or <code>false</code> to leave tabs alone.
+     */
+    public void setExpandTabs(boolean expandTabs) {
+        this.expandTabs = expandTabs;
+    }
+
+
     @Override
     public Document readObject(InputStream inputStream) throws IOException {
         Reader reader = new InputStreamReader(inputStream, charset);
@@ -70,6 +102,18 @@ public class PlainTextSerializer impleme
 
         String line = bufferedReader.readLine();
         while (line != null) {
+            if (expandTabs) {
+                int ix = 0;
+                StringBuilder buf = new StringBuilder(line);
+                while ((ix = buf.indexOf("\t", ix)) >= 0) {
+                    buf.deleteCharAt(ix);
+                    int spaces = tabWidth - (ix % tabWidth);
+                    for (int j = 0; j < spaces; j++) {
+                        buf.insert(ix++, ' ');
+                    }
+                }
+                line = buf.toString();
+            }
             document.add(new Paragraph(line));
             line = bufferedReader.readLine();
         }



RE: svn commit: r1551679 - in /pivot/trunk/wtk/src/org/apache/pivot/wtk: ./ skin/ text/

Posted by Sandro Martini <sa...@gmail.com>.
Hi Roger,
no problem for me, proceed as you prefer ...

Bye
 Il 17/dic/2013 21:33 "Roger L. Whitcomb" <Ro...@actian.com> ha
scritto:

> As I was reviewing this commit, I realized there are additional changes in
> here not related strictly to tab handling (namely other keyboard handling
> changes), that are, in fact, not complete, and likely not correct as-is.
>
> Given that this is "trunk" and not released yet, and that I am committed
> to getting this right sooner rather than later, I'm inclined to leave the
> code as-is and fix it up from here.  BUT, if anyone doesn't think that's a
> good idea, I can revert those parts that are not good (esp. in
> TextPaneSkin.java) and make another commit later once the keyboard stuff is
> working better.  Everything builds, so that shouldn't be a problem.
>
> Comments?
>
> Thanks,
> ~Roger
>
> -----Original Message-----
> From: rwhitcomb@apache.org [mailto:rwhitcomb@apache.org]
> Sent: Tuesday, December 17, 2013 12:18 PM
> To: commits@pivot.apache.org
> Subject: svn commit: r1551679 - in
> /pivot/trunk/wtk/src/org/apache/pivot/wtk: ./ skin/ text/
>
> Author: rwhitcomb
> Date: Tue Dec 17 20:17:48 2013
> New Revision: 1551679
>
> URL: http://svn.apache.org/r1551679
> Log:
> PIVOT-696: Support expansion of tabs in TextPane in the same ways as with
> TextArea.
>
> Add new style: tabWidth and property expandTabs.
>
> This also required changing the way we get a hold of the TextPane.Skin
> from within the skin view classes.  Also PlainTextSerializer needs the tab
> settings.
>
> 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/skin/TextPaneSkinBlockView.java
>
> pivot/trunk/wtk/src/org/apache/pivot/wtk/skin/TextPaneSkinBulletedListView.java
>
> pivot/trunk/wtk/src/org/apache/pivot/wtk/skin/TextPaneSkinComponentNodeView.java
>
> pivot/trunk/wtk/src/org/apache/pivot/wtk/skin/TextPaneSkinDocumentView.java
>
> pivot/trunk/wtk/src/org/apache/pivot/wtk/skin/TextPaneSkinElementView.java
>
> pivot/trunk/wtk/src/org/apache/pivot/wtk/skin/TextPaneSkinImageNodeView.java
>
> pivot/trunk/wtk/src/org/apache/pivot/wtk/skin/TextPaneSkinListItemView.java
>     pivot/trunk/wtk/src/org/apache/pivot/wtk/skin/TextPaneSkinListView.java
>     pivot/trunk/wtk/src/org/apache/pivot/wtk/skin/TextPaneSkinNodeView.java
>
> pivot/trunk/wtk/src/org/apache/pivot/wtk/skin/TextPaneSkinNumberedListView.java
>
> pivot/trunk/wtk/src/org/apache/pivot/wtk/skin/TextPaneSkinParagraphView.java
>     pivot/trunk/wtk/src/org/apache/pivot/wtk/skin/TextPaneSkinSpanView.java
>
> pivot/trunk/wtk/src/org/apache/pivot/wtk/skin/TextPaneSkinTextNodeView.java
>
> pivot/trunk/wtk/src/org/apache/pivot/wtk/skin/TextPaneSkinVerticalElementView.java
>     pivot/trunk/wtk/src/org/apache/pivot/wtk/text/PlainTextSerializer.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=1551679&r1=1551678&r2=1551679&view=diff
>
> ==============================================================================
> --- pivot/trunk/wtk/src/org/apache/pivot/wtk/TextPane.java (original)
> +++ pivot/trunk/wtk/src/org/apache/pivot/wtk/TextPane.java Tue Dec 17
> +++ 20:17:48 2013
> @@ -17,8 +17,12 @@
>  package org.apache.pivot.wtk;
>
>  import java.io.IOException;
> +import java.io.InputStream;
> +import java.io.InputStreamReader;
> +import java.io.Reader;
>  import java.io.StringReader;
>  import java.io.StringWriter;
> +import java.net.URL;
>
>  import org.apache.pivot.beans.DefaultProperty;
>  import org.apache.pivot.collections.LinkedList;
> @@ -96,6 +100,12 @@ public class TextPane extends Container
>           * @return The bounds of the character at the given offset.
>           */
>          public Bounds getCharacterBounds(int offset);
> +
> +        /**
> +         * Returns the current setting of the "tabWidth" style (so
> "setText"
> +         * uses the same value as Ctrl-Tab from user).
> +         */
> +        public int getTabWidth();
>      }
>
>      private interface Edit {
> @@ -199,6 +209,8 @@ public class TextPane extends Container
>      private int selectionStart = 0;
>      private int selectionLength = 0;
>
> +    private boolean expandTabs = false;
> +
>      private boolean editable = true;
>      private boolean undoingHistory = false;
>      private boolean bulkOperation = false; @@ -643,6 +655,8 @@ public
> class TextPane extends Container
>                  try {
>                      PlainTextSerializer serializer = new
> PlainTextSerializer();
>                      StringReader reader = new StringReader(text);
> +                    serializer.setExpandTabs(this.expandTabs);
> +                    serializer.setTabWidth(((TextPane.Skin)
> + getSkin()).getTabWidth());
>                      documentLocal = serializer.readObject(reader);
>                      n = documentLocal.getCharacterCount();
>
> @@ -716,14 +730,78 @@ public class TextPane extends Container
>      /**
>       * Convenience method to create a text-only document consisting of one
>       * paragraph per line of the given text.
> +     *
> +     * @param text
>       */
>      public void setText(String text) {
> +        if (text == null) {
> +            throw new IllegalArgumentException();
> +        }
> +
> +        try {
> +            setText(new StringReader(text));
> +        } catch (IOException exception) {
> +            throw new RuntimeException(exception);
> +        }
> +    }
> +
> +    public void setText(URL textURL) throws IOException {
> +        if (textURL == null) {
> +            throw new IllegalArgumentException();
> +        }
> +
> +        InputStream inputStream = null;
> +        try {
> +            inputStream = textURL.openStream();
> +            setText(new InputStreamReader(inputStream));
> +        } finally {
> +            if (inputStream != null) {
> +                inputStream.close();
> +            }
> +        }
> +    }
> +
> +    public void setText(Reader textReader) throws IOException {
> +        if (textReader == null) {
> +            throw new IllegalArgumentException();
> +        }
> +
> +        int tabPosition = 0;
> +        int tabWidth = ((TextPane.Skin) getSkin()).getTabWidth();
> +
>          Document doc = new Document();
> -        String[] lines = text.split("\r?\n");
> -        for (int i = 0; i < lines.length; i++) {
> -            Paragraph paragraph = new Paragraph(lines[i]);
> +        StringBuilder text = new StringBuilder();
> +
> +        int c = textReader.read();
> +        while (c != -1) {
> +            if (c == '\n') {
> +                Paragraph paragraph = new Paragraph(text.toString());
> +                doc.add(paragraph);
> +                text.setLength(0);
> +                tabPosition = 0;
> +            } else if (c == '\t') {
> +                if (expandTabs) {
> +                    int spaces = tabWidth - (tabPosition % tabWidth);
> +                    for (int i = 0; i < spaces; i++) {
> +                        text.append(' ');
> +                    }
> +                    tabPosition += spaces;
> +                } else {
> +                    text.append('\t');
> +                }
> +            } else {
> +                text.append((char) c);
> +                tabPosition++;
> +            }
> +
> +            c = textReader.read();
> +        }
> +
> +        if (text.length() != 0) {
> +            Paragraph paragraph = new Paragraph(text.toString());
>              doc.add(paragraph);
>          }
> +
>          setDocument(doc);
>      }
>
> @@ -876,6 +954,27 @@ public class TextPane extends Container
>          }
>      }
>
> +    public boolean getExpandTabs() {
> +        return expandTabs;
> +    }
> +
> +    /**
> +     * Sets whether tab characters (<code>\t</code>) are expanded to an
> +     * appropriate number of spaces during {@link #setText} and
> +     * {@link #paste} operations.  Note: doing this for keyboard input
> +     * is handled in the skin.
> +     *
> +     * @param expandTabs <code>true</code> to replace tab characters with
> space
> +     * characters (depending on the setting of the
> +     * {@link TextPane.Skin#getTabWidth} value) or <code>false</code> to
> leave
> +     * tabs alone. Note: this only affects tabs encountered during program
> +     * operations; tabs entered via the keyboard by the user are always
> +     * expanded, regardless of this setting.
> +     */
> +    public void setExpandTabs(boolean expandTabs) {
> +        this.expandTabs = expandTabs;
> +    }
> +
>      public int getInsertionPoint(int x, int y) {
>          TextPane.Skin textPaneSkin = (TextPane.Skin) getSkin();
>          return textPaneSkin.getInsertionPoint(x, y);
>
> 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=1551679&r1=1551678&r2=1551679&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
> +++ Dec 17 20:17:48 2013
> @@ -39,16 +39,9 @@ import org.apache.pivot.wtk.TextPane;  import
> org.apache.pivot.wtk.TextPaneListener;
>  import org.apache.pivot.wtk.TextPaneSelectionListener;
>  import org.apache.pivot.wtk.Theme;
> -import org.apache.pivot.wtk.text.BulletedList;
> -import org.apache.pivot.wtk.text.ComponentNode;
>  import org.apache.pivot.wtk.text.Document;
> -import org.apache.pivot.wtk.text.ImageNode;
> -import org.apache.pivot.wtk.text.List;
>  import org.apache.pivot.wtk.text.Node;
> -import org.apache.pivot.wtk.text.NumberedList;
>  import org.apache.pivot.wtk.text.Paragraph;
> -import org.apache.pivot.wtk.text.TextNode;
> -import org.apache.pivot.wtk.text.TextSpan;
>
>  /**
>   * Text pane skin.
> @@ -145,6 +138,8 @@ public class TextPaneSkin extends Contai
>      private Insets margin = new Insets(4);
>
>      private boolean wrapText = true;
> +    private int tabWidth = 4;
> +    private boolean acceptsTab = false;
>
>      private static final int SCROLL_RATE = 30;
>
> @@ -171,7 +166,7 @@ public class TextPaneSkin extends Contai
>
>          Document document = textPane.getDocument();
>          if (document != null) {
> -            documentView = (TextPaneSkinDocumentView)
> createNodeView(document);
> +            documentView = (TextPaneSkinDocumentView)
> + TextPaneSkinNodeView.createNodeView(this, document);
>              documentView.attach();
>              updateSelection();
>          }
> @@ -386,6 +381,45 @@ public class TextPaneSkin extends Contai
>          return characterBounds;
>      }
>
> +    /**
> +     * Gets current value of style that determines the behavior of
> <tt>TAB</tt>
> +     * and <tt>Ctrl-TAB</tt> characters.
> +     *
> +     * @return <tt>true</tt> if <tt>TAB</tt> inserts an appropriate
> number of
> +     * spaces, while <tt>Ctrl-TAB</tt> shifts focus to next component.
> +     * <tt>false</tt> (default) means <tt>TAB</tt> shifts focus and
> +     * <tt>Ctrl-TAB</tt> inserts spaces.
> +     */
> +    public boolean getAcceptsTab() {
> +        return acceptsTab;
> +    }
> +
> +    /**
> +     * Sets current value of style that determines the behavior of
> <tt>TAB</tt>
> +     * and <tt>Ctrl-TAB</tt> characters.
> +     *
> +     * @param acceptsTab <tt>true</tt> if <tt>TAB</tt> inserts an
> appropriate
> +     * number of spaces, while <tt>Ctrl-TAB</tt> shifts focus to next
> component.
> +     * <tt>false</tt> (default) means <tt>TAB</tt> shifts focus and
> +     * <tt>Ctrl-TAB</tt> inserts spaces.
> +     */
> +    public void setAcceptsTab(boolean acceptsTab) {
> +        this.acceptsTab = acceptsTab;
> +    }
> +
> +    @Override
> +    public int getTabWidth() {
> +        return tabWidth;
> +    }
> +
> +    public void setTabWidth(int tabWidth) {
> +        if (tabWidth < 0) {
> +            throw new IllegalArgumentException("tabWidth is negative.");
> +        }
> +
> +        this.tabWidth = tabWidth;
> +    }
> +
>      private void scrollCharacterToVisible(int offset) {
>          TextPane textPane = (TextPane) getComponent();
>          Bounds characterBounds = getCharacterBounds(offset); @@ -797,6
> +831,35 @@ public class TextPaneSkin extends Contai
>          return consumed;
>      }
>
> +    private int getRowOffset(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 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;
> +            }
> +        }
> +        return 0;
> +    }
> +
>      @Override
>      public boolean keyPressed(final Component component, int keyCode,
>          Keyboard.KeyLocation keyLocation) { @@ -805,25 +868,88 @@ public
> class TextPaneSkin extends Contai
>          final TextPane textPane = (TextPane) getComponent();
>          Document document = textPane.getDocument();
>
> +        int selectionStart = textPane.getSelectionStart();
> +        int selectionLength = textPane.getSelectionLength();
> +
>          Keyboard.Modifier commandModifier = Platform.getCommandModifier();
> +        boolean commandPressed = Keyboard.isPressed(commandModifier);
> +        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();
> +
>          if (document != null) {
> -            if (keyCode == Keyboard.KeyCode.ENTER &&
> textPane.isEditable()) {
> +            if (keyCode == Keyboard.KeyCode.ENTER && isEditable) {
>                  textPane.insertParagraph();
>
>                  consumed = true;
> -            } else if (keyCode == Keyboard.KeyCode.DELETE &&
> textPane.isEditable()) {
> +            } else if (keyCode == Keyboard.KeyCode.DELETE &&
> + isEditable) {
>                  textPane.delete(false);
>
>                  consumed = true;
> -            } else if (keyCode == Keyboard.KeyCode.BACKSPACE &&
> textPane.isEditable()) {
> +            } else if (keyCode == Keyboard.KeyCode.BACKSPACE &&
> + isEditable) {
>                  textPane.delete(true);
>
>                  consumed = true;
> -            } else if (keyCode == Keyboard.KeyCode.LEFT) {
> -                int selectionStart = textPane.getSelectionStart();
> -                int selectionLength = textPane.getSelectionLength();
> +            } else if (keyCode == Keyboard.KeyCode.HOME
> +                   || (keyCode == Keyboard.KeyCode.LEFT && metaPressed)) {
> +                int start;
> +                if (commandPressed) {
> +                    // Move the caret to the beginning of the text
> +                    start = 0;
> +                } else {
> +                    // Move the caret to the beginning of the line
> +                    start = getRowOffset(document, selectionStart);
> +                }
>
> -                if (Keyboard.isPressed(Keyboard.Modifier.SHIFT)) {
> +                if (shiftPressed) {
> +                    selectionLength += selectionStart - start;
> +                } else {
> +                    selectionLength = 0;
> +                }
> +
> +                if (selectionStart >= 0) {
> +                    textPane.setSelection(start, selectionLength);
> +                    scrollCharacterToVisible(start);
> +
> +                    consumed = true;
> +                }
> +            } else if (keyCode == Keyboard.KeyCode.END
> +                   || (keyCode == Keyboard.KeyCode.RIGHT && metaPressed))
> {
> +                int end;
> +                int index = selectionStart + selectionLength;
> +
> +                if (commandPressed) {
> +                    // Move the caret to end of the text
> +                    end = textPane.getCharacterCount() - 1;
> +                } else {
> +                    // Move the caret to the end of the line
> +                    int rowOffset = getRowOffset(document, index);
> +                    int rowLength = getRowLength(document, index);
> +                    end = rowOffset + rowLength;
> +                }
> +
> +                if (shiftPressed) {
> +                    selectionLength += end - index;
> +                } else {
> +                    selectionStart = end;
> +                    if (selectionStart < textPane.getCharacterCount()
> +                        && document.getCharacterAt(selectionStart) ==
> '\n') {
> +                        selectionStart--;
> +                    }
> +
> +                    selectionLength = 0;
> +                }
> +
> +                if (selectionStart + selectionLength <=
> textPane.getCharacterCount()) {
> +                    textPane.setSelection(selectionStart,
> selectionLength);
> +                    scrollCharacterToVisible(selectionStart +
> + selectionLength);
> +
> +                    consumed = true;
> +                }
> +            } else if (keyCode == Keyboard.KeyCode.LEFT) {
> +                if (shiftPressed) {
>                      // Add the previous character to the selection
>                      if (selectionStart > 0) {
>                          selectionStart--; @@ -862,10 +988,7 @@ public
> class TextPaneSkin extends Contai
>
>                  consumed = true;
>              } else if (keyCode == Keyboard.KeyCode.RIGHT) {
> -                int selectionStart = textPane.getSelectionStart();
> -                int selectionLength = textPane.getSelectionLength();
> -
> -                if (Keyboard.isPressed(Keyboard.Modifier.SHIFT)) {
> +                if (shiftPressed) {
>                      // Add the next character to the selection
>                      if (selectionStart + selectionLength <
> document.getCharacterCount()) {
>                          selectionLength++; @@ -911,8 +1034,6 @@ public
> class TextPaneSkin extends Contai
>
>                  consumed = true;
>              } else if (keyCode == Keyboard.KeyCode.UP) {
> -                int selectionStart = textPane.getSelectionStart();
> -
>                  int offset = getNextInsertionPoint(caretX, selectionStart,
>                      TextPane.ScrollDirection.UP);
>
> @@ -920,10 +1041,8 @@ public class TextPaneSkin extends Contai
>                      offset = 0;
>                  }
>
> -                int selectionLength;
> -                if (Keyboard.isPressed(Keyboard.Modifier.SHIFT)) {
> -                    int selectionEnd = selectionStart +
> textPane.getSelectionLength() - 1;
> -                    selectionLength = selectionEnd - offset + 1;
> +                if (shiftPressed) {
> +                    selectionLength = selectionStart + selectionLength
> + - offset;
>                  } else {
>                      selectionLength = 0;
>                  }
> @@ -933,10 +1052,8 @@ public class TextPaneSkin extends Contai
>
>                  consumed = true;
>              } else if (keyCode == Keyboard.KeyCode.DOWN) {
> -                int selectionStart = textPane.getSelectionStart();
> -                int selectionLength = textPane.getSelectionLength();
>
> -                if (Keyboard.isPressed(Keyboard.Modifier.SHIFT)) {
> +                if (shiftPressed) {
>                      int from;
>                      int x;
>                      if (selectionLength == 0) { @@ -993,27 +1110,42 @@
> public class TextPaneSkin extends Contai
>                  }
>
>                  consumed = true;
> -            } else if (Keyboard.isPressed(commandModifier) && keyCode ==
> Keyboard.KeyCode.TAB
> -                && textPane.isEditable()) {
> -                textPane.insert("\t");
> +            } else if (keyCode == Keyboard.KeyCode.TAB
> +                && (acceptsTab !=
> Keyboard.isPressed(Keyboard.Modifier.CTRL))
> +                && isEditable) {
> +                if (textPane.getExpandTabs()) {
> +                    int linePos = selectionStart - getRowOffset(document,
> selectionStart);
> +                    StringBuilder tabBuilder = new
> StringBuilder(tabWidth);
> +                    for (int i = 0; i < tabWidth - (linePos % tabWidth);
> i++) {
> +                        tabBuilder.append(" ");
> +                    }
> +                    textPane.insert(tabBuilder.toString());
> +                } else {
> +                    textPane.insert("\t");
> +                }
>                  showCaret(true);
>
>                  consumed = true;
> -            } else if (Keyboard.isPressed(commandModifier)) {
> +            } else if (keyCode == Keyboard.KeyCode.INSERT) {
> +                if (shiftPressed && isEditable) {
> +                    textPane.paste();
> +                    consumed = true;
> +                }
> +            } else if (commandPressed) {
>                  if (keyCode == Keyboard.KeyCode.A) {
>                      textPane.setSelection(0,
> document.getCharacterCount());
>                      consumed = true;
> -                } else if (keyCode == Keyboard.KeyCode.X &&
> textPane.isEditable()) {
> +                } else if (keyCode == Keyboard.KeyCode.X && isEditable)
> + {
>                      textPane.cut();
>                      consumed = true;
>                  } else if (keyCode == Keyboard.KeyCode.C) {
>                      textPane.copy();
>                      consumed = true;
> -                } else if (keyCode == Keyboard.KeyCode.V &&
> textPane.isEditable()) {
> +                } else if (keyCode == Keyboard.KeyCode.V && isEditable)
> + {
>                      textPane.paste();
>                      consumed = true;
> -                } else if (keyCode == Keyboard.KeyCode.Z &&
> textPane.isEditable()) {
> -                    if (Keyboard.isPressed(Keyboard.Modifier.SHIFT)) {
> +                } else if (keyCode == Keyboard.KeyCode.Z && isEditable) {
> +                    if (shiftPressed) {
>                          textPane.redo();
>                      } else {
>                          textPane.undo(); @@ -1021,33 +1153,6 @@ public
> class TextPaneSkin extends Contai
>
>                      consumed = true;
>                  }
> -            } else if (keyCode == Keyboard.KeyCode.HOME) {
> -                // Move the caret to the beginning of the text
> -                if (Keyboard.isPressed(Keyboard.Modifier.SHIFT)) {
> -                    textPane.setSelection(0,
> textPane.getSelectionStart());
> -                } else {
> -                    textPane.setSelection(0, 0);
> -                }
> -                scrollCharacterToVisible(0);
> -
> -                consumed = true;
> -            } else if (keyCode == Keyboard.KeyCode.END) {
> -                // Move the caret to the end of the text
> -                if (Keyboard.isPressed(Keyboard.Modifier.SHIFT)) {
> -                    int selectionStart = textPane.getSelectionStart();
> -                    textPane.setSelection(selectionStart,
> textPane.getCharacterCount()
> -                        - selectionStart);
> -                } else {
> -                    textPane.setSelection(textPane.getCharacterCount() -
> 1, 0);
> -                }
> -                scrollCharacterToVisible(textPane.getCharacterCount() -
> 1);
> -
> -                consumed = true;
> -            } else if (keyCode == Keyboard.KeyCode.INSERT) {
> -                if (Keyboard.isPressed(Keyboard.Modifier.SHIFT) &&
> textPane.isEditable()) {
> -                    textPane.paste();
> -                    consumed = true;
> -                }
>              } else {
>                  consumed = super.keyPressed(component, keyCode,
> keyLocation);
>              }
> @@ -1089,7 +1194,7 @@ public class TextPaneSkin extends Contai
>
>          Document document = textPane.getDocument();
>          if (document != null) {
> -            documentView = (TextPaneSkinDocumentView)
> createNodeView(document);
> +            documentView = (TextPaneSkinDocumentView)
> + TextPaneSkinNodeView.createNodeView(this, document);
>              documentView.attach();
>          }
>
> @@ -1132,35 +1237,6 @@ public class TextPaneSkin extends Contai
>          }
>      }
>
> -    TextPaneSkinNodeView createNodeView(Node node) {
> -        TextPaneSkinNodeView nodeView = null;
> -
> -        if (node instanceof Document) {
> -            nodeView = new TextPaneSkinDocumentView(this, (Document)
> node);
> -        } else if (node instanceof Paragraph) {
> -            nodeView = new TextPaneSkinParagraphView((Paragraph) node);
> -        } else if (node instanceof TextNode) {
> -            nodeView = new TextPaneSkinTextNodeView((TextNode) node);
> -        } else if (node instanceof ImageNode) {
> -            nodeView = new TextPaneSkinImageNodeView((ImageNode) node);
> -        } else if (node instanceof ComponentNode) {
> -            nodeView = new TextPaneSkinComponentNodeView((ComponentNode)
> node);
> -        } else if (node instanceof TextSpan) {
> -            nodeView = new TextPaneSkinSpanView((TextSpan) node);
> -        } else if (node instanceof NumberedList) {
> -            nodeView = new TextPaneSkinNumberedListView((NumberedList)
> node);
> -        } else if (node instanceof BulletedList) {
> -            nodeView = new TextPaneSkinBulletedListView((BulletedList)
> node);
> -        } else if (node instanceof List.Item) {
> -            nodeView = new TextPaneSkinListItemView((List.Item) node);
> -        } else {
> -            throw new IllegalArgumentException("Unsupported node type: "
> -                + node.getClass().getName());
> -        }
> -
> -        return nodeView;
> -    }
> -
>      private void updateSelection() {
>          if (documentView.getCharacterCount() > 0) {
>              TextPane textPane = (TextPane) getComponent();
>
> Modified:
> pivot/trunk/wtk/src/org/apache/pivot/wtk/skin/TextPaneSkinBlockView.java
> URL:
> http://svn.apache.org/viewvc/pivot/trunk/wtk/src/org/apache/pivot/wtk/skin/TextPaneSkinBlockView.java?rev=1551679&r1=1551678&r2=1551679&view=diff
>
> ==============================================================================
> ---
> pivot/trunk/wtk/src/org/apache/pivot/wtk/skin/TextPaneSkinBlockView.java
> (original)
> +++ pivot/trunk/wtk/src/org/apache/pivot/wtk/skin/TextPaneSkinBlockView.
> +++ java Tue Dec 17 20:17:48 2013
> @@ -22,8 +22,8 @@ import org.apache.pivot.wtk.text.BlockLi
>
>  abstract class TextPaneSkinBlockView extends TextPaneSkinElementView
> implements BlockListener {
>
> -    public TextPaneSkinBlockView(Block block) {
> -        super(block);
> +    public TextPaneSkinBlockView(TextPaneSkin textPaneSkin, Block block) {
> +        super(textPaneSkin, block);
>      }
>
>      @Override
>
> Modified:
> pivot/trunk/wtk/src/org/apache/pivot/wtk/skin/TextPaneSkinBulletedListView.java
> URL:
> http://svn.apache.org/viewvc/pivot/trunk/wtk/src/org/apache/pivot/wtk/skin/TextPaneSkinBulletedListView.java?rev=1551679&r1=1551678&r2=1551679&view=diff
>
> ==============================================================================
> ---
> pivot/trunk/wtk/src/org/apache/pivot/wtk/skin/TextPaneSkinBulletedListView.java
> (original)
> +++ pivot/trunk/wtk/src/org/apache/pivot/wtk/skin/TextPaneSkinBulletedLi
> +++ stView.java Tue Dec 17 20:17:48 2013
> @@ -21,8 +21,8 @@ import org.apache.pivot.wtk.text.Bullete
>
>  class TextPaneSkinBulletedListView extends TextPaneSkinListView
> implements BulletedListListener {
>
> -    public TextPaneSkinBulletedListView(BulletedList bulletedList) {
> -        super(bulletedList);
> +    public TextPaneSkinBulletedListView(TextPaneSkin textPaneSkin,
> BulletedList bulletedList) {
> +        super(textPaneSkin, bulletedList);
>      }
>
>      @Override
>
> Modified:
> pivot/trunk/wtk/src/org/apache/pivot/wtk/skin/TextPaneSkinComponentNodeView.java
> URL:
> http://svn.apache.org/viewvc/pivot/trunk/wtk/src/org/apache/pivot/wtk/skin/TextPaneSkinComponentNodeView.java?rev=1551679&r1=1551678&r2=1551679&view=diff
>
> ==============================================================================
> ---
> pivot/trunk/wtk/src/org/apache/pivot/wtk/skin/TextPaneSkinComponentNodeView.java
> (original)
> +++ pivot/trunk/wtk/src/org/apache/pivot/wtk/skin/TextPaneSkinComponentN
> +++ odeView.java Tue Dec 17 20:17:48 2013
> @@ -35,8 +35,8 @@ class TextPaneSkinComponentNodeView exte
>          }
>      };
>
> -    public TextPaneSkinComponentNodeView(ComponentNode componentNode) {
> -        super(componentNode);
> +    public TextPaneSkinComponentNodeView(TextPaneSkin textPaneSkin,
> ComponentNode componentNode) {
> +        super(textPaneSkin, componentNode);
>      }
>
>      @Override
>
> Modified:
> pivot/trunk/wtk/src/org/apache/pivot/wtk/skin/TextPaneSkinDocumentView.java
> URL:
> http://svn.apache.org/viewvc/pivot/trunk/wtk/src/org/apache/pivot/wtk/skin/TextPaneSkinDocumentView.java?rev=1551679&r1=1551678&r2=1551679&view=diff
>
> ==============================================================================
> ---
> pivot/trunk/wtk/src/org/apache/pivot/wtk/skin/TextPaneSkinDocumentView.java
> (original)
> +++ pivot/trunk/wtk/src/org/apache/pivot/wtk/skin/TextPaneSkinDocumentVi
> +++ ew.java Tue Dec 17 20:17:48 2013
> @@ -23,11 +23,8 @@ import org.apache.pivot.wtk.text.Documen
>   */
>  class TextPaneSkinDocumentView extends TextPaneSkinVerticalElementView {
>
> -    protected final TextPaneSkin textPaneSkin;
> -
>      public TextPaneSkinDocumentView(TextPaneSkin textPaneSkin, Document
> document) {
> -        super(document);
> -        this.textPaneSkin = textPaneSkin;
> +        super(textPaneSkin, document);
>      }
>
>      @Override
> @@ -43,8 +40,4 @@ class TextPaneSkinDocumentView extends T
>          textPaneSkin.invalidateComponent();
>      }
>
> -    @Override
> -    public TextPaneSkin getTextPaneSkin() {
> -        return textPaneSkin;
> -    }
>  }
> \ No newline at end of file
>
> Modified:
> pivot/trunk/wtk/src/org/apache/pivot/wtk/skin/TextPaneSkinElementView.java
> URL:
> http://svn.apache.org/viewvc/pivot/trunk/wtk/src/org/apache/pivot/wtk/skin/TextPaneSkinElementView.java?rev=1551679&r1=1551678&r2=1551679&view=diff
>
> ==============================================================================
> ---
> pivot/trunk/wtk/src/org/apache/pivot/wtk/skin/TextPaneSkinElementView.java
> (original)
> +++ pivot/trunk/wtk/src/org/apache/pivot/wtk/skin/TextPaneSkinElementVie
> +++ w.java Tue Dec 17 20:17:48 2013
> @@ -41,8 +41,8 @@ abstract class TextPaneSkinElementView e
>      private int skinX = 0;
>      private int skinY = 0;
>
> -    public TextPaneSkinElementView(Element element) {
> -        super(element);
> +    public TextPaneSkinElementView(TextPaneSkin textPaneSkin, Element
> element) {
> +        super(textPaneSkin, element);
>      }
>
>      @Override
> @@ -54,7 +54,7 @@ abstract class TextPaneSkinElementView e
>
>          // Attach child node views
>          for (Node node : element) {
> -            add(getTextPaneSkin().createNodeView(node));
> +            add(createNodeView(getTextPaneSkin(), node));
>          }
>      }
>
> @@ -235,7 +235,7 @@ abstract class TextPaneSkinElementView e
>              int nodeViewOffset = nodeView.getOffset();
>              int characterCount = nodeView.getCharacterCount();
>
> -            if (offset >= nodeViewOffset && offset < nodeViewOffset +
> characterCount) {
> +            if (offset >= nodeViewOffset && offset <= nodeViewOffset +
> + characterCount) {
>                  characterBounds = nodeView.getCharacterBounds(offset -
> nodeViewOffset);
>
>                  if (characterBounds != null) { @@ -255,7 +255,7 @@
> abstract class TextPaneSkinElementView e
>
>      @Override
>      public void nodeInserted(Element element, int index) {
> -        insert(getTextPaneSkin().createNodeView(element.get(index)),
> index);
> +        insert(createNodeView(getTextPaneSkin(), element.get(index)),
> + index);
>          invalidateUpTree();
>      }
>
>
> Modified:
> pivot/trunk/wtk/src/org/apache/pivot/wtk/skin/TextPaneSkinImageNodeView.java
> URL:
> http://svn.apache.org/viewvc/pivot/trunk/wtk/src/org/apache/pivot/wtk/skin/TextPaneSkinImageNodeView.java?rev=1551679&r1=1551678&r2=1551679&view=diff
>
> ==============================================================================
> ---
> pivot/trunk/wtk/src/org/apache/pivot/wtk/skin/TextPaneSkinImageNodeView.java
> (original)
> +++ pivot/trunk/wtk/src/org/apache/pivot/wtk/skin/TextPaneSkinImageNodeV
> +++ iew.java Tue Dec 17 20:17:48 2013
> @@ -28,8 +28,8 @@ import org.apache.pivot.wtk.text.ImageNo
>
>  class TextPaneSkinImageNodeView extends TextPaneSkinNodeView implements
> ImageNodeListener,
>      ImageListener {
> -    public TextPaneSkinImageNodeView(ImageNode imageNode) {
> -        super(imageNode);
> +    public TextPaneSkinImageNodeView(TextPaneSkin textPaneSkin, ImageNode
> imageNode) {
> +        super(textPaneSkin, imageNode);
>      }
>
>      @Override
>
> Modified:
> pivot/trunk/wtk/src/org/apache/pivot/wtk/skin/TextPaneSkinListItemView.java
> URL:
> http://svn.apache.org/viewvc/pivot/trunk/wtk/src/org/apache/pivot/wtk/skin/TextPaneSkinListItemView.java?rev=1551679&r1=1551678&r2=1551679&view=diff
>
> ==============================================================================
> ---
> pivot/trunk/wtk/src/org/apache/pivot/wtk/skin/TextPaneSkinListItemView.java
> (original)
> +++ pivot/trunk/wtk/src/org/apache/pivot/wtk/skin/TextPaneSkinListItemVi
> +++ ew.java Tue Dec 17 20:17:48 2013
> @@ -18,6 +18,7 @@ package org.apache.pivot.wtk.skin;
>
>  import java.util.Iterator;
>
> +import org.apache.pivot.wtk.text.List;
>  import org.apache.pivot.wtk.text.TextNode;
>
>  class TextPaneSkinListItemView extends TextPaneSkinVerticalElementView {
> @@ -25,8 +26,8 @@ class TextPaneSkinListItemView extends T
>      private TextNode indexTextNode;
>      private TextPaneSkinTextNodeView indexTextNodeView;
>
> -    public TextPaneSkinListItemView(org.apache.pivot.wtk.text.List.Item
> listItem) {
> -        super(listItem);
> +    public TextPaneSkinListItemView(TextPaneSkin textPaneSkin, List.Item
> listItem) {
> +        super(textPaneSkin, listItem);
>
>          this.indexTextNode = new TextNode("");
>      }
> @@ -36,7 +37,7 @@ class TextPaneSkinListItemView extends T
>          super.attach();
>
>          // add an extra TextNodeView to render the index text
> -        indexTextNodeView = new TextPaneSkinTextNodeView(indexTextNode);
> +        indexTextNodeView = new
> + TextPaneSkinTextNodeView(getTextPaneSkin(), indexTextNode);
>          indexTextNodeView.setLocation(0, 0);
>          insert(indexTextNodeView, 0);
>      }
>
> Modified:
> pivot/trunk/wtk/src/org/apache/pivot/wtk/skin/TextPaneSkinListView.java
> URL:
> http://svn.apache.org/viewvc/pivot/trunk/wtk/src/org/apache/pivot/wtk/skin/TextPaneSkinListView.java?rev=1551679&r1=1551678&r2=1551679&view=diff
>
> ==============================================================================
> ---
> pivot/trunk/wtk/src/org/apache/pivot/wtk/skin/TextPaneSkinListView.java
> (original)
> +++ pivot/trunk/wtk/src/org/apache/pivot/wtk/skin/TextPaneSkinListView.j
> +++ ava Tue Dec 17 20:17:48 2013
> @@ -22,8 +22,8 @@ class TextPaneSkinListView extends TextP
>
>      protected int maxIndexTextWidth;
>
> -    public TextPaneSkinListView(List list) {
> -        super(list);
> +    public TextPaneSkinListView(TextPaneSkin textPaneSkin, List list) {
> +        super(textPaneSkin, list);
>      }
>
>      public int getMaxIndexTextWidth() {
>
> Modified:
> pivot/trunk/wtk/src/org/apache/pivot/wtk/skin/TextPaneSkinNodeView.java
> URL:
> http://svn.apache.org/viewvc/pivot/trunk/wtk/src/org/apache/pivot/wtk/skin/TextPaneSkinNodeView.java?rev=1551679&r1=1551678&r2=1551679&view=diff
>
> ==============================================================================
> ---
> pivot/trunk/wtk/src/org/apache/pivot/wtk/skin/TextPaneSkinNodeView.java
> (original)
> +++ pivot/trunk/wtk/src/org/apache/pivot/wtk/skin/TextPaneSkinNodeView.j
> +++ ava Tue Dec 17 20:17:48 2013
> @@ -18,19 +18,33 @@ package org.apache.pivot.wtk.skin;
>
>  import java.awt.Graphics2D;
>
> +import java.lang.reflect.Constructor;
> +import java.lang.reflect.InvocationTargetException;
> +
> +import org.apache.pivot.collections.HashMap;
>  import org.apache.pivot.collections.Sequence;
>  import org.apache.pivot.wtk.Bounds;
>  import org.apache.pivot.wtk.Dimensions;  import
> org.apache.pivot.wtk.Point;  import org.apache.pivot.wtk.TextPane;
> +import org.apache.pivot.wtk.text.BulletedList;
> +import org.apache.pivot.wtk.text.ComponentNode;
> +import org.apache.pivot.wtk.text.Document;
>  import org.apache.pivot.wtk.text.Element;
> +import org.apache.pivot.wtk.text.ImageNode;
> +import org.apache.pivot.wtk.text.List;
>  import org.apache.pivot.wtk.text.Node;
>  import org.apache.pivot.wtk.text.NodeListener;
> +import org.apache.pivot.wtk.text.NumberedList;
> +import org.apache.pivot.wtk.text.Paragraph;
> +import org.apache.pivot.wtk.text.TextNode;
> +import org.apache.pivot.wtk.text.TextSpan;
>
>  /**
>   * Abstract base class for node views.
>   */
>  abstract class TextPaneSkinNodeView implements NodeListener {
> +    protected final TextPaneSkin textPaneSkin;
>      private Node node = null;
>      private TextPaneSkinElementView parent = null;
>
> @@ -42,7 +56,8 @@ abstract class TextPaneSkinNodeView impl
>
>      private boolean valid = false;
>
> -    public TextPaneSkinNodeView(Node node) {
> +    public TextPaneSkinNodeView(TextPaneSkin textPaneSkin, Node node) {
> +        this.textPaneSkin = textPaneSkin;
>          this.node = node;
>      }
>
> @@ -59,7 +74,7 @@ abstract class TextPaneSkinNodeView impl
>      }
>
>      protected TextPaneSkin getTextPaneSkin() {
> -        return getParent().getTextPaneSkin();
> +        return textPaneSkin;
>      }
>
>      protected void attach() {
> @@ -232,4 +247,41 @@ abstract class TextPaneSkinNodeView impl
>          // No-op
>      }
>
> +    private static HashMap<Class<? extends Node>, Class<? extends
> TextPaneSkinNodeView>>
> +            nodeViewSkinMap = new HashMap<>();
> +    static {
> +        nodeViewSkinMap.put(Document.class,
> TextPaneSkinDocumentView.class);
> +        nodeViewSkinMap.put(Paragraph.class,
> TextPaneSkinParagraphView.class);
> +        nodeViewSkinMap.put(TextNode.class,
> TextPaneSkinTextNodeView.class);
> +        nodeViewSkinMap.put(ImageNode.class,
> TextPaneSkinImageNodeView.class);
> +        nodeViewSkinMap.put(ComponentNode.class,
> TextPaneSkinComponentNodeView.class);
> +        nodeViewSkinMap.put(TextSpan.class, TextPaneSkinSpanView.class);
> +        nodeViewSkinMap.put(NumberedList.class,
> TextPaneSkinNumberedListView.class);
> +        nodeViewSkinMap.put(BulletedList.class,
> TextPaneSkinBulletedListView.class);
> +        nodeViewSkinMap.put(List.Item.class,
> TextPaneSkinListItemView.class);
> +    }
> +
> +    public static TextPaneSkinNodeView createNodeView(TextPaneSkin
> textPaneSkin, Node node) {
> +        TextPaneSkinNodeView nodeView = null;
> +
> +        Class<? extends Node> nodeClass = node.getClass();
> +        Class<? extends TextPaneSkinNodeView> skinClass =
> nodeViewSkinMap.get(nodeClass);
> +        if (skinClass != null) {
> +            try {
> +                Constructor<?> constructor =
> skinClass.getConstructor(TextPaneSkin.class, nodeClass);
> +                nodeView =
> (TextPaneSkinNodeView)constructor.newInstance(textPaneSkin, node);
> +            } catch (NoSuchMethodException | InstantiationException
> +                     | IllegalAccessException | InvocationTargetException
> ex) {
> +                throw new RuntimeException("Error instantiating node view
> for "
> +                    + nodeClass.getName(), ex);
> +            }
> +        }
> +        if (nodeView == null) {
> +            throw new IllegalArgumentException("Unsupported node type: "
> +                + nodeClass.getName());
> +        }
> +
> +        return nodeView;
> +    }
> +
>  }
>
> Modified:
> pivot/trunk/wtk/src/org/apache/pivot/wtk/skin/TextPaneSkinNumberedListView.java
> URL:
> http://svn.apache.org/viewvc/pivot/trunk/wtk/src/org/apache/pivot/wtk/skin/TextPaneSkinNumberedListView.java?rev=1551679&r1=1551678&r2=1551679&view=diff
>
> ==============================================================================
> ---
> pivot/trunk/wtk/src/org/apache/pivot/wtk/skin/TextPaneSkinNumberedListView.java
> (original)
> +++ pivot/trunk/wtk/src/org/apache/pivot/wtk/skin/TextPaneSkinNumberedLi
> +++ stView.java Tue Dec 17 20:17:48 2013
> @@ -59,8 +59,8 @@ class TextPaneSkinNumberedListView exten
>          return (char) ('A' + n - 1) + "";
>      }
>
> -    public TextPaneSkinNumberedListView(NumberedList numberedList) {
> -        super(numberedList);
> +    public TextPaneSkinNumberedListView(TextPaneSkin textPaneSkin,
> NumberedList numberedList) {
> +        super(textPaneSkin, numberedList);
>      }
>
>      @Override
>
> Modified:
> pivot/trunk/wtk/src/org/apache/pivot/wtk/skin/TextPaneSkinParagraphView.java
> URL:
> http://svn.apache.org/viewvc/pivot/trunk/wtk/src/org/apache/pivot/wtk/skin/TextPaneSkinParagraphView.java?rev=1551679&r1=1551678&r2=1551679&view=diff
>
> ==============================================================================
> ---
> pivot/trunk/wtk/src/org/apache/pivot/wtk/skin/TextPaneSkinParagraphView.java
> (original)
> +++ pivot/trunk/wtk/src/org/apache/pivot/wtk/skin/TextPaneSkinParagraphV
> +++ iew.java Tue Dec 17 20:17:48 2013
> @@ -54,8 +54,8 @@ class TextPaneSkinParagraphView extends
>      private ArrayList<Row> rows = null;
>      private Bounds terminatorBounds = new Bounds(0, 0, 0, 0);
>
> -    public TextPaneSkinParagraphView(Paragraph paragraph) {
> -        super(paragraph);
> +    public TextPaneSkinParagraphView(TextPaneSkin textPaneSkin, Paragraph
> paragraph) {
> +        super(textPaneSkin, paragraph);
>      }
>
>      @Override
>
> Modified:
> pivot/trunk/wtk/src/org/apache/pivot/wtk/skin/TextPaneSkinSpanView.java
> URL:
> http://svn.apache.org/viewvc/pivot/trunk/wtk/src/org/apache/pivot/wtk/skin/TextPaneSkinSpanView.java?rev=1551679&r1=1551678&r2=1551679&view=diff
>
> ==============================================================================
> ---
> pivot/trunk/wtk/src/org/apache/pivot/wtk/skin/TextPaneSkinSpanView.java
> (original)
> +++ pivot/trunk/wtk/src/org/apache/pivot/wtk/skin/TextPaneSkinSpanView.j
> +++ ava Tue Dec 17 20:17:48 2013
> @@ -25,8 +25,8 @@ import org.apache.pivot.wtk.text.TextSpa
>   */
>  class TextPaneSkinSpanView extends TextPaneSkinElementView {
>
> -    public TextPaneSkinSpanView(TextSpan span) {
> -        super(span);
> +    public TextPaneSkinSpanView(TextPaneSkin textPaneSkin, TextSpan span)
> {
> +        super(textPaneSkin, span);
>      }
>
>      @Override
>
> Modified:
> pivot/trunk/wtk/src/org/apache/pivot/wtk/skin/TextPaneSkinTextNodeView.java
> URL:
> http://svn.apache.org/viewvc/pivot/trunk/wtk/src/org/apache/pivot/wtk/skin/TextPaneSkinTextNodeView.java?rev=1551679&r1=1551678&r2=1551679&view=diff
>
> ==============================================================================
> ---
> pivot/trunk/wtk/src/org/apache/pivot/wtk/skin/TextPaneSkinTextNodeView.java
> (original)
> +++ pivot/trunk/wtk/src/org/apache/pivot/wtk/skin/TextPaneSkinTextNodeVi
> +++ ew.java Tue Dec 17 20:17:48 2013
> @@ -47,12 +47,12 @@ class TextPaneSkinTextNodeView extends T
>      private GlyphVector glyphVector = null;
>      private TextPaneSkinTextNodeView next = null;
>
> -    public TextPaneSkinTextNodeView(TextNode textNode) {
> -        this(textNode, 0);
> +    public TextPaneSkinTextNodeView(TextPaneSkin textPaneSkin, TextNode
> textNode) {
> +        this(textPaneSkin, textNode, 0);
>      }
>
> -    public TextPaneSkinTextNodeView(TextNode textNode, int start) {
> -        super(textNode);
> +    public TextPaneSkinTextNodeView(TextPaneSkin textPaneSkin, TextNode
> textNode, int start) {
> +        super(textPaneSkin, textNode);
>          this.start = start;
>      }
>
> @@ -134,7 +134,7 @@ class TextPaneSkinTextNodeView extends T
>
>          if (end < ci.getEndIndex()) {
>              length = end - start;
> -            next = new TextPaneSkinTextNodeView(textNode, end);
> +            next = new TextPaneSkinTextNodeView(getTextPaneSkin(),
> + textNode, end);
>              next.setParent(getParent());
>          } else {
>              length = ci.getEndIndex() - start; @@ -473,11 +473,18 @@
> class TextPaneSkinTextNodeView extends T
>
>      @Override
>      public Bounds getCharacterBounds(int offset) {
> -        Shape glyphBounds = glyphVector.getGlyphLogicalBounds(offset);
> +        // If the offest == length, then use the right hand edge of the
> previous
> +        // offset instead -- this is for positioning the caret at the end
> of the text
> +        Shape glyphBounds = glyphVector.getGlyphLogicalBounds(offset ==
> + length ? offset - 1 : offset);
>          Rectangle2D glyphBounds2D = glyphBounds.getBounds2D();
>
> -        return new Bounds((int) Math.floor(glyphBounds2D.getX()), 0,
> -            (int) Math.ceil(glyphBounds2D.getWidth()), getHeight());
> +        if (offset == length) {
> +            return new Bounds((int) Math.ceil(glyphBounds2D.getX() +
> glyphBounds2D.getWidth()), 0,
> +                1, getHeight());
> +        } else {
> +            return new Bounds((int) Math.floor(glyphBounds2D.getX()), 0,
> +                (int) Math.ceil(glyphBounds2D.getWidth()), getHeight());
> +        }
>      }
>
>      @Override
>
> Modified:
> pivot/trunk/wtk/src/org/apache/pivot/wtk/skin/TextPaneSkinVerticalElementView.java
> URL:
> http://svn.apache.org/viewvc/pivot/trunk/wtk/src/org/apache/pivot/wtk/skin/TextPaneSkinVerticalElementView.java?rev=1551679&r1=1551678&r2=1551679&view=diff
>
> ==============================================================================
> ---
> pivot/trunk/wtk/src/org/apache/pivot/wtk/skin/TextPaneSkinVerticalElementView.java
> (original)
> +++ pivot/trunk/wtk/src/org/apache/pivot/wtk/skin/TextPaneSkinVerticalEl
> +++ ementView.java Tue Dec 17 20:17:48 2013
> @@ -27,8 +27,8 @@ import org.apache.pivot.wtk.text.Element
>   */
>  abstract class TextPaneSkinVerticalElementView extends
> TextPaneSkinElementView {
>
> -    public TextPaneSkinVerticalElementView(Element element) {
> -        super(element);
> +    public TextPaneSkinVerticalElementView(TextPaneSkin textPaneSkin,
> Element element) {
> +        super(textPaneSkin, element);
>      }
>
>      @Override
>
> Modified:
> pivot/trunk/wtk/src/org/apache/pivot/wtk/text/PlainTextSerializer.java
> URL:
> http://svn.apache.org/viewvc/pivot/trunk/wtk/src/org/apache/pivot/wtk/text/PlainTextSerializer.java?rev=1551679&r1=1551678&r2=1551679&view=diff
>
> ==============================================================================
> --- pivot/trunk/wtk/src/org/apache/pivot/wtk/text/PlainTextSerializer.java
> (original)
> +++ pivot/trunk/wtk/src/org/apache/pivot/wtk/text/PlainTextSerializer.ja
> +++ va Tue Dec 17 20:17:48 2013
> @@ -39,6 +39,9 @@ public class PlainTextSerializer impleme
>      public static final String MIME_TYPE = "text/plain";
>      public static final int BUFFER_SIZE = 2048;
>
> +    private boolean expandTabs = false;
> +    private int tabWidth = 4;
> +
>      public PlainTextSerializer() {
>          this(Charset.defaultCharset());
>      }
> @@ -55,6 +58,35 @@ public class PlainTextSerializer impleme
>          this.charset = charset;
>      }
>
> +    public int getTabWidth() {
> +        return tabWidth;
> +    }
> +
> +    public void setTabWidth(int tabWidth) {
> +        if (tabWidth < 0) {
> +            throw new IllegalArgumentException("tabWidth is negative.");
> +        }
> +
> +        this.tabWidth = tabWidth;
> +    }
> +
> +    public boolean getExpandTabs() {
> +        return expandTabs;
> +    }
> +
> +    /**
> +     * Sets whether tab characters (<code>\t</code>) are expanded to an
> +     * appropriate number of spaces while reading the text.
> +     *
> +     * @param expandTabs <code>true</code> to replace tab characters with
> space
> +     * characters (depending on the setting of the {@link #getTabWidth}
> value)
> +     * or <code>false</code> to leave tabs alone.
> +     */
> +    public void setExpandTabs(boolean expandTabs) {
> +        this.expandTabs = expandTabs;
> +    }
> +
> +
>      @Override
>      public Document readObject(InputStream inputStream) throws
> IOException {
>          Reader reader = new InputStreamReader(inputStream, charset); @@
> -70,6 +102,18 @@ public class PlainTextSerializer impleme
>
>          String line = bufferedReader.readLine();
>          while (line != null) {
> +            if (expandTabs) {
> +                int ix = 0;
> +                StringBuilder buf = new StringBuilder(line);
> +                while ((ix = buf.indexOf("\t", ix)) >= 0) {
> +                    buf.deleteCharAt(ix);
> +                    int spaces = tabWidth - (ix % tabWidth);
> +                    for (int j = 0; j < spaces; j++) {
> +                        buf.insert(ix++, ' ');
> +                    }
> +                }
> +                line = buf.toString();
> +            }
>              document.add(new Paragraph(line));
>              line = bufferedReader.readLine();
>          }
>
>
>
>

RE: svn commit: r1551679 - in /pivot/trunk/wtk/src/org/apache/pivot/wtk: ./ skin/ text/

Posted by "Roger L. Whitcomb" <Ro...@actian.com>.
As I was reviewing this commit, I realized there are additional changes in here not related strictly to tab handling (namely other keyboard handling changes), that are, in fact, not complete, and likely not correct as-is.

Given that this is "trunk" and not released yet, and that I am committed to getting this right sooner rather than later, I'm inclined to leave the code as-is and fix it up from here.  BUT, if anyone doesn't think that's a good idea, I can revert those parts that are not good (esp. in TextPaneSkin.java) and make another commit later once the keyboard stuff is working better.  Everything builds, so that shouldn't be a problem.

Comments?

Thanks,
~Roger

-----Original Message-----
From: rwhitcomb@apache.org [mailto:rwhitcomb@apache.org] 
Sent: Tuesday, December 17, 2013 12:18 PM
To: commits@pivot.apache.org
Subject: svn commit: r1551679 - in /pivot/trunk/wtk/src/org/apache/pivot/wtk: ./ skin/ text/

Author: rwhitcomb
Date: Tue Dec 17 20:17:48 2013
New Revision: 1551679

URL: http://svn.apache.org/r1551679
Log:
PIVOT-696: Support expansion of tabs in TextPane in the same ways as with TextArea.

Add new style: tabWidth and property expandTabs.

This also required changing the way we get a hold of the TextPane.Skin from within the skin view classes.  Also PlainTextSerializer needs the tab settings.

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/skin/TextPaneSkinBlockView.java
    pivot/trunk/wtk/src/org/apache/pivot/wtk/skin/TextPaneSkinBulletedListView.java
    pivot/trunk/wtk/src/org/apache/pivot/wtk/skin/TextPaneSkinComponentNodeView.java
    pivot/trunk/wtk/src/org/apache/pivot/wtk/skin/TextPaneSkinDocumentView.java
    pivot/trunk/wtk/src/org/apache/pivot/wtk/skin/TextPaneSkinElementView.java
    pivot/trunk/wtk/src/org/apache/pivot/wtk/skin/TextPaneSkinImageNodeView.java
    pivot/trunk/wtk/src/org/apache/pivot/wtk/skin/TextPaneSkinListItemView.java
    pivot/trunk/wtk/src/org/apache/pivot/wtk/skin/TextPaneSkinListView.java
    pivot/trunk/wtk/src/org/apache/pivot/wtk/skin/TextPaneSkinNodeView.java
    pivot/trunk/wtk/src/org/apache/pivot/wtk/skin/TextPaneSkinNumberedListView.java
    pivot/trunk/wtk/src/org/apache/pivot/wtk/skin/TextPaneSkinParagraphView.java
    pivot/trunk/wtk/src/org/apache/pivot/wtk/skin/TextPaneSkinSpanView.java
    pivot/trunk/wtk/src/org/apache/pivot/wtk/skin/TextPaneSkinTextNodeView.java
    pivot/trunk/wtk/src/org/apache/pivot/wtk/skin/TextPaneSkinVerticalElementView.java
    pivot/trunk/wtk/src/org/apache/pivot/wtk/text/PlainTextSerializer.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=1551679&r1=1551678&r2=1551679&view=diff
==============================================================================
--- pivot/trunk/wtk/src/org/apache/pivot/wtk/TextPane.java (original)
+++ pivot/trunk/wtk/src/org/apache/pivot/wtk/TextPane.java Tue Dec 17 
+++ 20:17:48 2013
@@ -17,8 +17,12 @@
 package org.apache.pivot.wtk;
 
 import java.io.IOException;
+import java.io.InputStream;
+import java.io.InputStreamReader;
+import java.io.Reader;
 import java.io.StringReader;
 import java.io.StringWriter;
+import java.net.URL;
 
 import org.apache.pivot.beans.DefaultProperty;
 import org.apache.pivot.collections.LinkedList;
@@ -96,6 +100,12 @@ public class TextPane extends Container 
          * @return The bounds of the character at the given offset.
          */
         public Bounds getCharacterBounds(int offset);
+
+        /**
+         * Returns the current setting of the "tabWidth" style (so "setText"
+         * uses the same value as Ctrl-Tab from user).
+         */
+        public int getTabWidth();
     }
 
     private interface Edit {
@@ -199,6 +209,8 @@ public class TextPane extends Container 
     private int selectionStart = 0;
     private int selectionLength = 0;
 
+    private boolean expandTabs = false;
+
     private boolean editable = true;
     private boolean undoingHistory = false;
     private boolean bulkOperation = false; @@ -643,6 +655,8 @@ public class TextPane extends Container 
                 try {
                     PlainTextSerializer serializer = new PlainTextSerializer();
                     StringReader reader = new StringReader(text);
+                    serializer.setExpandTabs(this.expandTabs);
+                    serializer.setTabWidth(((TextPane.Skin) 
+ getSkin()).getTabWidth());
                     documentLocal = serializer.readObject(reader);
                     n = documentLocal.getCharacterCount();
 
@@ -716,14 +730,78 @@ public class TextPane extends Container 
     /**
      * Convenience method to create a text-only document consisting of one
      * paragraph per line of the given text.
+     *
+     * @param text
      */
     public void setText(String text) {
+        if (text == null) {
+            throw new IllegalArgumentException();
+        }
+
+        try {
+            setText(new StringReader(text));
+        } catch (IOException exception) {
+            throw new RuntimeException(exception);
+        }
+    }
+
+    public void setText(URL textURL) throws IOException {
+        if (textURL == null) {
+            throw new IllegalArgumentException();
+        }
+
+        InputStream inputStream = null;
+        try {
+            inputStream = textURL.openStream();
+            setText(new InputStreamReader(inputStream));
+        } finally {
+            if (inputStream != null) {
+                inputStream.close();
+            }
+        }
+    }
+
+    public void setText(Reader textReader) throws IOException {
+        if (textReader == null) {
+            throw new IllegalArgumentException();
+        }
+
+        int tabPosition = 0;
+        int tabWidth = ((TextPane.Skin) getSkin()).getTabWidth();
+
         Document doc = new Document();
-        String[] lines = text.split("\r?\n");
-        for (int i = 0; i < lines.length; i++) {
-            Paragraph paragraph = new Paragraph(lines[i]);
+        StringBuilder text = new StringBuilder();
+
+        int c = textReader.read();
+        while (c != -1) {
+            if (c == '\n') {
+                Paragraph paragraph = new Paragraph(text.toString());
+                doc.add(paragraph);
+                text.setLength(0);
+                tabPosition = 0;
+            } else if (c == '\t') {
+                if (expandTabs) {
+                    int spaces = tabWidth - (tabPosition % tabWidth);
+                    for (int i = 0; i < spaces; i++) {
+                        text.append(' ');
+                    }
+                    tabPosition += spaces;
+                } else {
+                    text.append('\t');
+                }
+            } else {
+                text.append((char) c);
+                tabPosition++;
+            }
+
+            c = textReader.read();
+        }
+
+        if (text.length() != 0) {
+            Paragraph paragraph = new Paragraph(text.toString());
             doc.add(paragraph);
         }
+
         setDocument(doc);
     }
 
@@ -876,6 +954,27 @@ public class TextPane extends Container 
         }
     }
 
+    public boolean getExpandTabs() {
+        return expandTabs;
+    }
+
+    /**
+     * Sets whether tab characters (<code>\t</code>) are expanded to an
+     * appropriate number of spaces during {@link #setText} and
+     * {@link #paste} operations.  Note: doing this for keyboard input
+     * is handled in the skin.
+     *
+     * @param expandTabs <code>true</code> to replace tab characters with space
+     * characters (depending on the setting of the
+     * {@link TextPane.Skin#getTabWidth} value) or <code>false</code> to leave
+     * tabs alone. Note: this only affects tabs encountered during program
+     * operations; tabs entered via the keyboard by the user are always
+     * expanded, regardless of this setting.
+     */
+    public void setExpandTabs(boolean expandTabs) {
+        this.expandTabs = expandTabs;
+    }
+
     public int getInsertionPoint(int x, int y) {
         TextPane.Skin textPaneSkin = (TextPane.Skin) getSkin();
         return textPaneSkin.getInsertionPoint(x, y);

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=1551679&r1=1551678&r2=1551679&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 
+++ Dec 17 20:17:48 2013
@@ -39,16 +39,9 @@ import org.apache.pivot.wtk.TextPane;  import org.apache.pivot.wtk.TextPaneListener;
 import org.apache.pivot.wtk.TextPaneSelectionListener;
 import org.apache.pivot.wtk.Theme;
-import org.apache.pivot.wtk.text.BulletedList;
-import org.apache.pivot.wtk.text.ComponentNode;
 import org.apache.pivot.wtk.text.Document;
-import org.apache.pivot.wtk.text.ImageNode;
-import org.apache.pivot.wtk.text.List;
 import org.apache.pivot.wtk.text.Node;
-import org.apache.pivot.wtk.text.NumberedList;
 import org.apache.pivot.wtk.text.Paragraph;
-import org.apache.pivot.wtk.text.TextNode;
-import org.apache.pivot.wtk.text.TextSpan;
 
 /**
  * Text pane skin.
@@ -145,6 +138,8 @@ public class TextPaneSkin extends Contai
     private Insets margin = new Insets(4);
 
     private boolean wrapText = true;
+    private int tabWidth = 4;
+    private boolean acceptsTab = false;
 
     private static final int SCROLL_RATE = 30;
 
@@ -171,7 +166,7 @@ public class TextPaneSkin extends Contai
 
         Document document = textPane.getDocument();
         if (document != null) {
-            documentView = (TextPaneSkinDocumentView) createNodeView(document);
+            documentView = (TextPaneSkinDocumentView) 
+ TextPaneSkinNodeView.createNodeView(this, document);
             documentView.attach();
             updateSelection();
         }
@@ -386,6 +381,45 @@ public class TextPaneSkin extends Contai
         return characterBounds;
     }
 
+    /**
+     * Gets current value of style that determines the behavior of <tt>TAB</tt>
+     * and <tt>Ctrl-TAB</tt> characters.
+     *
+     * @return <tt>true</tt> if <tt>TAB</tt> inserts an appropriate number of
+     * spaces, while <tt>Ctrl-TAB</tt> shifts focus to next component.
+     * <tt>false</tt> (default) means <tt>TAB</tt> shifts focus and
+     * <tt>Ctrl-TAB</tt> inserts spaces.
+     */
+    public boolean getAcceptsTab() {
+        return acceptsTab;
+    }
+
+    /**
+     * Sets current value of style that determines the behavior of <tt>TAB</tt>
+     * and <tt>Ctrl-TAB</tt> characters.
+     *
+     * @param acceptsTab <tt>true</tt> if <tt>TAB</tt> inserts an appropriate
+     * number of spaces, while <tt>Ctrl-TAB</tt> shifts focus to next component.
+     * <tt>false</tt> (default) means <tt>TAB</tt> shifts focus and
+     * <tt>Ctrl-TAB</tt> inserts spaces.
+     */
+    public void setAcceptsTab(boolean acceptsTab) {
+        this.acceptsTab = acceptsTab;
+    }
+
+    @Override
+    public int getTabWidth() {
+        return tabWidth;
+    }
+
+    public void setTabWidth(int tabWidth) {
+        if (tabWidth < 0) {
+            throw new IllegalArgumentException("tabWidth is negative.");
+        }
+
+        this.tabWidth = tabWidth;
+    }
+
     private void scrollCharacterToVisible(int offset) {
         TextPane textPane = (TextPane) getComponent();
         Bounds characterBounds = getCharacterBounds(offset); @@ -797,6 +831,35 @@ public class TextPaneSkin extends Contai
         return consumed;
     }
 
+    private int getRowOffset(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 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;
+            }
+        }
+        return 0;
+    }
+
     @Override
     public boolean keyPressed(final Component component, int keyCode,
         Keyboard.KeyLocation keyLocation) { @@ -805,25 +868,88 @@ public class TextPaneSkin extends Contai
         final TextPane textPane = (TextPane) getComponent();
         Document document = textPane.getDocument();
 
+        int selectionStart = textPane.getSelectionStart();
+        int selectionLength = textPane.getSelectionLength();
+
         Keyboard.Modifier commandModifier = Platform.getCommandModifier();
+        boolean commandPressed = Keyboard.isPressed(commandModifier);
+        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();
+
         if (document != null) {
-            if (keyCode == Keyboard.KeyCode.ENTER && textPane.isEditable()) {
+            if (keyCode == Keyboard.KeyCode.ENTER && isEditable) {
                 textPane.insertParagraph();
 
                 consumed = true;
-            } else if (keyCode == Keyboard.KeyCode.DELETE && textPane.isEditable()) {
+            } else if (keyCode == Keyboard.KeyCode.DELETE && 
+ isEditable) {
                 textPane.delete(false);
 
                 consumed = true;
-            } else if (keyCode == Keyboard.KeyCode.BACKSPACE && textPane.isEditable()) {
+            } else if (keyCode == Keyboard.KeyCode.BACKSPACE && 
+ isEditable) {
                 textPane.delete(true);
 
                 consumed = true;
-            } else if (keyCode == Keyboard.KeyCode.LEFT) {
-                int selectionStart = textPane.getSelectionStart();
-                int selectionLength = textPane.getSelectionLength();
+            } else if (keyCode == Keyboard.KeyCode.HOME
+                   || (keyCode == Keyboard.KeyCode.LEFT && metaPressed)) {
+                int start;
+                if (commandPressed) {
+                    // Move the caret to the beginning of the text
+                    start = 0;
+                } else {
+                    // Move the caret to the beginning of the line
+                    start = getRowOffset(document, selectionStart);
+                }
 
-                if (Keyboard.isPressed(Keyboard.Modifier.SHIFT)) {
+                if (shiftPressed) {
+                    selectionLength += selectionStart - start;
+                } else {
+                    selectionLength = 0;
+                }
+
+                if (selectionStart >= 0) {
+                    textPane.setSelection(start, selectionLength);
+                    scrollCharacterToVisible(start);
+
+                    consumed = true;
+                }
+            } else if (keyCode == Keyboard.KeyCode.END
+                   || (keyCode == Keyboard.KeyCode.RIGHT && metaPressed)) {
+                int end;
+                int index = selectionStart + selectionLength;
+
+                if (commandPressed) {
+                    // Move the caret to end of the text
+                    end = textPane.getCharacterCount() - 1;
+                } else {
+                    // Move the caret to the end of the line
+                    int rowOffset = getRowOffset(document, index);
+                    int rowLength = getRowLength(document, index);
+                    end = rowOffset + rowLength;
+                }
+
+                if (shiftPressed) {
+                    selectionLength += end - index;
+                } else {
+                    selectionStart = end;
+                    if (selectionStart < textPane.getCharacterCount()
+                        && document.getCharacterAt(selectionStart) == '\n') {
+                        selectionStart--;
+                    }
+
+                    selectionLength = 0;
+                }
+
+                if (selectionStart + selectionLength <= textPane.getCharacterCount()) {
+                    textPane.setSelection(selectionStart, selectionLength);
+                    scrollCharacterToVisible(selectionStart + 
+ selectionLength);
+
+                    consumed = true;
+                }
+            } else if (keyCode == Keyboard.KeyCode.LEFT) {
+                if (shiftPressed) {
                     // Add the previous character to the selection
                     if (selectionStart > 0) {
                         selectionStart--; @@ -862,10 +988,7 @@ public class TextPaneSkin extends Contai
 
                 consumed = true;
             } else if (keyCode == Keyboard.KeyCode.RIGHT) {
-                int selectionStart = textPane.getSelectionStart();
-                int selectionLength = textPane.getSelectionLength();
-
-                if (Keyboard.isPressed(Keyboard.Modifier.SHIFT)) {
+                if (shiftPressed) {
                     // Add the next character to the selection
                     if (selectionStart + selectionLength < document.getCharacterCount()) {
                         selectionLength++; @@ -911,8 +1034,6 @@ public class TextPaneSkin extends Contai
 
                 consumed = true;
             } else if (keyCode == Keyboard.KeyCode.UP) {
-                int selectionStart = textPane.getSelectionStart();
-
                 int offset = getNextInsertionPoint(caretX, selectionStart,
                     TextPane.ScrollDirection.UP);
 
@@ -920,10 +1041,8 @@ public class TextPaneSkin extends Contai
                     offset = 0;
                 }
 
-                int selectionLength;
-                if (Keyboard.isPressed(Keyboard.Modifier.SHIFT)) {
-                    int selectionEnd = selectionStart + textPane.getSelectionLength() - 1;
-                    selectionLength = selectionEnd - offset + 1;
+                if (shiftPressed) {
+                    selectionLength = selectionStart + selectionLength 
+ - offset;
                 } else {
                     selectionLength = 0;
                 }
@@ -933,10 +1052,8 @@ public class TextPaneSkin extends Contai
 
                 consumed = true;
             } else if (keyCode == Keyboard.KeyCode.DOWN) {
-                int selectionStart = textPane.getSelectionStart();
-                int selectionLength = textPane.getSelectionLength();
 
-                if (Keyboard.isPressed(Keyboard.Modifier.SHIFT)) {
+                if (shiftPressed) {
                     int from;
                     int x;
                     if (selectionLength == 0) { @@ -993,27 +1110,42 @@ public class TextPaneSkin extends Contai
                 }
 
                 consumed = true;
-            } else if (Keyboard.isPressed(commandModifier) && keyCode == Keyboard.KeyCode.TAB
-                && textPane.isEditable()) {
-                textPane.insert("\t");
+            } else if (keyCode == Keyboard.KeyCode.TAB
+                && (acceptsTab != Keyboard.isPressed(Keyboard.Modifier.CTRL))
+                && isEditable) {
+                if (textPane.getExpandTabs()) {
+                    int linePos = selectionStart - getRowOffset(document, selectionStart);
+                    StringBuilder tabBuilder = new StringBuilder(tabWidth);
+                    for (int i = 0; i < tabWidth - (linePos % tabWidth); i++) {
+                        tabBuilder.append(" ");
+                    }
+                    textPane.insert(tabBuilder.toString());
+                } else {
+                    textPane.insert("\t");
+                }
                 showCaret(true);
 
                 consumed = true;
-            } else if (Keyboard.isPressed(commandModifier)) {
+            } else if (keyCode == Keyboard.KeyCode.INSERT) {
+                if (shiftPressed && isEditable) {
+                    textPane.paste();
+                    consumed = true;
+                }
+            } else if (commandPressed) {
                 if (keyCode == Keyboard.KeyCode.A) {
                     textPane.setSelection(0, document.getCharacterCount());
                     consumed = true;
-                } else if (keyCode == Keyboard.KeyCode.X && textPane.isEditable()) {
+                } else if (keyCode == Keyboard.KeyCode.X && isEditable) 
+ {
                     textPane.cut();
                     consumed = true;
                 } else if (keyCode == Keyboard.KeyCode.C) {
                     textPane.copy();
                     consumed = true;
-                } else if (keyCode == Keyboard.KeyCode.V && textPane.isEditable()) {
+                } else if (keyCode == Keyboard.KeyCode.V && isEditable) 
+ {
                     textPane.paste();
                     consumed = true;
-                } else if (keyCode == Keyboard.KeyCode.Z && textPane.isEditable()) {
-                    if (Keyboard.isPressed(Keyboard.Modifier.SHIFT)) {
+                } else if (keyCode == Keyboard.KeyCode.Z && isEditable) {
+                    if (shiftPressed) {
                         textPane.redo();
                     } else {
                         textPane.undo(); @@ -1021,33 +1153,6 @@ public class TextPaneSkin extends Contai
 
                     consumed = true;
                 }
-            } else if (keyCode == Keyboard.KeyCode.HOME) {
-                // Move the caret to the beginning of the text
-                if (Keyboard.isPressed(Keyboard.Modifier.SHIFT)) {
-                    textPane.setSelection(0, textPane.getSelectionStart());
-                } else {
-                    textPane.setSelection(0, 0);
-                }
-                scrollCharacterToVisible(0);
-
-                consumed = true;
-            } else if (keyCode == Keyboard.KeyCode.END) {
-                // Move the caret to the end of the text
-                if (Keyboard.isPressed(Keyboard.Modifier.SHIFT)) {
-                    int selectionStart = textPane.getSelectionStart();
-                    textPane.setSelection(selectionStart, textPane.getCharacterCount()
-                        - selectionStart);
-                } else {
-                    textPane.setSelection(textPane.getCharacterCount() - 1, 0);
-                }
-                scrollCharacterToVisible(textPane.getCharacterCount() - 1);
-
-                consumed = true;
-            } else if (keyCode == Keyboard.KeyCode.INSERT) {
-                if (Keyboard.isPressed(Keyboard.Modifier.SHIFT) && textPane.isEditable()) {
-                    textPane.paste();
-                    consumed = true;
-                }
             } else {
                 consumed = super.keyPressed(component, keyCode, keyLocation);
             }
@@ -1089,7 +1194,7 @@ public class TextPaneSkin extends Contai
 
         Document document = textPane.getDocument();
         if (document != null) {
-            documentView = (TextPaneSkinDocumentView) createNodeView(document);
+            documentView = (TextPaneSkinDocumentView) 
+ TextPaneSkinNodeView.createNodeView(this, document);
             documentView.attach();
         }
 
@@ -1132,35 +1237,6 @@ public class TextPaneSkin extends Contai
         }
     }
 
-    TextPaneSkinNodeView createNodeView(Node node) {
-        TextPaneSkinNodeView nodeView = null;
-
-        if (node instanceof Document) {
-            nodeView = new TextPaneSkinDocumentView(this, (Document) node);
-        } else if (node instanceof Paragraph) {
-            nodeView = new TextPaneSkinParagraphView((Paragraph) node);
-        } else if (node instanceof TextNode) {
-            nodeView = new TextPaneSkinTextNodeView((TextNode) node);
-        } else if (node instanceof ImageNode) {
-            nodeView = new TextPaneSkinImageNodeView((ImageNode) node);
-        } else if (node instanceof ComponentNode) {
-            nodeView = new TextPaneSkinComponentNodeView((ComponentNode) node);
-        } else if (node instanceof TextSpan) {
-            nodeView = new TextPaneSkinSpanView((TextSpan) node);
-        } else if (node instanceof NumberedList) {
-            nodeView = new TextPaneSkinNumberedListView((NumberedList) node);
-        } else if (node instanceof BulletedList) {
-            nodeView = new TextPaneSkinBulletedListView((BulletedList) node);
-        } else if (node instanceof List.Item) {
-            nodeView = new TextPaneSkinListItemView((List.Item) node);
-        } else {
-            throw new IllegalArgumentException("Unsupported node type: "
-                + node.getClass().getName());
-        }
-
-        return nodeView;
-    }
-
     private void updateSelection() {
         if (documentView.getCharacterCount() > 0) {
             TextPane textPane = (TextPane) getComponent();

Modified: pivot/trunk/wtk/src/org/apache/pivot/wtk/skin/TextPaneSkinBlockView.java
URL: http://svn.apache.org/viewvc/pivot/trunk/wtk/src/org/apache/pivot/wtk/skin/TextPaneSkinBlockView.java?rev=1551679&r1=1551678&r2=1551679&view=diff
==============================================================================
--- pivot/trunk/wtk/src/org/apache/pivot/wtk/skin/TextPaneSkinBlockView.java (original)
+++ pivot/trunk/wtk/src/org/apache/pivot/wtk/skin/TextPaneSkinBlockView.
+++ java Tue Dec 17 20:17:48 2013
@@ -22,8 +22,8 @@ import org.apache.pivot.wtk.text.BlockLi
 
 abstract class TextPaneSkinBlockView extends TextPaneSkinElementView implements BlockListener {
 
-    public TextPaneSkinBlockView(Block block) {
-        super(block);
+    public TextPaneSkinBlockView(TextPaneSkin textPaneSkin, Block block) {
+        super(textPaneSkin, block);
     }
 
     @Override

Modified: pivot/trunk/wtk/src/org/apache/pivot/wtk/skin/TextPaneSkinBulletedListView.java
URL: http://svn.apache.org/viewvc/pivot/trunk/wtk/src/org/apache/pivot/wtk/skin/TextPaneSkinBulletedListView.java?rev=1551679&r1=1551678&r2=1551679&view=diff
==============================================================================
--- pivot/trunk/wtk/src/org/apache/pivot/wtk/skin/TextPaneSkinBulletedListView.java (original)
+++ pivot/trunk/wtk/src/org/apache/pivot/wtk/skin/TextPaneSkinBulletedLi
+++ stView.java Tue Dec 17 20:17:48 2013
@@ -21,8 +21,8 @@ import org.apache.pivot.wtk.text.Bullete
 
 class TextPaneSkinBulletedListView extends TextPaneSkinListView implements BulletedListListener {
 
-    public TextPaneSkinBulletedListView(BulletedList bulletedList) {
-        super(bulletedList);
+    public TextPaneSkinBulletedListView(TextPaneSkin textPaneSkin, BulletedList bulletedList) {
+        super(textPaneSkin, bulletedList);
     }
 
     @Override

Modified: pivot/trunk/wtk/src/org/apache/pivot/wtk/skin/TextPaneSkinComponentNodeView.java
URL: http://svn.apache.org/viewvc/pivot/trunk/wtk/src/org/apache/pivot/wtk/skin/TextPaneSkinComponentNodeView.java?rev=1551679&r1=1551678&r2=1551679&view=diff
==============================================================================
--- pivot/trunk/wtk/src/org/apache/pivot/wtk/skin/TextPaneSkinComponentNodeView.java (original)
+++ pivot/trunk/wtk/src/org/apache/pivot/wtk/skin/TextPaneSkinComponentN
+++ odeView.java Tue Dec 17 20:17:48 2013
@@ -35,8 +35,8 @@ class TextPaneSkinComponentNodeView exte
         }
     };
 
-    public TextPaneSkinComponentNodeView(ComponentNode componentNode) {
-        super(componentNode);
+    public TextPaneSkinComponentNodeView(TextPaneSkin textPaneSkin, ComponentNode componentNode) {
+        super(textPaneSkin, componentNode);
     }
 
     @Override

Modified: pivot/trunk/wtk/src/org/apache/pivot/wtk/skin/TextPaneSkinDocumentView.java
URL: http://svn.apache.org/viewvc/pivot/trunk/wtk/src/org/apache/pivot/wtk/skin/TextPaneSkinDocumentView.java?rev=1551679&r1=1551678&r2=1551679&view=diff
==============================================================================
--- pivot/trunk/wtk/src/org/apache/pivot/wtk/skin/TextPaneSkinDocumentView.java (original)
+++ pivot/trunk/wtk/src/org/apache/pivot/wtk/skin/TextPaneSkinDocumentVi
+++ ew.java Tue Dec 17 20:17:48 2013
@@ -23,11 +23,8 @@ import org.apache.pivot.wtk.text.Documen
  */
 class TextPaneSkinDocumentView extends TextPaneSkinVerticalElementView {
 
-    protected final TextPaneSkin textPaneSkin;
-
     public TextPaneSkinDocumentView(TextPaneSkin textPaneSkin, Document document) {
-        super(document);
-        this.textPaneSkin = textPaneSkin;
+        super(textPaneSkin, document);
     }
 
     @Override
@@ -43,8 +40,4 @@ class TextPaneSkinDocumentView extends T
         textPaneSkin.invalidateComponent();
     }
 
-    @Override
-    public TextPaneSkin getTextPaneSkin() {
-        return textPaneSkin;
-    }
 }
\ No newline at end of file

Modified: pivot/trunk/wtk/src/org/apache/pivot/wtk/skin/TextPaneSkinElementView.java
URL: http://svn.apache.org/viewvc/pivot/trunk/wtk/src/org/apache/pivot/wtk/skin/TextPaneSkinElementView.java?rev=1551679&r1=1551678&r2=1551679&view=diff
==============================================================================
--- pivot/trunk/wtk/src/org/apache/pivot/wtk/skin/TextPaneSkinElementView.java (original)
+++ pivot/trunk/wtk/src/org/apache/pivot/wtk/skin/TextPaneSkinElementVie
+++ w.java Tue Dec 17 20:17:48 2013
@@ -41,8 +41,8 @@ abstract class TextPaneSkinElementView e
     private int skinX = 0;
     private int skinY = 0;
 
-    public TextPaneSkinElementView(Element element) {
-        super(element);
+    public TextPaneSkinElementView(TextPaneSkin textPaneSkin, Element element) {
+        super(textPaneSkin, element);
     }
 
     @Override
@@ -54,7 +54,7 @@ abstract class TextPaneSkinElementView e
 
         // Attach child node views
         for (Node node : element) {
-            add(getTextPaneSkin().createNodeView(node));
+            add(createNodeView(getTextPaneSkin(), node));
         }
     }
 
@@ -235,7 +235,7 @@ abstract class TextPaneSkinElementView e
             int nodeViewOffset = nodeView.getOffset();
             int characterCount = nodeView.getCharacterCount();
 
-            if (offset >= nodeViewOffset && offset < nodeViewOffset + characterCount) {
+            if (offset >= nodeViewOffset && offset <= nodeViewOffset + 
+ characterCount) {
                 characterBounds = nodeView.getCharacterBounds(offset - nodeViewOffset);
 
                 if (characterBounds != null) { @@ -255,7 +255,7 @@ abstract class TextPaneSkinElementView e
 
     @Override
     public void nodeInserted(Element element, int index) {
-        insert(getTextPaneSkin().createNodeView(element.get(index)), index);
+        insert(createNodeView(getTextPaneSkin(), element.get(index)), 
+ index);
         invalidateUpTree();
     }
 

Modified: pivot/trunk/wtk/src/org/apache/pivot/wtk/skin/TextPaneSkinImageNodeView.java
URL: http://svn.apache.org/viewvc/pivot/trunk/wtk/src/org/apache/pivot/wtk/skin/TextPaneSkinImageNodeView.java?rev=1551679&r1=1551678&r2=1551679&view=diff
==============================================================================
--- pivot/trunk/wtk/src/org/apache/pivot/wtk/skin/TextPaneSkinImageNodeView.java (original)
+++ pivot/trunk/wtk/src/org/apache/pivot/wtk/skin/TextPaneSkinImageNodeV
+++ iew.java Tue Dec 17 20:17:48 2013
@@ -28,8 +28,8 @@ import org.apache.pivot.wtk.text.ImageNo
 
 class TextPaneSkinImageNodeView extends TextPaneSkinNodeView implements ImageNodeListener,
     ImageListener {
-    public TextPaneSkinImageNodeView(ImageNode imageNode) {
-        super(imageNode);
+    public TextPaneSkinImageNodeView(TextPaneSkin textPaneSkin, ImageNode imageNode) {
+        super(textPaneSkin, imageNode);
     }
 
     @Override

Modified: pivot/trunk/wtk/src/org/apache/pivot/wtk/skin/TextPaneSkinListItemView.java
URL: http://svn.apache.org/viewvc/pivot/trunk/wtk/src/org/apache/pivot/wtk/skin/TextPaneSkinListItemView.java?rev=1551679&r1=1551678&r2=1551679&view=diff
==============================================================================
--- pivot/trunk/wtk/src/org/apache/pivot/wtk/skin/TextPaneSkinListItemView.java (original)
+++ pivot/trunk/wtk/src/org/apache/pivot/wtk/skin/TextPaneSkinListItemVi
+++ ew.java Tue Dec 17 20:17:48 2013
@@ -18,6 +18,7 @@ package org.apache.pivot.wtk.skin;
 
 import java.util.Iterator;
 
+import org.apache.pivot.wtk.text.List;
 import org.apache.pivot.wtk.text.TextNode;
 
 class TextPaneSkinListItemView extends TextPaneSkinVerticalElementView { @@ -25,8 +26,8 @@ class TextPaneSkinListItemView extends T
     private TextNode indexTextNode;
     private TextPaneSkinTextNodeView indexTextNodeView;
 
-    public TextPaneSkinListItemView(org.apache.pivot.wtk.text.List.Item listItem) {
-        super(listItem);
+    public TextPaneSkinListItemView(TextPaneSkin textPaneSkin, List.Item listItem) {
+        super(textPaneSkin, listItem);
 
         this.indexTextNode = new TextNode("");
     }
@@ -36,7 +37,7 @@ class TextPaneSkinListItemView extends T
         super.attach();
 
         // add an extra TextNodeView to render the index text
-        indexTextNodeView = new TextPaneSkinTextNodeView(indexTextNode);
+        indexTextNodeView = new 
+ TextPaneSkinTextNodeView(getTextPaneSkin(), indexTextNode);
         indexTextNodeView.setLocation(0, 0);
         insert(indexTextNodeView, 0);
     }

Modified: pivot/trunk/wtk/src/org/apache/pivot/wtk/skin/TextPaneSkinListView.java
URL: http://svn.apache.org/viewvc/pivot/trunk/wtk/src/org/apache/pivot/wtk/skin/TextPaneSkinListView.java?rev=1551679&r1=1551678&r2=1551679&view=diff
==============================================================================
--- pivot/trunk/wtk/src/org/apache/pivot/wtk/skin/TextPaneSkinListView.java (original)
+++ pivot/trunk/wtk/src/org/apache/pivot/wtk/skin/TextPaneSkinListView.j
+++ ava Tue Dec 17 20:17:48 2013
@@ -22,8 +22,8 @@ class TextPaneSkinListView extends TextP
 
     protected int maxIndexTextWidth;
 
-    public TextPaneSkinListView(List list) {
-        super(list);
+    public TextPaneSkinListView(TextPaneSkin textPaneSkin, List list) {
+        super(textPaneSkin, list);
     }
 
     public int getMaxIndexTextWidth() {

Modified: pivot/trunk/wtk/src/org/apache/pivot/wtk/skin/TextPaneSkinNodeView.java
URL: http://svn.apache.org/viewvc/pivot/trunk/wtk/src/org/apache/pivot/wtk/skin/TextPaneSkinNodeView.java?rev=1551679&r1=1551678&r2=1551679&view=diff
==============================================================================
--- pivot/trunk/wtk/src/org/apache/pivot/wtk/skin/TextPaneSkinNodeView.java (original)
+++ pivot/trunk/wtk/src/org/apache/pivot/wtk/skin/TextPaneSkinNodeView.j
+++ ava Tue Dec 17 20:17:48 2013
@@ -18,19 +18,33 @@ package org.apache.pivot.wtk.skin;
 
 import java.awt.Graphics2D;
 
+import java.lang.reflect.Constructor;
+import java.lang.reflect.InvocationTargetException;
+
+import org.apache.pivot.collections.HashMap;
 import org.apache.pivot.collections.Sequence;
 import org.apache.pivot.wtk.Bounds;
 import org.apache.pivot.wtk.Dimensions;  import org.apache.pivot.wtk.Point;  import org.apache.pivot.wtk.TextPane;
+import org.apache.pivot.wtk.text.BulletedList;
+import org.apache.pivot.wtk.text.ComponentNode;
+import org.apache.pivot.wtk.text.Document;
 import org.apache.pivot.wtk.text.Element;
+import org.apache.pivot.wtk.text.ImageNode;
+import org.apache.pivot.wtk.text.List;
 import org.apache.pivot.wtk.text.Node;
 import org.apache.pivot.wtk.text.NodeListener;
+import org.apache.pivot.wtk.text.NumberedList;
+import org.apache.pivot.wtk.text.Paragraph;
+import org.apache.pivot.wtk.text.TextNode;
+import org.apache.pivot.wtk.text.TextSpan;
 
 /**
  * Abstract base class for node views.
  */
 abstract class TextPaneSkinNodeView implements NodeListener {
+    protected final TextPaneSkin textPaneSkin;
     private Node node = null;
     private TextPaneSkinElementView parent = null;
 
@@ -42,7 +56,8 @@ abstract class TextPaneSkinNodeView impl
 
     private boolean valid = false;
 
-    public TextPaneSkinNodeView(Node node) {
+    public TextPaneSkinNodeView(TextPaneSkin textPaneSkin, Node node) {
+        this.textPaneSkin = textPaneSkin;
         this.node = node;
     }
 
@@ -59,7 +74,7 @@ abstract class TextPaneSkinNodeView impl
     }
 
     protected TextPaneSkin getTextPaneSkin() {
-        return getParent().getTextPaneSkin();
+        return textPaneSkin;
     }
 
     protected void attach() {
@@ -232,4 +247,41 @@ abstract class TextPaneSkinNodeView impl
         // No-op
     }
 
+    private static HashMap<Class<? extends Node>, Class<? extends TextPaneSkinNodeView>>
+            nodeViewSkinMap = new HashMap<>();
+    static {
+        nodeViewSkinMap.put(Document.class, TextPaneSkinDocumentView.class);
+        nodeViewSkinMap.put(Paragraph.class, TextPaneSkinParagraphView.class);
+        nodeViewSkinMap.put(TextNode.class, TextPaneSkinTextNodeView.class);
+        nodeViewSkinMap.put(ImageNode.class, TextPaneSkinImageNodeView.class);
+        nodeViewSkinMap.put(ComponentNode.class, TextPaneSkinComponentNodeView.class);
+        nodeViewSkinMap.put(TextSpan.class, TextPaneSkinSpanView.class);
+        nodeViewSkinMap.put(NumberedList.class, TextPaneSkinNumberedListView.class);
+        nodeViewSkinMap.put(BulletedList.class, TextPaneSkinBulletedListView.class);
+        nodeViewSkinMap.put(List.Item.class, TextPaneSkinListItemView.class);
+    }
+
+    public static TextPaneSkinNodeView createNodeView(TextPaneSkin textPaneSkin, Node node) {
+        TextPaneSkinNodeView nodeView = null;
+
+        Class<? extends Node> nodeClass = node.getClass();
+        Class<? extends TextPaneSkinNodeView> skinClass = nodeViewSkinMap.get(nodeClass);
+        if (skinClass != null) {
+            try {
+                Constructor<?> constructor = skinClass.getConstructor(TextPaneSkin.class, nodeClass);
+                nodeView = (TextPaneSkinNodeView)constructor.newInstance(textPaneSkin, node);
+            } catch (NoSuchMethodException | InstantiationException
+                     | IllegalAccessException | InvocationTargetException ex) {
+                throw new RuntimeException("Error instantiating node view for "
+                    + nodeClass.getName(), ex);
+            }
+        }
+        if (nodeView == null) {
+            throw new IllegalArgumentException("Unsupported node type: "
+                + nodeClass.getName());
+        }
+
+        return nodeView;
+    }
+
 }

Modified: pivot/trunk/wtk/src/org/apache/pivot/wtk/skin/TextPaneSkinNumberedListView.java
URL: http://svn.apache.org/viewvc/pivot/trunk/wtk/src/org/apache/pivot/wtk/skin/TextPaneSkinNumberedListView.java?rev=1551679&r1=1551678&r2=1551679&view=diff
==============================================================================
--- pivot/trunk/wtk/src/org/apache/pivot/wtk/skin/TextPaneSkinNumberedListView.java (original)
+++ pivot/trunk/wtk/src/org/apache/pivot/wtk/skin/TextPaneSkinNumberedLi
+++ stView.java Tue Dec 17 20:17:48 2013
@@ -59,8 +59,8 @@ class TextPaneSkinNumberedListView exten
         return (char) ('A' + n - 1) + "";
     }
 
-    public TextPaneSkinNumberedListView(NumberedList numberedList) {
-        super(numberedList);
+    public TextPaneSkinNumberedListView(TextPaneSkin textPaneSkin, NumberedList numberedList) {
+        super(textPaneSkin, numberedList);
     }
 
     @Override

Modified: pivot/trunk/wtk/src/org/apache/pivot/wtk/skin/TextPaneSkinParagraphView.java
URL: http://svn.apache.org/viewvc/pivot/trunk/wtk/src/org/apache/pivot/wtk/skin/TextPaneSkinParagraphView.java?rev=1551679&r1=1551678&r2=1551679&view=diff
==============================================================================
--- pivot/trunk/wtk/src/org/apache/pivot/wtk/skin/TextPaneSkinParagraphView.java (original)
+++ pivot/trunk/wtk/src/org/apache/pivot/wtk/skin/TextPaneSkinParagraphV
+++ iew.java Tue Dec 17 20:17:48 2013
@@ -54,8 +54,8 @@ class TextPaneSkinParagraphView extends 
     private ArrayList<Row> rows = null;
     private Bounds terminatorBounds = new Bounds(0, 0, 0, 0);
 
-    public TextPaneSkinParagraphView(Paragraph paragraph) {
-        super(paragraph);
+    public TextPaneSkinParagraphView(TextPaneSkin textPaneSkin, Paragraph paragraph) {
+        super(textPaneSkin, paragraph);
     }
 
     @Override

Modified: pivot/trunk/wtk/src/org/apache/pivot/wtk/skin/TextPaneSkinSpanView.java
URL: http://svn.apache.org/viewvc/pivot/trunk/wtk/src/org/apache/pivot/wtk/skin/TextPaneSkinSpanView.java?rev=1551679&r1=1551678&r2=1551679&view=diff
==============================================================================
--- pivot/trunk/wtk/src/org/apache/pivot/wtk/skin/TextPaneSkinSpanView.java (original)
+++ pivot/trunk/wtk/src/org/apache/pivot/wtk/skin/TextPaneSkinSpanView.j
+++ ava Tue Dec 17 20:17:48 2013
@@ -25,8 +25,8 @@ import org.apache.pivot.wtk.text.TextSpa
  */
 class TextPaneSkinSpanView extends TextPaneSkinElementView {
 
-    public TextPaneSkinSpanView(TextSpan span) {
-        super(span);
+    public TextPaneSkinSpanView(TextPaneSkin textPaneSkin, TextSpan span) {
+        super(textPaneSkin, span);
     }
 
     @Override

Modified: pivot/trunk/wtk/src/org/apache/pivot/wtk/skin/TextPaneSkinTextNodeView.java
URL: http://svn.apache.org/viewvc/pivot/trunk/wtk/src/org/apache/pivot/wtk/skin/TextPaneSkinTextNodeView.java?rev=1551679&r1=1551678&r2=1551679&view=diff
==============================================================================
--- pivot/trunk/wtk/src/org/apache/pivot/wtk/skin/TextPaneSkinTextNodeView.java (original)
+++ pivot/trunk/wtk/src/org/apache/pivot/wtk/skin/TextPaneSkinTextNodeVi
+++ ew.java Tue Dec 17 20:17:48 2013
@@ -47,12 +47,12 @@ class TextPaneSkinTextNodeView extends T
     private GlyphVector glyphVector = null;
     private TextPaneSkinTextNodeView next = null;
 
-    public TextPaneSkinTextNodeView(TextNode textNode) {
-        this(textNode, 0);
+    public TextPaneSkinTextNodeView(TextPaneSkin textPaneSkin, TextNode textNode) {
+        this(textPaneSkin, textNode, 0);
     }
 
-    public TextPaneSkinTextNodeView(TextNode textNode, int start) {
-        super(textNode);
+    public TextPaneSkinTextNodeView(TextPaneSkin textPaneSkin, TextNode textNode, int start) {
+        super(textPaneSkin, textNode);
         this.start = start;
     }
 
@@ -134,7 +134,7 @@ class TextPaneSkinTextNodeView extends T
 
         if (end < ci.getEndIndex()) {
             length = end - start;
-            next = new TextPaneSkinTextNodeView(textNode, end);
+            next = new TextPaneSkinTextNodeView(getTextPaneSkin(), 
+ textNode, end);
             next.setParent(getParent());
         } else {
             length = ci.getEndIndex() - start; @@ -473,11 +473,18 @@ class TextPaneSkinTextNodeView extends T
 
     @Override
     public Bounds getCharacterBounds(int offset) {
-        Shape glyphBounds = glyphVector.getGlyphLogicalBounds(offset);
+        // If the offest == length, then use the right hand edge of the previous
+        // offset instead -- this is for positioning the caret at the end of the text
+        Shape glyphBounds = glyphVector.getGlyphLogicalBounds(offset == 
+ length ? offset - 1 : offset);
         Rectangle2D glyphBounds2D = glyphBounds.getBounds2D();
 
-        return new Bounds((int) Math.floor(glyphBounds2D.getX()), 0,
-            (int) Math.ceil(glyphBounds2D.getWidth()), getHeight());
+        if (offset == length) {
+            return new Bounds((int) Math.ceil(glyphBounds2D.getX() + glyphBounds2D.getWidth()), 0,
+                1, getHeight());
+        } else {
+            return new Bounds((int) Math.floor(glyphBounds2D.getX()), 0,
+                (int) Math.ceil(glyphBounds2D.getWidth()), getHeight());
+        }
     }
 
     @Override

Modified: pivot/trunk/wtk/src/org/apache/pivot/wtk/skin/TextPaneSkinVerticalElementView.java
URL: http://svn.apache.org/viewvc/pivot/trunk/wtk/src/org/apache/pivot/wtk/skin/TextPaneSkinVerticalElementView.java?rev=1551679&r1=1551678&r2=1551679&view=diff
==============================================================================
--- pivot/trunk/wtk/src/org/apache/pivot/wtk/skin/TextPaneSkinVerticalElementView.java (original)
+++ pivot/trunk/wtk/src/org/apache/pivot/wtk/skin/TextPaneSkinVerticalEl
+++ ementView.java Tue Dec 17 20:17:48 2013
@@ -27,8 +27,8 @@ import org.apache.pivot.wtk.text.Element
  */
 abstract class TextPaneSkinVerticalElementView extends TextPaneSkinElementView {
 
-    public TextPaneSkinVerticalElementView(Element element) {
-        super(element);
+    public TextPaneSkinVerticalElementView(TextPaneSkin textPaneSkin, Element element) {
+        super(textPaneSkin, element);
     }
 
     @Override

Modified: pivot/trunk/wtk/src/org/apache/pivot/wtk/text/PlainTextSerializer.java
URL: http://svn.apache.org/viewvc/pivot/trunk/wtk/src/org/apache/pivot/wtk/text/PlainTextSerializer.java?rev=1551679&r1=1551678&r2=1551679&view=diff
==============================================================================
--- pivot/trunk/wtk/src/org/apache/pivot/wtk/text/PlainTextSerializer.java (original)
+++ pivot/trunk/wtk/src/org/apache/pivot/wtk/text/PlainTextSerializer.ja
+++ va Tue Dec 17 20:17:48 2013
@@ -39,6 +39,9 @@ public class PlainTextSerializer impleme
     public static final String MIME_TYPE = "text/plain";
     public static final int BUFFER_SIZE = 2048;
 
+    private boolean expandTabs = false;
+    private int tabWidth = 4;
+
     public PlainTextSerializer() {
         this(Charset.defaultCharset());
     }
@@ -55,6 +58,35 @@ public class PlainTextSerializer impleme
         this.charset = charset;
     }
 
+    public int getTabWidth() {
+        return tabWidth;
+    }
+
+    public void setTabWidth(int tabWidth) {
+        if (tabWidth < 0) {
+            throw new IllegalArgumentException("tabWidth is negative.");
+        }
+
+        this.tabWidth = tabWidth;
+    }
+
+    public boolean getExpandTabs() {
+        return expandTabs;
+    }
+
+    /**
+     * Sets whether tab characters (<code>\t</code>) are expanded to an
+     * appropriate number of spaces while reading the text.
+     *
+     * @param expandTabs <code>true</code> to replace tab characters with space
+     * characters (depending on the setting of the {@link #getTabWidth} value)
+     * or <code>false</code> to leave tabs alone.
+     */
+    public void setExpandTabs(boolean expandTabs) {
+        this.expandTabs = expandTabs;
+    }
+
+
     @Override
     public Document readObject(InputStream inputStream) throws IOException {
         Reader reader = new InputStreamReader(inputStream, charset); @@ -70,6 +102,18 @@ public class PlainTextSerializer impleme
 
         String line = bufferedReader.readLine();
         while (line != null) {
+            if (expandTabs) {
+                int ix = 0;
+                StringBuilder buf = new StringBuilder(line);
+                while ((ix = buf.indexOf("\t", ix)) >= 0) {
+                    buf.deleteCharAt(ix);
+                    int spaces = tabWidth - (ix % tabWidth);
+                    for (int j = 0; j < spaces; j++) {
+                        buf.insert(ix++, ' ');
+                    }
+                }
+                line = buf.toString();
+            }
             document.add(new Paragraph(line));
             line = bufferedReader.readLine();
         }