You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@pivot.apache.org by gb...@apache.org on 2010/09/03 21:34:00 UTC

svn commit: r992428 - in /pivot/trunk/wtk/src/org/apache/pivot/wtk: FocusTraversalDirection.java MenuBar.java TextArea2.java TextAreaContentListener2.java skin/TextAreaSkin2.java

Author: gbrown
Date: Fri Sep  3 19:33:59 2010
New Revision: 992428

URL: http://svn.apache.org/viewvc?rev=992428&view=rev
Log:
Continue building out new plain TextArea component.

Modified:
    pivot/trunk/wtk/src/org/apache/pivot/wtk/FocusTraversalDirection.java
    pivot/trunk/wtk/src/org/apache/pivot/wtk/MenuBar.java
    pivot/trunk/wtk/src/org/apache/pivot/wtk/TextArea2.java
    pivot/trunk/wtk/src/org/apache/pivot/wtk/TextAreaContentListener2.java
    pivot/trunk/wtk/src/org/apache/pivot/wtk/skin/TextAreaSkin2.java

Modified: pivot/trunk/wtk/src/org/apache/pivot/wtk/FocusTraversalDirection.java
URL: http://svn.apache.org/viewvc/pivot/trunk/wtk/src/org/apache/pivot/wtk/FocusTraversalDirection.java?rev=992428&r1=992427&r2=992428&view=diff
==============================================================================
--- pivot/trunk/wtk/src/org/apache/pivot/wtk/FocusTraversalDirection.java (original)
+++ pivot/trunk/wtk/src/org/apache/pivot/wtk/FocusTraversalDirection.java Fri Sep  3 19:33:59 2010
@@ -17,7 +17,7 @@
 package org.apache.pivot.wtk;
 
 /**
- * Enumeration representing a two-dimensional direction.
+ * Enumeration representing a focus traversal direction.
  */
 public enum FocusTraversalDirection {
     FORWARD,

Modified: pivot/trunk/wtk/src/org/apache/pivot/wtk/MenuBar.java
URL: http://svn.apache.org/viewvc/pivot/trunk/wtk/src/org/apache/pivot/wtk/MenuBar.java?rev=992428&r1=992427&r2=992428&view=diff
==============================================================================
--- pivot/trunk/wtk/src/org/apache/pivot/wtk/MenuBar.java (original)
+++ pivot/trunk/wtk/src/org/apache/pivot/wtk/MenuBar.java Fri Sep  3 19:33:59 2010
@@ -170,7 +170,19 @@ public class MenuBar extends Container {
      * Item listener interface.
      */
     public interface ItemListener {
+        /**
+         * Called when an item's menu has changed.
+         *
+         * @param item
+         * @param previousMenu
+         */
         public void menuChanged(Item item, Menu previousMenu);
+
+        /**
+         * Called when an item's active state has changed.
+         *
+         * @param item
+         */
         public void activeChanged(Item item);
     }
 

Modified: pivot/trunk/wtk/src/org/apache/pivot/wtk/TextArea2.java
URL: http://svn.apache.org/viewvc/pivot/trunk/wtk/src/org/apache/pivot/wtk/TextArea2.java?rev=992428&r1=992427&r2=992428&view=diff
==============================================================================
--- pivot/trunk/wtk/src/org/apache/pivot/wtk/TextArea2.java (original)
+++ pivot/trunk/wtk/src/org/apache/pivot/wtk/TextArea2.java Fri Sep  3 19:33:59 2010
@@ -23,10 +23,13 @@ import java.io.InputStreamReader;
 import java.io.Reader;
 import java.io.StringReader;
 import java.net.URL;
+import java.util.Iterator;
 import java.util.Locale;
 
 import org.apache.pivot.collections.ArrayList;
+import org.apache.pivot.collections.Sequence;
 import org.apache.pivot.json.JSON;
+import org.apache.pivot.util.ImmutableIterator;
 import org.apache.pivot.util.ListenerList;
 
 /**
@@ -34,6 +37,96 @@ import org.apache.pivot.util.ListenerLis
  */
 public class TextArea2 extends Component {
     /**
+     * Class representing a paragraph of text.
+     */
+    public final class Paragraph {
+        private StringBuilder characters;
+
+        private Paragraph() {
+            characters = new StringBuilder(INITIAL_PARAGRAPH_CAPACITY);
+        }
+
+        public CharSequence getCharacters() {
+            return characters;
+        }
+
+        public void insertText(String text, int index) {
+            characters.insert(index, text);
+
+            // TODO Fire event
+            // TODO Update selection state
+        }
+
+        public void removeText(int index, int count) {
+            characters.delete(index, index + count);
+
+            // TODO Fire event
+            // TODO Update selection state
+        }
+
+        public int getOffset() {
+            // TODO
+            return -1;
+        }
+
+        // TODO Add listener list accessor
+    }
+
+    /**
+     * Paragraph listener interface.
+     */
+    public interface ParagraphListener {
+        /**
+         * Paragraph listener interface adapter.
+         */
+        public static class Adapter implements ParagraphListener {
+            @Override
+            public void textInserted(Paragraph paragraph, int index, int count) {
+            }
+
+            @Override
+            public void textRemoved(Paragraph paragraph, int index, int count) {
+            }
+        }
+
+        /**
+         * Called when text has been inserted into a paragraph.
+         *
+         * @param paragraph
+         * The source of the event.
+         *
+         * @param index
+         * The index at which the text was inserted.
+         *
+         * @param count
+         * The number of characters that were inserted.
+         */
+        public void textInserted(Paragraph paragraph, int index, int count);
+
+        /**
+         * Called when characters have been removed from a paragraph.
+         *
+         * @param paragraph
+         * The source of the event.
+         *
+         * @param index
+         * The index from which the text was removed.
+         *
+         * @param count
+         * The number of characters that were removed.
+         */
+        public void textRemoved(Paragraph paragraph, int index, int count);
+    }
+
+    /**
+     * Enum representing a scroll direction.
+     */
+    public enum ScrollDirection {
+        UP,
+        DOWN
+    }
+
+    /**
      * Text area skin interface. Text area skins are required to implement
      * this.
      */
@@ -54,7 +147,7 @@ public class TextArea2 extends Component
          * @param from
          * @param direction
          */
-        public int getNextInsertionPoint(int x, int from, FocusTraversalDirection direction);
+        public int getNextInsertionPoint(int x, int from, ScrollDirection direction);
 
         /**
          * Returns the row index of the character at a given index.
@@ -97,6 +190,51 @@ public class TextArea2 extends Component
         public Object valueOf(String text);
     }
 
+    public final class ParagraphSequence implements Sequence<Paragraph>, Iterable<Paragraph> {
+        public int add(Paragraph paragraph) {
+            // TODO
+            return -1;
+        }
+
+        public void insert(Paragraph paragraph, int index) {
+            // TODO
+        }
+
+        public Paragraph update(int index, Paragraph paragraph) {
+            // TODO
+            return null;
+        }
+
+        public int remove(Paragraph paragraph){
+            // TODO
+            return -1;
+        }
+
+        public Sequence<Paragraph> remove(int index, int count) {
+            // TODO
+            return null;
+        }
+
+        public Paragraph get(int index) {
+            // TODO
+            return null;
+        }
+
+        public int indexOf(Paragraph paragraph) {
+            // TODO
+            return -1;
+        }
+
+        public int getLength() {
+            // TODO
+            return -1;
+        }
+
+        public Iterator<Paragraph> iterator() {
+            return new ImmutableIterator<Paragraph>(paragraphs.iterator());
+        }
+    }
+
     private static class TextAreaListenerList extends ListenerList<TextAreaListener2>
         implements TextAreaListener2 {
         @Override
@@ -117,16 +255,16 @@ public class TextArea2 extends Component
     private static class TextAreaContentListenerList extends ListenerList<TextAreaContentListener2>
         implements TextAreaContentListener2 {
         @Override
-        public void textInserted(TextArea2 textArea, int index, int count) {
+        public void paragraphInserted(TextArea2 textArea, int index) {
             for (TextAreaContentListener2 listener : this) {
-                listener.textInserted(textArea, index, count);
+                listener.paragraphInserted(textArea, index);
             }
         }
 
         @Override
-        public void textRemoved(TextArea2 textArea, int index, int count) {
+        public void paragraphsRemoved(TextArea2 textArea, int index, Sequence<TextArea2.Paragraph> removed) {
             for (TextAreaContentListener2 listener : this) {
-                listener.textRemoved(textArea, index, count);
+                listener.paragraphsRemoved(textArea, index, removed);
             }
         }
 
@@ -174,7 +312,9 @@ public class TextArea2 extends Component
         }
     }
 
-    private ArrayList<StringBuilder> paragraphs = null;
+    private ArrayList<Paragraph> paragraphs = new ArrayList<Paragraph>();
+    private ParagraphSequence paragraphSequence = new ParagraphSequence();
+
     private int characterCount = 0;
 
     private int selectionStart = 0;
@@ -195,8 +335,6 @@ public class TextArea2 extends Component
     private static final int INITIAL_PARAGRAPH_CAPACITY = 256;
 
     public TextArea2() {
-        setText("");
-
         installThemeSkin(TextArea2.class);
     }
 
@@ -241,36 +379,7 @@ public class TextArea2 extends Component
 
         StringBuilder textBuilder = new StringBuilder(endIndex - beginIndex);
 
-        // Determine the start of the range
-        int paragraphIndex = 0;
-        int characterOffset = 0;
-
-        StringBuilder paragraph = paragraphs.get(paragraphIndex);
-        int paragraphLength = paragraph.length();
-
-        while (characterOffset + paragraphLength < beginIndex) {
-            characterOffset += paragraphLength;
-
-            paragraph = paragraphs.get(++paragraphIndex);
-            paragraphLength = paragraph.length();
-        }
-
-        characterOffset = beginIndex - characterOffset;
-
-        // Walk from begin index to end index copying characters into text builder
-        for (int index = beginIndex; index < endIndex; index++) {
-            textBuilder.append(paragraph.charAt(characterOffset++));
-
-            if (characterOffset == paragraphLength) {
-                if (index != endIndex - 1) {
-                    textBuilder.append('\n');
-                }
-
-                paragraph = paragraphs.get(++paragraphIndex);
-                paragraphLength = paragraph.length();
-                characterOffset = 0;
-            }
-        }
+        // TODO
 
         return textBuilder.toString();
     }
@@ -318,26 +427,12 @@ public class TextArea2 extends Component
         }
 
         // Construct the paragraph list
-        paragraphs = new ArrayList<StringBuilder>();
+        paragraphs.clear();
         characterCount = 0;
 
-        StringBuilder paragraph = new StringBuilder(INITIAL_PARAGRAPH_CAPACITY);
-
-        int c = textReader.read();
-        while (c != -1) {
-            if (c == '\n') {
-                paragraphs.add(paragraph);
-                paragraph = new StringBuilder(INITIAL_PARAGRAPH_CAPACITY);
-            } else {
-                paragraph.append(c);
-            }
-
-            characterCount++;
-
-            c = textReader.read();
-        }
+        // TODO
 
-        paragraphs.add(paragraph);
+        // TODO set characterCount
 
         // Update selection
         int previousSelectionStart = selectionStart;
@@ -369,53 +464,7 @@ public class TextArea2 extends Component
         }
 
         if (text.length() > 0) {
-            // Determine the insertion point
-            int paragraphIndex, characterOffset;
-            if (index == characterCount) {
-                paragraphIndex = paragraphs.getLength() - 1;
-                characterOffset = paragraphs.get(paragraphIndex).length();
-            } else {
-                paragraphIndex = 0;
-                characterOffset = 0;
-
-                int paragraphCount = paragraphs.getLength();
-                while (paragraphIndex < paragraphCount) {
-                    StringBuilder paragraph = paragraphs.get(paragraphIndex);
-                    int paragraphLength = paragraph.length();
-
-                    if (characterOffset + paragraphLength > index) {
-                        break;
-                    } else {
-                        characterOffset += paragraphLength;
-                        paragraphIndex++;
-                    }
-                }
-
-                characterOffset = index - characterOffset;
-            }
-
-            // Insert the text
-            StringBuilder textBuilder = new StringBuilder();
-
-            for (int i = 0, n = text.length(); i < n; i++) {
-                char c = text.charAt(i);
-
-                if (c == '\n') {
-                    StringBuilder paragraph = paragraphs.get(paragraphIndex);
-                    paragraph.insert(characterOffset, textBuilder);
-
-                    characterOffset += textBuilder.length();
-                    textBuilder = new StringBuilder(paragraph.substring(characterOffset));
-                    paragraphs.insert(textBuilder, ++paragraphIndex);
-                } else {
-                    textBuilder.append(c);
-                }
-            }
-
-            StringBuilder paragraph = paragraphs.get(paragraphIndex);
-            paragraph.insert(characterOffset, textBuilder);
-
-            characterCount += text.length();
+            // TODO
 
             // Update selection
             int previousSelectionStart = selectionStart;
@@ -424,7 +473,6 @@ public class TextArea2 extends Component
             selectionLength = 0;
 
             // Fire change events
-            textAreaContentListeners.textInserted(this, previousSelectionStart, text.length());
             textAreaContentListeners.textChanged(this);
 
             if (selectionStart != previousSelectionStart
@@ -436,10 +484,7 @@ public class TextArea2 extends Component
 
     public void removeText(int index, int count) {
         if (count > 0) {
-            // TODO Identify begin paragraph index/offset
-            // TODO Identify end paragraph index/offset
-            // TODO Remove leading/trailing characters
-            // TODO Removing any intervening paragraphs
+            // TODO
 
             characterCount -= count;
 
@@ -450,7 +495,6 @@ public class TextArea2 extends Component
             selectionLength = 0;
 
             // Fire change events
-            textAreaContentListeners.textRemoved(this, selectionStart, count);
             textAreaContentListeners.textChanged(this);
 
             if (selectionStart != previousSelectionStart
@@ -461,19 +505,31 @@ public class TextArea2 extends Component
     }
 
     /**
-     * Returns a character sequence representing a paragraph's content.
+     * Returns the text area's paragraph sequence.
+     */
+    public ParagraphSequence getParagraphs() {
+        return paragraphSequence;
+    }
+
+    /**
+     * Returns the index of the paragraph containing a given character index.
      *
      * @param index
      */
-    public CharSequence getParagraph(int index) {
-        return paragraphs.get(index);
+    public int getParagraphAt(int index) {
+        // TODO Be sure to return paragraph corresponding to terminator character,
+        // including implicit final terminator
+        return -1;
     }
 
     /**
-     * Returns the number of paragraphs in the text area.
+     * Returns the character at a given index.
+     *
+     * @param index
      */
-    public int getParagraphCount() {
-        return paragraphs.getLength();
+    public char getCharacterAt(int index) {
+        // TODO
+        return 0x00;
     }
 
     /**
@@ -812,27 +868,27 @@ public class TextArea2 extends Component
     }
 
     public int getInsertionPoint(int x, int y) {
-        TextArea.Skin textAreaSkin = (TextArea.Skin)getSkin();
+        TextArea2.Skin textAreaSkin = (TextArea2.Skin)getSkin();
         return textAreaSkin.getInsertionPoint(x, y);
     }
 
-    public int getNextInsertionPoint(int x, int from, FocusTraversalDirection direction) {
-        TextArea.Skin textAreaSkin = (TextArea.Skin)getSkin();
+    public int getNextInsertionPoint(int x, int from, ScrollDirection direction) {
+        TextArea2.Skin textAreaSkin = (TextArea2.Skin)getSkin();
         return textAreaSkin.getNextInsertionPoint(x, from, direction);
     }
 
     public int getRowIndex(int index) {
-        TextArea.Skin textAreaSkin = (TextArea.Skin)getSkin();
+        TextArea2.Skin textAreaSkin = (TextArea2.Skin)getSkin();
         return textAreaSkin.getRowIndex(index);
     }
 
     public int getRowCount() {
-        TextArea.Skin textAreaSkin = (TextArea.Skin)getSkin();
+        TextArea2.Skin textAreaSkin = (TextArea2.Skin)getSkin();
         return textAreaSkin.getRowCount();
     }
 
     public Bounds getCharacterBounds(int index) {
-        TextArea.Skin textAreaSkin = (TextArea.Skin)getSkin();
+        TextArea2.Skin textAreaSkin = (TextArea2.Skin)getSkin();
         return textAreaSkin.getCharacterBounds(index);
     }
 

Modified: pivot/trunk/wtk/src/org/apache/pivot/wtk/TextAreaContentListener2.java
URL: http://svn.apache.org/viewvc/pivot/trunk/wtk/src/org/apache/pivot/wtk/TextAreaContentListener2.java?rev=992428&r1=992427&r2=992428&view=diff
==============================================================================
--- pivot/trunk/wtk/src/org/apache/pivot/wtk/TextAreaContentListener2.java (original)
+++ pivot/trunk/wtk/src/org/apache/pivot/wtk/TextAreaContentListener2.java Fri Sep  3 19:33:59 2010
@@ -16,6 +16,8 @@
  */
 package org.apache.pivot.wtk;
 
+import org.apache.pivot.collections.Sequence;
+
 /**
  * Text area text listener interface.
  */
@@ -25,11 +27,11 @@ public interface TextAreaContentListener
      */
     public static class Adapter implements TextAreaContentListener2 {
         @Override
-        public void textInserted(TextArea2 textArea, int index, int count) {
+        public void paragraphInserted(TextArea2 textArea, int index) {
         }
 
         @Override
-        public void textRemoved(TextArea2 textArea, int index, int count) {
+        public void paragraphsRemoved(TextArea2 textArea, int index, Sequence<TextArea2.Paragraph> removed) {
         }
 
         @Override
@@ -38,32 +40,31 @@ public interface TextAreaContentListener
     }
 
     /**
-     * Called when text has been inserted into a text area.
+     * Called when a paragraph has been inserted into a text area's paragraph
+     * sequence.
      *
      * @param textArea
      * The source of the event.
      *
      * @param index
-     * The index at which the text was inserted.
-     *
-     * @param count
-     * The number of characters that were inserted.
+     * The index at which the paragraph was inserted.
      */
-    public void textInserted(TextArea2 textArea, int index, int count);
+    public void paragraphInserted(TextArea2 textArea, int index);
 
     /**
-     * Called when characters have been removed from a text area.
+     * Called when paragraphs have been removed from a text area's paragraph
+     * sequence.
      *
      * @param textArea
      * The source of the event.
      *
      * @param index
-     * The index from which the text was removed.
+     * The starting index from which the paragraphs were removed.
      *
-     * @param count
-     * The number of characters that were removed.
+     * @param removed
+     * The paragraphs that were removed.
      */
-    public void textRemoved(TextArea2 textArea, int index, int count);
+    public void paragraphsRemoved(TextArea2 textArea, int index, Sequence<TextArea2.Paragraph> removed);
 
     /**
      * Called when a text area's text has changed.

Modified: pivot/trunk/wtk/src/org/apache/pivot/wtk/skin/TextAreaSkin2.java
URL: http://svn.apache.org/viewvc/pivot/trunk/wtk/src/org/apache/pivot/wtk/skin/TextAreaSkin2.java?rev=992428&r1=992427&r2=992428&view=diff
==============================================================================
--- pivot/trunk/wtk/src/org/apache/pivot/wtk/skin/TextAreaSkin2.java (original)
+++ pivot/trunk/wtk/src/org/apache/pivot/wtk/skin/TextAreaSkin2.java Fri Sep  3 19:33:59 2010
@@ -19,26 +19,194 @@ package org.apache.pivot.wtk.skin;
 import java.awt.Color;
 import java.awt.Font;
 import java.awt.Graphics2D;
+import java.awt.Rectangle;
 import java.awt.Transparency;
+import java.awt.font.GlyphVector;
+import java.awt.geom.Area;
 
+import org.apache.pivot.collections.ArrayList;
 import org.apache.pivot.collections.Dictionary;
+import org.apache.pivot.collections.Sequence;
+import org.apache.pivot.wtk.ApplicationContext;
 import org.apache.pivot.wtk.Bounds;
 import org.apache.pivot.wtk.Component;
 import org.apache.pivot.wtk.Dimensions;
-import org.apache.pivot.wtk.FocusTraversalDirection;
 import org.apache.pivot.wtk.GraphicsUtilities;
 import org.apache.pivot.wtk.Insets;
+import org.apache.pivot.wtk.Keyboard;
+import org.apache.pivot.wtk.Mouse;
+import org.apache.pivot.wtk.Platform;
 import org.apache.pivot.wtk.TextArea2;
 import org.apache.pivot.wtk.TextAreaListener2;
 import org.apache.pivot.wtk.TextAreaContentListener2;
 import org.apache.pivot.wtk.TextAreaSelectionListener2;
 import org.apache.pivot.wtk.Theme;
+import org.apache.pivot.wtk.Visual;
 
 /**
  * Text area skin.
  */
 public class TextAreaSkin2 extends ComponentSkin implements TextArea2.Skin,
     TextAreaListener2, TextAreaContentListener2, TextAreaSelectionListener2 {
+    /**
+     * Paragraph view.
+     */
+    public class ParagraphView implements Visual, TextArea2.ParagraphListener {
+        private TextArea2.Paragraph paragraph;
+
+        private int width = 0;
+        private int height = 0;
+
+        private int breakWidth = Integer.MAX_VALUE;
+
+        private boolean valid = false;
+        private ArrayList<GlyphVector> rows = new ArrayList<GlyphVector>();
+
+        public ParagraphView(TextArea2.Paragraph paragraph) {
+            this.paragraph = paragraph;
+        }
+
+        public TextArea2.Paragraph getParagraph() {
+            return paragraph;
+        }
+
+        @Override
+        public int getWidth() {
+            validate();
+            return width;
+        }
+
+        @Override
+        public int getHeight() {
+            validate();
+            return height;
+        }
+
+        public int getBreakWidth() {
+            return breakWidth;
+        }
+
+        public void setBreakWidth(int breakWidth) {
+            int previousBreakWidth = this.breakWidth;
+            this.breakWidth = breakWidth;
+
+            if (previousBreakWidth > breakWidth) {
+                invalidate();
+            }
+        }
+
+        @Override
+        public int getBaseline() {
+            return -1;
+        }
+
+        @Override
+        public void paint(Graphics2D graphics) {
+            // Iterate over rows and paint each
+            // TODO Only paint visible glyphs
+            for (GlyphVector line : rows) {
+                // TODO
+                graphics.drawGlyphVector(line, 0, 0);
+            }
+        }
+
+        public void invalidate() {
+            valid = false;
+        }
+
+        public void validate() {
+            if (!valid) {
+                // TODO Re-layout glyphs and recalculate size
+                rows = new ArrayList<GlyphVector>();
+                width = 0;
+                height = 0;
+            }
+
+            valid = true;
+        }
+
+        @Override
+        public void textInserted(TextArea2.Paragraph paragraph, int index, int count) {
+            invalidate();
+            invalidateComponent();
+        }
+
+        @Override
+        public void textRemoved(TextArea2.Paragraph paragraph, int index, int count) {
+            invalidate();
+            invalidateComponent();
+        }
+    }
+
+    private class BlinkCaretCallback implements Runnable {
+        @Override
+        public void run() {
+            caretOn = !caretOn;
+
+            if (selection == null) {
+                TextArea2 textArea = (TextArea2)getComponent();
+                textArea.repaint(caret.x, caret.y, caret.width, caret.height, true);
+            }
+        }
+    }
+
+    private class ScrollSelectionCallback implements Runnable {
+        @Override
+        public void run() {
+            TextArea2 textArea = (TextArea2)getComponent();
+            int selectionStart = textArea.getSelectionStart();
+            int selectionLength = textArea.getSelectionLength();
+            int selectionEnd = selectionStart + selectionLength - 1;
+
+            switch (scrollDirection) {
+                case UP: {
+                    // Get previous offset
+                    int offset = getNextInsertionPoint(mouseX, selectionStart, scrollDirection);
+
+                    if (offset != -1) {
+                        textArea.setSelection(offset, selectionEnd - offset + 1);
+                        scrollCharacterToVisible(offset + 1);
+                    }
+
+                    break;
+                }
+
+                case DOWN: {
+                    // Get next offset
+                    int offset = getNextInsertionPoint(mouseX, selectionEnd, scrollDirection);
+
+                    if (offset != -1) {
+                        // If the next character is a paragraph terminator, increment the selection
+                        if (textArea.getCharacterAt(offset) == '\n') {
+                            offset++;
+                        }
+
+                        textArea.setSelection(selectionStart, offset - selectionStart);
+                        scrollCharacterToVisible(offset - 1);
+                    }
+
+                    break;
+                }
+            }
+        }
+    }
+
+    private int caretX = 0;
+    private Rectangle caret = new Rectangle();
+    private Area selection = null;
+
+    private boolean caretOn = false;
+
+    private int anchor = -1;
+    private TextArea2.ScrollDirection scrollDirection = null;
+    private int mouseX = -1;
+
+    private BlinkCaretCallback blinkCaretCallback = new BlinkCaretCallback();
+    private ApplicationContext.ScheduledCallback scheduledBlinkCaretCallback = null;
+
+    private ScrollSelectionCallback scrollSelectionCallback = new ScrollSelectionCallback();
+    private ApplicationContext.ScheduledCallback scheduledScrollSelectionCallback = null;
+
     private Font font;
     private Color color;
     private Color backgroundColor;
@@ -47,11 +215,11 @@ public class TextAreaSkin2 extends Compo
     private Color selectionBackgroundColor;
     private Color inactiveSelectionColor;
     private Color inactiveSelectionBackgroundColor;
-
     private Insets margin = new Insets(4);
-
     private boolean wrapText = true;
 
+    private ArrayList<ParagraphView> paragraphViews = new ArrayList<ParagraphView>();
+
     public TextAreaSkin2() {
         Theme theme = Theme.getTheme();
         font = theme.getFont();
@@ -68,35 +236,77 @@ public class TextAreaSkin2 extends Compo
     public void install(Component component) {
         super.install(component);
 
-        // TODO
+        TextArea2 textArea = (TextArea2)component;
+        textArea.getTextAreaListeners().add(this);
+        textArea.getTextAreaContentListeners().add(this);
+        textArea.getTextAreaSelectionListeners().add(this);
     }
 
     @Override
     public int getPreferredWidth(int height) {
-        // TODO
-        return 0;
+        int preferredWidth = 0;
+
+        for (ParagraphView paragraphView : paragraphViews) {
+            paragraphView.setBreakWidth(Integer.MAX_VALUE);
+            preferredWidth = Math.max(preferredWidth, paragraphView.getWidth());
+        }
+
+        preferredWidth += margin.left + margin.right;
+
+        return preferredWidth;
     }
 
     @Override
     public int getPreferredHeight(int width) {
-        // TODO
-        return 0;
+        int preferredHeight = 0;
+
+        // Include margin in constraint
+        int breakWidth = (wrapText
+            && width != -1) ? Math.max(width - (margin.left + margin.right), 0) : Integer.MAX_VALUE;
+
+        for (ParagraphView paragraphView : paragraphViews) {
+            paragraphView.setBreakWidth(breakWidth);
+            preferredHeight += paragraphView.getHeight();
+        }
+
+        preferredHeight += margin.top + margin.bottom;
+
+        return preferredHeight;
     }
 
     @Override
     public Dimensions getPreferredSize() {
-        // TODO
-        return new Dimensions(0, 0);
+        int preferredWidth = 0;
+        int preferredHeight = 0;
+
+        for (ParagraphView paragraphView : paragraphViews) {
+            paragraphView.setBreakWidth(Integer.MAX_VALUE);
+            preferredWidth = Math.max(preferredWidth, paragraphView.getWidth());
+            preferredHeight += paragraphView.getHeight();
+        }
+
+        preferredWidth += margin.left + margin.right;
+        preferredHeight += margin.top + margin.bottom;
+
+        return new Dimensions(preferredWidth, preferredHeight);
     }
 
     @Override
     public void layout() {
-        // TODO
+        int width = getWidth();
+
+        for (ParagraphView paragraphView : paragraphViews) {
+            paragraphView.setBreakWidth(width);
+
+            // TODO Set location
+        }
     }
 
     @Override
     public void paint(Graphics2D graphics) {
-        // TODO
+        // TODO Paint selection state
+        // TODO Walk paragraph view list and translate graphics/paint each view (only
+        // paint visible views)
     }
 
     @Override
@@ -110,7 +320,7 @@ public class TextAreaSkin2 extends Compo
         return -1;
     }
 
-    public int getNextInsertionPoint(int x, int from, FocusTraversalDirection direction) {
+    public int getNextInsertionPoint(int x, int from, TextArea2.ScrollDirection direction) {
         // TODO
         return -1;
     }
@@ -126,10 +336,20 @@ public class TextAreaSkin2 extends Compo
     }
 
     public Bounds getCharacterBounds(int index) {
-        // TODO
+        // TODO Check for paragraph terminators including implicit final terminator
         return null;
     }
 
+    private void scrollCharacterToVisible(int offset) {
+        TextArea2 textArea = (TextArea2)getComponent();
+        Bounds characterBounds = getCharacterBounds(offset);
+
+        if (characterBounds != null) {
+            textArea.scrollAreaToVisible(characterBounds.x, characterBounds.y,
+                characterBounds.width, characterBounds.height);
+        }
+    }
+
     public Font getFont() {
         return font;
     }
@@ -353,26 +573,93 @@ public class TextAreaSkin2 extends Compo
     }
 
     @Override
-    public void maximumLengthChanged(TextArea2 textArea, int previousMaximumLength) {
+    public boolean mouseMove(Component component, int x, int y) {
+        boolean consumed = super.mouseMove(component, x, y);
+
         // TODO
+
+        return consumed;
     }
 
     @Override
-    public void editableChanged(TextArea2 textArea) {
+    public boolean mouseDown(Component component, Mouse.Button button, int x, int y) {
+        boolean consumed = super.mouseDown(component, button, x, y);
+
+        // TODO
+
+        return consumed;
+    }
+
+    @Override
+    public boolean mouseUp(Component component, Mouse.Button button, int x, int y) {
+        boolean consumed = super.mouseUp(component, button, x, y);
+
+        // TODO
+
+        return consumed;
+    }
+
+    @Override
+    public boolean keyTyped(Component component, char character) {
+        boolean consumed = super.keyTyped(component, character);
+
+        // TODO
+
+        return consumed;
+    }
+
+    @Override
+    public boolean keyPressed(Component component, int keyCode, Keyboard.KeyLocation keyLocation) {
+        boolean consumed = super.keyPressed(component, keyCode, keyLocation);
+
         // TODO
+
+        return consumed;
+    }
+
+    @Override
+    public void enabledChanged(Component component) {
+        super.enabledChanged(component);
+        repaintComponent();
+    }
+
+    @Override
+    public void focusedChanged(Component component, Component obverseComponent) {
+        super.focusedChanged(component, obverseComponent);
+
+        TextArea2 textArea = (TextArea2)getComponent();
+        if (textArea.isFocused()
+            && textArea.getSelectionLength() == 0) {
+            scrollCharacterToVisible(textArea.getSelectionStart());
+            showCaret(true);
+        } else {
+            showCaret(false);
+        }
+
+        repaintComponent();
     }
 
     @Override
-    public void textInserted(TextArea2 textArea, int index, int count) {
+    public void maximumLengthChanged(TextArea2 textArea, int previousMaximumLength) {
         // TODO
     }
 
     @Override
-    public void textRemoved(TextArea2 textArea, int index, int count) {
+    public void editableChanged(TextArea2 textArea) {
         // TODO
     }
 
     @Override
+    public void paragraphInserted(TextArea2 textArea, int index) {
+        // TODO Create paragraph view and add as paragraph listener
+    }
+
+    @Override
+    public void paragraphsRemoved(TextArea2 textArea, int index, Sequence<TextArea2.Paragraph> removed) {
+        // TODO Remove paragraph views and remove as paragraph listeners
+    }
+
+    @Override
     public void textChanged(TextArea2 textArea) {
         // TODO
     }
@@ -382,4 +669,78 @@ public class TextAreaSkin2 extends Compo
         int previousSelectionLength) {
         // TODO
     }
+
+    private void updateSelection() {
+        TextArea2 textArea = (TextArea2)getComponent();
+
+        if (textArea.getCharacterCount() > 0) {
+            // Update the caret
+            int selectionStart = textArea.getSelectionStart();
+
+            Bounds leadingSelectionBounds = getCharacterBounds(selectionStart);
+            caret = leadingSelectionBounds.toRectangle();
+            caret.width = 1;
+
+            // Update the selection
+            int selectionLength = textArea.getSelectionLength();
+
+            if (selectionLength > 0) {
+                int selectionEnd = selectionStart + selectionLength - 1;
+                Bounds trailingSelectionBounds = getCharacterBounds(selectionEnd);
+                selection = new Area();
+
+                int firstRowIndex = getRowIndex(selectionStart);
+                int lastRowIndex = getRowIndex(selectionEnd);
+
+                if (firstRowIndex == lastRowIndex) {
+                    selection.add(new Area(new Rectangle(leadingSelectionBounds.x, leadingSelectionBounds.y,
+                        trailingSelectionBounds.x + trailingSelectionBounds.width - leadingSelectionBounds.x,
+                        trailingSelectionBounds.y + trailingSelectionBounds.height - leadingSelectionBounds.y)));
+                } else {
+                    int width = getWidth();
+
+                    selection.add(new Area(new Rectangle(leadingSelectionBounds.x,
+                        leadingSelectionBounds.y,
+                        width - margin.right - leadingSelectionBounds.x,
+                        leadingSelectionBounds.height)));
+
+                    if (lastRowIndex - firstRowIndex > 0) {
+                        selection.add(new Area(new Rectangle(margin.left,
+                            leadingSelectionBounds.y + leadingSelectionBounds.height,
+                            width - (margin.left + margin.right),
+                            trailingSelectionBounds.y - (leadingSelectionBounds.y
+                                + leadingSelectionBounds.height))));
+                    }
+
+                    selection.add(new Area(new Rectangle(margin.left, trailingSelectionBounds.y,
+                        trailingSelectionBounds.x + trailingSelectionBounds.width - margin.left,
+                        trailingSelectionBounds.height)));
+                }
+            } else {
+                selection = null;
+            }
+        } else {
+            // Clear the caret and the selection
+            caret = new Rectangle();
+            selection = null;
+        }
+    }
+
+    private void showCaret(boolean show) {
+        if (scheduledBlinkCaretCallback != null) {
+            scheduledBlinkCaretCallback.cancel();
+        }
+
+        if (show) {
+            caretOn = true;
+            scheduledBlinkCaretCallback =
+                ApplicationContext.scheduleRecurringCallback(blinkCaretCallback,
+                    Platform.getCursorBlinkRate());
+
+            // Run the callback once now to show the cursor immediately
+            blinkCaretCallback.run();
+        } else {
+            scheduledBlinkCaretCallback = null;
+        }
+    }
 }