You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@pivot.apache.org by no...@apache.org on 2010/07/02 11:08:55 UTC

svn commit: r959922 [1/2] - /pivot/trunk/wtk/src/org/apache/pivot/wtk/skin/

Author: noelgrandin
Date: Fri Jul  2 09:08:54 2010
New Revision: 959922

URL: http://svn.apache.org/viewvc?rev=959922&view=rev
Log:
move the NodeView classes in TextAreaSkin to their own top-level files

Added:
    pivot/trunk/wtk/src/org/apache/pivot/wtk/skin/TextAreaSkinComponentNodeView.java
    pivot/trunk/wtk/src/org/apache/pivot/wtk/skin/TextAreaSkinDocumentView.java
    pivot/trunk/wtk/src/org/apache/pivot/wtk/skin/TextAreaSkinElementView.java
    pivot/trunk/wtk/src/org/apache/pivot/wtk/skin/TextAreaSkinImageNodeView.java
    pivot/trunk/wtk/src/org/apache/pivot/wtk/skin/TextAreaSkinNodeView.java
    pivot/trunk/wtk/src/org/apache/pivot/wtk/skin/TextAreaSkinParagraphView.java
    pivot/trunk/wtk/src/org/apache/pivot/wtk/skin/TextAreaSkinSpanView.java
    pivot/trunk/wtk/src/org/apache/pivot/wtk/skin/TextAreaSkinTextNodeView.java
Modified:
    pivot/trunk/wtk/src/org/apache/pivot/wtk/skin/TextAreaSkin.java

Modified: pivot/trunk/wtk/src/org/apache/pivot/wtk/skin/TextAreaSkin.java
URL: http://svn.apache.org/viewvc/pivot/trunk/wtk/src/org/apache/pivot/wtk/skin/TextAreaSkin.java?rev=959922&r1=959921&r2=959922&view=diff
==============================================================================
--- pivot/trunk/wtk/src/org/apache/pivot/wtk/skin/TextAreaSkin.java (original)
+++ pivot/trunk/wtk/src/org/apache/pivot/wtk/skin/TextAreaSkin.java Fri Jul  2 09:08:54 2010
@@ -20,23 +20,14 @@ import java.awt.Color;
 import java.awt.Font;
 import java.awt.Graphics2D;
 import java.awt.Rectangle;
-import java.awt.Shape;
 import java.awt.font.FontRenderContext;
-import java.awt.font.GlyphVector;
 import java.awt.font.LineMetrics;
 import java.awt.geom.Area;
-import java.awt.geom.Rectangle2D;
-import java.text.CharacterIterator;
-import java.util.Iterator;
 
-import org.apache.pivot.collections.ArrayList;
 import org.apache.pivot.collections.Dictionary;
-import org.apache.pivot.collections.Sequence;
-import org.apache.pivot.util.ImmutableIterator;
 import org.apache.pivot.wtk.ApplicationContext;
 import org.apache.pivot.wtk.Bounds;
 import org.apache.pivot.wtk.Component;
-import org.apache.pivot.wtk.ComponentListener;
 import org.apache.pivot.wtk.Cursor;
 import org.apache.pivot.wtk.Dimensions;
 import org.apache.pivot.wtk.FocusTraversalDirection;
@@ -46,1671 +37,22 @@ import org.apache.pivot.wtk.Keyboard;
 import org.apache.pivot.wtk.Mouse;
 import org.apache.pivot.wtk.Orientation;
 import org.apache.pivot.wtk.Platform;
-import org.apache.pivot.wtk.Point;
-import org.apache.pivot.wtk.Span;
 import org.apache.pivot.wtk.TextArea;
 import org.apache.pivot.wtk.TextAreaListener;
 import org.apache.pivot.wtk.TextAreaSelectionListener;
 import org.apache.pivot.wtk.Theme;
-import org.apache.pivot.wtk.Visual;
-import org.apache.pivot.wtk.media.Image;
-import org.apache.pivot.wtk.media.ImageListener;
 import org.apache.pivot.wtk.text.ComponentNode;
-import org.apache.pivot.wtk.text.ComponentNodeListener;
 import org.apache.pivot.wtk.text.Document;
-import org.apache.pivot.wtk.text.Element;
-import org.apache.pivot.wtk.text.ElementListener;
 import org.apache.pivot.wtk.text.ImageNode;
-import org.apache.pivot.wtk.text.ImageNodeListener;
 import org.apache.pivot.wtk.text.Node;
-import org.apache.pivot.wtk.text.NodeListener;
 import org.apache.pivot.wtk.text.Paragraph;
 import org.apache.pivot.wtk.text.TextNode;
-import org.apache.pivot.wtk.text.TextNodeListener;
 
 /**
  * Text area skin.
  */
 public class TextAreaSkin extends ContainerSkin implements TextArea.Skin, TextAreaListener,
     TextAreaSelectionListener {
-    /**
-     * Abstract base class for node views.
-     */
-    public abstract class NodeView implements Visual, NodeListener {
-        private Node node = null;
-        private ElementView parent = null;
-
-        private int width = 0;
-        private int height = 0;
-        private int x = 0;
-        private int y = 0;
-
-        private int breakWidth = -1;
-
-        private boolean valid = false;
-
-        public NodeView(Node node) {
-            this.node = node;
-        }
-
-        public Node getNode() {
-            return node;
-        }
-
-        public ElementView getParent() {
-            return parent;
-        }
-
-        protected void setParent(ElementView parent) {
-            this.parent = parent;
-        }
-
-        protected void attach() {
-            node.getNodeListeners().add(this);
-        }
-
-        protected void detach() {
-            node.getNodeListeners().remove(this);
-        }
-
-        @Override
-        public int getWidth() {
-            return width;
-        }
-
-        @Override
-        public int getHeight() {
-            return height;
-        }
-
-        @Override
-        public int getBaseline() {
-            return -1;
-        }
-
-        public Dimensions getSize() {
-            return new Dimensions(width, height);
-        }
-
-        protected void setSize(int width, int height) {
-            assert(width >= 0);
-            assert(height >= 0);
-
-            // Redraw the region formerly occupied by this view
-            repaint();
-
-            this.width = width;
-            this.height = height;
-
-            // Redraw the region currently occupied by this view
-            repaint();
-        }
-
-        public int getX() {
-            return x;
-        }
-
-        public int getY() {
-            return y;
-        }
-
-        public Point getLocation() {
-            return new Point(x, y);
-        }
-
-        protected void setLocation(int x, int y) {
-            // Redraw the region formerly occupied by this view
-            repaint();
-
-            this.x = x;
-            this.y = y;
-
-            // Redraw the region currently occupied by this view
-            repaint();
-        }
-
-        /**
-         * This is needed by the ComponentViewNode to correctly position child Component's.
-         *
-         * @param skinX the X coordinate in the skin's frame of reference
-         * @param skinY the Y coordinate in the skin's frame of reference
-         */
-        protected abstract void setSkinLocation(int skinX, int skinY);
-
-        public Bounds getBounds() {
-            return new Bounds(x, y, width, height);
-        }
-
-        public void repaint() {
-            repaint(0, 0, width, height);
-        }
-
-        public void repaint(int x, int y, int width, int height) {
-            assert(width >= 0);
-            assert(height >= 0);
-
-            if (parent != null) {
-                parent.repaint(x + this.x, y + this.y, width, height);
-            }
-        }
-
-        public boolean isValid() {
-            return valid;
-        }
-
-        public void invalidate() {
-            valid = false;
-
-            if (parent != null) {
-                parent.invalidate();
-            }
-        }
-
-        public void validate() {
-            valid = true;
-        }
-
-        public int getBreakWidth() {
-            return breakWidth;
-        }
-
-        public void setBreakWidth(int breakWidth) {
-            int previousBreakWidth = this.breakWidth;
-
-            if (previousBreakWidth != breakWidth) {
-                this.breakWidth = breakWidth;
-
-                // NOTE We can't call invalidate() here because it would ultimately
-                // trigger a call to invalidateComponent(), which we don't want; this method
-                // is called during preferred size calculations as well as layout, neither
-                // of which should ever trigger an invalidate.
-                valid = false;
-            }
-        }
-
-        public int getOffset() {
-            return node.getOffset();
-        }
-
-        public int getDocumentOffset() {
-            return (parent == null) ? 0 : parent.getDocumentOffset() + getOffset();
-        }
-
-        public int getCharacterCount() {
-            return node.getCharacterCount();
-        }
-
-        public abstract NodeView getNext();
-        public abstract int getInsertionPoint(int x, int y);
-        public abstract int getNextInsertionPoint(int x, int from, FocusTraversalDirection direction);
-        public abstract int getRowIndex(int offset);
-        public abstract int getRowCount();
-        public abstract Bounds getCharacterBounds(int offset);
-
-        @Override
-        public void parentChanged(Node node, Element previousParent) {
-            // No-op
-        }
-
-        @Override
-        public void offsetChanged(Node node, int previousOffset) {
-            // No-op
-        }
-
-        @Override
-        public void rangeInserted(Node node, int offset, int span) {
-            // No-op
-        }
-
-        @Override
-        public void rangeRemoved(Node node, int offset, int characterCount) {
-            // No-op
-        }
-
-        @Override
-        public void nodesRemoved(Node node, Sequence<Node> removed, int offset) {
-            // No-op
-        }
-
-        @Override
-        public void nodeInserted(Node node, int offset) {
-            // No-op
-        }
-    }
-
-    /**
-     * Abstract base class for element views.
-     */
-    public abstract class ElementView extends NodeView
-        implements Sequence<NodeView>, Iterable<NodeView>, ElementListener {
-        private ArrayList<NodeView> nodeViews = new ArrayList<NodeView>();
-
-        public ElementView(Element element) {
-            super(element);
-        }
-
-        @Override
-        protected void attach() {
-            super.attach();
-
-            Element element = (Element)getNode();
-            element.getElementListeners().add(this);
-
-            // NOTE We don't attach child views here because this may not
-            // be efficient for all subclasses (e.g. paragraph views need to
-            // recreate child views when breaking across multiple lines)
-        }
-
-        @Override
-        protected void detach() {
-            Element element = (Element)getNode();
-            element.getElementListeners().remove(this);
-
-            // Detach child node views
-            for (NodeView nodeView : this) {
-                nodeView.detach();
-            }
-
-            super.detach();
-        }
-
-        @Override
-        public int add(NodeView nodeView) {
-            int index = getLength();
-            insert(nodeView, index);
-
-            return index;
-        }
-
-        @Override
-        public void insert(NodeView nodeView, int index) {
-            nodeView.setParent(this);
-            nodeView.attach();
-
-            nodeViews.insert(nodeView, index);
-        }
-
-        @Override
-        public NodeView update(int index, NodeView nodeView) {
-            throw new UnsupportedOperationException();
-        }
-
-        @Override
-        public int remove(NodeView nodeView) {
-            int index = indexOf(nodeView);
-            if (index != -1) {
-                remove(index, 1);
-            }
-
-            return index;
-        }
-
-        @Override
-        public Sequence<NodeView> remove(int index, int count) {
-            Sequence<NodeView> removed = nodeViews.remove(index, count);
-
-            for (int i = 0, n = removed.getLength(); i < n; i++) {
-                NodeView nodeView = removed.get(i);
-                nodeView.setParent(null);
-                nodeView.detach();
-            }
-
-            return removed;
-        }
-
-        @Override
-        public NodeView get(int index) {
-            return nodeViews.get(index);
-        }
-
-        @Override
-        public int indexOf(NodeView nodeView) {
-            return nodeViews.indexOf(nodeView);
-        }
-
-        @Override
-        public int getLength() {
-            return nodeViews.getLength();
-        }
-
-        @Override
-        public void paint(Graphics2D graphics) {
-            // Determine the paint bounds
-            Bounds paintBounds = new Bounds(0, 0, getWidth(), getHeight());
-            Rectangle clipBounds = graphics.getClipBounds();
-            if (clipBounds != null) {
-                paintBounds = paintBounds.intersect(new Bounds(clipBounds));
-            }
-
-            for (NodeView nodeView : nodeViews) {
-                Bounds nodeViewBounds = nodeView.getBounds();
-
-                // Only paint node views that intersect the current clip rectangle
-                if (nodeViewBounds.intersects(paintBounds)) {
-                    // Create a copy of the current graphics context and
-                    // translate to the node view's coordinate system
-                    Graphics2D nodeViewGraphics = (Graphics2D)graphics.create();
-                    nodeViewGraphics.translate(nodeViewBounds.x, nodeViewBounds.y);
-
-                    Color styledBackgroundColor = getStyledBackgroundColor();
-                    if (styledBackgroundColor != null) {
-                        nodeViewGraphics.setColor(styledBackgroundColor);
-                        nodeViewGraphics.fillRect(nodeViewBounds.x, nodeViewBounds.y, nodeViewBounds.width, nodeViewBounds.height);
-                    }
-
-                    // NOTE We don't clip here because views should generally
-                    // not overlap and clipping would impose an unnecessary
-                    // performance penalty
-
-                    // Paint the node view
-                    nodeView.paint(nodeViewGraphics);
-
-                    // Dispose of the node views's graphics
-                    nodeViewGraphics.dispose();
-                }
-            }
-        }
-
-        private Color getStyledBackgroundColor() {
-            Color backgroundColor = null;
-            Node node = getNode();
-            // run up the tree until we find a Element's style to apply
-            while (node != null) {
-                if (node instanceof Element) {
-                    backgroundColor = ((Element) node).getBackgroundColor();
-                    if (backgroundColor != null) {
-                        break;
-                    }
-                }
-                node = node.getParent();
-            }
-            return backgroundColor;
-        }
-
-        @Override
-        public Bounds getCharacterBounds(int offset) {
-            Bounds characterBounds = null;
-
-            for (int i = 0, n = nodeViews.getLength(); i < n; i++) {
-                NodeView nodeView = nodeViews.get(i);
-                int nodeViewOffset = nodeView.getOffset();
-                int characterCount = nodeView.getCharacterCount();
-
-                if (offset >= nodeViewOffset
-                    && offset < nodeViewOffset + characterCount) {
-                    characterBounds = nodeView.getCharacterBounds(offset - nodeViewOffset);
-
-                    if (characterBounds != null) {
-                        characterBounds = characterBounds.translate(nodeView.getX(), nodeView.getY());
-                    }
-
-                    break;
-                }
-            }
-
-            if (characterBounds != null) {
-                characterBounds = characterBounds.intersect(0, 0, getWidth(), getHeight());
-            }
-
-            return characterBounds;
-        }
-
-        @Override
-        public void nodeInserted(Element element, int index) {
-            invalidate();
-        }
-
-        @Override
-        public void nodesRemoved(Element element, int index, Sequence<Node> nodes) {
-            invalidate();
-        }
-
-        @Override
-        public void fontChanged(Element element, Font previousFont) {
-            invalidate();
-        }
-
-        @Override
-        public void backgroundColorChanged(Element element, Color previousBackgroundColor) {
-            invalidate();
-        }
-
-        @Override
-        public void foregroundColorChanged(Element element, Color previousForegroundColor) {
-            invalidate();
-        }
-
-        @Override
-        public Iterator<NodeView> iterator() {
-            return new ImmutableIterator<NodeView>(nodeViews.iterator());
-        }
-    }
-
-    /**
-     * Document view.
-     */
-    public class DocumentView extends ElementView {
-        public DocumentView(Document document) {
-            super(document);
-        }
-
-        @Override
-        public void attach() {
-            super.attach();
-
-            // Attach child node views
-            Document document = (Document)getNode();
-            for (Node node : document) {
-                add(createNodeView(node));
-            }
-        }
-
-        @Override
-        public void repaint(int x, int y, int width, int height) {
-            super.repaint(x, y, width, height);
-
-            repaintComponent(x, y, width, height);
-        }
-
-        @Override
-        public void invalidate() {
-            super.invalidate();
-            invalidateComponent();
-        }
-
-        @Override
-        public void validate() {
-            // TODO At some point, we may want to optimize this method by deferring layout of
-            // non-visible views. If so, we should not recycle views but rather recreate them
-            // (as is done in ParagraphView). This way, we avoid thread contention over the
-            // existing views (e.g. trying to paint one while modifying its size/location, etc.).
-            // Any invalid node views are simply replaced (in the queued callback, when the
-            // thread has finished processing the new ones). This allows the definition of
-            // validate() to remain as-is. Of course, if we redefine NodeView to implement
-            // ConstrainedVisual, this may no longer be an issue.
-            // Note that, if anything happens to invalidate the existence of the new views before
-            // they are added to the document view, we need to make sure they are disposed (i.e.
-            // detached).
-
-            if (!isValid()) {
-                int breakWidth = getBreakWidth();
-
-                int width = 0;
-                int height = 0;
-
-                int i = 0;
-                int n = getLength();
-
-                while (i < n) {
-                    NodeView nodeView = get(i++);
-                    nodeView.setBreakWidth(breakWidth);
-                    nodeView.validate();
-
-                    nodeView.setLocation(0, height);
-
-                    width = Math.max(width, nodeView.getWidth());
-                    height += nodeView.getHeight();
-                }
-
-                setSize(width, height);
-
-                super.validate();
-            }
-        }
-
-        @Override
-        protected void setSkinLocation(int skinX, int skinY) {
-            for (NodeView nodeView : this) {
-                nodeView.setSkinLocation(skinX, skinY + nodeView.getY());
-            }
-        }
-
-        @Override
-        public int getInsertionPoint(int x, int y) {
-            int offset = -1;
-
-            for (int i = 0, n = getLength(); i < n; i++) {
-                NodeView nodeView = get(i);
-                Bounds nodeViewBounds = nodeView.getBounds();
-
-                if (y >= nodeViewBounds.y
-                    && y < nodeViewBounds.y + nodeViewBounds.height) {
-                    offset = nodeView.getInsertionPoint(x - nodeView.getX(), y - nodeView.getY())
-                        + nodeView.getOffset();
-                    break;
-                }
-            }
-
-            return offset;
-        }
-
-        @Override
-        public int getNextInsertionPoint(int x, int from, FocusTraversalDirection direction) {
-            int offset = -1;
-
-            if (getLength() > 0) {
-                if (from == -1) {
-                    int i = (direction == FocusTraversalDirection.FORWARD) ? 0 : getLength() - 1;
-                    NodeView nodeView = get(i);
-                    offset = nodeView.getNextInsertionPoint(x - nodeView.getX(), -1, direction);
-
-                    if (offset != -1) {
-                        offset += nodeView.getOffset();
-                    }
-                } else {
-                    // Find the node view that contains the offset
-                    int n = getLength();
-                    int i = 0;
-
-                    while (i < n) {
-                        NodeView nodeView = get(i);
-                        int nodeViewOffset = nodeView.getOffset();
-                        int characterCount = nodeView.getCharacterCount();
-
-                        if (from >= nodeViewOffset
-                            && from < nodeViewOffset + characterCount) {
-                            break;
-                        }
-
-                        i++;
-                    }
-
-                    if (i < n) {
-                        NodeView nodeView = get(i);
-                        offset = nodeView.getNextInsertionPoint(x - nodeView.getX(),
-                            from - nodeView.getOffset(), direction);
-
-                        if (offset == -1) {
-                            // Move to the next or previous node view
-                            if (direction == FocusTraversalDirection.FORWARD) {
-                                nodeView = (i < n - 1) ? get(i + 1) : null;
-                            } else {
-                                nodeView = (i > 0) ? get(i - 1) : null;
-                            }
-
-                            if (nodeView != null) {
-                                offset = nodeView.getNextInsertionPoint(x - nodeView.getX(), -1, direction);
-                            }
-                        }
-
-                        if (offset != -1) {
-                            offset += nodeView.getOffset();
-                        }
-                    }
-                }
-            }
-
-            return offset;
-        }
-
-        @Override
-        public int getRowIndex(int offset) {
-            int rowIndex = 0;
-
-            for (NodeView nodeView : this) {
-                int nodeViewOffset = nodeView.getOffset();
-                int characterCount = nodeView.getCharacterCount();
-
-                if (offset >= nodeViewOffset
-                    && offset < nodeViewOffset + characterCount) {
-                    rowIndex += nodeView.getRowIndex(offset - nodeView.getOffset());
-                    break;
-                }
-
-                rowIndex += nodeView.getRowCount();
-            }
-
-            return rowIndex;
-        }
-
-        @Override
-        public int getRowCount() {
-            int rowCount = 0;
-
-            for (NodeView nodeView : this) {
-                rowCount += nodeView.getRowCount();
-            }
-
-            return rowCount;
-        }
-
-        @Override
-        public NodeView getNext() {
-            return null;
-        }
-
-        @Override
-        public void nodeInserted(Element element, int index) {
-            super.nodeInserted(element, index);
-
-            Document document = (Document)getNode();
-            insert(createNodeView(document.get(index)), index);
-        }
-
-        @Override
-        public void nodesRemoved(Element element, int index, Sequence<Node> nodes) {
-            remove(index, nodes.getLength());
-
-            super.nodesRemoved(element, index, nodes);
-        }
-    }
-
-    public class ParagraphView extends ElementView {
-        private class Row {
-            public int x = 0;
-            public int y = 0;
-            public int width = 0;
-            public int height = 0;
-            public ArrayList<NodeView> nodeViews = new ArrayList<NodeView>();
-        }
-
-        private ArrayList<Row> rows = null;
-        private Bounds terminatorBounds = new Bounds(0, 0, 0, 0);
-
-        public ParagraphView(Paragraph paragraph) {
-            super(paragraph);
-        }
-
-        @Override
-        public void invalidate() {
-            super.invalidate();
-            terminatorBounds = null;
-        }
-
-        @Override
-        public void validate() {
-            if (!isValid()) {
-                // Break the views into multiple rows
-                int breakWidth = getBreakWidth();
-
-                Paragraph paragraph = (Paragraph)getNode();
-                rows = new ArrayList<Row>();
-
-                Row row = new Row();
-                for (Node node : paragraph) {
-                    NodeView nodeView = createNodeView(node);
-
-                    nodeView.setBreakWidth(Math.max(breakWidth - (row.width
-                        + PARAGRAPH_TERMINATOR_WIDTH), 0));
-                    nodeView.validate();
-
-                    int nodeViewWidth = nodeView.getWidth();
-
-                    if (row.width + nodeViewWidth > breakWidth
-                        && row.width > 0) {
-                        // The view is too big to fit in the remaining space,
-                        // and it is not the only view in this row
-                        rows.add(row);
-                        row = new Row();
-                        row.width = 0;
-                    }
-
-                    // Add the view to the row
-                    row.nodeViews.add(nodeView);
-                    row.width += nodeViewWidth;
-
-                    // If the view was split into multiple views, add them to
-                    // their own rows
-                    nodeView = nodeView.getNext();
-                    while (nodeView != null) {
-                        rows.add(row);
-                        row = new Row();
-
-                        nodeView.setBreakWidth(breakWidth);
-                        nodeView.validate();
-
-                        row.nodeViews.add(nodeView);
-                        row.width = nodeView.getWidth();
-
-                        nodeView = nodeView.getNext();
-                    }
-                }
-
-                // Add the last row
-                if (row.nodeViews.getLength() > 0) {
-                    rows.add(row);
-                }
-
-                // Clear all existing views
-                remove(0, getLength());
-
-                // Add the row views to this view, lay out, and calculate height
-                int x = 0;
-                int width = 0;
-                int height = 0;
-                for (int i = 0, n = rows.getLength(); i < n; i++) {
-                    row = rows.get(i);
-                    row.y = height;
-
-                    width = Math.max(width, row.width);
-
-                    // Determine the row height
-                    for (NodeView nodeView : row.nodeViews) {
-                        row.height = Math.max(row.height, nodeView.getHeight());
-                    }
-
-                    // TODO Align horizontally when Elements support a horizontal
-                    // alignment property
-                    x = 0;
-                    for (NodeView nodeView : row.nodeViews) {
-                        // TODO Align to baseline
-                        int y = row.height - nodeView.getHeight();
-
-                        nodeView.setLocation(x, y + height);
-                        x += nodeView.getWidth();
-
-                        add(nodeView);
-                    }
-
-                    height += row.height;
-                }
-
-                // Recalculate terminator bounds
-                FontRenderContext fontRenderContext = Platform.getFontRenderContext();
-                LineMetrics lm = font.getLineMetrics("", 0, 0, fontRenderContext);
-                int terminatorHeight = (int)Math.ceil(lm.getHeight());
-
-                int terminatorY;
-                if (getCharacterCount() == 1) {
-                    // The terminator is the only character in this paragraph
-                    terminatorY = 0;
-                } else {
-                    terminatorY = height - terminatorHeight;
-                }
-
-                terminatorBounds = new Bounds(x, terminatorY,
-                    PARAGRAPH_TERMINATOR_WIDTH, terminatorHeight);
-
-                // Ensure that the paragraph is visible even when empty
-                width += terminatorBounds.width;
-                height = Math.max(height, terminatorBounds.height);
-
-                setSize(width, height);
-            }
-
-            super.validate();
-        }
-
-        @Override
-        protected void setSkinLocation(int skinX, int skinY) {
-            for (int i = 0, n = rows.getLength(); i < n; i++) {
-                Row row = rows.get(i);
-                for (NodeView nodeView : row.nodeViews) {
-                    nodeView.setSkinLocation(skinX + nodeView.getX(), skinY + nodeView.getY());
-                }
-            }
-        }
-
-        @Override
-        public NodeView getNext() {
-            return null;
-        }
-
-        @Override
-        public int getInsertionPoint(int x, int y) {
-            int offset = -1;
-
-            int n = rows.getLength();
-            if (n > 0) {
-                for (int i = 0; i < n; i++) {
-                    Row row = rows.get(i);
-
-                    if (y >= row.y
-                        && y < row.y + row.height) {
-                        if (x < row.x) {
-                            NodeView firstNodeView = row.nodeViews.get(0);
-                            offset = firstNodeView.getOffset();
-                        } else if (x > row.x + row.width - 1) {
-                            NodeView lastNodeView = row.nodeViews.get(row.nodeViews.getLength() - 1);
-                            offset = lastNodeView.getOffset() + lastNodeView.getCharacterCount();
-
-                            if (offset < getCharacterCount() - 1) {
-                                offset--;
-                            }
-                        } else {
-                            for (NodeView nodeView : row.nodeViews) {
-                                Bounds nodeViewBounds = nodeView.getBounds();
-
-                                if (nodeViewBounds.contains(x, y)) {
-                                    offset = nodeView.getInsertionPoint(x - nodeView.getX(), y - nodeView.getY())
-                                        + nodeView.getOffset();
-                                    break;
-                                }
-                            }
-                        }
-                    }
-
-                    if (offset != -1) {
-                        break;
-                    }
-                }
-            } else {
-                offset = getCharacterCount() - 1;
-            }
-
-            return offset;
-        }
-
-        @Override
-        public int getNextInsertionPoint(int x, int from, FocusTraversalDirection direction) {
-            int offset = -1;
-
-            int n = rows.getLength();
-            if (n == 0
-                && from == -1) {
-                // There are no rows; select the terminator character
-                offset = 0;
-            } else {
-                int i;
-                if (from == -1) {
-                    i = (direction == FocusTraversalDirection.FORWARD) ? -1 : rows.getLength();
-                } else {
-                    // Find the row that contains offset
-                    if (from == getCharacterCount() - 1) {
-                        i = rows.getLength() - 1;
-                    } else {
-                        i = 0;
-                        while (i < n) {
-                            Row row = rows.get(i);
-                            NodeView firstNodeView = row.nodeViews.get(0);
-                            NodeView lastNodeView = row.nodeViews.get(row.nodeViews.getLength() - 1);
-                            if (from >= firstNodeView.getOffset()
-                                && from < lastNodeView.getOffset() + lastNodeView.getCharacterCount()) {
-                                break;
-                            }
-
-                            i++;
-                        }
-                    }
-                }
-
-                // Move to the next or previous row
-                if (direction == FocusTraversalDirection.FORWARD) {
-                    i++;
-                } else {
-                    i--;
-                }
-
-                if (i >= 0
-                    && i < n) {
-                    // Find the node view that contains x and get the insertion point from it
-                    Row row = rows.get(i);
-
-                    for (NodeView nodeView : row.nodeViews) {
-                        Bounds bounds = nodeView.getBounds();
-                        if (x >= bounds.x
-                            && x < bounds.x + bounds.width) {
-                            offset = nodeView.getNextInsertionPoint(x - nodeView.getX(), -1, direction)
-                                + nodeView.getOffset();
-                            break;
-                        }
-                    }
-
-                    if (offset == -1) {
-                        // No node view contained the x position; move to the end of the row
-                        NodeView lastNodeView = row.nodeViews.get(row.nodeViews.getLength() - 1);
-                        offset = lastNodeView.getOffset() + lastNodeView.getCharacterCount();
-
-                        if (offset < getCharacterCount() - 1) {
-                            offset--;
-                        }
-                    }
-                }
-            }
-
-            return offset;
-        }
-
-        @Override
-        public int getRowIndex(int offset) {
-            int rowIndex = -1;
-
-            if (offset == getCharacterCount() - 1) {
-                rowIndex = (rows.getLength() == 0) ? 0 : rows.getLength() - 1;
-            } else {
-                for (int i = 0, n = rows.getLength(); i < n; i++) {
-                    Row row = rows.get(i);
-                    NodeView firstNodeView = row.nodeViews.get(0);
-                    NodeView lastNodeView = row.nodeViews.get(row.nodeViews.getLength() - 1);
-
-                    if (offset >= firstNodeView.getOffset()
-                        && offset < lastNodeView.getOffset() + lastNodeView.getCharacterCount()) {
-                        rowIndex = i;
-                        break;
-                    }
-                }
-            }
-
-            return rowIndex;
-        }
-
-        @Override
-        public int getRowCount() {
-            return Math.max(rows.getLength(), 1);
-        }
-
-        @Override
-        public Bounds getCharacterBounds(int offset) {
-            Bounds bounds;
-
-            if (offset == getCharacterCount() - 1) {
-                bounds = terminatorBounds;
-            } else {
-                bounds = super.getCharacterBounds(offset);
-            }
-
-            return bounds;
-        }
-    }
-
-    public class SpanView extends ElementView {
-
-        public SpanView(org.apache.pivot.wtk.text.Span span) {
-            super(span);
-        }
-
-        @Override
-        public void validate() {
-            if (!isValid()) {
-                // I have to re-create my children here instead of in attach(), because that is how ParagraphView works,
-                // and ParagraphView is always my super-node.
-
-                // Clear all existing views
-                remove(0, getLength());
-
-                // Attach child node views
-                org.apache.pivot.wtk.text.Span span = (org.apache.pivot.wtk.text.Span)getNode();
-                for (Node node : span) {
-                    add(createNodeView(node));
-                }
-
-                int breakWidth = getBreakWidth();
-
-                int width = 0;
-                int height = 0;
-
-                int i = 0;
-                int n = getLength();
-
-                while (i < n) {
-                    NodeView nodeView = get(i++);
-                    nodeView.setBreakWidth(breakWidth);
-                    nodeView.validate();
-
-                    nodeView.setLocation(0, height);
-
-                    width = Math.max(width, nodeView.getWidth());
-                    height += nodeView.getHeight();
-                }
-
-                setSize(width, height);
-
-                super.validate();
-            }
-        }
-
-        @Override
-        public int getInsertionPoint(int x, int y) {
-            int offset = -1;
-
-            for (int i = 0, n = getLength(); i < n; i++) {
-                NodeView nodeView = get(i);
-                Bounds nodeViewBounds = nodeView.getBounds();
-
-                if (y >= nodeViewBounds.y
-                    && y < nodeViewBounds.y + nodeViewBounds.height) {
-                    offset = nodeView.getInsertionPoint(x - nodeView.getX(), y - nodeView.getY())
-                        + nodeView.getOffset();
-                    break;
-                }
-            }
-
-            return offset;
-        }
-
-        @Override
-        public NodeView getNext() {
-            return null;
-        }
-
-        @Override
-        public int getNextInsertionPoint(int x, int from, FocusTraversalDirection direction) {
-            // TODO Auto-generated method stub
-            return 0;
-        }
-
-        @Override
-        public int getRowCount() {
-            int rowCount = 0;
-
-            for (NodeView nodeView : this) {
-                rowCount += nodeView.getRowCount();
-            }
-
-            return rowCount;
-        }
-
-        @Override
-        public int getRowIndex(int offset) {
-            int rowIndex = 0;
-
-            for (NodeView nodeView : this) {
-                int nodeViewOffset = nodeView.getOffset();
-                int characterCount = nodeView.getCharacterCount();
-
-                if (offset >= nodeViewOffset
-                    && offset < nodeViewOffset + characterCount) {
-                    rowIndex += nodeView.getRowIndex(offset - nodeView.getOffset());
-                    break;
-                }
-
-                rowIndex += nodeView.getRowCount();
-            }
-
-            return rowIndex;
-        }
-
-        @Override
-        protected void setSkinLocation(int skinX, int skinY) {
-            for (NodeView nodeView : this) {
-                nodeView.setSkinLocation(skinX, skinY + nodeView.getY());
-            }
-        }
-
-        @Override
-        public void nodeInserted(Element element, int index) {
-            super.nodeInserted(element, index);
-
-            org.apache.pivot.wtk.text.Span span = (org.apache.pivot.wtk.text.Span)getNode();
-            insert(createNodeView(span.get(index)), index);
-        }
-
-        @Override
-        public void nodesRemoved(Element element, int index, Sequence<Node> nodes) {
-            remove(index, nodes.getLength());
-
-            super.nodesRemoved(element, index, nodes);
-        }
-    }
-
-    /**
-     * Text node view.
-     */
-    public class TextNodeView extends NodeView implements TextNodeListener {
-        private int start;
-
-        private int length = 0;
-        private GlyphVector glyphVector = null;
-        private TextNodeView next = null;
-
-        public TextNodeView(TextNode textNode) {
-            this(textNode, 0);
-        }
-
-        public TextNodeView(TextNode textNode, int start) {
-            super(textNode);
-            this.start = start;
-        }
-
-        @Override
-        protected void attach() {
-            super.attach();
-
-            TextNode textNode = (TextNode)getNode();
-            textNode.getTextNodeListeners().add(this);
-        }
-
-        @Override
-        protected void detach() {
-            super.detach();
-
-            TextNode textNode = (TextNode)getNode();
-            textNode.getTextNodeListeners().remove(this);
-        }
-
-        @Override
-        public void invalidate() {
-            length = 0;
-            next = null;
-            glyphVector = null;
-
-            super.invalidate();
-        }
-
-        @Override
-        public void validate() {
-            if (!isValid()) {
-                TextNode textNode = (TextNode)getNode();
-                FontRenderContext fontRenderContext = Platform.getFontRenderContext();
-
-                int breakWidth = getBreakWidth();
-                CharacterIterator ci = textNode.getCharacterIterator(start);
-
-                float lineWidth = 0;
-                int lastWhitespaceIndex = -1;
-
-                Font effectiveFont = getEffectiveFont();
-                char c = ci.first();
-                while (c != CharacterIterator.DONE
-                    && lineWidth < breakWidth) {
-                    if (Character.isWhitespace(c)) {
-                        lastWhitespaceIndex = ci.getIndex();
-                    }
-
-                    int i = ci.getIndex();
-                    Rectangle2D characterBounds = effectiveFont.getStringBounds(ci, i, i + 1, fontRenderContext);
-                    lineWidth += characterBounds.getWidth();
-
-                    c = ci.current();
-                }
-
-                int end;
-                if (wrapText) {
-                    if (textNode.getCharacterCount() == 0) {
-                        end = start;
-                    } else {
-                        if (lineWidth < breakWidth) {
-                            end = ci.getEndIndex();
-                        } else {
-                            if (lastWhitespaceIndex == -1) {
-                                end = ci.getIndex() - 1;
-                                if (end <= start) {
-                                    end = start + 1;
-                                }
-                            } else {
-                                end = lastWhitespaceIndex + 1;
-                            }
-                        }
-                    }
-                } else {
-                    end = ci.getEndIndex();
-                }
-
-                glyphVector = getEffectiveFont().createGlyphVector(fontRenderContext,
-                    textNode.getCharacterIterator(start, end));
-
-                if (end < ci.getEndIndex()) {
-                    length = end - start;
-                    next = new TextNodeView(textNode, end);
-                } else {
-                    length = ci.getEndIndex() - start;
-                }
-
-                Rectangle2D textBounds = glyphVector.getLogicalBounds();
-                setSize((int)Math.ceil(textBounds.getWidth()),
-                    (int)Math.ceil(textBounds.getHeight()));
-            }
-
-            super.validate();
-        }
-
-        @Override
-        protected void setSkinLocation(int skinX, int skinY) {
-        }
-
-        @Override
-        public void paint(Graphics2D graphics) {
-            if (glyphVector != null) {
-                TextArea textArea = (TextArea)getComponent();
-
-                FontRenderContext fontRenderContext = Platform.getFontRenderContext();
-                LineMetrics lm = getEffectiveFont().getLineMetrics("", fontRenderContext);
-                float ascent = lm.getAscent();
-
-                graphics.setFont(getEffectiveFont());
-
-                int selectionStart = textArea.getSelectionStart();
-                int selectionLength = textArea.getSelectionLength();
-                Span selectionRange = new Span(selectionStart, selectionStart + selectionLength - 1);
-
-                int documentOffset = getDocumentOffset();
-                Span characterRange = new Span(documentOffset, documentOffset + getCharacterCount() - 1);
-
-                if (selectionLength > 0
-                    && characterRange.intersects(selectionRange)) {
-                    // Determine the selection bounds
-                    int width = getWidth();
-                    int height = getHeight();
-
-                    int x0;
-                    if (selectionRange.start > characterRange.start) {
-                        Bounds leadingSelectionBounds = getCharacterBounds(selectionRange.start - documentOffset);
-                        x0 = leadingSelectionBounds.x;
-                    } else {
-                        x0 = 0;
-                    }
-
-                    int x1;
-                    if (selectionRange.end < characterRange.end) {
-                        Bounds trailingSelectionBounds = getCharacterBounds(selectionRange.end - documentOffset);
-                        x1 = trailingSelectionBounds.x + trailingSelectionBounds.width;
-                    } else {
-                        x1 = width;
-                    }
-
-                    Rectangle selection = new Rectangle(x0, 0, x1 - x0, height);
-
-                    // Paint the unselected text
-                    Area unselectedArea = new Area();
-                    unselectedArea.add(new Area(new Rectangle(0, 0, width, height)));
-                    unselectedArea.subtract(new Area(selection));
-
-                    Graphics2D textGraphics = (Graphics2D)graphics.create();
-                    textGraphics.setColor(getEffectiveForegroundColor());
-                    textGraphics.clip(unselectedArea);
-                    textGraphics.drawGlyphVector(glyphVector, 0, ascent);
-                    textGraphics.dispose();
-
-                    // Paint the selection
-                    Color selectionColor;
-                    if (textArea.isFocused()) {
-                        selectionColor = TextAreaSkin.this.selectionColor;
-                    } else {
-                        selectionColor = inactiveSelectionColor;
-                    }
-
-                    Graphics2D selectedTextGraphics = (Graphics2D)graphics.create();
-                    selectedTextGraphics.setColor(textArea.isFocused() &&
-                        textArea.isEditable() ? selectionColor : inactiveSelectionColor);
-                    selectedTextGraphics.clip(selection.getBounds());
-                    selectedTextGraphics.drawGlyphVector(glyphVector, 0, ascent);
-                    selectedTextGraphics.dispose();
-                } else {
-                    // Draw the text
-                    graphics.setColor(getEffectiveForegroundColor());
-                    graphics.drawGlyphVector(glyphVector, 0, ascent);
-                }
-            }
-        }
-
-        @Override
-        public int getOffset() {
-            return super.getOffset() + start;
-        }
-
-        @Override
-        public int getCharacterCount() {
-            return length;
-        }
-
-        @Override
-        public NodeView getNext() {
-            return next;
-        }
-
-        @Override
-        public int getInsertionPoint(int x, int y) {
-            FontRenderContext fontRenderContext = Platform.getFontRenderContext();
-            LineMetrics lm = getEffectiveFont().getLineMetrics("", fontRenderContext);
-            float ascent = lm.getAscent();
-
-            int n = glyphVector.getNumGlyphs();
-            int i = 0;
-
-            while (i < n) {
-                Shape glyphBounds = glyphVector.getGlyphLogicalBounds(i);
-
-                if (glyphBounds.contains(x, y - ascent)) {
-                    Rectangle2D glyphBounds2D = glyphBounds.getBounds2D();
-
-                    if (x - glyphBounds2D.getX() > glyphBounds2D.getWidth() / 2
-                        && i < n - 1) {
-                        // The user clicked on the right half of the character; select
-                        // the next character
-                        i++;
-                    }
-
-                    break;
-                }
-
-                i++;
-            }
-
-            return i;
-        }
-
-        private Font getEffectiveFont() {
-            Font font = null;
-            // run up the tree until we find an element's style to apply
-            Element element = getNode().getParent();
-            while (element != null) {
-                font = element.getFont();
-                if (font != null) {
-                    break;
-                }
-
-                element = element.getParent();
-            }
-            // if we find nothing, use the default font
-            if (element == null) {
-                font = TextAreaSkin.this.font;
-            }
-            return font;
-        }
-
-        private Color getEffectiveForegroundColor() {
-            Color foregroundColor = null;
-            // run up the tree until we find an element's style to apply
-            Element element = getNode().getParent();
-            while (element != null) {
-                foregroundColor = element.getForegroundColor();
-                if (foregroundColor != null) {
-                    break;
-                }
-
-                element = element.getParent();
-            }
-            // if we find nothing, use the default color
-            if (element == null) {
-                foregroundColor = TextAreaSkin.this.color;
-            }
-            return foregroundColor;
-        }
-
-        @Override
-        public int getNextInsertionPoint(int x, int from, FocusTraversalDirection direction) {
-            int offset = -1;
-
-            if (from == -1) {
-                int n = glyphVector.getNumGlyphs();
-                int i = 0;
-
-                while (i < n) {
-                    Shape glyphBounds = glyphVector.getGlyphLogicalBounds(i);
-                    Rectangle2D glyphBounds2D = glyphBounds.getBounds2D();
-
-                    float glyphX = (float)glyphBounds2D.getX();
-                    float glyphWidth = (float)glyphBounds2D.getWidth();
-
-                    if (x >= glyphX && x < glyphX + glyphWidth) {
-                        if (x - glyphX > glyphWidth / 2
-                            && i < n - 1) {
-                            // The x position falls within the right half of the character;
-                            // select the next character
-                            i++;
-                        }
-
-                        offset = i;
-                        break;
-                    }
-
-                    i++;
-                }
-            }
-
-            return offset;
-        }
-
-        @Override
-        public int getRowIndex(int offset) {
-            return -1;
-        }
-
-        @Override
-        public int getRowCount() {
-            return 0;
-        }
-
-        @Override
-        public Bounds getCharacterBounds(int offset) {
-            Shape glyphBounds = glyphVector.getGlyphLogicalBounds(offset);
-            Rectangle2D glyphBounds2D = glyphBounds.getBounds2D();
-
-            return new Bounds((int)Math.floor(glyphBounds2D.getX()), 0,
-                (int)Math.ceil(glyphBounds2D.getWidth()), getHeight());
-        }
-
-        @Override
-        public void charactersInserted(TextNode textNode, int index, int count) {
-            invalidate();
-        }
-
-        @Override
-        public void charactersRemoved(TextNode textNode, int index, String characters) {
-            invalidate();
-        }
-
-        @Override
-        public String toString() {
-            TextNode textNode = (TextNode)getNode();
-            String text = textNode.getText();
-            return "[" + text.substring(start, start + length) + "]";
-        }
-    }
-
-    public class ImageNodeView extends NodeView implements ImageNodeListener, ImageListener {
-        public ImageNodeView(ImageNode imageNode) {
-            super(imageNode);
-        }
-
-        @Override
-        protected void attach() {
-            super.attach();
-
-            ImageNode imageNode = (ImageNode)getNode();
-            imageNode.getImageNodeListeners().add(this);
-
-            Image image = imageNode.getImage();
-            if (image != null) {
-                image.getImageListeners().add(this);
-            }
-        }
-
-        @Override
-        protected void detach() {
-            super.detach();
-
-            ImageNode imageNode = (ImageNode)getNode();
-            imageNode.getImageNodeListeners().remove(this);
-        }
-
-        @Override
-        public void validate() {
-            if (!isValid()) {
-                ImageNode imageNode = (ImageNode)getNode();
-                Image image = imageNode.getImage();
-
-                if (image == null) {
-                    setSize(0, 0);
-                } else {
-                    setSize(image.getWidth(), image.getHeight());
-                }
-
-                super.validate();
-            }
-        }
-
-        @Override
-        protected void setSkinLocation(int skinX, int skinY) {
-        }
-
-        @Override
-        public void paint(Graphics2D graphics) {
-            ImageNode imageNode = (ImageNode)getNode();
-            Image image = imageNode.getImage();
-
-            if (image != null) {
-                image.paint(graphics);
-            }
-        }
-
-        @Override
-        public NodeView getNext() {
-            return null;
-        }
-
-        @Override
-        public int getInsertionPoint(int x, int y) {
-            return 0;
-        }
-
-        @Override
-        public int getNextInsertionPoint(int x, int from, FocusTraversalDirection direction) {
-            return (from == -1) ? 0 : -1;
-        }
-
-        @Override
-        public int getRowIndex(int offset) {
-            return -1;
-        }
-
-        @Override
-        public int getRowCount() {
-            return 0;
-        }
-
-        @Override
-        public Bounds getCharacterBounds(int offset) {
-            return new Bounds(0, 0, getWidth(), getHeight());
-        }
-
-        @Override
-        public void imageChanged(ImageNode imageNode, Image previousImage) {
-            invalidate();
-
-            Image image = imageNode.getImage();
-            if (image != null) {
-                image.getImageListeners().add(this);
-            }
-
-            if (previousImage != null) {
-                previousImage.getImageListeners().remove(this);
-            }
-        }
-
-        @Override
-        public void sizeChanged(Image image, int previousWidth, int previousHeight) {
-            invalidate();
-        }
-
-        @Override
-        public void baselineChanged(Image image, int previousBaseline) {
-            // TODO Invalidate once baseline alignment of node view is supported
-        }
-
-        @Override
-        public void regionUpdated(Image image, int x, int y, int width, int height) {
-            // TODO Repaint the corresponding area of the component (add a repaint()
-            // method to NodeView to facilitate this as well as paint-only updates
-            // such as color changes)
-        }
-    }
-
-    public class ComponentNodeView extends NodeView implements ComponentNodeListener {
-
-        private final ComponentListener myComponentListener = new ComponentListener.Adapter() {
-            @Override
-            public void sizeChanged(Component component, int previousWidth, int previousHeight) {
-                invalidate();
-            }
-        };
-
-        public ComponentNodeView(ComponentNode componentNode) {
-            super(componentNode);
-        }
-
-        @Override
-        protected void attach() {
-            super.attach();
-
-            ComponentNode componentNode = (ComponentNode) getNode();
-            componentNode.getComponentNodeListeners().add(this);
-
-            Component component = componentNode.getComponent();
-            if (component != null) {
-                component.getComponentListeners().add(myComponentListener);
-            }
-        }
-
-        @Override
-        protected void detach() {
-            super.detach();
-
-            ComponentNode componentNode = (ComponentNode) getNode();
-            componentNode.getComponentNodeListeners().remove(this);
-        }
-
-        @Override
-        public void validate() {
-            if (!isValid()) {
-                ComponentNode componentNode = (ComponentNode) getNode();
-                Component component = componentNode.getComponent();
-
-                if (component == null) {
-                    setSize(0, 0);
-                } else {
-                    component.validate();
-                    component.setSize(component.getPreferredWidth(), component.getPreferredHeight());
-                    setSize(component.getWidth(), component.getHeight());
-                }
-
-                super.validate();
-            }
-        }
-
-        @Override
-        protected void setSkinLocation(int skinX, int skinY) {
-            ComponentNode componentNode = (ComponentNode) getNode();
-            Component component = componentNode.getComponent();
-
-            if (component != null) {
-                // I have to un-translate the x and y coordinates because the
-                // component is painted by the Container object, and it's co-ordinates
-                // are relative to the Container object, not to the document node hierarchy.
-                component.setLocation(skinX, skinY);
-            }
-        }
-
-        @Override
-        public void paint(Graphics2D graphics) {
-            // do nothing
-        }
-
-        @Override
-        public NodeView getNext() {
-            return null;
-        }
-
-        @Override
-        public int getInsertionPoint(int x, int y) {
-            return 0;
-        }
-
-        @Override
-        public int getNextInsertionPoint(int x, int from, FocusTraversalDirection direction) {
-            return (from == -1) ? 0 : -1;
-        }
-
-        @Override
-        public int getRowIndex(int offset) {
-            return -1;
-        }
-
-        @Override
-        public int getRowCount() {
-            return 0;
-        }
-
-        @Override
-        public Bounds getCharacterBounds(int offset) {
-            return new Bounds(0, 0, getWidth(), getHeight());
-        }
-
-        @Override
-        public void componentChanged(ComponentNode componentNode, Component previousComponent) {
-            invalidate();
-
-            Component component = componentNode.getComponent();
-            if (component != null) {
-                component.getComponentListeners().add(myComponentListener);
-            }
-
-            if (previousComponent != null) {
-                previousComponent.getComponentListeners().remove(myComponentListener);
-            }
-        }
-    }
-
     private class BlinkCaretCallback implements Runnable {
         @Override
         public void run() {
@@ -1771,7 +113,7 @@ public class TextAreaSkin extends Contai
         }
     }
 
-    private DocumentView documentView = null;
+    private TextAreaSkinDocumentView documentView = null;
 
     private int caretX = 0;
     private Rectangle caret = new Rectangle();
@@ -1801,7 +143,6 @@ public class TextAreaSkin extends Contai
 
     private boolean wrapText = true;
 
-    private static final int PARAGRAPH_TERMINATOR_WIDTH = 4;
     private static final int SCROLL_RATE = 30;
 
     public TextAreaSkin() {
@@ -1827,7 +168,7 @@ public class TextAreaSkin extends Contai
 
         Document document = textArea.getDocument();
         if (document != null) {
-            documentView = (DocumentView)createNodeView(document);
+            documentView = (TextAreaSkinDocumentView)createNodeView(document);
             documentView.attach();
             updateSelection();
         }
@@ -2683,7 +1024,7 @@ public class TextAreaSkin extends Contai
 
         Document document = textArea.getDocument();
         if (document != null) {
-            documentView = (DocumentView)createNodeView(document);
+            documentView = (TextAreaSkinDocumentView)createNodeView(document);
             documentView.attach();
         }
 
@@ -2726,21 +1067,21 @@ public class TextAreaSkin extends Contai
         }
     }
 
-    private NodeView createNodeView(Node node) {
-        NodeView nodeView = null;
+    TextAreaSkinNodeView createNodeView(Node node) {
+        TextAreaSkinNodeView nodeView = null;
 
         if (node instanceof Document) {
-            nodeView = new DocumentView((Document)node);
+            nodeView = new TextAreaSkinDocumentView(this, (Document)node);
         } else if (node instanceof Paragraph) {
-            nodeView = new ParagraphView((Paragraph)node);
+            nodeView = new TextAreaSkinParagraphView(this, (Paragraph)node);
         } else if (node instanceof TextNode) {
-            nodeView = new TextNodeView((TextNode)node);
+            nodeView = new TextAreaSkinTextNodeView(this, (TextNode)node);
         } else if (node instanceof ImageNode) {
-            nodeView = new ImageNodeView((ImageNode)node);
+            nodeView = new TextAreaSkinImageNodeView((ImageNode)node);
         } else if (node instanceof ComponentNode) {
-            nodeView = new ComponentNodeView((ComponentNode)node);
+            nodeView = new TextAreaSkinComponentNodeView((ComponentNode)node);
         } else if (node instanceof org.apache.pivot.wtk.text.Span) {
-            nodeView = new SpanView((org.apache.pivot.wtk.text.Span)node);
+            nodeView = new TextAreaSkinSpanView(this, (org.apache.pivot.wtk.text.Span)node);
         } else {
             throw new IllegalArgumentException("Unsupported node type: "
                 + node.getClass().getName());

Added: pivot/trunk/wtk/src/org/apache/pivot/wtk/skin/TextAreaSkinComponentNodeView.java
URL: http://svn.apache.org/viewvc/pivot/trunk/wtk/src/org/apache/pivot/wtk/skin/TextAreaSkinComponentNodeView.java?rev=959922&view=auto
==============================================================================
--- pivot/trunk/wtk/src/org/apache/pivot/wtk/skin/TextAreaSkinComponentNodeView.java (added)
+++ pivot/trunk/wtk/src/org/apache/pivot/wtk/skin/TextAreaSkinComponentNodeView.java Fri Jul  2 09:08:54 2010
@@ -0,0 +1,141 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to you under the Apache License,
+ * Version 2.0 (the "License"); you may not use this file except in
+ * compliance with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.pivot.wtk.skin;
+
+import java.awt.Graphics2D;
+
+import org.apache.pivot.wtk.Bounds;
+import org.apache.pivot.wtk.Component;
+import org.apache.pivot.wtk.ComponentListener;
+import org.apache.pivot.wtk.FocusTraversalDirection;
+import org.apache.pivot.wtk.text.ComponentNode;
+import org.apache.pivot.wtk.text.ComponentNodeListener;
+
+public class TextAreaSkinComponentNodeView extends TextAreaSkinNodeView implements ComponentNodeListener {
+
+    private final ComponentListener myComponentListener = new ComponentListener.Adapter() {
+        @Override
+        public void sizeChanged(Component component, int previousWidth, int previousHeight) {
+            invalidate();
+        }
+    };
+
+    public TextAreaSkinComponentNodeView(ComponentNode componentNode) {
+        super(componentNode);
+    }
+
+    @Override
+    protected void attach() {
+        super.attach();
+
+        ComponentNode componentNode = (ComponentNode) getNode();
+        componentNode.getComponentNodeListeners().add(this);
+
+        Component component = componentNode.getComponent();
+        if (component != null) {
+            component.getComponentListeners().add(myComponentListener);
+        }
+    }
+
+    @Override
+    protected void detach() {
+        super.detach();
+
+        ComponentNode componentNode = (ComponentNode) getNode();
+        componentNode.getComponentNodeListeners().remove(this);
+    }
+
+    @Override
+    public void validate() {
+        if (!isValid()) {
+            ComponentNode componentNode = (ComponentNode) getNode();
+            Component component = componentNode.getComponent();
+
+            if (component == null) {
+                setSize(0, 0);
+            } else {
+                component.validate();
+                component.setSize(component.getPreferredWidth(), component.getPreferredHeight());
+                setSize(component.getWidth(), component.getHeight());
+            }
+
+            super.validate();
+        }
+    }
+
+    @Override
+    protected void setSkinLocation(int skinX, int skinY) {
+        ComponentNode componentNode = (ComponentNode) getNode();
+        Component component = componentNode.getComponent();
+
+        if (component != null) {
+            // I have to un-translate the x and y coordinates because the
+            // component is painted by the Container object, and it's co-ordinates
+            // are relative to the Container object, not to the document node hierarchy.
+            component.setLocation(skinX, skinY);
+        }
+    }
+
+    @Override
+    public void paint(Graphics2D graphics) {
+        // do nothing
+    }
+
+    @Override
+    public TextAreaSkinNodeView getNext() {
+        return null;
+    }
+
+    @Override
+    public int getInsertionPoint(int x, int y) {
+        return 0;
+    }
+
+    @Override
+    public int getNextInsertionPoint(int x, int from, FocusTraversalDirection direction) {
+        return (from == -1) ? 0 : -1;
+    }
+
+    @Override
+    public int getRowIndex(int offset) {
+        return -1;
+    }
+
+    @Override
+    public int getRowCount() {
+        return 0;
+    }
+
+    @Override
+    public Bounds getCharacterBounds(int offset) {
+        return new Bounds(0, 0, getWidth(), getHeight());
+    }
+
+    @Override
+    public void componentChanged(ComponentNode componentNode, Component previousComponent) {
+        invalidate();
+
+        Component component = componentNode.getComponent();
+        if (component != null) {
+            component.getComponentListeners().add(myComponentListener);
+        }
+
+        if (previousComponent != null) {
+            previousComponent.getComponentListeners().remove(myComponentListener);
+        }
+    }
+}
\ No newline at end of file

Added: pivot/trunk/wtk/src/org/apache/pivot/wtk/skin/TextAreaSkinDocumentView.java
URL: http://svn.apache.org/viewvc/pivot/trunk/wtk/src/org/apache/pivot/wtk/skin/TextAreaSkinDocumentView.java?rev=959922&view=auto
==============================================================================
--- pivot/trunk/wtk/src/org/apache/pivot/wtk/skin/TextAreaSkinDocumentView.java (added)
+++ pivot/trunk/wtk/src/org/apache/pivot/wtk/skin/TextAreaSkinDocumentView.java Fri Jul  2 09:08:54 2010
@@ -0,0 +1,236 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to you under the Apache License,
+ * Version 2.0 (the "License"); you may not use this file except in
+ * compliance with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.pivot.wtk.skin;
+
+import org.apache.pivot.collections.Sequence;
+import org.apache.pivot.wtk.Bounds;
+import org.apache.pivot.wtk.FocusTraversalDirection;
+import org.apache.pivot.wtk.text.Document;
+import org.apache.pivot.wtk.text.Element;
+import org.apache.pivot.wtk.text.Node;
+
+/**
+ * Document view.
+ */
+class TextAreaSkinDocumentView extends TextAreaSkinElementView {
+    private final TextAreaSkin textAreaSkin;
+
+    public TextAreaSkinDocumentView(TextAreaSkin textAreaSkin, Document document) {
+        super(document);
+        this.textAreaSkin = textAreaSkin;
+    }
+
+    @Override
+    public void attach() {
+        super.attach();
+
+        // Attach child node views
+        Document document = (Document)getNode();
+        for (Node node : document) {
+            add(textAreaSkin.createNodeView(node));
+        }
+    }
+
+    @Override
+    public void repaint(int x, int y, int width, int height) {
+        super.repaint(x, y, width, height);
+
+        textAreaSkin.repaintComponent(x, y, width, height);
+    }
+
+    @Override
+    public void invalidate() {
+        super.invalidate();
+        textAreaSkin.invalidateComponent();
+    }
+
+    @Override
+    public void validate() {
+        // TODO At some point, we may want to optimize this method by deferring layout of
+        // non-visible views. If so, we should not recycle views but rather recreate them
+        // (as is done in ParagraphView). This way, we avoid thread contention over the
+        // existing views (e.g. trying to paint one while modifying its size/location, etc.).
+        // Any invalid node views are simply replaced (in the queued callback, when the
+        // thread has finished processing the new ones). This allows the definition of
+        // validate() to remain as-is. Of course, if we redefine NodeView to implement
+        // ConstrainedVisual, this may no longer be an issue.
+        // Note that, if anything happens to invalidate the existence of the new views before
+        // they are added to the document view, we need to make sure they are disposed (i.e.
+        // detached).
+
+        if (!isValid()) {
+            int breakWidth = getBreakWidth();
+
+            int width = 0;
+            int height = 0;
+
+            int i = 0;
+            int n = getLength();
+
+            while (i < n) {
+                TextAreaSkinNodeView nodeView = get(i++);
+                nodeView.setBreakWidth(breakWidth);
+                nodeView.validate();
+
+                nodeView.setLocation(0, height);
+
+                width = Math.max(width, nodeView.getWidth());
+                height += nodeView.getHeight();
+            }
+
+            setSize(width, height);
+
+            super.validate();
+        }
+    }
+
+    @Override
+    protected void setSkinLocation(int skinX, int skinY) {
+        for (TextAreaSkinNodeView nodeView : this) {
+            nodeView.setSkinLocation(skinX, skinY + nodeView.getY());
+        }
+    }
+
+    @Override
+    public int getInsertionPoint(int x, int y) {
+        int offset = -1;
+
+        for (int i = 0, n = getLength(); i < n; i++) {
+            TextAreaSkinNodeView nodeView = get(i);
+            Bounds nodeViewBounds = nodeView.getBounds();
+
+            if (y >= nodeViewBounds.y
+                && y < nodeViewBounds.y + nodeViewBounds.height) {
+                offset = nodeView.getInsertionPoint(x - nodeView.getX(), y - nodeView.getY())
+                    + nodeView.getOffset();
+                break;
+            }
+        }
+
+        return offset;
+    }
+
+    @Override
+    public int getNextInsertionPoint(int x, int from, FocusTraversalDirection direction) {
+        int offset = -1;
+
+        if (getLength() > 0) {
+            if (from == -1) {
+                int i = (direction == FocusTraversalDirection.FORWARD) ? 0 : getLength() - 1;
+                TextAreaSkinNodeView nodeView = get(i);
+                offset = nodeView.getNextInsertionPoint(x - nodeView.getX(), -1, direction);
+
+                if (offset != -1) {
+                    offset += nodeView.getOffset();
+                }
+            } else {
+                // Find the node view that contains the offset
+                int n = getLength();
+                int i = 0;
+
+                while (i < n) {
+                    TextAreaSkinNodeView nodeView = get(i);
+                    int nodeViewOffset = nodeView.getOffset();
+                    int characterCount = nodeView.getCharacterCount();
+
+                    if (from >= nodeViewOffset
+                        && from < nodeViewOffset + characterCount) {
+                        break;
+                    }
+
+                    i++;
+                }
+
+                if (i < n) {
+                    TextAreaSkinNodeView nodeView = get(i);
+                    offset = nodeView.getNextInsertionPoint(x - nodeView.getX(),
+                        from - nodeView.getOffset(), direction);
+
+                    if (offset == -1) {
+                        // Move to the next or previous node view
+                        if (direction == FocusTraversalDirection.FORWARD) {
+                            nodeView = (i < n - 1) ? get(i + 1) : null;
+                        } else {
+                            nodeView = (i > 0) ? get(i - 1) : null;
+                        }
+
+                        if (nodeView != null) {
+                            offset = nodeView.getNextInsertionPoint(x - nodeView.getX(), -1, direction);
+                        }
+                    }
+
+                    if (offset != -1) {
+                        offset += nodeView.getOffset();
+                    }
+                }
+            }
+        }
+
+        return offset;
+    }
+
+    @Override
+    public int getRowIndex(int offset) {
+        int rowIndex = 0;
+
+        for (TextAreaSkinNodeView nodeView : this) {
+            int nodeViewOffset = nodeView.getOffset();
+            int characterCount = nodeView.getCharacterCount();
+
+            if (offset >= nodeViewOffset
+                && offset < nodeViewOffset + characterCount) {
+                rowIndex += nodeView.getRowIndex(offset - nodeView.getOffset());
+                break;
+            }
+
+            rowIndex += nodeView.getRowCount();
+        }
+
+        return rowIndex;
+    }
+
+    @Override
+    public int getRowCount() {
+        int rowCount = 0;
+
+        for (TextAreaSkinNodeView nodeView : this) {
+            rowCount += nodeView.getRowCount();
+        }
+
+        return rowCount;
+    }
+
+    @Override
+    public TextAreaSkinNodeView getNext() {
+        return null;
+    }
+
+    @Override
+    public void nodeInserted(Element element, int index) {
+        super.nodeInserted(element, index);
+
+        Document document = (Document)getNode();
+        insert(textAreaSkin.createNodeView(document.get(index)), index);
+    }
+
+    @Override
+    public void nodesRemoved(Element element, int index, Sequence<Node> nodes) {
+        remove(index, nodes.getLength());
+
+        super.nodesRemoved(element, index, nodes);
+    }
+}
\ No newline at end of file

Added: pivot/trunk/wtk/src/org/apache/pivot/wtk/skin/TextAreaSkinElementView.java
URL: http://svn.apache.org/viewvc/pivot/trunk/wtk/src/org/apache/pivot/wtk/skin/TextAreaSkinElementView.java?rev=959922&view=auto
==============================================================================
--- pivot/trunk/wtk/src/org/apache/pivot/wtk/skin/TextAreaSkinElementView.java (added)
+++ pivot/trunk/wtk/src/org/apache/pivot/wtk/skin/TextAreaSkinElementView.java Fri Jul  2 09:08:54 2010
@@ -0,0 +1,239 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to you under the Apache License,
+ * Version 2.0 (the "License"); you may not use this file except in
+ * compliance with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.pivot.wtk.skin;
+
+import java.awt.Color;
+import java.awt.Font;
+import java.awt.Graphics2D;
+import java.awt.Rectangle;
+import java.util.Iterator;
+
+import org.apache.pivot.collections.ArrayList;
+import org.apache.pivot.collections.Sequence;
+import org.apache.pivot.util.ImmutableIterator;
+import org.apache.pivot.wtk.Bounds;
+import org.apache.pivot.wtk.text.Element;
+import org.apache.pivot.wtk.text.ElementListener;
+import org.apache.pivot.wtk.text.Node;
+
+/**
+ * Abstract base class for element views.
+ */
+abstract class TextAreaSkinElementView extends TextAreaSkinNodeView
+    implements Sequence<TextAreaSkinNodeView>, Iterable<TextAreaSkinNodeView>, ElementListener {
+    private ArrayList<TextAreaSkinNodeView> nodeViews = new ArrayList<TextAreaSkinNodeView>();
+
+    public TextAreaSkinElementView(Element element) {
+        super(element);
+    }
+
+    @Override
+    protected void attach() {
+        super.attach();
+
+        Element element = (Element)getNode();
+        element.getElementListeners().add(this);
+
+        // NOTE We don't attach child views here because this may not
+        // be efficient for all subclasses (e.g. paragraph views need to
+        // recreate child views when breaking across multiple lines)
+    }
+
+    @Override
+    protected void detach() {
+        Element element = (Element)getNode();
+        element.getElementListeners().remove(this);
+
+        // Detach child node views
+        for (TextAreaSkinNodeView nodeView : this) {
+            nodeView.detach();
+        }
+
+        super.detach();
+    }
+
+    @Override
+    public int add(TextAreaSkinNodeView nodeView) {
+        int index = getLength();
+        insert(nodeView, index);
+
+        return index;
+    }
+
+    @Override
+    public void insert(TextAreaSkinNodeView nodeView, int index) {
+        nodeView.setParent(this);
+        nodeView.attach();
+
+        nodeViews.insert(nodeView, index);
+    }
+
+    @Override
+    public TextAreaSkinNodeView update(int index, TextAreaSkinNodeView nodeView) {
+        throw new UnsupportedOperationException();
+    }
+
+    @Override
+    public int remove(TextAreaSkinNodeView nodeView) {
+        int index = indexOf(nodeView);
+        if (index != -1) {
+            remove(index, 1);
+        }
+
+        return index;
+    }
+
+    @Override
+    public Sequence<TextAreaSkinNodeView> remove(int index, int count) {
+        Sequence<TextAreaSkinNodeView> removed = nodeViews.remove(index, count);
+
+        for (int i = 0, n = removed.getLength(); i < n; i++) {
+            TextAreaSkinNodeView nodeView = removed.get(i);
+            nodeView.setParent(null);
+            nodeView.detach();
+        }
+
+        return removed;
+    }
+
+    @Override
+    public TextAreaSkinNodeView get(int index) {
+        return nodeViews.get(index);
+    }
+
+    @Override
+    public int indexOf(TextAreaSkinNodeView nodeView) {
+        return nodeViews.indexOf(nodeView);
+    }
+
+    @Override
+    public int getLength() {
+        return nodeViews.getLength();
+    }
+
+    @Override
+    public void paint(Graphics2D graphics) {
+        // Determine the paint bounds
+        Bounds paintBounds = new Bounds(0, 0, getWidth(), getHeight());
+        Rectangle clipBounds = graphics.getClipBounds();
+        if (clipBounds != null) {
+            paintBounds = paintBounds.intersect(new Bounds(clipBounds));
+        }
+
+        for (TextAreaSkinNodeView nodeView : nodeViews) {
+            Bounds nodeViewBounds = nodeView.getBounds();
+
+            // Only paint node views that intersect the current clip rectangle
+            if (nodeViewBounds.intersects(paintBounds)) {
+                // Create a copy of the current graphics context and
+                // translate to the node view's coordinate system
+                Graphics2D nodeViewGraphics = (Graphics2D)graphics.create();
+                nodeViewGraphics.translate(nodeViewBounds.x, nodeViewBounds.y);
+
+                Color styledBackgroundColor = getStyledBackgroundColor();
+                if (styledBackgroundColor != null) {
+                    nodeViewGraphics.setColor(styledBackgroundColor);
+                    nodeViewGraphics.fillRect(nodeViewBounds.x, nodeViewBounds.y, nodeViewBounds.width, nodeViewBounds.height);
+                }
+
+                // NOTE We don't clip here because views should generally
+                // not overlap and clipping would impose an unnecessary
+                // performance penalty
+
+                // Paint the node view
+                nodeView.paint(nodeViewGraphics);
+
+                // Dispose of the node views's graphics
+                nodeViewGraphics.dispose();
+            }
+        }
+    }
+
+    private Color getStyledBackgroundColor() {
+        Color backgroundColor = null;
+        Node node = getNode();
+        // run up the tree until we find a Element's style to apply
+        while (node != null) {
+            if (node instanceof Element) {
+                backgroundColor = ((Element) node).getBackgroundColor();
+                if (backgroundColor != null) {
+                    break;
+                }
+            }
+            node = node.getParent();
+        }
+        return backgroundColor;
+    }
+
+    @Override
+    public Bounds getCharacterBounds(int offset) {
+        Bounds characterBounds = null;
+
+        for (int i = 0, n = nodeViews.getLength(); i < n; i++) {
+            TextAreaSkinNodeView nodeView = nodeViews.get(i);
+            int nodeViewOffset = nodeView.getOffset();
+            int characterCount = nodeView.getCharacterCount();
+
+            if (offset >= nodeViewOffset
+                && offset < nodeViewOffset + characterCount) {
+                characterBounds = nodeView.getCharacterBounds(offset - nodeViewOffset);
+
+                if (characterBounds != null) {
+                    characterBounds = characterBounds.translate(nodeView.getX(), nodeView.getY());
+                }
+
+                break;
+            }
+        }
+
+        if (characterBounds != null) {
+            characterBounds = characterBounds.intersect(0, 0, getWidth(), getHeight());
+        }
+
+        return characterBounds;
+    }
+
+    @Override
+    public void nodeInserted(Element element, int index) {
+        invalidate();
+    }
+
+    @Override
+    public void nodesRemoved(Element element, int index, Sequence<Node> nodes) {
+        invalidate();
+    }
+
+    @Override
+    public void fontChanged(Element element, Font previousFont) {
+        invalidate();
+    }
+
+    @Override
+    public void backgroundColorChanged(Element element, Color previousBackgroundColor) {
+        invalidate();
+    }
+
+    @Override
+    public void foregroundColorChanged(Element element, Color previousForegroundColor) {
+        invalidate();
+    }
+
+    @Override
+    public Iterator<TextAreaSkinNodeView> iterator() {
+        return new ImmutableIterator<TextAreaSkinNodeView>(nodeViews.iterator());
+    }
+}
\ No newline at end of file

Added: pivot/trunk/wtk/src/org/apache/pivot/wtk/skin/TextAreaSkinImageNodeView.java
URL: http://svn.apache.org/viewvc/pivot/trunk/wtk/src/org/apache/pivot/wtk/skin/TextAreaSkinImageNodeView.java?rev=959922&view=auto
==============================================================================
--- pivot/trunk/wtk/src/org/apache/pivot/wtk/skin/TextAreaSkinImageNodeView.java (added)
+++ pivot/trunk/wtk/src/org/apache/pivot/wtk/skin/TextAreaSkinImageNodeView.java Fri Jul  2 09:08:54 2010
@@ -0,0 +1,144 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to you under the Apache License,
+ * Version 2.0 (the "License"); you may not use this file except in
+ * compliance with the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.pivot.wtk.skin;
+
+import java.awt.Graphics2D;
+
+import org.apache.pivot.wtk.Bounds;
+import org.apache.pivot.wtk.FocusTraversalDirection;
+import org.apache.pivot.wtk.media.Image;
+import org.apache.pivot.wtk.media.ImageListener;
+import org.apache.pivot.wtk.text.ImageNode;
+import org.apache.pivot.wtk.text.ImageNodeListener;
+
+class TextAreaSkinImageNodeView extends TextAreaSkinNodeView implements ImageNodeListener, ImageListener {
+    public TextAreaSkinImageNodeView(ImageNode imageNode) {
+        super(imageNode);
+    }
+
+    @Override
+    protected void attach() {
+        super.attach();
+
+        ImageNode imageNode = (ImageNode)getNode();
+        imageNode.getImageNodeListeners().add(this);
+
+        Image image = imageNode.getImage();
+        if (image != null) {
+            image.getImageListeners().add(this);
+        }
+    }
+
+    @Override
+    protected void detach() {
+        super.detach();
+
+        ImageNode imageNode = (ImageNode)getNode();
+        imageNode.getImageNodeListeners().remove(this);
+    }
+
+    @Override
+    public void validate() {
+        if (!isValid()) {
+            ImageNode imageNode = (ImageNode)getNode();
+            Image image = imageNode.getImage();
+
+            if (image == null) {
+                setSize(0, 0);
+            } else {
+                setSize(image.getWidth(), image.getHeight());
+            }
+
+            super.validate();
+        }
+    }
+
+    @Override
+    protected void setSkinLocation(int skinX, int skinY) {
+    }
+
+    @Override
+    public void paint(Graphics2D graphics) {
+        ImageNode imageNode = (ImageNode)getNode();
+        Image image = imageNode.getImage();
+
+        if (image != null) {
+            image.paint(graphics);
+        }
+    }
+
+    @Override
+    public TextAreaSkinNodeView getNext() {
+        return null;
+    }
+
+    @Override
+    public int getInsertionPoint(int x, int y) {
+        return 0;
+    }
+
+    @Override
+    public int getNextInsertionPoint(int x, int from, FocusTraversalDirection direction) {
+        return (from == -1) ? 0 : -1;
+    }
+
+    @Override
+    public int getRowIndex(int offset) {
+        return -1;
+    }
+
+    @Override
+    public int getRowCount() {
+        return 0;
+    }
+
+    @Override
+    public Bounds getCharacterBounds(int offset) {
+        return new Bounds(0, 0, getWidth(), getHeight());
+    }
+
+    @Override
+    public void imageChanged(ImageNode imageNode, Image previousImage) {
+        invalidate();
+
+        Image image = imageNode.getImage();
+        if (image != null) {
+            image.getImageListeners().add(this);
+        }
+
+        if (previousImage != null) {
+            previousImage.getImageListeners().remove(this);
+        }
+    }
+
+    @Override
+    public void sizeChanged(Image image, int previousWidth, int previousHeight) {
+        invalidate();
+    }
+
+    @Override
+    public void baselineChanged(Image image, int previousBaseline) {
+        // TODO Invalidate once baseline alignment of node view is supported
+    }
+
+    @Override
+    public void regionUpdated(Image image, int x, int y, int width, int height) {
+        // TODO Repaint the corresponding area of the component (add a repaint()
+        // method to NodeView to facilitate this as well as paint-only updates
+        // such as color changes)
+    }
+}
\ No newline at end of file