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

svn commit: r995869 - in /pivot/trunk/wtk/src/org/apache/pivot/wtk/skin: TextAreaSkin2.java TextAreaSkinParagraphView2.java

Author: gbrown
Date: Fri Sep 10 16:59:33 2010
New Revision: 995869

URL: http://svn.apache.org/viewvc?rev=995869&view=rev
Log:
Move TextAreaSkin2$ParagraphView into its own class.

Added:
    pivot/trunk/wtk/src/org/apache/pivot/wtk/skin/TextAreaSkinParagraphView2.java
Modified:
    pivot/trunk/wtk/src/org/apache/pivot/wtk/skin/TextAreaSkin2.java

Modified: pivot/trunk/wtk/src/org/apache/pivot/wtk/skin/TextAreaSkin2.java
URL: http://svn.apache.org/viewvc/pivot/trunk/wtk/src/org/apache/pivot/wtk/skin/TextAreaSkin2.java?rev=995869&r1=995868&r2=995869&view=diff
==============================================================================
--- pivot/trunk/wtk/src/org/apache/pivot/wtk/skin/TextAreaSkin2.java (original)
+++ pivot/trunk/wtk/src/org/apache/pivot/wtk/skin/TextAreaSkin2.java Fri Sep 10 16:59:33 2010
@@ -20,21 +20,16 @@ import java.awt.Color;
 import java.awt.Font;
 import java.awt.Graphics2D;
 import java.awt.Rectangle;
-import java.awt.Shape;
 import java.awt.Toolkit;
 import java.awt.Transparency;
 import java.awt.font.FontRenderContext;
-import java.awt.font.GlyphVector;
 import java.awt.font.LineMetrics;
-import java.awt.geom.AffineTransform;
 import java.awt.geom.Area;
-import java.awt.geom.Rectangle2D;
 import java.util.Locale;
 
 import org.apache.pivot.collections.ArrayList;
 import org.apache.pivot.collections.Dictionary;
 import org.apache.pivot.collections.Sequence;
-import org.apache.pivot.text.CharSequenceCharacterIterator;
 import org.apache.pivot.wtk.ApplicationContext;
 import org.apache.pivot.wtk.Bounds;
 import org.apache.pivot.wtk.Component;
@@ -45,415 +40,17 @@ 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.Span;
 import org.apache.pivot.wtk.TextArea2;
 import org.apache.pivot.wtk.TextAreaListener2;
 import org.apache.pivot.wtk.TextAreaContentListener2;
 import org.apache.pivot.wtk.TextAreaSelectionListener2;
 import org.apache.pivot.wtk.Theme;
-import org.apache.pivot.wtk.Visual;
 
 /**
  * Text area skin.
  */
 public class TextAreaSkin2 extends ComponentSkin implements TextArea2.Skin, TextAreaListener2,
     TextAreaContentListener2, TextAreaSelectionListener2 {
-    /**
-     * Class representing a row of text within a paragraph.
-     */
-    public static class Row {
-        public final GlyphVector glyphVector;
-        public final int offset;
-
-        public Row(GlyphVector glyphVector, int offset) {
-            this.glyphVector = glyphVector;
-            this.offset = offset;
-        }
-    }
-
-    /**
-     * Paragraph view.
-     */
-    public class ParagraphView implements Visual, TextArea2.ParagraphListener {
-        private TextArea2.Paragraph paragraph;
-
-        private int x = 0;
-        private int y = 0;
-        private float width = 0;
-        private float height = 0;
-
-        private int breakWidth = Integer.MAX_VALUE;
-
-        private int rowOffset = 0;
-
-        private boolean valid = false;
-        private ArrayList<Row> rows = new ArrayList<Row>();
-
-        public ParagraphView(TextArea2.Paragraph paragraph) {
-            this.paragraph = paragraph;
-        }
-
-        public TextArea2.Paragraph getParagraph() {
-            return paragraph;
-        }
-
-        @Override
-        public int getWidth() {
-            validate();
-            return (int)Math.ceil(width);
-        }
-
-        @Override
-        public int getHeight() {
-            validate();
-            return (int)Math.ceil(height);
-        }
-
-        public int getBreakWidth() {
-            return breakWidth;
-        }
-
-        public void setBreakWidth(int breakWidth) {
-            int previousBreakWidth = this.breakWidth;
-            this.breakWidth = breakWidth;
-
-            if (previousBreakWidth > breakWidth) {
-                invalidate();
-            }
-        }
-
-        @Override
-        public int getBaseline() {
-            return -1;
-        }
-
-        @Override
-        public void paint(Graphics2D graphics) {
-            TextArea2 textArea = (TextArea2)getComponent();
-
-            int selectionStart = textArea.getSelectionStart();
-            int selectionLength = textArea.getSelectionLength();
-            Span selectionRange = new Span(selectionStart, selectionStart + selectionLength - 1);
-
-            int paragraphOffset = paragraph.getOffset();
-            Span characterRange = new Span(paragraphOffset, paragraphOffset
-                + paragraph.getCharacters().length() - 1);
-
-            if (selectionLength > 0
-                && characterRange.intersects(selectionRange)) {
-                boolean focused = textArea.isFocused();
-
-                // Determine the selected and unselected areas
-                Area selectedArea = selection.createTransformedArea(AffineTransform.getTranslateInstance(-x, -y));
-                Area unselectedArea = new Area();
-                unselectedArea.add(new Area(new Rectangle2D.Float(0, 0, width, height)));
-                unselectedArea.subtract(new Area(selectedArea));
-
-                // Paint the unselected text
-                Graphics2D unselectedGraphics = (Graphics2D)graphics.create();
-                unselectedGraphics.clip(unselectedArea);
-                paint(unselectedGraphics, focused, false);
-                unselectedGraphics.dispose();
-
-                // Paint the selected text
-                Graphics2D selectedGraphics = (Graphics2D)graphics.create();
-                selectedGraphics.clip(selectedArea);
-                paint(selectedGraphics, focused, true);
-                selectedGraphics.dispose();
-            } else {
-                paint(graphics, textArea.isFocused(), false);
-            }
-        }
-
-        private void paint(Graphics2D graphics, boolean focused, boolean selected) {
-            int width = getWidth();
-
-            FontRenderContext fontRenderContext = Platform.getFontRenderContext();
-            LineMetrics lm = font.getLineMetrics("", fontRenderContext);
-            float ascent = lm.getAscent();
-            float rowHeight = ascent + lm.getDescent();
-
-            Rectangle clipBounds = graphics.getClipBounds();
-
-            float rowY = 0;
-            for (int i = 0, n = rows.getLength(); i < n; i++) {
-                Row row = rows.get(i);
-
-                Rectangle2D textBounds = row.glyphVector.getLogicalBounds();
-                float rowWidth = (float)textBounds.getWidth();
-
-                float rowX = 0;
-                switch (horizontalAlignment) {
-                    case LEFT: {
-                        rowX = 0;
-                        break;
-                    }
-
-                    case RIGHT: {
-                        rowX = width - rowWidth;
-                        break;
-                    }
-
-                    case CENTER: {
-                        rowX = (width - rowWidth) / 2;
-                        break;
-                    }
-                }
-
-                if (clipBounds.intersects(new Rectangle2D.Float(rowX, rowY, rowWidth, rowHeight))) {
-                    if (selected) {
-                        // TODO
-                        graphics.setPaint(focused ? selectionColor : inactiveSelectionColor);
-
-                        graphics.drawGlyphVector(row.glyphVector, rowX, rowY + ascent);
-                    } else {
-                        graphics.setPaint(color);
-                        graphics.drawGlyphVector(row.glyphVector, rowX, rowY + ascent);
-                    }
-                }
-
-                rowY += textBounds.getHeight();
-            }
-        }
-
-        public void invalidate() {
-            valid = false;
-        }
-
-        public void validate() {
-            // TODO Validate from known invalid offset rather than 0, so we don't need to
-            // recalculate all glyph vectors
-            if (!valid) {
-                rows = new ArrayList<Row>();
-                width = 0;
-                height = 0;
-
-                // Re-layout glyphs and recalculate size
-                FontRenderContext fontRenderContext = Platform.getFontRenderContext();
-
-                CharSequence characters = paragraph.getCharacters();
-                int n = characters.length();
-
-                int i = 0;
-                int start = 0;
-                float rowWidth = 0;
-                int lastWhitespaceIndex = -1;
-
-                // NOTE We use a character iterator here only because it is the most
-                // efficient way to measure the character bounds (as of Java 6, the version
-                // of Font#getStringBounds() that takes a String performs a string copy,
-                // whereas the version that takes a character iterator does not)
-                CharSequenceCharacterIterator ci = new CharSequenceCharacterIterator(characters);
-                while (i < n) {
-                    char c = characters.charAt(i);
-                    if (Character.isWhitespace(c)) {
-                        lastWhitespaceIndex = i;
-                    }
-
-                    Rectangle2D characterBounds = font.getStringBounds(ci, i, i + 1,
-                        fontRenderContext);
-                    rowWidth += characterBounds.getWidth();
-
-                    if (rowWidth > breakWidth) {
-                        if (lastWhitespaceIndex == -1) {
-                            if (start == i) {
-                                appendLine(characters, start, start + 1, fontRenderContext);
-                            } else {
-                                appendLine(characters, start, i, fontRenderContext);
-                                i--;
-                            }
-                        } else {
-                            appendLine(characters, start, lastWhitespaceIndex + 1,
-                                fontRenderContext);
-                            i = lastWhitespaceIndex;
-                        }
-
-                        start = i + 1;
-
-                        rowWidth = 0;
-                        lastWhitespaceIndex = -1;
-                    }
-
-                    i++;
-                }
-
-                appendLine(characters, start, i, fontRenderContext);
-            }
-
-            valid = true;
-        }
-
-        private void appendLine(CharSequence characters, int start, int end,
-            FontRenderContext fontRenderContext) {
-            CharSequenceCharacterIterator line = new CharSequenceCharacterIterator(characters,
-                start, end, start);
-            GlyphVector glyphVector = font.createGlyphVector(fontRenderContext, line);
-            rows.add(new Row(glyphVector, start));
-
-            Rectangle2D textBounds = glyphVector.getLogicalBounds();
-            width = Math.max(width, (float)textBounds.getWidth());
-            height += textBounds.getHeight();
-        }
-
-        public int getInsertionPoint(int x, int y) {
-            FontRenderContext fontRenderContext = Platform.getFontRenderContext();
-            LineMetrics lm = font.getLineMetrics("", fontRenderContext);
-            float rowHeight = lm.getAscent() + lm.getDescent();
-
-            int i = (int)Math.floor((float)y / rowHeight);
-
-            return getRowInsertionPoint(i, x);
-        }
-
-        public int getNextInsertionPoint(int x, int from, TextArea2.ScrollDirection direction) {
-            // Identify the row that contains the from index
-            int n = rows.getLength();
-            int i;
-            if (from == -1) {
-                i = (direction == TextArea2.ScrollDirection.DOWN) ? -1 : n;
-            } else {
-                i = getRowAt(from);
-            }
-
-            // Move to the next or previous row
-            if (direction == TextArea2.ScrollDirection.DOWN) {
-                i++;
-            } else {
-                i--;
-            }
-
-            return (i < 0
-                || i >= n) ? -1 : getRowInsertionPoint(i, x);
-        }
-
-        private int getRowInsertionPoint(int rowIndex, float x) {
-            Row row = rows.get(rowIndex);
-
-            Rectangle2D glyphVectorBounds = row.glyphVector.getLogicalBounds();
-            float rowWidth = (float)glyphVectorBounds.getWidth();
-
-            // Translate x to glyph vector coordinates
-            float rowX = 0;
-            switch (horizontalAlignment) {
-                case LEFT: {
-                    rowX = 0;
-                    break;
-                }
-
-                case RIGHT: {
-                    rowX = width - rowWidth;
-                    break;
-                }
-
-                case CENTER: {
-                    rowX = (width - rowWidth) / 2;
-                    break;
-                }
-            }
-
-            x -= rowX;
-
-            int index;
-            if (x < 0) {
-                index = 0;
-            } else if (x > rowWidth) {
-                index = row.glyphVector.getNumGlyphs();
-
-                // If this is not the last row, decrement the index so the insertion
-                // point remains on this line
-                if (rowIndex < rows.getLength() - 1) {
-                    index--;
-                }
-            } else {
-                index = 0;
-                int n = row.glyphVector.getNumGlyphs();
-
-                while (index < n) {
-                    Shape glyphBounds = row.glyphVector.getGlyphLogicalBounds(index);
-                    Rectangle2D glyphBounds2D = glyphBounds.getBounds2D();
-
-                    if (glyphBounds2D.contains(x, glyphBounds2D.getY())) {
-                        // Determine the bias; if the user clicks on the right half of the
-                        // character; select the next character
-                        if (x - glyphBounds2D.getX() > glyphBounds2D.getWidth() / 2
-                            && index < n - 1) {
-                            index++;
-                        }
-
-                        break;
-                    }
-
-                    index++;
-                }
-            }
-
-            return index + row.offset;
-        }
-
-        public int getRowAt(int index) {
-            int rowIndex = rows.getLength() - 1;
-            Row row = rows.get(rowIndex);
-
-            while (row.offset > index) {
-                row = rows.get(--rowIndex);
-            }
-
-            return rowIndex;
-        }
-
-        public int getRowCount() {
-            return rows.getLength();
-        }
-
-        public Bounds getCharacterBounds(int index) {
-            Bounds characterBounds = null;
-
-            CharSequence characters = paragraph.getCharacters();
-            int characterCount = characters.length();
-
-            int rowIndex, x, width;
-            if (index == characterCount) {
-                // This is the terminator character
-                rowIndex = rows.getLength() - 1;
-                Row row = rows.get(rowIndex);
-
-                Rectangle2D glyphVectorBounds = row.glyphVector.getLogicalBounds();
-                x = (int)Math.floor(glyphVectorBounds.getWidth());
-                width = PARAGRAPH_TERMINATOR_WIDTH;
-            } else {
-                // This is a visible character
-                rowIndex = getRowAt(index);
-                Row row = rows.get(rowIndex);
-
-                Shape glyphBounds = row.glyphVector.getGlyphLogicalBounds(index - row.offset);
-                Rectangle2D glyphBounds2D = glyphBounds.getBounds2D();
-                x = (int)Math.floor(glyphBounds2D.getX());
-                width = (int)Math.ceil(glyphBounds2D.getWidth());
-            }
-
-            FontRenderContext fontRenderContext = Platform.getFontRenderContext();
-            LineMetrics lm = font.getLineMetrics("", fontRenderContext);
-            float rowHeight = lm.getAscent() + lm.getDescent();
-
-            characterBounds = new Bounds(x, (int)Math.floor(rowIndex * rowHeight), width,
-                (int)Math.ceil(rowHeight));
-
-            return characterBounds;
-        }
-
-        @Override
-        public void textInserted(TextArea2.Paragraph paragraph, int index, int count) {
-            invalidate();
-            invalidateComponent();
-        }
-
-        @Override
-        public void textRemoved(TextArea2.Paragraph paragraph, int index, int count) {
-            invalidate();
-            invalidateComponent();
-        }
-    }
-
     private class BlinkCaretCallback implements Runnable {
         @Override
         public void run() {
@@ -532,15 +129,12 @@ public class TextAreaSkin2 extends Compo
     private Color selectionBackgroundColor;
     private Color inactiveSelectionColor;
     private Color inactiveSelectionBackgroundColor;
-
-    // TODO Rename to alignment? Or fix FlowPane/Text shape?
-    private HorizontalAlignment horizontalAlignment;
+    private HorizontalAlignment alignment;
     private Insets margin;
     private boolean wrapText;
 
-    private ArrayList<ParagraphView> paragraphViews = new ArrayList<ParagraphView>();
+    private ArrayList<TextAreaSkinParagraphView2> paragraphViews = new ArrayList<TextAreaSkinParagraphView2>();
 
-    private static final int PARAGRAPH_TERMINATOR_WIDTH = 2;
     private static final int SCROLL_RATE = 30;
 
     public TextAreaSkin2() {
@@ -553,7 +147,7 @@ public class TextAreaSkin2 extends Compo
         selectionBackgroundColor = Color.BLACK;
         inactiveSelectionColor = Color.LIGHT_GRAY;
         inactiveSelectionBackgroundColor = Color.BLACK;
-        horizontalAlignment = HorizontalAlignment.LEFT;
+        alignment = HorizontalAlignment.LEFT;
         margin = new Insets(4);
         wrapText = true;
     }
@@ -572,7 +166,7 @@ public class TextAreaSkin2 extends Compo
     public int getPreferredWidth(int height) {
         int preferredWidth = 0;
 
-        for (ParagraphView paragraphView : paragraphViews) {
+        for (TextAreaSkinParagraphView2 paragraphView : paragraphViews) {
             paragraphView.setBreakWidth(Integer.MAX_VALUE);
             preferredWidth = Math.max(preferredWidth, paragraphView.getWidth());
         }
@@ -590,7 +184,7 @@ public class TextAreaSkin2 extends Compo
         int breakWidth = (wrapText
             && width != -1) ? Math.max(width - (margin.left + margin.right), 0) : Integer.MAX_VALUE;
 
-        for (ParagraphView paragraphView : paragraphViews) {
+        for (TextAreaSkinParagraphView2 paragraphView : paragraphViews) {
             paragraphView.setBreakWidth(breakWidth);
             preferredHeight += paragraphView.getHeight();
         }
@@ -605,7 +199,7 @@ public class TextAreaSkin2 extends Compo
         int preferredWidth = 0;
         int preferredHeight = 0;
 
-        for (ParagraphView paragraphView : paragraphViews) {
+        for (TextAreaSkinParagraphView2 paragraphView : paragraphViews) {
             paragraphView.setBreakWidth(Integer.MAX_VALUE);
             preferredWidth = Math.max(preferredWidth, paragraphView.getWidth());
             preferredHeight += paragraphView.getHeight();
@@ -628,31 +222,31 @@ public class TextAreaSkin2 extends Compo
         int y = margin.top;
 
         int rowOffset = 0;
-        for (ParagraphView paragraphView : paragraphViews) {
+        for (TextAreaSkinParagraphView2 paragraphView : paragraphViews) {
             paragraphView.setBreakWidth(breakWidth);
 
             // Set location
-            switch (horizontalAlignment) {
+            switch (alignment) {
                 case LEFT: {
-                    paragraphView.x = margin.left;
+                    paragraphView.setX(margin.left);
                     break;
                 }
 
                 case RIGHT: {
-                    paragraphView.x = width - (paragraphView.getWidth() + margin.right);
+                    paragraphView.setX(width - (paragraphView.getWidth() + margin.right));
                     break;
                 }
 
                 case CENTER: {
-                    paragraphView.x = (width - paragraphView.getWidth()) / 2;
+                    paragraphView.setX((width - paragraphView.getWidth()) / 2);
                     break;
                 }
             }
 
-            paragraphView.y = y;
+            paragraphView.setY(y);
             y += paragraphView.getHeight();
 
-            paragraphView.rowOffset = rowOffset;
+            paragraphView.setRowOffset(rowOffset);
             rowOffset += paragraphView.getRowCount();
         }
 
@@ -705,11 +299,12 @@ public class TextAreaSkin2 extends Compo
         graphics.translate(0, margin.top);
 
         for (int i = 0, n = paragraphViews.getLength(); i < n; i++) {
-            ParagraphView paragraphView = paragraphViews.get(i);
+            TextAreaSkinParagraphView2 paragraphView = paragraphViews.get(i);
 
-            graphics.translate(paragraphView.x, 0);
+            int x = paragraphView.getX();
+            graphics.translate(x, 0);
             paragraphView.paint(graphics);
-            graphics.translate(-paragraphView.x, 0);
+            graphics.translate(-x, 0);
 
             graphics.translate(0, paragraphView.getHeight());
         }
@@ -726,23 +321,24 @@ public class TextAreaSkin2 extends Compo
         int index;
         if (y > getHeight() - margin.bottom) {
             // Select the character at x in the first row
-            ParagraphView paragraphView = paragraphViews.get(paragraphViews.getLength() - 1);
+            TextAreaSkinParagraphView2 paragraphView = paragraphViews.get(paragraphViews.getLength() - 1);
             index = paragraphView.getNextInsertionPoint(x, -1, TextArea2.ScrollDirection.UP)
-                + paragraphView.paragraph.getOffset();
+                + paragraphView.getParagraph().getOffset();
         } else if (y < margin.top) {
             // Select the character at x in the last row
-            ParagraphView paragraphView = paragraphViews.get(0);
+            TextAreaSkinParagraphView2 paragraphView = paragraphViews.get(0);
             index = paragraphView.getNextInsertionPoint(x, -1, TextArea2.ScrollDirection.DOWN);
         } else {
             // Select the character at x in the row at y
             index = -1;
             for (int i = 0, n = paragraphViews.getLength(); i < n; i++) {
-                ParagraphView paragraphView = paragraphViews.get(i);
+                TextAreaSkinParagraphView2 paragraphView = paragraphViews.get(i);
 
-                if (y >= paragraphView.y
-                    && y < paragraphView.y + paragraphView.getHeight()) {
-                    index = paragraphView.getInsertionPoint(x - paragraphView.x, y - paragraphView.y)
-                        + paragraphView.paragraph.getOffset();
+                int paragraphViewY = paragraphView.getY();
+                if (y >= paragraphViewY
+                    && y < paragraphViewY + paragraphView.getHeight()) {
+                    index = paragraphView.getInsertionPoint(x - paragraphView.getX(), y - paragraphViewY)
+                        + paragraphView.getParagraph().getOffset();
                     break;
                 }
             }
@@ -757,19 +353,19 @@ public class TextAreaSkin2 extends Compo
         if (from == -1) {
             int i = (direction == TextArea2.ScrollDirection.DOWN) ? 0 : paragraphViews.getLength() - 1;
 
-            ParagraphView paragraphView = paragraphViews.get(i);
-            index = paragraphView.getNextInsertionPoint(x - paragraphView.x, -1, direction);
+            TextAreaSkinParagraphView2 paragraphView = paragraphViews.get(i);
+            index = paragraphView.getNextInsertionPoint(x - paragraphView.getX(), -1, direction);
 
             if (index != -1) {
-                index += paragraphView.paragraph.getOffset();
+                index += paragraphView.getParagraph().getOffset();
             }
         } else {
             TextArea2 textArea = (TextArea2)getComponent();
             int i = textArea.getParagraphAt(from);
 
-            ParagraphView paragraphView = paragraphViews.get(i);
-            index = paragraphView.getNextInsertionPoint(x - paragraphView.x,
-                from - paragraphView.paragraph.getOffset(), direction);
+            TextAreaSkinParagraphView2 paragraphView = paragraphViews.get(i);
+            index = paragraphView.getNextInsertionPoint(x - paragraphView.getX(),
+                from - paragraphView.getParagraph().getOffset(), direction);
 
             if (index == -1) {
                 // Move to the next or previous paragraph view
@@ -780,12 +376,12 @@ public class TextAreaSkin2 extends Compo
                 }
 
                 if (paragraphView != null) {
-                    index = paragraphView.getNextInsertionPoint(x - paragraphView.x, -1, direction);
+                    index = paragraphView.getNextInsertionPoint(x - paragraphView.getX(), -1, direction);
                 }
             }
 
             if (index != -1) {
-                index += paragraphView.paragraph.getOffset();
+                index += paragraphView.getParagraph().getOffset();
             }
         }
 
@@ -795,17 +391,17 @@ public class TextAreaSkin2 extends Compo
     @Override
     public int getRowAt(int index) {
         TextArea2 textArea = (TextArea2)getComponent();
-        ParagraphView paragraphView = paragraphViews.get(textArea.getParagraphAt(index));
+        TextAreaSkinParagraphView2 paragraphView = paragraphViews.get(textArea.getParagraphAt(index));
 
-        return paragraphView.getRowAt(index - paragraphView.paragraph.getOffset())
-            + paragraphView.rowOffset;
+        return paragraphView.getRowAt(index - paragraphView.getParagraph().getOffset())
+            + paragraphView.getRowOffset();
     }
 
     @Override
     public int getRowCount() {
         int rowCount = 0;
 
-        for (ParagraphView paragraphView : paragraphViews) {
+        for (TextAreaSkinParagraphView2 paragraphView : paragraphViews) {
             rowCount += paragraphView.getRowCount();
         }
 
@@ -814,14 +410,19 @@ public class TextAreaSkin2 extends Compo
 
     public Bounds getCharacterBounds(int index) {
         TextArea2 textArea = (TextArea2)getComponent();
-        ParagraphView paragraphView = paragraphViews.get(textArea.getParagraphAt(index));
-        Bounds characterBounds = paragraphView.getCharacterBounds(index - paragraphView.paragraph.getOffset());
+        TextAreaSkinParagraphView2 paragraphView = paragraphViews.get(textArea.getParagraphAt(index));
+        Bounds characterBounds = paragraphView.getCharacterBounds(index
+            - paragraphView.getParagraph().getOffset());
 
-        return new Bounds(characterBounds.x + paragraphView.x,
-            characterBounds.y + paragraphView.y,
+        return new Bounds(characterBounds.x + paragraphView.getX(),
+            characterBounds.y + paragraphView.getY(),
             characterBounds.width, characterBounds.height);
     }
 
+    public Area getSelection() {
+        return selection;
+    }
+
     private void scrollCharacterToVisible(int index) {
         TextArea2 textArea = (TextArea2)getComponent();
         Bounds characterBounds = getCharacterBounds(index);
@@ -1004,25 +605,25 @@ public class TextAreaSkin2 extends Compo
         setInactiveSelectionBackgroundColor(GraphicsUtilities.decodeColor(inactiveSelectionBackgroundColor));
     }
 
-    public HorizontalAlignment getHorizontalAlignment() {
-        return horizontalAlignment;
+    public HorizontalAlignment getAlignment() {
+        return alignment;
     }
 
-    public void setHorizontalAlignment(HorizontalAlignment horizontalAlignment) {
-        if (horizontalAlignment == null) {
-            throw new IllegalArgumentException("horizontalAlignment is null.");
+    public void setAlignment(HorizontalAlignment alignment) {
+        if (alignment == null) {
+            throw new IllegalArgumentException("alignment is null.");
         }
 
-        this.horizontalAlignment = horizontalAlignment;
+        this.alignment = alignment;
         repaintComponent();
     }
 
-    public final void setHorizontalAlignment(String horizontalAlignment) {
-        if (horizontalAlignment == null) {
-            throw new IllegalArgumentException("horizontalAlignment is null.");
+    public final void setHorizontalAlignment(String alignment) {
+        if (alignment == null) {
+            throw new IllegalArgumentException("alignment is null.");
         }
 
-        setHorizontalAlignment(HorizontalAlignment.valueOf(horizontalAlignment.toUpperCase(Locale.ENGLISH)));
+        setAlignment(HorizontalAlignment.valueOf(alignment.toUpperCase(Locale.ENGLISH)));
     }
 
     public Insets getMargin() {
@@ -1506,7 +1107,7 @@ public class TextAreaSkin2 extends Compo
     public void paragraphInserted(TextArea2 textArea, int index) {
         // Create paragraph view and add as paragraph listener
         TextArea2.Paragraph paragraph = textArea.getParagraphs().get(index);
-        ParagraphView paragraphView = new ParagraphView(paragraph);
+        TextAreaSkinParagraphView2 paragraphView = new TextAreaSkinParagraphView2(this, paragraph);
         paragraph.getParagraphListeners().add(paragraphView);
 
         // Insert view
@@ -1520,7 +1121,7 @@ public class TextAreaSkin2 extends Compo
 
         for (int i = 0; i < count; i++) {
             TextArea2.Paragraph paragraph = removed.get(i);
-            ParagraphView paragraphView = paragraphViews.get(i + index);
+            TextAreaSkinParagraphView2 paragraphView = paragraphViews.get(i + index);
             paragraph.getParagraphListeners().remove(paragraphView);
         }
 

Added: pivot/trunk/wtk/src/org/apache/pivot/wtk/skin/TextAreaSkinParagraphView2.java
URL: http://svn.apache.org/viewvc/pivot/trunk/wtk/src/org/apache/pivot/wtk/skin/TextAreaSkinParagraphView2.java?rev=995869&view=auto
==============================================================================
--- pivot/trunk/wtk/src/org/apache/pivot/wtk/skin/TextAreaSkinParagraphView2.java (added)
+++ pivot/trunk/wtk/src/org/apache/pivot/wtk/skin/TextAreaSkinParagraphView2.java Fri Sep 10 16:59:33 2010
@@ -0,0 +1,462 @@
+/*
+ * 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.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.AffineTransform;
+import java.awt.geom.Area;
+import java.awt.geom.Rectangle2D;
+
+import org.apache.pivot.collections.ArrayList;
+import org.apache.pivot.text.CharSequenceCharacterIterator;
+import org.apache.pivot.wtk.Bounds;
+import org.apache.pivot.wtk.HorizontalAlignment;
+import org.apache.pivot.wtk.Platform;
+import org.apache.pivot.wtk.Span;
+import org.apache.pivot.wtk.TextArea2;
+import org.apache.pivot.wtk.Visual;
+
+class TextAreaSkinParagraphView2 implements Visual, TextArea2.ParagraphListener {
+    private static class Row {
+        public final GlyphVector glyphVector;
+        public final int offset;
+
+        public Row(GlyphVector glyphVector, int offset) {
+            this.glyphVector = glyphVector;
+            this.offset = offset;
+        }
+    }
+
+    private TextAreaSkin2 textAreaSkin;
+    private TextArea2.Paragraph paragraph;
+
+    private int x = 0;
+    private int y = 0;
+    private float width = 0;
+    private float height = 0;
+
+    private int breakWidth = Integer.MAX_VALUE;
+
+    private int rowOffset = 0;
+
+    private boolean valid = false;
+    private ArrayList<Row> rows = new ArrayList<Row>();
+
+    private static final int PARAGRAPH_TERMINATOR_WIDTH = 2;
+
+    public TextAreaSkinParagraphView2(TextAreaSkin2 textAreaSkin, TextArea2.Paragraph paragraph) {
+        this.textAreaSkin = textAreaSkin;
+        this.paragraph = paragraph;
+    }
+
+    public TextArea2.Paragraph getParagraph() {
+        return paragraph;
+    }
+
+    public int getX() {
+        return x;
+    }
+
+    public void setX(int x) {
+        this.x = x;
+    }
+
+    public int getY() {
+        return y;
+    }
+
+    public void setY(int y) {
+        this.y = y;
+    }
+
+    public int getRowOffset() {
+        return rowOffset;
+    }
+
+    public void setRowOffset(int rowOffset) {
+        this.rowOffset = rowOffset;
+    }
+
+    @Override
+    public int getWidth() {
+        validate();
+        return (int)Math.ceil(width);
+    }
+
+    @Override
+    public int getHeight() {
+        validate();
+        return (int)Math.ceil(height);
+    }
+
+    public int getBreakWidth() {
+        return breakWidth;
+    }
+
+    public void setBreakWidth(int breakWidth) {
+        int previousBreakWidth = this.breakWidth;
+        this.breakWidth = breakWidth;
+
+        if (previousBreakWidth > breakWidth) {
+            invalidate();
+        }
+    }
+
+    @Override
+    public int getBaseline() {
+        return -1;
+    }
+
+    @Override
+    public void paint(Graphics2D graphics) {
+        TextArea2 textArea = (TextArea2)textAreaSkin.getComponent();
+
+        int selectionStart = textArea.getSelectionStart();
+        int selectionLength = textArea.getSelectionLength();
+        Span selectionRange = new Span(selectionStart, selectionStart + selectionLength - 1);
+
+        int paragraphOffset = paragraph.getOffset();
+        Span characterRange = new Span(paragraphOffset, paragraphOffset
+            + paragraph.getCharacters().length() - 1);
+
+        if (selectionLength > 0
+            && characterRange.intersects(selectionRange)) {
+            boolean focused = textArea.isFocused();
+
+            // Determine the selected and unselected areas
+            Area selection = textAreaSkin.getSelection();
+            Area selectedArea = selection.createTransformedArea(AffineTransform.getTranslateInstance(-x, -y));
+            Area unselectedArea = new Area();
+            unselectedArea.add(new Area(new Rectangle2D.Float(0, 0, width, height)));
+            unselectedArea.subtract(new Area(selectedArea));
+
+            // Paint the unselected text
+            Graphics2D unselectedGraphics = (Graphics2D)graphics.create();
+            unselectedGraphics.clip(unselectedArea);
+            paint(unselectedGraphics, focused, false);
+            unselectedGraphics.dispose();
+
+            // Paint the selected text
+            Graphics2D selectedGraphics = (Graphics2D)graphics.create();
+            selectedGraphics.clip(selectedArea);
+            paint(selectedGraphics, focused, true);
+            selectedGraphics.dispose();
+        } else {
+            paint(graphics, textArea.isFocused(), false);
+        }
+    }
+
+    private void paint(Graphics2D graphics, boolean focused, boolean selected) {
+        int width = getWidth();
+
+        Font font = textAreaSkin.getFont();
+        FontRenderContext fontRenderContext = Platform.getFontRenderContext();
+        LineMetrics lm = font.getLineMetrics("", fontRenderContext);
+        float ascent = lm.getAscent();
+        float rowHeight = ascent + lm.getDescent();
+
+        Rectangle clipBounds = graphics.getClipBounds();
+
+        float rowY = 0;
+        for (int i = 0, n = rows.getLength(); i < n; i++) {
+            Row row = rows.get(i);
+
+            Rectangle2D textBounds = row.glyphVector.getLogicalBounds();
+            float rowWidth = (float)textBounds.getWidth();
+
+            float rowX = 0;
+            HorizontalAlignment alignment = textAreaSkin.getAlignment();
+            switch (alignment) {
+                case LEFT: {
+                    rowX = 0;
+                    break;
+                }
+
+                case RIGHT: {
+                    rowX = width - rowWidth;
+                    break;
+                }
+
+                case CENTER: {
+                    rowX = (width - rowWidth) / 2;
+                    break;
+                }
+            }
+
+            if (clipBounds.intersects(new Rectangle2D.Float(rowX, rowY, rowWidth, rowHeight))) {
+                if (selected) {
+                    graphics.setPaint(focused ?
+                        textAreaSkin.getSelectionColor() : textAreaSkin.getInactiveSelectionColor());
+                } else {
+                    graphics.setPaint(focused ?
+                        textAreaSkin.getColor() : textAreaSkin.getInactiveColor());
+                }
+
+                graphics.drawGlyphVector(row.glyphVector, rowX, rowY + ascent);
+            }
+
+            rowY += textBounds.getHeight();
+        }
+    }
+
+    public void invalidate() {
+        valid = false;
+    }
+
+    public void validate() {
+        // TODO Validate from known invalid offset rather than 0, so we don't need to
+        // recalculate all glyph vectors
+        if (!valid) {
+            rows = new ArrayList<Row>();
+            width = 0;
+            height = 0;
+
+            // Re-layout glyphs and recalculate size
+            Font font = textAreaSkin.getFont();
+            FontRenderContext fontRenderContext = Platform.getFontRenderContext();
+
+            CharSequence characters = paragraph.getCharacters();
+            int n = characters.length();
+
+            int i = 0;
+            int start = 0;
+            float rowWidth = 0;
+            int lastWhitespaceIndex = -1;
+
+            // NOTE We use a character iterator here only because it is the most
+            // efficient way to measure the character bounds (as of Java 6, the version
+            // of Font#getStringBounds() that takes a String performs a string copy,
+            // whereas the version that takes a character iterator does not)
+            CharSequenceCharacterIterator ci = new CharSequenceCharacterIterator(characters);
+            while (i < n) {
+                char c = characters.charAt(i);
+                if (Character.isWhitespace(c)) {
+                    lastWhitespaceIndex = i;
+                }
+
+                Rectangle2D characterBounds = font.getStringBounds(ci, i, i + 1,
+                    fontRenderContext);
+                rowWidth += characterBounds.getWidth();
+
+                if (rowWidth > breakWidth) {
+                    if (lastWhitespaceIndex == -1) {
+                        if (start == i) {
+                            appendLine(characters, start, start + 1, font, fontRenderContext);
+                        } else {
+                            appendLine(characters, start, i, font, fontRenderContext);
+                            i--;
+                        }
+                    } else {
+                        appendLine(characters, start, lastWhitespaceIndex + 1,
+                            font, fontRenderContext);
+                        i = lastWhitespaceIndex;
+                    }
+
+                    start = i + 1;
+
+                    rowWidth = 0;
+                    lastWhitespaceIndex = -1;
+                }
+
+                i++;
+            }
+
+            appendLine(characters, start, i, font, fontRenderContext);
+        }
+
+        valid = true;
+    }
+
+    private void appendLine(CharSequence characters, int start, int end,
+        Font font, FontRenderContext fontRenderContext) {
+        CharSequenceCharacterIterator line = new CharSequenceCharacterIterator(characters,
+            start, end, start);
+        GlyphVector glyphVector = font.createGlyphVector(fontRenderContext, line);
+        rows.add(new Row(glyphVector, start));
+
+        Rectangle2D textBounds = glyphVector.getLogicalBounds();
+        width = Math.max(width, (float)textBounds.getWidth());
+        height += textBounds.getHeight();
+    }
+
+    public int getInsertionPoint(int x, int y) {
+        Font font = textAreaSkin.getFont();
+        FontRenderContext fontRenderContext = Platform.getFontRenderContext();
+        LineMetrics lm = font.getLineMetrics("", fontRenderContext);
+        float rowHeight = lm.getAscent() + lm.getDescent();
+
+        int i = (int)Math.floor((float)y / rowHeight);
+
+        return getRowInsertionPoint(i, x);
+    }
+
+    public int getNextInsertionPoint(int x, int from, TextArea2.ScrollDirection direction) {
+        // Identify the row that contains the from index
+        int n = rows.getLength();
+        int i;
+        if (from == -1) {
+            i = (direction == TextArea2.ScrollDirection.DOWN) ? -1 : n;
+        } else {
+            i = getRowAt(from);
+        }
+
+        // Move to the next or previous row
+        if (direction == TextArea2.ScrollDirection.DOWN) {
+            i++;
+        } else {
+            i--;
+        }
+
+        return (i < 0
+            || i >= n) ? -1 : getRowInsertionPoint(i, x);
+    }
+
+    private int getRowInsertionPoint(int rowIndex, float x) {
+        Row row = rows.get(rowIndex);
+
+        Rectangle2D glyphVectorBounds = row.glyphVector.getLogicalBounds();
+        float rowWidth = (float)glyphVectorBounds.getWidth();
+
+        // Translate x to glyph vector coordinates
+        float rowX = 0;
+        HorizontalAlignment alignment = textAreaSkin.getAlignment();
+        switch (alignment) {
+            case LEFT: {
+                rowX = 0;
+                break;
+            }
+
+            case RIGHT: {
+                rowX = width - rowWidth;
+                break;
+            }
+
+            case CENTER: {
+                rowX = (width - rowWidth) / 2;
+                break;
+            }
+        }
+
+        x -= rowX;
+
+        int index;
+        if (x < 0) {
+            index = 0;
+        } else if (x > rowWidth) {
+            index = row.glyphVector.getNumGlyphs();
+
+            // If this is not the last row, decrement the index so the insertion
+            // point remains on this line
+            if (rowIndex < rows.getLength() - 1) {
+                index--;
+            }
+        } else {
+            index = 0;
+            int n = row.glyphVector.getNumGlyphs();
+
+            while (index < n) {
+                Shape glyphBounds = row.glyphVector.getGlyphLogicalBounds(index);
+                Rectangle2D glyphBounds2D = glyphBounds.getBounds2D();
+
+                if (glyphBounds2D.contains(x, glyphBounds2D.getY())) {
+                    // Determine the bias; if the user clicks on the right half of the
+                    // character; select the next character
+                    if (x - glyphBounds2D.getX() > glyphBounds2D.getWidth() / 2
+                        && index < n - 1) {
+                        index++;
+                    }
+
+                    break;
+                }
+
+                index++;
+            }
+        }
+
+        return index + row.offset;
+    }
+
+    public int getRowAt(int index) {
+        int rowIndex = rows.getLength() - 1;
+        Row row = rows.get(rowIndex);
+
+        while (row.offset > index) {
+            row = rows.get(--rowIndex);
+        }
+
+        return rowIndex;
+    }
+
+    public int getRowCount() {
+        return rows.getLength();
+    }
+
+    public Bounds getCharacterBounds(int index) {
+        Bounds characterBounds = null;
+
+        CharSequence characters = paragraph.getCharacters();
+        int characterCount = characters.length();
+
+        int rowIndex, x, width;
+        if (index == characterCount) {
+            // This is the terminator character
+            rowIndex = rows.getLength() - 1;
+            Row row = rows.get(rowIndex);
+
+            Rectangle2D glyphVectorBounds = row.glyphVector.getLogicalBounds();
+            x = (int)Math.floor(glyphVectorBounds.getWidth());
+            width = PARAGRAPH_TERMINATOR_WIDTH;
+        } else {
+            // This is a visible character
+            rowIndex = getRowAt(index);
+            Row row = rows.get(rowIndex);
+
+            Shape glyphBounds = row.glyphVector.getGlyphLogicalBounds(index - row.offset);
+            Rectangle2D glyphBounds2D = glyphBounds.getBounds2D();
+            x = (int)Math.floor(glyphBounds2D.getX());
+            width = (int)Math.ceil(glyphBounds2D.getWidth());
+        }
+
+        Font font = textAreaSkin.getFont();
+        FontRenderContext fontRenderContext = Platform.getFontRenderContext();
+        LineMetrics lm = font.getLineMetrics("", fontRenderContext);
+        float rowHeight = lm.getAscent() + lm.getDescent();
+
+        characterBounds = new Bounds(x, (int)Math.floor(rowIndex * rowHeight), width,
+            (int)Math.ceil(rowHeight));
+
+        return characterBounds;
+    }
+
+    @Override
+    public void textInserted(TextArea2.Paragraph paragraph, int index, int count) {
+        invalidate();
+        textAreaSkin.invalidateComponent();
+    }
+
+    @Override
+    public void textRemoved(TextArea2.Paragraph paragraph, int index, int count) {
+        invalidate();
+        textAreaSkin.invalidateComponent();
+    }
+}