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();
         }


Reply via email to