Package org.apache.pivot.wtk.skin

Source Code of org.apache.pivot.wtk.skin.TextAreaSkin

/*
* 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.awt.Shape;
import java.awt.Transparency;
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.Cursor;
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.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.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 ComponentSkin 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();
        }

        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 span) {
            // 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);

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

        @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 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
        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
        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;
        }
    }

    /**
     * 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;

                char c = ci.first();
                while (c != CharacterIterator.DONE
                    && lineWidth < breakWidth) {
                    if (Character.isWhitespace(c)) {
                        lastWhitespaceIndex = ci.getIndex();
                    }

                    int i = ci.getIndex();
                    Rectangle2D characterBounds = font.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 = font.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
        public void paint(Graphics2D graphics) {
            if (glyphVector != null) {
                TextArea textArea = (TextArea)getComponent();

                FontRenderContext fontRenderContext = Platform.getFontRenderContext();
                LineMetrics lm = font.getLineMetrics("", fontRenderContext);
                float ascent = lm.getAscent();

                graphics.setFont(font);

                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(color);
                    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(color);
                    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 = font.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;
        }

        @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
        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)
        }
    }

    private class BlinkCaretCallback implements Runnable {
        @Override
        public void run() {
            caretOn = !caretOn;

            if (selection == null) {
                TextArea textArea = (TextArea)getComponent();
                textArea.repaint(caret.x, caret.y, caret.width, caret.height);
            }
        }
    }

    private class ScrollSelectionCallback implements Runnable {
        @Override
        public void run() {
            TextArea textArea = (TextArea)getComponent();
            int selectionStart = textArea.getSelectionStart();
            int selectionLength = textArea.getSelectionLength();
            int selectionEnd = selectionStart + selectionLength - 1;

            switch (scrollDirection) {
                case FORWARD: {
                    // Get next offset
                    int offset = getNextInsertionPoint(mouseX, selectionEnd, scrollDirection);

                    if (offset != -1) {
                        // If the next character is a paragraph terminator and is not the
                        // final terminator character, increment the selection
                        Document document = textArea.getDocument();
                        if (document.getCharacterAt(offset) == '\n'
                            && offset < documentView.getCharacterCount() - 1) {
                            offset++;
                        }

                        textArea.setSelection(selectionStart, offset - selectionStart);
                        scrollCharacterToVisible(offset - 1);
                    }

                    break;
                }

                case BACKWARD: {
                    // Get previous offset
                    int offset = getNextInsertionPoint(mouseX, selectionStart, scrollDirection);

                    if (offset != -1) {
                        textArea.setSelection(offset, selectionEnd - offset + 1);
                        scrollCharacterToVisible(offset + 1);
                    }

                    break;
                }

                default: {
                    throw new RuntimeException();
                }
            }
        }
    }

    private DocumentView documentView = null;

    private int caretX = 0;
    private Rectangle caret = new Rectangle();
    private Area selection = null;

    private boolean caretOn = false;

    private int anchor = -1;
    private FocusTraversalDirection 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 inactiveColor;
    private Color backgroundColor;
    private Color selectionColor;
    private Color selectionBackgroundColor;
    private Color inactiveSelectionColor;
    private Color inactiveSelectionBackgroundColor;

    private Insets margin = new Insets(4);

    private boolean wrapText = true;

    private static final int PARAGRAPH_TERMINATOR_WIDTH = 4;
    private static final int SCROLL_RATE = 30;

    public TextAreaSkin() {
        Theme theme = Theme.getTheme();
        font = theme.getFont();
        color = Color.BLACK;
        inactiveColor = Color.GRAY;
        backgroundColor = null;
        selectionColor = Color.LIGHT_GRAY;
        selectionBackgroundColor = Color.BLACK;
        inactiveSelectionColor = Color.LIGHT_GRAY;
        inactiveSelectionBackgroundColor = Color.BLACK;
    }

    @Override
    public void install(Component component) {
        super.install(component);

        TextArea textArea = (TextArea)component;
        textArea.getTextAreaListeners().add(this);
        textArea.getTextAreaSelectionListeners().add(this);

        textArea.setCursor(Cursor.TEXT);

        Document document = textArea.getDocument();
        if (document != null) {
            documentView = (DocumentView)createNodeView(document);
            documentView.attach();
            updateSelection();
        }
    }

    @Override
    public boolean isFocusable() {
        return true;
    }

    @Override
    public int getPreferredWidth(int height) {
        int preferredWidth;

        if (documentView == null) {
           preferredWidth = 0;
        } else {
            documentView.setBreakWidth(Integer.MAX_VALUE);
            documentView.validate();

            preferredWidth = documentView.getWidth() + margin.left + margin.right;
        }

        return preferredWidth;
    }

    @Override
    public int getPreferredHeight(int width) {
        int preferredHeight;

        if (documentView == null
            || width == -1) {
            preferredHeight = 0;
        } else {
            int breakWidth;
            if (wrapText) {
                breakWidth = Math.max(width - (margin.left + margin.right), 0);
            } else {
                breakWidth = Integer.MAX_VALUE;
            }

            documentView.setBreakWidth(breakWidth);
            documentView.validate();

            preferredHeight = documentView.getHeight() + margin.top + margin.bottom;
        }

        return preferredHeight;
    }

    @Override
    public Dimensions getPreferredSize() {
        int preferredHeight;
        int preferredWidth;

        if (documentView == null) {
           preferredWidth = 0;
           preferredHeight = 0;
        } else {
            documentView.setBreakWidth(Integer.MAX_VALUE);
            documentView.validate();

            preferredWidth = documentView.getWidth() + margin.left + margin.right;
            preferredHeight = documentView.getHeight() + margin.top + margin.bottom;
        }

        return new Dimensions(preferredWidth, preferredHeight);
    }

    @Override
    public int getBaseline(int width, int height) {
        FontRenderContext fontRenderContext = Platform.getFontRenderContext();
        LineMetrics lm = font.getLineMetrics("", fontRenderContext);
        float ascent = lm.getAscent();
        return margin.top + Math.round(ascent);
    }

    @Override
    public void layout() {
        if (documentView != null) {
            TextArea textArea = (TextArea)getComponent();
            int width = getWidth();

            documentView.setBreakWidth(Math.max(width - (margin.left + margin.right), 0));
            documentView.validate();

            updateSelection();
            caretX = caret.x;

            if (textArea.isFocused()) {
                scrollCharacterToVisible(textArea.getSelectionStart());
            }

            showCaret(textArea.isFocused()
                && textArea.getSelectionLength() == 0);
        }
    }

    @Override
    public void paint(Graphics2D graphics) {
        TextArea textArea = (TextArea)getComponent();
        int width = getWidth();
        int height = getHeight();

        if (backgroundColor != null) {
            graphics.setColor(backgroundColor);
            graphics.fillRect(0, 0, width, height);
        }

        if (documentView != null) {
            // Draw the selection highlight
            if (selection != null) {
                graphics.setColor(textArea.isFocused()
                    && textArea.isEditable() ?
                    selectionBackgroundColor : inactiveSelectionBackgroundColor);
                graphics.fill(selection);
            }

            // Draw the document content
            graphics.translate(margin.left, margin.top);
            documentView.paint(graphics);
            graphics.translate(-margin.left, -margin.top);

            // Draw the caret
            if (selection == null
                && caretOn
                && textArea.isFocused()) {
                graphics.setColor(textArea.isEditable() ? color : inactiveColor);
                graphics.fill(caret);
            }
        }
    }

    @Override
    public boolean isOpaque() {
        return (backgroundColor != null
            && backgroundColor.getTransparency() == Transparency.OPAQUE);
    }

    @Override
    public int getInsertionPoint(int x, int y) {
        int offset;

        if (documentView == null) {
            offset = -1;
        } else {
            x = Math.min(documentView.getWidth() - 1, Math.max(x - margin.left, 0));

            if (y < margin.top) {
                offset = documentView.getNextInsertionPoint(x, -1, FocusTraversalDirection.FORWARD);
            } else if (y > documentView.getHeight() + margin.top) {
                offset = documentView.getNextInsertionPoint(x, -1, FocusTraversalDirection.BACKWARD);
            } else {
                offset = documentView.getInsertionPoint(x, y - margin.top);
            }
        }

        return offset;
    }

    @Override
    public int getNextInsertionPoint(int x, int from, FocusTraversalDirection direction) {
        int offset;

        if (documentView == null) {
            offset = -1;
        } else {
            offset = documentView.getNextInsertionPoint(x - margin.left, from, direction);
        }

        return offset;
    }

    @Override
    public int getRowIndex(int offset) {
        int rowIndex;

        if (documentView == null) {
            rowIndex = -1;
        } else {
            rowIndex = documentView.getRowIndex(offset);
        }

        return rowIndex;
    }

    @Override
    public int getRowCount() {
        int rowCount;

        if (documentView == null) {
            rowCount = 0;
        } else {
            rowCount = documentView.getRowCount();
        }

        return rowCount;
    }

    @Override
    public Bounds getCharacterBounds(int offset) {
        Bounds characterBounds;

        if (documentView == null) {
            characterBounds = null;
        } else {
            characterBounds = documentView.getCharacterBounds(offset);

            if (characterBounds != null) {
                characterBounds = characterBounds.translate(margin.left, margin.top);
            }
        }

        return characterBounds;
    }

    private void scrollCharacterToVisible(int offset) {
        TextArea textArea = (TextArea)getComponent();
        Bounds characterBounds = getCharacterBounds(offset);

        if (characterBounds != null) {
            textArea.scrollAreaToVisible(characterBounds.x, characterBounds.y,
                characterBounds.width, characterBounds.height);
        }
    }

    public Color getColor() {
        return color;
    }

    public void setColor(Color color) {
        if (color == null) {
            throw new IllegalArgumentException("color is null.");
        }

        this.color = color;
        repaintComponent();
    }

    public final void setColor(String color) {
        if (color == null) {
            throw new IllegalArgumentException("color is null.");
        }

        setColor(GraphicsUtilities.decodeColor(color));
    }

    public Color getInactiveColor() {
        return inactiveColor;
    }

    public void setInactiveColor(Color inactiveColor) {
        if (inactiveColor == null) {
            throw new IllegalArgumentException("inactiveColor is null.");
        }

        this.inactiveColor = inactiveColor;
        repaintComponent();
    }

    public final void setInactiveColor(String inactiveColor) {
        if (inactiveColor == null) {
            throw new IllegalArgumentException("inactiveColor is null.");
        }

        setColor(GraphicsUtilities.decodeColor(inactiveColor));
    }

    public Color getBackgroundColor() {
        return backgroundColor;
    }

    public void setBackgroundColor(Color backgroundColor) {
        this.backgroundColor = backgroundColor;
        repaintComponent();
    }

    public final void setBackgroundColor(String backgroundColor) {
        if (backgroundColor == null) {
            throw new IllegalArgumentException("backgroundColor is null");
        }

        setBackgroundColor(GraphicsUtilities.decodeColor(backgroundColor));
    }

    public Font getFont() {
        return font;
    }

    public void setFont(Font font) {
        if (font == null) {
            throw new IllegalArgumentException("font is null.");
        }

        this.font = font;
        invalidateComponent();
    }

    public final void setFont(String font) {
        if (font == null) {
            throw new IllegalArgumentException("font is null.");
        }

        setFont(decodeFont(font));
    }

    public final void setFont(Dictionary<String, ?> font) {
        if (font == null) {
            throw new IllegalArgumentException("font is null.");
        }

        setFont(Theme.deriveFont(font));
    }

    public Color getSelectionColor() {
        return selectionColor;
    }

    public void setSelectionColor(Color selectionColor) {
        if (selectionColor == null) {
            throw new IllegalArgumentException("selectionColor is null.");
        }

        this.selectionColor = selectionColor;
        repaintComponent();
    }

    public final void setSelectionColor(String selectionColor) {
        if (selectionColor == null) {
            throw new IllegalArgumentException("selectionColor is null.");
        }

        setSelectionColor(GraphicsUtilities.decodeColor(selectionColor));
    }

    public Color getSelectionBackgroundColor() {
        return selectionBackgroundColor;
    }

    public void setSelectionBackgroundColor(Color selectionBackgroundColor) {
        if (selectionBackgroundColor == null) {
            throw new IllegalArgumentException("selectionBackgroundColor is null.");
        }

        this.selectionBackgroundColor = selectionBackgroundColor;
        repaintComponent();
    }

    public final void setSelectionBackgroundColor(String selectionBackgroundColor) {
        if (selectionBackgroundColor == null) {
            throw new IllegalArgumentException("selectionBackgroundColor is null.");
        }

        setSelectionBackgroundColor(GraphicsUtilities.decodeColor(selectionBackgroundColor));
    }

    public Color getInactiveSelectionColor() {
        return inactiveSelectionColor;
    }

    public void setInactiveSelectionColor(Color inactiveSelectionColor) {
        if (inactiveSelectionColor == null) {
            throw new IllegalArgumentException("inactiveSelectionColor is null.");
        }

        this.inactiveSelectionColor = inactiveSelectionColor;
        repaintComponent();
    }

    public final void setInactiveSelectionColor(String inactiveSelectionColor) {
        if (inactiveSelectionColor == null) {
            throw new IllegalArgumentException("inactiveSelectionColor is null.");
        }

        setInactiveSelectionColor(GraphicsUtilities.decodeColor(inactiveSelectionColor));
    }

    public Color getInactiveSelectionBackgroundColor() {
        return inactiveSelectionBackgroundColor;
    }

    public void setInactiveSelectionBackgroundColor(Color inactiveSelectionBackgroundColor) {
        if (inactiveSelectionBackgroundColor == null) {
            throw new IllegalArgumentException("inactiveSelectionBackgroundColor is null.");
        }

        this.inactiveSelectionBackgroundColor = inactiveSelectionBackgroundColor;
        repaintComponent();
    }

    public final void setInactiveSelectionBackgroundColor(String inactiveSelectionBackgroundColor) {
        if (inactiveSelectionBackgroundColor == null) {
            throw new IllegalArgumentException("inactiveSelectionBackgroundColor is null.");
        }

        setInactiveSelectionBackgroundColor(GraphicsUtilities.decodeColor(inactiveSelectionBackgroundColor));
    }

    public Insets getMargin() {
        return margin;
    }

    public void setMargin(Insets margin) {
        if (margin == null) {
            throw new IllegalArgumentException("margin is null.");
        }

        this.margin = margin;
        invalidateComponent();
    }

    public final void setMargin(Dictionary<String, ?> margin) {
        if (margin == null) {
            throw new IllegalArgumentException("margin is null.");
        }

        setMargin(new Insets(margin));
    }

    public final void setMargin(int margin) {
        setMargin(new Insets(margin));
    }

    public final void setMargin(Number margin) {
        if (margin == null) {
            throw new IllegalArgumentException("margin is null.");
        }

        setMargin(margin.intValue());
    }

    public final void setMargin(String margin) {
        if (margin == null) {
            throw new IllegalArgumentException("margin is null.");
        }

        setMargin(Insets.decode(margin));
    }

    public boolean getWrapText() {
        return wrapText;
    }

    public void setWrapText(boolean wrapText) {
        if (this.wrapText != wrapText) {
            this.wrapText = wrapText;

            if (documentView != null) {
                documentView.invalidate();
            }
        }
    }
    @Override
    public boolean mouseMove(Component component, int x, int y) {
        boolean consumed = super.mouseMove(component, x, y);

        if (Mouse.getCapturer() == component) {
            TextArea textArea = (TextArea)getComponent();

            Bounds visibleArea = textArea.getVisibleArea();
            visibleArea = new Bounds(visibleArea.x, visibleArea.y,
                visibleArea.width, visibleArea.height);

            if (y >= visibleArea.y
                && y < visibleArea.y + visibleArea.height) {
                // Stop the scroll selection timer
                if (scheduledScrollSelectionCallback != null) {
                    scheduledScrollSelectionCallback.cancel();
                    scheduledScrollSelectionCallback = null;
                }

                scrollDirection = null;
                int offset = getInsertionPoint(x, y);

                if (offset != -1) {
                    // Select the range
                    if (offset > anchor) {
                        textArea.setSelection(anchor, offset - anchor);
                    } else {
                        textArea.setSelection(offset, anchor - offset);
                    }
                }
            } else {
                if (scheduledScrollSelectionCallback == null) {
                    scrollDirection = (y < visibleArea.y) ? FocusTraversalDirection.BACKWARD : FocusTraversalDirection.FORWARD;

                    scheduledScrollSelectionCallback =
                        ApplicationContext.scheduleRecurringCallback(scrollSelectionCallback,
                            SCROLL_RATE);

                    // Run the callback once now to scroll the selection immediately
                    scrollSelectionCallback.run();
                }
            }

            mouseX = x;
        } else {
            if (Mouse.isPressed(Mouse.Button.LEFT)
                && Mouse.getCapturer() == null
                && anchor != -1) {
                // Capture the mouse so we can select text
                Mouse.capture(component);
            }
        }

        return consumed;
    }

    @Override
    public boolean mouseDown(Component component, Mouse.Button button, int x, int y) {
        boolean consumed = super.mouseDown(component, button, x, y);

        if (button == Mouse.Button.LEFT) {
            TextArea textArea = (TextArea)component;

            anchor = getInsertionPoint(x, y);

            if (anchor != -1) {
                if (Keyboard.isPressed(Keyboard.Modifier.SHIFT)) {
                    // Select the range
                    int selectionStart = textArea.getSelectionStart();

                    if (anchor > selectionStart) {
                        textArea.setSelection(selectionStart, anchor - selectionStart);
                    } else {
                        textArea.setSelection(anchor, selectionStart - anchor);
                    }
                } else {
                    // Move the caret to the insertion point
                    textArea.setSelection(anchor, 0);
                    consumed = true;
                }
            }

            caretX = caret.x;

            // Set focus to the text input
            textArea.requestFocus();
        }

        return consumed;
    }

    @Override
    public boolean mouseUp(Component component, Mouse.Button button, int x, int y) {
        boolean consumed = super.mouseUp(component, button, x, y);

        if (Mouse.getCapturer() == component) {
            // Stop the scroll selection timer
            if (scheduledScrollSelectionCallback != null) {
                scheduledScrollSelectionCallback.cancel();
                scheduledScrollSelectionCallback = null;
            }

            Mouse.release();
        }

        anchor = -1;
        scrollDirection = null;
        mouseX = -1;

        return consumed;
    }


    @Override
    public boolean keyTyped(final Component component, char character) {
        boolean consumed = super.keyTyped(component, character);

        final TextArea textArea = (TextArea)getComponent();

        if (textArea.isEditable()) {
            Document document = textArea.getDocument();

            if (document != null) {
                // Ignore characters in the control range and the ASCII delete
                // character as well as meta key presses
                if (character > 0x1F
                    && character != 0x7F
                    && !Keyboard.isPressed(Keyboard.Modifier.META)) {
                    textArea.insert(character);
                    showCaret(true);
                }
            }
        }

        return consumed;
    }

    @Override
    public boolean keyPressed(final Component component, int keyCode, Keyboard.KeyLocation keyLocation) {
        boolean consumed = false;

        final TextArea textArea = (TextArea)getComponent();
        Document document = textArea.getDocument();

        Keyboard.Modifier commandModifier = Platform.getCommandModifier();
        if (document != null) {
            if (keyCode == Keyboard.KeyCode.ENTER
                && textArea.isEditable()) {
                textArea.insertParagraph();

                consumed = true;
            } else if (keyCode == Keyboard.KeyCode.DELETE
                && textArea.isEditable()) {
                textArea.delete(false);

                consumed = true;
            } else if (keyCode == Keyboard.KeyCode.BACKSPACE
                && textArea.isEditable()) {
                textArea.delete(true);

                consumed = true;
            } else if (keyCode == Keyboard.KeyCode.LEFT) {
                int selectionStart = textArea.getSelectionStart();
                int selectionLength = textArea.getSelectionLength();

                if (Keyboard.isPressed(Keyboard.Modifier.SHIFT)) {
                    // Add the previous character to the selection
                    if (selectionStart > 0) {
                        selectionStart--;
                        selectionLength++;
                    }
                } else if (Keyboard.isPressed(Keyboard.Modifier.CTRL)) {
                    // Move the caret to the start of the next word to our left
                    if (selectionStart > 0) {
                        // first, skip over any space immediately to our left
                        while (selectionStart > 0
                                && Character.isWhitespace(document.getCharacterAt(selectionStart - 1))) {
                            selectionStart--;
                        }
                        // then, skip over any word-letters to our left
                        while (selectionStart > 0
                                && !Character.isWhitespace(document.getCharacterAt(selectionStart - 1))) {
                            selectionStart--;
                        }

                        selectionLength = 0;
                    }
                } else {
                    // Clear the selection and move the caret back by one
                    // character
                    if (selectionLength == 0
                        && selectionStart > 0) {
                        selectionStart--;
                    }

                    selectionLength = 0;
                }

                textArea.setSelection(selectionStart, selectionLength);
                scrollCharacterToVisible(selectionStart);

                caretX = caret.x;

                consumed = true;
            } else if (keyCode == Keyboard.KeyCode.RIGHT) {
                int selectionStart = textArea.getSelectionStart();
                int selectionLength = textArea.getSelectionLength();

                if (Keyboard.isPressed(Keyboard.Modifier.SHIFT)) {
                    // Add the next character to the selection
                    if (selectionStart + selectionLength < document.getCharacterCount()) {
                        selectionLength++;
                    }

                    textArea.setSelection(selectionStart, selectionLength);
                    scrollCharacterToVisible(selectionStart + selectionLength);
                } else if (Keyboard.isPressed(Keyboard.Modifier.CTRL)) {
                    // Move the caret to the start of the next word to our right
                    if (selectionStart < document.getCharacterCount()) {
                        // first, skip over any word-letters to our right
                        while (selectionStart < document.getCharacterCount() - 1
                                && !Character.isWhitespace(document.getCharacterAt(selectionStart))) {
                            selectionStart++;
                        }
                        // then, skip over any space immediately to our right
                        while (selectionStart < document.getCharacterCount() - 1
                                && Character.isWhitespace(document.getCharacterAt(selectionStart))) {
                            selectionStart++;
                        }

                        textArea.setSelection(selectionStart, 0);
                        scrollCharacterToVisible(selectionStart);

                        caretX = caret.x;
                    }
                } else {
                    // Clear the selection and move the caret forward by one
                    // character
                    if (selectionLength > 0) {
                        selectionStart += selectionLength - 1;
                    }

                    if (selectionStart < document.getCharacterCount() - 1) {
                        selectionStart++;
                    }

                    textArea.setSelection(selectionStart, 0);
                    scrollCharacterToVisible(selectionStart);

                    caretX = caret.x;
                }

                consumed = true;
            } else if (keyCode == Keyboard.KeyCode.UP) {
                int selectionStart = textArea.getSelectionStart();

                int offset = getNextInsertionPoint(caretX, selectionStart, FocusTraversalDirection.BACKWARD);

                if (offset == -1) {
                    offset = 0;
                }

                int selectionLength;
                if (Keyboard.isPressed(Keyboard.Modifier.SHIFT)) {
                    int selectionEnd = selectionStart + textArea.getSelectionLength() - 1;
                    selectionLength = selectionEnd - offset + 1;
                } else {
                    selectionLength = 0;
                }

                textArea.setSelection(offset, selectionLength);
                scrollCharacterToVisible(offset);

                consumed = true;
            } else if (keyCode == Keyboard.KeyCode.DOWN) {
                int selectionStart = textArea.getSelectionStart();
                int selectionLength = textArea.getSelectionLength();

                if (Keyboard.isPressed(Keyboard.Modifier.SHIFT)) {
                    int from;
                    int x;
                    if (selectionLength == 0) {
                        // Get next insertion point from leading selection character
                        from = selectionStart;
                        x = caretX;
                    } else {
                        // Get next insertion point from right edge of trailing selection
                        // character
                        from = selectionStart + selectionLength - 1;

                        Bounds trailingSelectionBounds = getCharacterBounds(from);
                        x = trailingSelectionBounds.x + trailingSelectionBounds.width;
                    }

                    int offset = getNextInsertionPoint(x, from, FocusTraversalDirection.FORWARD);

                    if (offset == -1) {
                        offset = documentView.getCharacterCount() - 1;
                    } else {
                        // If the next character is a paragraph terminator and is not the
                        // final terminator character, increment the selection
                        if (document.getCharacterAt(offset) == '\n'
                            && offset < documentView.getCharacterCount() - 1) {
                            offset++;
                        }
                    }

                    textArea.setSelection(selectionStart, offset - selectionStart);
                    scrollCharacterToVisible(offset);
                } else {
                    int from;
                    if (selectionLength == 0) {
                        // Get next insertion point from leading selection character
                        from = selectionStart;
                    } else {
                        // Get next insertion point from trailing selection character
                        from = selectionStart + selectionLength - 1;
                    }

                    int offset = getNextInsertionPoint(caretX, from, FocusTraversalDirection.FORWARD);

                    if (offset == -1) {
                        offset = documentView.getCharacterCount() - 1;
                    }

                    textArea.setSelection(offset, 0);
                    scrollCharacterToVisible(offset);
                }

                consumed = true;
            } else if (Keyboard.isPressed(commandModifier)) {
                if (keyCode == Keyboard.KeyCode.A) {
                    textArea.setSelection(0, document.getCharacterCount());
                    consumed = true;
                } else if (keyCode == Keyboard.KeyCode.X
                    && textArea.isEditable()) {
                    textArea.cut();
                    consumed = true;
                } else if (keyCode == Keyboard.KeyCode.C) {
                    textArea.copy();
                    consumed = true;
                } else if (keyCode == Keyboard.KeyCode.V
                    && textArea.isEditable()) {
                    textArea.paste();
                    consumed = true;
                } else if (keyCode == Keyboard.KeyCode.Z
                    && textArea.isEditable()) {
                    if (Keyboard.isPressed(Keyboard.Modifier.SHIFT)) {
                        textArea.undo();
                    } else {
                        textArea.redo();
                    }

                    consumed = true;
                }
            } else if (keyCode == Keyboard.KeyCode.HOME) {
                // Move the caret to the beginning of the text
                if (Keyboard.isPressed(Keyboard.Modifier.SHIFT)) {
                    textArea.setSelection(0, textArea.getSelectionStart());
                } else {
                    textArea.setSelection(0, 0);
                }
                scrollCharacterToVisible(0);

                consumed = true;
            } else if (keyCode == Keyboard.KeyCode.END) {
                // Move the caret to the end of the text
                if (Keyboard.isPressed(Keyboard.Modifier.SHIFT)) {
                    int selectionStart = textArea.getSelectionStart();
                    textArea.setSelection(selectionStart, textArea.getCharacterCount()
                        - selectionStart);
                } else {
                    textArea.setSelection(textArea.getCharacterCount() - 1, 0);
                }
                scrollCharacterToVisible(textArea.getCharacterCount() - 1);

                consumed = true;
            } else {
                consumed = super.keyPressed(component, keyCode, keyLocation);
            }
        }

        return consumed;
    }

    // Component state events
    @Override
    public void enabledChanged(Component component) {
        super.enabledChanged(component);

        repaintComponent();
    }

    @Override
    public void focusedChanged(Component component, Component obverseComponent) {
        super.focusedChanged(component, obverseComponent);

        TextArea textArea = (TextArea)getComponent();
        if (textArea.isFocused()
            && textArea.getSelectionLength() == 0) {
            scrollCharacterToVisible(textArea.getSelectionStart());
            showCaret(true);
        } else {
            showCaret(false);
        }

        repaintComponent();
    }

    // Text area events
    @Override
    public void documentChanged(TextArea textArea, Document previousDocument) {
        if (documentView != null) {
            documentView.detach();
            documentView = null;
        }

        Document document = textArea.getDocument();
        if (document != null) {
            documentView = (DocumentView)createNodeView(document);
            documentView.attach();
        }

        invalidateComponent();
    }

    @Override
    public void editableChanged(TextArea textArea) {
        // No-op
    }

    // Text area selection events
    @Override
    public void selectionChanged(TextArea textArea, int previousSelectionStart,
        int previousSelectionLength) {
        // If the document view is valid, repaint the selection state; otherwise,
        // the selection will be updated in layout()
        if (documentView != null
            && documentView.isValid()) {
            if (selection == null) {
                // Repaint previous caret bounds
                textArea.repaint(caret.x, caret.y, caret.width, caret.height);
            } else {
                // Repaint previous selection bounds
                Rectangle bounds = selection.getBounds();
                textArea.repaint(bounds.x, bounds.y, bounds.width, bounds.height);
            }

            updateSelection();

            if (selection == null) {
                showCaret(textArea.isFocused());
            } else {
                showCaret(false);

                // Repaint current selection bounds
                Rectangle bounds = selection.getBounds();
                textArea.repaint(bounds.x, bounds.y, bounds.width, bounds.height);
            }
        }
    }

    private NodeView createNodeView(Node node) {
        NodeView nodeView = null;

        if (node instanceof Document) {
            nodeView = new DocumentView((Document)node);
        } else if (node instanceof Paragraph) {
            nodeView = new ParagraphView((Paragraph)node);
        } else if (node instanceof TextNode) {
            nodeView = new TextNodeView((TextNode)node);
        } else if (node instanceof ImageNode) {
            nodeView = new ImageNodeView((ImageNode)node);
        } else {
            throw new IllegalArgumentException("Unsupported node type: "
                + node.getClass().getName());
        }

        return nodeView;
    }

    private void updateSelection() {
        if (documentView.getCharacterCount() > 0) {
            TextArea textArea = (TextArea)getComponent();

            // 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;
        }
    }
}
TOP

Related Classes of org.apache.pivot.wtk.skin.TextAreaSkin

TOP
Copyright © 2018 www.massapi.com. All rights reserved.
All source code are property of their respective owners. Java is a trademark of Sun Microsystems, Inc and owned by ORACLE Inc. Contact coftware#gmail.com.