You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@pivot.apache.org by rw...@apache.org on 2017/05/18 01:50:14 UTC

svn commit: r1795471 - in /pivot/trunk: core/src/org/apache/pivot/text/ core/src/org/apache/pivot/util/ wtk-terra/src/org/apache/pivot/wtk/skin/terra/ wtk/src/org/apache/pivot/wtk/ wtk/src/org/apache/pivot/wtk/skin/

Author: rwhitcomb
Date: Thu May 18 01:50:14 2017
New Revision: 1795471

URL: http://svn.apache.org/viewvc?rev=1795471&view=rev
Log:
PIVOT-850:  First phase of Input Method Editor support for the text controls:
1) Add the infrastructure to pass the message in/out of the text fields from
   the DisplayHost (which is the AWT component where they get sent first).
2) Changes to TerraTextInputSkin to do all the drawing / hit testing, etc.
   using TextLayout instead of GlyphVector.
3) Implement the required interfaces in TerraTextInputSkin, which are
   subclassed from new classes.
4) Define the interfaces to deal with AttributedCharacterIterator and
   InputMethodRequests (and also InputMethodListener).

There are some unresolved issues left having to do with cursor movement
when composed text is present.  These will be resolved in a later
submission.

Note also, the basic infrastructure is there, but only TextInput is
presently dealing with it.  TextArea and TextPane, which are more
difficult will come later.

Added:
    pivot/trunk/core/src/org/apache/pivot/text/AttributedStringCharacterIterator.java
    pivot/trunk/core/src/org/apache/pivot/text/CompositeIterator.java
    pivot/trunk/core/src/org/apache/pivot/util/StringUtils.java
    pivot/trunk/wtk/src/org/apache/pivot/wtk/TextInputMethodListener.java
Modified:
    pivot/trunk/wtk-terra/src/org/apache/pivot/wtk/skin/terra/TerraTextInputSkin.java
    pivot/trunk/wtk/src/org/apache/pivot/wtk/ApplicationContext.java
    pivot/trunk/wtk/src/org/apache/pivot/wtk/Component.java
    pivot/trunk/wtk/src/org/apache/pivot/wtk/GraphicsUtilities.java
    pivot/trunk/wtk/src/org/apache/pivot/wtk/TextInput.java
    pivot/trunk/wtk/src/org/apache/pivot/wtk/skin/ComponentSkin.java
    pivot/trunk/wtk/src/org/apache/pivot/wtk/skin/TextAreaSkin.java

Added: pivot/trunk/core/src/org/apache/pivot/text/AttributedStringCharacterIterator.java
URL: http://svn.apache.org/viewvc/pivot/trunk/core/src/org/apache/pivot/text/AttributedStringCharacterIterator.java?rev=1795471&view=auto
==============================================================================
--- pivot/trunk/core/src/org/apache/pivot/text/AttributedStringCharacterIterator.java (added)
+++ pivot/trunk/core/src/org/apache/pivot/text/AttributedStringCharacterIterator.java Thu May 18 01:50:14 2017
@@ -0,0 +1,202 @@
+/*
+ * 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.text;
+
+import java.awt.Font;
+import java.awt.font.TextAttribute;
+import java.text.AttributedCharacterIterator;
+import java.text.AttributedString;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+import org.apache.pivot.util.Utils;
+
+
+/**
+ * A sequence of text along with associated attributes, backed by a
+ * {@link AttributedString}, which itself implements all of these
+ * methods.
+ */
+public class AttributedStringCharacterIterator implements AttributedCharacterIterator {
+    private AttributedString storage = null;
+    private AttributedCharacterIterator.Attribute[] attributes = null;
+    private AttributedCharacterIterator iterator = null;
+
+    public AttributedStringCharacterIterator(String text) {
+        this.storage = new AttributedString(text);
+    }
+
+    public AttributedStringCharacterIterator(CharSequence charSequence, Font font) {
+        this.storage = new AttributedString(charSequence.toString());
+        this.storage.addAttribute(TextAttribute.FONT, font);
+    }
+
+    public AttributedStringCharacterIterator(String text, int beginIndex, int endIndex) {
+        this.storage = new AttributedString(text.substring(beginIndex, endIndex));
+    }
+
+    public AttributedStringCharacterIterator(AttributedCharacterIterator iter, int beginIndex, int endIndex) {
+        this.storage = new AttributedString(iter, beginIndex, endIndex);
+    }
+
+    public AttributedStringCharacterIterator(String text, AttributedCharacterIterator.Attribute[] attributes) {
+        this.storage = new AttributedString(text);
+        this.attributes = attributes;
+    }
+
+    public AttributedStringCharacterIterator(String text, int beginIndex, int endIndex, AttributedCharacterIterator.Attribute[] attributes) {
+        this.storage = new AttributedString(text.substring(beginIndex, endIndex));
+        this.attributes = attributes;
+    }
+
+    public AttributedStringCharacterIterator(AttributedString attributedString) {
+        // TODO: maybe we should make a copy? instead of adding the reference???
+        this.storage = attributedString;
+    }
+
+    // TODO: many more constructors needed, esp. those with attributes already in place
+
+    // TODO: do we need more parameters here?  for start position or anything else?
+    private AttributedCharacterIterator getIter() {
+        Utils.checkNull(this.storage, "source text");
+
+        if (this.iterator == null) {
+            if (this.attributes != null) {
+                this.iterator = storage.getIterator(attributes);
+            } else {
+                this.iterator = storage.getIterator();
+            }
+        }
+        return this.iterator;
+    }
+
+    /**
+     * Reset this iterator, meaning recreate the underlying iterator
+     * on the next call.
+     */
+    public void reset() {
+        this.iterator = null;
+    }
+
+    @Override
+    public Set<Attribute> getAllAttributeKeys() {
+        return getIter().getAllAttributeKeys();
+    }
+
+    @Override
+    public Object getAttribute(Attribute attribute) {
+        return getIter().getAttribute(attribute);
+    }
+
+    @Override
+    public Map<Attribute, Object> getAttributes() {
+        return getIter().getAttributes();
+    }
+
+    @Override
+    public int getRunLimit() {
+        return getIter().getRunLimit();
+    }
+
+    @Override
+    public int getRunLimit(Attribute attribute) {
+        return getIter().getRunLimit(attribute);
+    }
+
+    @Override
+    public int getRunLimit(Set<? extends Attribute> attributes) {
+        return getIter().getRunLimit(attributes);
+    }
+
+    @Override
+    public int getRunStart() {
+        return getIter().getRunStart();
+    }
+
+    @Override
+    public int getRunStart(Attribute attribute) {
+        return getIter().getRunStart(attribute);
+    }
+
+    @Override
+    public int getRunStart(Set<? extends Attribute> attributes) {
+        return getIter().getRunStart(attributes);
+    }
+
+    @Override
+    public char first() {
+        return getIter().first();
+    }
+
+    @Override
+    public char last() {
+        return getIter().last();
+    }
+
+    @Override
+    public char current() {
+        return getIter().current();
+    }
+
+    @Override
+    public char next() {
+        return getIter().next();
+    }
+
+    @Override
+    public char previous() {
+        return getIter().previous();
+    }
+
+    @Override
+    public char setIndex(int position) {
+        return getIter().setIndex(position);
+    }
+
+    @Override
+    public int getBeginIndex() {
+        return getIter().getBeginIndex();
+    }
+
+    @Override
+    public int getEndIndex() {
+        return getIter().getEndIndex();
+    }
+
+    @Override
+    public int getIndex() {
+        return getIter().getIndex();
+    }
+
+    @Override
+    public Object clone() {
+        return new AttributedStringCharacterIterator(this.storage);
+    }
+
+    @Override
+    public String toString() {
+        AttributedCharacterIterator iter = storage.getIterator();
+        StringBuilder buf = new StringBuilder(iter.getEndIndex());
+        char ch = iter.first();
+        while (ch != DONE) {
+            buf.append(ch);
+            ch = iter.next();
+        }
+        return buf.toString();
+    }
+
+}

Added: pivot/trunk/core/src/org/apache/pivot/text/CompositeIterator.java
URL: http://svn.apache.org/viewvc/pivot/trunk/core/src/org/apache/pivot/text/CompositeIterator.java?rev=1795471&view=auto
==============================================================================
--- pivot/trunk/core/src/org/apache/pivot/text/CompositeIterator.java (added)
+++ pivot/trunk/core/src/org/apache/pivot/text/CompositeIterator.java Thu May 18 01:50:14 2017
@@ -0,0 +1,185 @@
+/*
+ * 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.text;
+
+import java.text.AttributedCharacterIterator;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+import org.apache.pivot.collections.ArrayList;
+import org.apache.pivot.collections.List;
+import org.apache.pivot.util.Utils;
+
+
+/**
+ * An {@link AttributedCharacterIterator} that implements iterating over
+ * several (typically two or three) underlying iterators.  The assumption
+ * is that no attributes will ever cross the underlying iterator boundaries
+ * (which is always the case for insertion of composed but uncommitted text
+ * in the middle of two pieces of committed text (for instance).
+ */
+public class CompositeIterator implements AttributedCharacterIterator {
+
+    private List<AttributedCharacterIterator> iterators = new ArrayList<>();
+    private int endIndex;
+    private int currentIndex;
+    private AttributedCharacterIterator currentIterator;
+    private int currentIteratorDelta;
+
+    /**
+     * Constructs a CompositeIterator that iterates over the concatenation
+     * of one or more iterators.
+     * @param iterators The base iterators that this composite iterator concatenates.
+     */
+    public CompositeIterator(AttributedCharacterIterator... iterators) {
+        int fullLength = 0;
+        for (AttributedCharacterIterator iter : iterators) {
+            int beginIndex = iter.getBeginIndex();    // inclusive
+            int endIndex   = iter.getEndIndex();      // exclusive
+            int range = (endIndex - beginIndex);
+            this.iterators.add(iter);
+            fullLength += range;
+        }
+        this.endIndex = fullLength;
+        __setIndex(0);
+    }
+
+    // CharacterIterator implementations
+
+    public char first() {
+        return __setIndex(0);
+    }
+
+    public char last() {
+        if (endIndex == 0) {
+            return __setIndex(endIndex);
+        } else {
+            return __setIndex(endIndex - 1);
+        }
+    }
+
+    public char next() {
+        if (currentIndex < endIndex) {
+            return __setIndex(currentIndex + 1);
+        } else {
+            return DONE;
+        }
+    }
+
+    public char previous() {
+        if (currentIndex > 0) {
+            return __setIndex(currentIndex - 1);
+        } else {
+            return DONE;
+        }
+    }
+
+    public char current() {
+        return currentIterator.setIndex(currentIndex - currentIteratorDelta);
+    }
+
+    public char setIndex(int position) {
+        // Note: this is a (0 < position <= endIndex) check, since "endIndex" is a valid value here
+        Utils.checkIndexBounds(position, 0, endIndex);
+
+        return __setIndex(position);
+    }
+
+    private char __setIndex(int position) {
+        currentIndex = position;
+        int cumLength = 0;
+        for (AttributedCharacterIterator iter : iterators) {
+            int beginIndex = iter.getBeginIndex();
+            int endIndex   = iter.getEndIndex();
+            int range = endIndex - beginIndex;
+            if (currentIndex < endIndex + cumLength) {
+                currentIterator = iter;
+                currentIteratorDelta = beginIndex + cumLength;
+                // TODO: not sure this is going to be correct for > 2 iterators
+                return currentIterator.setIndex(currentIndex - currentIteratorDelta);
+            }
+            cumLength += range;
+        }
+        return DONE;
+    }
+
+    public int getBeginIndex() {
+        return 0;
+    }
+
+    public int getEndIndex() {
+        return endIndex;
+    }
+
+    public int getIndex() {
+        return currentIndex;
+    }
+
+    // AttributedCharacterIterator implementations
+
+    public int getRunStart() {
+        return currentIterator.getRunStart() + currentIteratorDelta;
+    }
+
+    public int getRunLimit() {
+        return currentIterator.getRunLimit() + currentIteratorDelta;
+    }
+
+    public int getRunStart(Attribute attribute) {
+        return currentIterator.getRunStart(attribute) + currentIteratorDelta;
+    }
+
+    public int getRunLimit(Attribute attribute) {
+        return currentIterator.getRunLimit(attribute) + currentIteratorDelta;
+    }
+
+    public int getRunStart(Set<? extends Attribute> attributes) {
+        return currentIterator.getRunStart(attributes) + currentIteratorDelta;
+    }
+
+    public int getRunLimit(Set<? extends Attribute> attributes) {
+        return currentIterator.getRunLimit(attributes) + currentIteratorDelta;
+    }
+
+    public Map<Attribute, Object> getAttributes() {
+        return currentIterator.getAttributes();
+    }
+
+    public Set<Attribute> getAllAttributeKeys() {
+        Set<Attribute> keys = new HashSet<>();
+        for (AttributedCharacterIterator iter : iterators) {
+            keys.addAll(iter.getAllAttributeKeys());
+        }
+        return keys;
+    }
+
+    public Object getAttribute(Attribute attribute) {
+        return currentIterator.getAttribute(attribute);
+    }
+
+    // Object implementations
+
+    public Object clone() {
+        try {
+            CompositeIterator other = (CompositeIterator) super.clone();
+            return other;
+        } catch (CloneNotSupportedException e) {
+            throw new InternalError();
+        }
+    }
+
+}

Added: pivot/trunk/core/src/org/apache/pivot/util/StringUtils.java
URL: http://svn.apache.org/viewvc/pivot/trunk/core/src/org/apache/pivot/util/StringUtils.java?rev=1795471&view=auto
==============================================================================
--- pivot/trunk/core/src/org/apache/pivot/util/StringUtils.java (added)
+++ pivot/trunk/core/src/org/apache/pivot/util/StringUtils.java Thu May 18 01:50:14 2017
@@ -0,0 +1,54 @@
+/*
+ * 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.util;
+
+/**
+ * A set of static methods that perform various string manipulation
+ * functions.
+ */
+public class StringUtils {
+
+    /**
+     * Make a string the consists of "n" copies of the given character.
+     * <p> Note: "n" must be positive and less than 512K (arbitrary).
+     *
+     * @param ch The character to copy.
+     * @param n  The number of times to copy this character.
+     * @return   The resulting string.
+     */
+    public static String fromNChars(char ch, int n) {
+        if (n == 0) {
+            return "";
+        }
+        if (n < 0 || n > Integer.MAX_VALUE / 4) {
+           throw new IllegalArgumentException("Requested string size " + n + " is out of range.");
+        }
+
+        // Nothing fancy here, but allocate the space and set length upfront
+        // because we know how big the result should be.
+        StringBuilder builder = new StringBuilder(n);
+        builder.setLength(n);
+        if (ch != '\0') {
+            for (int i = 0; i < n; i++) {
+                builder.setCharAt(i, ch);
+            }
+        }
+        return builder.toString();
+    }
+
+}
+

Modified: pivot/trunk/wtk-terra/src/org/apache/pivot/wtk/skin/terra/TerraTextInputSkin.java
URL: http://svn.apache.org/viewvc/pivot/trunk/wtk-terra/src/org/apache/pivot/wtk/skin/terra/TerraTextInputSkin.java?rev=1795471&r1=1795470&r2=1795471&view=diff
==============================================================================
--- pivot/trunk/wtk-terra/src/org/apache/pivot/wtk/skin/terra/TerraTextInputSkin.java (original)
+++ pivot/trunk/wtk-terra/src/org/apache/pivot/wtk/skin/terra/TerraTextInputSkin.java Thu May 18 01:50:14 2017
@@ -24,14 +24,20 @@ import java.awt.Rectangle;
 import java.awt.RenderingHints;
 import java.awt.Shape;
 import java.awt.Toolkit;
+import java.awt.event.InputMethodEvent;
 import java.awt.font.FontRenderContext;
-import java.awt.font.GlyphVector;
+import java.awt.font.TextLayout;
 import java.awt.font.LineMetrics;
+import java.awt.font.TextHitInfo;
 import java.awt.geom.Area;
 import java.awt.geom.Rectangle2D;
+import java.text.AttributedCharacterIterator;
 
 import org.apache.pivot.collections.Dictionary;
-import org.apache.pivot.text.CharSequenceCharacterIterator;
+import org.apache.pivot.text.AttributedStringCharacterIterator;
+import org.apache.pivot.text.CompositeIterator;
+import org.apache.pivot.util.StringUtils;
+import org.apache.pivot.util.Utils;
 import org.apache.pivot.util.Vote;
 import org.apache.pivot.wtk.ApplicationContext;
 import org.apache.pivot.wtk.Bounds;
@@ -51,6 +57,7 @@ import org.apache.pivot.wtk.Platform;
 import org.apache.pivot.wtk.TextInput;
 import org.apache.pivot.wtk.TextInputContentListener;
 import org.apache.pivot.wtk.TextInputListener;
+import org.apache.pivot.wtk.TextInputMethodListener;
 import org.apache.pivot.wtk.TextInputSelectionListener;
 import org.apache.pivot.wtk.Theme;
 import org.apache.pivot.wtk.Window;
@@ -62,6 +69,7 @@ import org.apache.pivot.wtk.validation.V
  */
 public class TerraTextInputSkin extends ComponentSkin implements TextInput.Skin, TextInputListener,
     TextInputContentListener, TextInputSelectionListener {
+
     private class BlinkCaretCallback implements Runnable {
         @Override
         public void run() {
@@ -110,7 +118,105 @@ public class TerraTextInputSkin extends
         }
     }
 
-    private GlyphVector glyphVector = null;
+    /**
+     * Private class that handles interaction with the Input Method Editor,
+     * including requests and events.
+     */
+    private class TextInputMethodHandler extends TextInputMethodListener.Adapter {
+
+        @Override
+        public AttributedCharacterIterator getCommittedText(int beginIndex, int endIndex, AttributedCharacterIterator.Attribute[] attributes) {
+            TextInput textInput = (TextInput)getComponent();
+            return new AttributedStringCharacterIterator(textInput.getText(), beginIndex, endIndex, attributes);
+        }
+
+        @Override
+        public int getCommittedTextLength() {
+            TextInput textInput = (TextInput)getComponent();
+            return textInput.getCharacterCount();
+        }
+
+        @Override
+        public int getInsertPositionOffset() {
+            TextInput textInput = (TextInput)getComponent();
+            return textInput.getSelectionStart();
+        }
+
+        @Override
+        public TextHitInfo getLocationOffset(int x, int y) {
+System.out.println("TextInputSkin.getLocationOffset called");
+            return null;
+        }
+
+        @Override
+        public AttributedCharacterIterator getSelectedText(AttributedCharacterIterator.Attribute[] attributes) {
+            TextInput textInput = (TextInput)getComponent();
+            return new AttributedStringCharacterIterator(textInput.getSelectedText(), attributes);
+        }
+
+        @Override
+        public Rectangle getTextLocation(TextHitInfo offset) {
+            if (composedText == null) {
+                return caret;
+            } else {
+                // The offset should be into the composed text, not the whole text
+                if (composedText.getEndIndex() == 0) {
+                    return new Rectangle();
+                } else {
+                    FontRenderContext fontRenderContext = Platform.getFontRenderContext();
+                    TextLayout layout = new TextLayout(composedText, fontRenderContext);
+                    Shape caretShape = layout.getCaretShape(offset);
+                    return caretShape.getBounds();
+                }
+            }
+        }
+
+        private String getCommittedText(AttributedCharacterIterator fullTextIter, int count) {
+            StringBuilder buf = new StringBuilder(count);
+            buf.setLength(count);
+            if (fullTextIter != null) {
+                char ch = fullTextIter.first();
+                for (int i = 0; i < count; i++) {
+                    buf.setCharAt(i, ch);
+                    ch = fullTextIter.next();
+                }
+            }
+            return buf.toString();
+        }
+
+        private AttributedStringCharacterIterator getComposedText(AttributedCharacterIterator fullTextIter, int start) {
+            return fullTextIter != null ?
+                new AttributedStringCharacterIterator(fullTextIter, start, fullTextIter.getEndIndex()) :
+                null;
+        }
+
+        @Override
+        public void inputMethodTextChanged(InputMethodEvent event) {
+            TextInput textInput = (TextInput)getComponent();
+            AttributedCharacterIterator iter = event.getText();
+            // TODO: IS THIS RIGHT??  Just ignore empty text changes
+            if (iter != null) {
+                int endOfCommittedText = event.getCommittedCharacterCount();
+                textInput.insertText(getCommittedText(iter, endOfCommittedText), textInput.getSelectionStart());
+                composedText = getComposedText(iter, endOfCommittedText);
+                // TODO: do we need to do this? should this be the total length?
+                //textInput.setSelection(endOfCommittedText, 0);
+                layout();
+                repaintComponent();
+            }
+        }
+
+        @Override
+        public void caretPositionChanged(InputMethodEvent event) {
+System.out.format("TextInputSkin.caretPositionChanged called: event=%1$s%n", event);
+            TextInput textInput = (TextInput)getComponent();
+System.out.format("\tCARET_POSITION_CHANGED%n");
+            // TODO:  so far I have not seen this called, so ??? 
+        }
+
+    }
+
+    private TextLayout textLayout = null;
 
     private int anchor = -1;
     private Rectangle caret = new Rectangle();
@@ -153,6 +259,10 @@ public class TerraTextInputSkin extends
 
     private Dimensions averageCharacterSize;
 
+    private TextInputMethodHandler textInputMethodHandler = new TextInputMethodHandler();
+    private AttributedStringCharacterIterator composedText = null;
+
+
     private static final int SCROLL_RATE = 50;
     private static final char BULLET = 0x2022;
 
@@ -226,47 +336,70 @@ public class TerraTextInputSkin extends
         return baseline;
     }
 
+    private AttributedCharacterIterator getCharIterator(TextInput textInput, int start, int end) {
+        CharSequence characters;
+        int num = end - start;
+        if (textInput.isPassword()) {
+            characters = StringUtils.fromNChars(BULLET, num);
+        } else {
+            if (num == textInput.getCharacterCount()) {
+                characters = textInput.getCharacters();
+            } else {
+                characters = textInput.getCharacters(start, end);
+            }
+        }
+        return new AttributedStringCharacterIterator(characters, font);
+    }
+
     @Override
     public void layout() {
         TextInput textInput = (TextInput) getComponent();
 
-        glyphVector = null;
+        textLayout = null;
 
         int n = textInput.getCharacterCount();
-        if (n > 0) {
-            CharSequence characters;
-            if (textInput.isPassword()) {
-                StringBuilder passwordBuilder = new StringBuilder(n);
-                for (int i = 0; i < n; i++) {
-                    passwordBuilder.append(BULLET);
-                }
+        if (n > 0 || composedText != null) {
+            AttributedCharacterIterator text = null;
 
-                characters = passwordBuilder;
+            if (n > 0) {
+                int insertPos = textInput.getSelectionStart();
+                if (composedText == null) {
+                    text = getCharIterator(textInput, 0, n);
+                } else if (insertPos == n) {
+                    // The composed text position is the end of the committed text
+                    text = new CompositeIterator(getCharIterator(textInput, 0, n), composedText);
+                } else if (insertPos == 0) {
+                    text = new CompositeIterator(composedText, getCharIterator(textInput, 0, n));
+                } else {
+                    // The composed text is somewhere in the middle of the text
+                    text = new CompositeIterator(
+                            getCharIterator(textInput, 0, insertPos),
+                            composedText,
+                            getCharIterator(textInput, insertPos, n));
+                }
             } else {
-                characters = textInput.getCharacters();
+                text = composedText;
             }
 
-            CharSequenceCharacterIterator ci = new CharSequenceCharacterIterator(characters);
-
             FontRenderContext fontRenderContext = Platform.getFontRenderContext();
-            glyphVector = font.createGlyphVector(fontRenderContext, ci);
-
-            Rectangle2D textBounds = glyphVector.getLogicalBounds();
+            textLayout = new TextLayout(text, fontRenderContext);
+            Rectangle2D textBounds = textLayout.getBounds();
             int textWidth = (int) textBounds.getWidth();
             int width = getWidth();
 
             if (textWidth - scrollLeft + padding.left + 1 < width - padding.right - 1) {
                 // The right edge of the text is less than the right inset;
-                // align
-                // the text's right edge with the inset
+                // align the text's right edge with the inset
                 scrollLeft = Math.max(textWidth + (padding.left + padding.right + 2) - width, 0);
             } else {
-                // Scroll lead selection to visible
+                // Scroll selection start to visible
                 int selectionStart = textInput.getSelectionStart();
                 if (selectionStart <= n && textInput.isFocused()) {
                     scrollCharacterToVisible(selectionStart);
                 }
             }
+        } else {
+            scrollLeft = 0;
         }
 
         updateSelection();
@@ -280,16 +413,16 @@ public class TerraTextInputSkin extends
                 break;
             case CENTER: {
                 TextInput textInput = (TextInput) getComponent();
-                double txtWidth = glyphVector == null ? 0
-                    : glyphVector.getLogicalBounds().getWidth();
+                double txtWidth = textLayout == null ? 0
+                    : textLayout.getBounds().getWidth();
                 int availWidth = textInput.getWidth() - (padding.left + padding.right + 2);
                 alignmentDeltaX = (int) (availWidth - txtWidth) / 2;
                 break;
             }
             case RIGHT: {
                 TextInput textInput = (TextInput) getComponent();
-                double txtWidth = glyphVector == null ? 0
-                    : glyphVector.getLogicalBounds().getWidth();
+                double txtWidth = textLayout == null ? 0
+                    : textLayout.getBounds().getWidth();
                 int availWidth = textInput.getWidth()
                     - (padding.left + padding.right + 2 + caret.width);
                 alignmentDeltaX = (int) (availWidth - txtWidth);
@@ -355,13 +488,8 @@ public class TerraTextInputSkin extends
         int alignmentDeltaX = getAlignmentDeltaX();
         int xpos = padding.left - scrollLeft + 1 + alignmentDeltaX;
 
-        if (glyphVector == null && prompt != null) {
-            graphics.setFont(font);
-            graphics.setColor(promptColor);
-            graphics.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING,
-                fontRenderContext.getAntiAliasingHint());
-            graphics.setRenderingHint(RenderingHints.KEY_FRACTIONALMETRICS,
-                fontRenderContext.getFractionalMetricsHint());
+        if (textLayout == null && prompt != null) {
+            GraphicsUtilities.prepareForText(graphics, fontRenderContext, font, promptColor);
             graphics.drawString(prompt, xpos, (height - textHeight) / 2 + ascent);
 
             caretColor = color;
@@ -381,13 +509,15 @@ public class TerraTextInputSkin extends
 
             caretColor = colorLocal;
 
-            if (glyphVector != null) {
+            if (textLayout != null) {
                 graphics.setFont(font);
 
+                float ypos = (height - textHeight) / 2 + ascent;
+
                 if (selection == null) {
                     // Paint the text
                     graphics.setColor(colorLocal);
-                    graphics.drawGlyphVector(glyphVector, xpos, (height - textHeight) / 2 + ascent);
+                    textLayout.draw(graphics, xpos, ypos);
                 } else {
                     // Paint the unselected text
                     Area unselectedArea = new Area();
@@ -397,8 +527,7 @@ public class TerraTextInputSkin extends
                     Graphics2D textGraphics = (Graphics2D) graphics.create();
                     textGraphics.setColor(colorLocal);
                     textGraphics.clip(unselectedArea);
-                    textGraphics.drawGlyphVector(glyphVector, xpos, (height - textHeight) / 2
-                        + ascent);
+                    textLayout.draw(textGraphics, xpos, ypos);
                     textGraphics.dispose();
 
                     // Paint the selection
@@ -419,8 +548,7 @@ public class TerraTextInputSkin extends
                     Graphics2D selectedTextGraphics = (Graphics2D) graphics.create();
                     selectedTextGraphics.setColor(selectionColorLocal);
                     selectedTextGraphics.clip(selection.getBounds());
-                    selectedTextGraphics.drawGlyphVector(glyphVector, xpos, (height - textHeight)
-                        / 2 + ascent);
+                    textLayout.draw(selectedTextGraphics, xpos, ypos);
                     selectedTextGraphics.dispose();
                 }
             }
@@ -439,68 +567,59 @@ public class TerraTextInputSkin extends
         }
     }
 
+    /**
+     * Calculate the text insertion point given the mouse X-position relative
+     * to the component's X-position.
+     * <p> Note: if the given X-position is on the right-hand side of a glyph
+     * then the insertion point will be after that character, while an X-position
+     * within the left half of a glyph will position the insert before that
+     * character.
+     *
+     * @param x The relative X-position.
+     * @return The offset into the text determined by the X-position.
+     */
     @Override
     public int getInsertionPoint(int x) {
         int offset = -1;
 
-        if (glyphVector == null) {
+        if (textLayout == null) {
             offset = 0;
         } else {
             // Translate to glyph coordinates
-            int xt = x - (padding.left - scrollLeft + 1 + getAlignmentDeltaX());
-
-            Rectangle2D textBounds = glyphVector.getLogicalBounds();
-
-            if (xt < 0) {
-                offset = 0;
-            } else if (xt > textBounds.getWidth()) {
-                offset = glyphVector.getNumGlyphs();
-            } else {
-                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 (xt >= glyphX && xt < glyphX + glyphWidth) {
-
-                        if (xt - glyphX > glyphWidth / 2) {
-                            // The user clicked on the right half of the
-                            // character; select
-                            // the next character
-                            i++;
-                        }
-
-                        offset = i;
-                        break;
-                    }
+            float xt = x - (padding.left - scrollLeft + 1 + getAlignmentDeltaX());
 
-                    i++;
-                }
-            }
+            TextHitInfo hitInfo = textLayout.hitTestChar(xt, 0);
+            offset = hitInfo.getInsertionIndex();
         }
 
         return offset;
     }
 
+    /**
+     * Determine the bounding box of the character at the given index
+     * in the text in coordinates relative to the entire component (that is,
+     * adding in the insets and padding, etc.).
+     *
+     * @param index The 0-based index of the character to inquire about.
+     * @return The bounding box within the component where that character
+     * will be displayed, or {@code null} if there is no text.
+     */
     @Override
     public Bounds getCharacterBounds(int index) {
         Bounds characterBounds = null;
 
-        if (glyphVector != null) {
+        if (textLayout != null) {
             int x, width;
-            if (index < glyphVector.getNumGlyphs()) {
-                Shape glyphBounds = glyphVector.getGlyphLogicalBounds(index);
-                Rectangle2D glyphBounds2D = glyphBounds.getBounds2D();
+            if (index < textLayout.getCharacterCount()) {
+                Shape glyphShape = textLayout.getLogicalHighlightShape(index, index + 1);
+                Rectangle2D glyphBounds2D = glyphShape.getBounds2D();
 
                 x = (int) Math.floor(glyphBounds2D.getX());
                 width = (int) Math.ceil(glyphBounds2D.getWidth());
             } else {
                 // This is the terminator character
-                Rectangle2D glyphVectorBounds = glyphVector.getLogicalBounds();
-                x = (int) Math.floor(glyphVectorBounds.getWidth());
+                Rectangle2D textLayoutBounds = textLayout.getBounds();
+                x = (int) Math.floor(textLayoutBounds.getWidth());
                 width = 0;
             }
 
@@ -527,8 +646,7 @@ public class TerraTextInputSkin extends
             if (characterBounds.x < padding.left + 1) {
                 setScrollLeft(glyphX);
             } else if (characterBounds.x + characterBounds.width > width - (padding.right + 1)) {
-                setScrollLeft(glyphX + (padding.left + padding.right + 2) + characterBounds.width
-                    - width);
+                setScrollLeft(glyphX + (padding.left + padding.right + 2) + characterBounds.width - width);
             }
         }
     }
@@ -537,39 +655,33 @@ public class TerraTextInputSkin extends
         return font;
     }
 
+    /**
+     * Set the new font to use to render the text in this component.
+     * <p> Also calculate the {@link #averageCharacterSize} value based
+     * on this font, which will be the width of the "missing glyph code"
+     * and the maximum height of any character in the font.
+     *
+     * @param font The new font to use.
+     * @throws IllegalArgumentException if the <tt>font</tt> argument is <tt>null</tt>.
+     */
     public void setFont(Font font) {
-        if (font == null) {
-            throw new IllegalArgumentException("font is null.");
-        }
+        Utils.checkNull(font, "font");
 
         this.font = font;
 
-        int missingGlyphCode = font.getMissingGlyphCode();
-        FontRenderContext fontRenderContext = Platform.getFontRenderContext();
-
-        GlyphVector missingGlyphVector = font.createGlyphVector(fontRenderContext,
-            new int[] { missingGlyphCode });
-        Rectangle2D textBounds = missingGlyphVector.getLogicalBounds();
-
-        Rectangle2D maxCharBounds = font.getMaxCharBounds(fontRenderContext);
-        averageCharacterSize = new Dimensions((int) Math.ceil(textBounds.getWidth()),
-            (int) Math.ceil(maxCharBounds.getHeight()));
+        averageCharacterSize = GraphicsUtilities.getAverageCharacterSize(font);
 
         invalidateComponent();
     }
 
     public final void setFont(String font) {
-        if (font == null) {
-            throw new IllegalArgumentException("font is null.");
-        }
+        Utils.checkNull(font, "font");
 
         setFont(decodeFont(font));
     }
 
     public final void setFont(Dictionary<String, ?> font) {
-        if (font == null) {
-            throw new IllegalArgumentException("font is null.");
-        }
+        Utils.checkNull(font, "font");
 
         setFont(Theme.deriveFont(font));
     }
@@ -579,18 +691,15 @@ public class TerraTextInputSkin extends
     }
 
     public void setColor(Color color) {
-        if (color == null) {
-            throw new IllegalArgumentException("color is null.");
-        }
+        Utils.checkNull(color, "color");
 
         this.color = color;
+
         repaintComponent();
     }
 
     public final void setColor(String color) {
-        if (color == null) {
-            throw new IllegalArgumentException("color is null.");
-        }
+        Utils.checkNull(color, "color");
 
         setColor(GraphicsUtilities.decodeColor(color));
     }
@@ -605,18 +714,15 @@ public class TerraTextInputSkin extends
     }
 
     public void setPromptColor(Color promptColor) {
-        if (promptColor == null) {
-            throw new IllegalArgumentException("promptColor is null.");
-        }
+        Utils.checkNull(promptColor, "promptColor");
 
         this.promptColor = promptColor;
+
         repaintComponent();
     }
 
     public final void setPromptColor(String promptColor) {
-        if (promptColor == null) {
-            throw new IllegalArgumentException("promptColor is null.");
-        }
+        Utils.checkNull(promptColor, "promptColor");
 
         setPromptColor(GraphicsUtilities.decodeColor(promptColor));
     }
@@ -631,18 +737,15 @@ public class TerraTextInputSkin extends
     }
 
     public void setDisabledColor(Color disabledColor) {
-        if (disabledColor == null) {
-            throw new IllegalArgumentException("disabledColor is null.");
-        }
+        Utils.checkNull(disabledColor, "disabledColor");
 
         this.disabledColor = disabledColor;
+
         repaintComponent();
     }
 
     public final void setDisabledColor(String disabledColor) {
-        if (disabledColor == null) {
-            throw new IllegalArgumentException("disabledColor is null.");
-        }
+        Utils.checkNull(disabledColor, "disabledColor");
 
         setDisabledColor(GraphicsUtilities.decodeColor(disabledColor));
     }
@@ -657,19 +760,16 @@ public class TerraTextInputSkin extends
     }
 
     public void setBackgroundColor(Color backgroundColor) {
-        if (backgroundColor == null) {
-            throw new IllegalArgumentException("backgroundColor is null.");
-        }
+        Utils.checkNull(backgroundColor, "backgroundColor");
 
         this.backgroundColor = backgroundColor;
         bevelColor = TerraTheme.darken(backgroundColor);
+
         repaintComponent();
     }
 
     public final void setBackgroundColor(String backgroundColor) {
-        if (backgroundColor == null) {
-            throw new IllegalArgumentException("backgroundColor is null.");
-        }
+        Utils.checkNull(backgroundColor, "backgroundColor");
 
         setBackgroundColor(GraphicsUtilities.decodeColor(backgroundColor));
     }
@@ -684,18 +784,15 @@ public class TerraTextInputSkin extends
     }
 
     public void setInvalidColor(Color color) {
-        if (color == null) {
-            throw new IllegalArgumentException("color is null.");
-        }
+        Utils.checkNull(color, "invalidColor");
 
         this.invalidColor = color;
+
         repaintComponent();
     }
 
     public final void setInvalidColor(String color) {
-        if (color == null) {
-            throw new IllegalArgumentException("color is null.");
-        }
+        Utils.checkNull(color, "invalidColor");
 
         setInvalidColor(GraphicsUtilities.decodeColor(color));
     }
@@ -710,19 +807,16 @@ public class TerraTextInputSkin extends
     }
 
     public void setInvalidBackgroundColor(Color color) {
-        if (color == null) {
-            throw new IllegalArgumentException("color is null.");
-        }
+        Utils.checkNull(color, "invalidBackgroundColor");
 
         this.invalidBackgroundColor = color;
         invalidBevelColor = TerraTheme.darken(color);
+
         repaintComponent();
     }
 
     public final void setInvalidBackgroundColor(String color) {
-        if (color == null) {
-            throw new IllegalArgumentException("invalidBackgroundColor is null.");
-        }
+        Utils.checkNull(color, "invalidBackgroundColor");
 
         setInvalidBackgroundColor(GraphicsUtilities.decodeColor(color));
     }
@@ -737,19 +831,16 @@ public class TerraTextInputSkin extends
     }
 
     public void setDisabledBackgroundColor(Color disabledBackgroundColor) {
-        if (disabledBackgroundColor == null) {
-            throw new IllegalArgumentException("disabledBackgroundColor is null.");
-        }
+        Utils.checkNull(disabledBackgroundColor, "disabledBackgroundColor");
 
         this.disabledBackgroundColor = disabledBackgroundColor;
         disabledBevelColor = disabledBackgroundColor;
+
         repaintComponent();
     }
 
     public final void setDisabledBackgroundColor(String disabledBackgroundColor) {
-        if (disabledBackgroundColor == null) {
-            throw new IllegalArgumentException("disabledBackgroundColor is null.");
-        }
+        Utils.checkNull(disabledBackgroundColor, "disabledBackgroundColor");
 
         setDisabledBackgroundColor(GraphicsUtilities.decodeColor(disabledBackgroundColor));
     }
@@ -764,18 +855,15 @@ public class TerraTextInputSkin extends
     }
 
     public void setBorderColor(Color borderColor) {
-        if (borderColor == null) {
-            throw new IllegalArgumentException("borderColor is null.");
-        }
+        Utils.checkNull(borderColor, "borderColor");
 
         this.borderColor = borderColor;
+
         repaintComponent();
     }
 
     public final void setBorderColor(String borderColor) {
-        if (borderColor == null) {
-            throw new IllegalArgumentException("borderColor is null.");
-        }
+        Utils.checkNull(borderColor, "borderColor");
 
         setBorderColor(GraphicsUtilities.decodeColor(borderColor));
     }
@@ -790,18 +878,15 @@ public class TerraTextInputSkin extends
     }
 
     public void setDisabledBorderColor(Color disabledBorderColor) {
-        if (disabledBorderColor == null) {
-            throw new IllegalArgumentException("disabledBorderColor is null.");
-        }
+        Utils.checkNull(disabledBorderColor, "disabledBorderColor");
 
         this.disabledBorderColor = disabledBorderColor;
+
         repaintComponent();
     }
 
     public final void setDisabledBorderColor(String disabledBorderColor) {
-        if (disabledBorderColor == null) {
-            throw new IllegalArgumentException("disabledBorderColor is null.");
-        }
+        Utils.checkNull(disabledBorderColor, "disabledBorderColor");
 
         setDisabledBorderColor(GraphicsUtilities.decodeColor(disabledBorderColor));
     }
@@ -816,18 +901,15 @@ public class TerraTextInputSkin extends
     }
 
     public void setSelectionColor(Color selectionColor) {
-        if (selectionColor == null) {
-            throw new IllegalArgumentException("selectionColor is null.");
-        }
+        Utils.checkNull(selectionColor, "selectionColor");
 
         this.selectionColor = selectionColor;
+
         repaintComponent();
     }
 
     public final void setSelectionColor(String selectionColor) {
-        if (selectionColor == null) {
-            throw new IllegalArgumentException("selectionColor is null.");
-        }
+        Utils.checkNull(selectionColor, "selectionColor");
 
         setSelectionColor(GraphicsUtilities.decodeColor(selectionColor));
     }
@@ -842,18 +924,15 @@ public class TerraTextInputSkin extends
     }
 
     public void setSelectionBackgroundColor(Color selectionBackgroundColor) {
-        if (selectionBackgroundColor == null) {
-            throw new IllegalArgumentException("selectionBackgroundColor is null.");
-        }
+        Utils.checkNull(selectionBackgroundColor, "selectionBackgroundColor");
 
         this.selectionBackgroundColor = selectionBackgroundColor;
+
         repaintComponent();
     }
 
     public final void setSelectionBackgroundColor(String selectionBackgroundColor) {
-        if (selectionBackgroundColor == null) {
-            throw new IllegalArgumentException("selectionBackgroundColor is null.");
-        }
+        Utils.checkNull(selectionBackgroundColor, "selectionBackgroundColor");
 
         setSelectionBackgroundColor(GraphicsUtilities.decodeColor(selectionBackgroundColor));
     }
@@ -868,18 +947,15 @@ public class TerraTextInputSkin extends
     }
 
     public void setInactiveSelectionColor(Color inactiveSelectionColor) {
-        if (inactiveSelectionColor == null) {
-            throw new IllegalArgumentException("inactiveSelectionColor is null.");
-        }
+        Utils.checkNull(inactiveSelectionColor, "inactiveSelectionColor");
 
         this.inactiveSelectionColor = inactiveSelectionColor;
+
         repaintComponent();
     }
 
     public final void setInactiveSelectionColor(String inactiveSelectionColor) {
-        if (inactiveSelectionColor == null) {
-            throw new IllegalArgumentException("inactiveSelectionColor is null.");
-        }
+        Utils.checkNull(inactiveSelectionColor, "inactiveSelectionColor");
 
         setInactiveSelectionColor(GraphicsUtilities.decodeColor(inactiveSelectionColor));
     }
@@ -894,18 +970,15 @@ public class TerraTextInputSkin extends
     }
 
     public void setInactiveSelectionBackgroundColor(Color inactiveSelectionBackgroundColor) {
-        if (inactiveSelectionBackgroundColor == null) {
-            throw new IllegalArgumentException("inactiveSelectionBackgroundColor is null.");
-        }
+        Utils.checkNull(inactiveSelectionBackgroundColor, "inactiveSelectionBackgroundColor");
 
         this.inactiveSelectionBackgroundColor = inactiveSelectionBackgroundColor;
+
         repaintComponent();
     }
 
     public final void setInactiveSelectionBackgroundColor(String inactiveSelectionBackgroundColor) {
-        if (inactiveSelectionBackgroundColor == null) {
-            throw new IllegalArgumentException("inactiveSelectionBackgroundColor is null.");
-        }
+        Utils.checkNull(inactiveSelectionBackgroundColor, "inactiveSelectionBackgroundColor");
 
         setInactiveSelectionBackgroundColor(GraphicsUtilities.decodeColor(inactiveSelectionBackgroundColor));
     }
@@ -920,18 +993,15 @@ public class TerraTextInputSkin extends
     }
 
     public void setPadding(Insets padding) {
-        if (padding == null) {
-            throw new IllegalArgumentException("padding is null.");
-        }
+        Utils.checkNull(padding, "padding");
 
         this.padding = padding;
+
         invalidateComponent();
     }
 
     public final void setPadding(Dictionary<String, ?> padding) {
-        if (padding == null) {
-            throw new IllegalArgumentException("padding is null.");
-        }
+        Utils.checkNull(padding, "padding");
 
         setPadding(new Insets(padding));
     }
@@ -941,17 +1011,13 @@ public class TerraTextInputSkin extends
     }
 
     public final void setPadding(Number padding) {
-        if (padding == null) {
-            throw new IllegalArgumentException("padding is null.");
-        }
+        Utils.checkNull(padding, "padding");
 
         setPadding(padding.intValue());
     }
 
     public final void setPadding(String padding) {
-        if (padding == null) {
-            throw new IllegalArgumentException("padding is null.");
-        }
+        Utils.checkNull(padding, "padding");
 
         setPadding(Insets.decode(padding));
     }
@@ -961,11 +1027,10 @@ public class TerraTextInputSkin extends
     }
 
     public final void setHorizontalAlignment(HorizontalAlignment alignment) {
-        if (alignment == null) {
-            throw new IllegalArgumentException("horizontalAlignment is null.");
-        }
+        Utils.checkNull(alignment, "horizontalAlignment");
 
         this.horizontalAlignment = alignment;
+
         invalidateComponent();
     }
 
@@ -1001,12 +1066,9 @@ public class TerraTextInputSkin extends
                     scrollDirection = (x < 0) ? FocusTraversalDirection.BACKWARD
                         : FocusTraversalDirection.FORWARD;
 
-                    scheduledScrollSelectionCallback = ApplicationContext.scheduleRecurringCallback(
+                    // Run the callback once now to scroll the selection immediately
+                    scheduledScrollSelectionCallback = ApplicationContext.runAndScheduleRecurringCallback(
                         scrollSelectionCallback, SCROLL_RATE);
-
-                    // Run the callback once now to scroll the selection
-                    // immediately
-                    scrollSelectionCallback.run();
                 }
             }
         } else {
@@ -1028,6 +1090,7 @@ public class TerraTextInputSkin extends
 
             anchor = getInsertionPoint(x);
 
+            // TODO: this logic won't work in the presence of composed (but not yet committed) text...
             if (anchor != -1) {
                 if (Keyboard.isPressed(Keyboard.Modifier.SHIFT)) {
                     // Select the range
@@ -1074,6 +1137,8 @@ public class TerraTextInputSkin extends
     @Override
     public boolean mouseClick(Component component, Mouse.Button button, int x, int y, int count) {
         if (button == Mouse.Button.LEFT && count > 1) {
+            // TODO: double click to select the current word, triple click to select all
+            // (like TextArea and TextPane now)
             TextInput textInput = (TextInput) getComponent();
             textInput.selectAll();
         }
@@ -1096,8 +1161,7 @@ public class TerraTextInputSkin extends
                 if (textInput.getCharacterCount() - selectionLength + 1 > textInput.getMaximumLength()) {
                     Toolkit.getDefaultToolkit().beep();
                 } else {
-                    // NOTE We explicitly call getSelectionStart() twice here in
-                    // case the remove
+                    // NOTE We explicitly call getSelectionStart() twice here in case the remove
                     // event is vetoed
                     textInput.removeText(textInput.getSelectionStart(), selectionLength);
                     textInput.insertText(Character.toString(character),
@@ -1110,41 +1174,52 @@ public class TerraTextInputSkin extends
     }
 
     /**
-     * {@link KeyCode#DELETE DELETE} Delete the character after the caret or the
-     * entire selection if there is one.<br> {@link KeyCode#BACKSPACE BACKSPACE}
-     * Delete the character before the caret or the entire selection if there is
-     * one.<p> {@link KeyCode#HOME HOME} Move the caret to the beginning of the
-     * text. <br> {@link KeyCode#LEFT LEFT} + {@link Modifier#META META} Move
-     * the caret to the beginning of the text.<p> {@link KeyCode#HOME HOME} +
-     * {@link Modifier#SHIFT SHIFT} Select from the caret to the beginning of
-     * the text.<br> {@link KeyCode#LEFT LEFT} + {@link Modifier#META META} +
-     * {@link Modifier#SHIFT SHIFT} Select from the caret to the beginning of
-     * the text.<p> {@link KeyCode#END END} Move the caret to the end of the
-     * text.<br> {@link KeyCode#RIGHT RIGHT} + {@link Modifier#META META} Move
-     * the caret to the end of the text.<p> {@link KeyCode#END END} +
-     * {@link Modifier#SHIFT SHIFT} Select from the caret to the end of the
-     * text.<br> {@link KeyCode#RIGHT RIGHT} + {@link Modifier#META META} +
-     * {@link Modifier#SHIFT SHIFT} Select from the caret to the end of the
-     * text.<p> {@link KeyCode#LEFT LEFT} Clear the selection and move the caret
-     * back by one character.<br> {@link KeyCode#LEFT LEFT} +
-     * {@link Modifier#SHIFT SHIFT} Add the previous character to the
-     * selection.<br> {@link KeyCode#LEFT LEFT} + {@link Modifier#CTRL CTRL}
+     * {@link KeyCode#DELETE DELETE}
+     * Delete the character after the caret or the entire selection if there is one.<br>
+     * {@link KeyCode#BACKSPACE BACKSPACE}
+     * Delete the character before the caret or the entire selection if there is one.
+     * <p> {@link KeyCode#HOME HOME}
+     * Move the caret to the beginning of the text. <br>
+     * {@link KeyCode#LEFT LEFT} + {@link Modifier#META META}
+     * Move the caret to the beginning of the text.
+     * <p> {@link KeyCode#HOME HOME} + {@link Modifier#SHIFT SHIFT}
+     * Select from the caret to the beginning of the text.<br>
+     * {@link KeyCode#LEFT LEFT} + {@link Modifier#META META} + {@link Modifier#SHIFT SHIFT}
+     * Select from the caret to the beginning of the text.
+     * <p> {@link KeyCode#END END}
+     * Move the caret to the end of the text.<br>
+     * {@link KeyCode#RIGHT RIGHT} + {@link Modifier#META META}
+     * Move the caret to the end of the text.
+     * <p> {@link KeyCode#END END} + {@link Modifier#SHIFT SHIFT}
+     * Select from the caret to the end of the text.<br>
+     * {@link KeyCode#RIGHT RIGHT} + {@link Modifier#META META} + {@link Modifier#SHIFT SHIFT}
+     * Select from the caret to the end of the text.
+     * <p> {@link KeyCode#LEFT LEFT}
+     * Clear the selection and move the caret back by one character.<br>
+     * {@link KeyCode#LEFT LEFT} + {@link Modifier#SHIFT SHIFT}
+     * Add the previous character to the selection.<br>
+     * {@link KeyCode#LEFT LEFT} + {@link Modifier#CTRL CTRL}
      * Clear the selection and move the caret to the beginning of the text.<br>
-     * {@link KeyCode#LEFT LEFT} + {@link Modifier#CTRL CTRL} +
-     * {@link Modifier#SHIFT SHIFT} Add all preceding text to the selection. <p>
-     * {@link KeyCode#RIGHT RIGHT} Clear the selection and move the caret
-     * forward by one character.<br> {@link KeyCode#RIGHT RIGHT} +
-     * {@link Modifier#SHIFT SHIFT} Add the next character to the selection.<br>
-     * {@link KeyCode#RIGHT RIGHT} + {@link Modifier#CTRL CTRL} Clear the
-     * selection and move the caret to the end of the text.<br>
-     * {@link KeyCode#RIGHT RIGHT} + {@link Modifier#CTRL CTRL} +
-     * {@link Modifier#SHIFT SHIFT} Add all subsequent text to the selection.
-     * <p> CommandModifier + {@link KeyCode#A A} Select all.<br> CommandModifier
-     * + {@link KeyCode#X X} Cut selection to clipboard (if not a password
-     * TextInput).<br> CommandModifier + {@link KeyCode#C C} Copy selection to
-     * clipboard (if not a password TextInput).<br> CommandModifier +
-     * {@link KeyCode#V V} Paste from clipboard.<br> CommandModifier +
-     * {@link KeyCode#Z Z} Undo.
+     * {@link KeyCode#LEFT LEFT} + {@link Modifier#CTRL CTRL} + {@link Modifier#SHIFT SHIFT}
+     * Add all preceding text to the selection.
+     * <p> {@link KeyCode#RIGHT RIGHT}
+     * Clear the selection and move the caret forward by one character.<br>
+     * {@link KeyCode#RIGHT RIGHT} + {@link Modifier#SHIFT SHIFT}
+     * Add the next character to the selection.<br>
+     * {@link KeyCode#RIGHT RIGHT} + {@link Modifier#CTRL CTRL}
+     * Clear the selection and move the caret to the end of the text.<br>
+     * {@link KeyCode#RIGHT RIGHT} + {@link Modifier#CTRL CTRL} + {@link Modifier#SHIFT SHIFT}
+     * Add all subsequent text to the selection.
+     * <p> CommandModifier + {@link KeyCode#A A}
+     * Select all.<br>
+     * CommandModifier + {@link KeyCode#X X}
+     * Cut selection to clipboard (if not a password TextInput).<br>
+     * CommandModifier + {@link KeyCode#C C}
+     * Copy selection to clipboard (if not a password TextInput).<br>
+     * CommandModifier + {@link KeyCode#V V}
+     * Paste from clipboard.<br>
+     * CommandModifier + {@link KeyCode#Z Z}
+     * Undo.
      *
      * @see Platform#getCommandModifier()
      */
@@ -1559,7 +1634,7 @@ public class TerraTextInputSkin extends
             if (n == 0) {
                 x = padding.left - scrollLeft + 1 + getAlignmentDeltaX();
             } else {
-                Rectangle2D textBounds = glyphVector.getLogicalBounds();
+                Rectangle2D textBounds = textLayout.getBounds();
                 x = (int) Math.ceil(textBounds.getWidth())
                     + (padding.left - scrollLeft + 1 + getAlignmentDeltaX());
             }
@@ -1590,14 +1665,17 @@ public class TerraTextInputSkin extends
 
         if (show) {
             caretOn = true;
-            scheduledBlinkCaretCallback = ApplicationContext.scheduleRecurringCallback(
-                blinkCaretCallback, Platform.getCursorBlinkRate());
-
             // Run the callback once now to show the cursor immediately
-            blinkCaretCallback.run();
+            scheduledBlinkCaretCallback = ApplicationContext.runAndScheduleRecurringCallback(
+                blinkCaretCallback, Platform.getCursorBlinkRate());
         } else {
             scheduledBlinkCaretCallback = null;
         }
     }
 
+    @Override
+    public TextInputMethodListener getTextInputMethodListener() {
+        return textInputMethodHandler;
+    }
+
 }

Modified: pivot/trunk/wtk/src/org/apache/pivot/wtk/ApplicationContext.java
URL: http://svn.apache.org/viewvc/pivot/trunk/wtk/src/org/apache/pivot/wtk/ApplicationContext.java?rev=1795471&r1=1795470&r2=1795471&view=diff
==============================================================================
--- pivot/trunk/wtk/src/org/apache/pivot/wtk/ApplicationContext.java (original)
+++ pivot/trunk/wtk/src/org/apache/pivot/wtk/ApplicationContext.java Thu May 18 01:50:14 2017
@@ -22,6 +22,7 @@ import java.awt.Graphics;
 import java.awt.Graphics2D;
 import java.awt.GraphicsConfiguration;
 import java.awt.PrintGraphics;
+import java.awt.Rectangle;
 import java.awt.RenderingHints;
 import java.awt.Transparency;
 import java.awt.dnd.DnDConstants;
@@ -39,9 +40,12 @@ import java.awt.dnd.DropTargetListener;
 import java.awt.event.ComponentEvent;
 import java.awt.event.FocusEvent;
 import java.awt.event.InputEvent;
+import java.awt.event.InputMethodEvent;
 import java.awt.event.KeyEvent;
 import java.awt.event.MouseEvent;
 import java.awt.event.MouseWheelEvent;
+import java.awt.font.TextHitInfo;
+import java.awt.im.InputMethodRequests;
 import java.awt.image.BufferedImage;
 import java.awt.image.VolatileImage;
 import java.awt.print.PrinterGraphics;
@@ -52,6 +56,7 @@ import java.net.MalformedURLException;
 import java.net.URI;
 import java.net.URISyntaxException;
 import java.net.URL;
+import java.text.AttributedCharacterIterator;
 import java.util.Iterator;
 import java.util.Random;
 import java.util.Timer;
@@ -298,10 +303,108 @@ public abstract class ApplicationContext
             }
         };
 
+        private transient TextInputMethodListener textInputMethodListener = new TextInputMethodListener() {
+
+            private TextInputMethodListener getCurrentListener() {
+                Component focusedComponent = Component.getFocusedComponent();
+                if (focusedComponent != null) {
+                    return focusedComponent.getTextInputMethodListener();
+                }
+                return null;
+            }
+
+            @Override
+            public AttributedCharacterIterator cancelLatestCommittedText(AttributedCharacterIterator.Attribute[] attributes) {
+                TextInputMethodListener listener = getCurrentListener();
+                if (listener != null) {
+                    return listener.cancelLatestCommittedText(attributes);
+                }
+                return null;
+            }
+
+            @Override
+            public AttributedCharacterIterator getCommittedText(int beginIndex, int endIndex, AttributedCharacterIterator.Attribute[] attributes) {
+                TextInputMethodListener listener = getCurrentListener();
+                if (listener != null) {
+                    return listener.getCommittedText(beginIndex, endIndex, attributes);
+                }
+                return null;
+            }
+
+            @Override
+            public int getCommittedTextLength() {
+                TextInputMethodListener listener = getCurrentListener();
+                if (listener != null) {
+                    return listener.getCommittedTextLength();
+                }
+                return 0;
+            }
+
+            @Override
+            public int getInsertPositionOffset() {
+                TextInputMethodListener listener = getCurrentListener();
+                if (listener != null) {
+                    return listener.getInsertPositionOffset();
+                }
+                return 0;
+            }
+
+            @Override
+            public TextHitInfo getLocationOffset(int x, int y) {
+                TextInputMethodListener listener = getCurrentListener();
+                if (listener != null) {
+                    return listener.getLocationOffset(x, y);
+                }
+                return null;
+            }
+
+            @Override
+            public AttributedCharacterIterator getSelectedText(AttributedCharacterIterator.Attribute[] attributes) {
+                TextInputMethodListener listener = getCurrentListener();
+                if (listener != null) {
+                    return listener.getSelectedText(attributes);
+                }
+                return null;
+            }
+
+            @Override
+            public Rectangle getTextLocation(TextHitInfo offset) {
+                TextInputMethodListener listener = getCurrentListener();
+                if (listener != null) {
+                    return listener.getTextLocation(offset);
+                }
+                return new Rectangle();
+            }
+
+            @Override
+            public void inputMethodTextChanged(InputMethodEvent event) {
+                TextInputMethodListener listener = getCurrentListener();
+                if (listener != null) {
+                    listener.inputMethodTextChanged(event);
+                }
+            }
+
+            @Override
+            public void caretPositionChanged(InputMethodEvent event) {
+                TextInputMethodListener listener = getCurrentListener();
+                if (listener != null) {
+                    listener.caretPositionChanged(event);
+                }
+            }
+
+        };
+
+        @Override
+        public InputMethodRequests getInputMethodRequests() {
+            return textInputMethodListener;
+        }
+
         public DisplayHost() {
             enableEvents(AWTEvent.COMPONENT_EVENT_MASK | AWTEvent.FOCUS_EVENT_MASK
                 | AWTEvent.MOUSE_EVENT_MASK | AWTEvent.MOUSE_MOTION_EVENT_MASK
                 | AWTEvent.MOUSE_WHEEL_EVENT_MASK | AWTEvent.KEY_EVENT_MASK);
+            enableInputMethods(true);
+            addInputMethodListener(textInputMethodListener);
 
             try {
                 System.setProperty("sun.awt.noerasebackground", "true");
@@ -896,6 +999,11 @@ public abstract class ApplicationContext
         }
 
         @Override
+        protected void processInputMethodEvent(InputMethodEvent event) {
+            super.processInputMethodEvent(event);
+        }
+
+        @Override
         protected void processMouseEvent(MouseEvent event) {
             super.processMouseEvent(event);
 
@@ -1925,6 +2033,54 @@ public abstract class ApplicationContext
 
         return scheduledCallback;
     }
+
+    /**
+     * Runs a task and then schedules it for repeated execution.
+     * The task will be executed on the UI thread and will begin
+     * executing immediately.
+     *
+     * @param callback The task to execute.
+     * @param period The interval at which the task will be repeated (in
+     * milliseconds).
+     * @return The callback object.
+     */
+    public static ScheduledCallback runAndScheduleRecurringCallback(Runnable callback, long period) {
+        return runAndScheduleRecurringCallback(callback, 0, period);
+    }
+
+    /**
+     * Runs a task and then schedules it for repeated execution. The task will be executed on the
+     * UI thread.
+     *
+     * @param callback The task to execute.
+     * @param delay The length of time to wait before the first execution of the
+     * task (milliseconds).
+     * @param period The interval at which the task will be repeated (also in
+     * milliseconds).
+     * @return The callback object.
+     */
+    public static ScheduledCallback runAndScheduleRecurringCallback(Runnable callback, long delay,
+        long period) {
+
+        ScheduledCallback scheduledCallback = new ScheduledCallback(callback);
+
+        // TODO This is a workaround for a potential OS X bug; revisit
+        try {
+            try {
+                timer.schedule(scheduledCallback, delay, period);
+            } catch (IllegalStateException exception) {
+                createTimer();
+                timer.schedule(scheduledCallback, delay, period);
+            }
+        } catch (Throwable throwable) {
+            System.err.println("Unable to schedule callback: " + throwable);
+        }
+
+        // Before returning, run the task once to start things off
+        callback.run();
+
+        return scheduledCallback;
+    }
 
     /**
      * Queues a task to execute after all pending events have been processed and

Modified: pivot/trunk/wtk/src/org/apache/pivot/wtk/Component.java
URL: http://svn.apache.org/viewvc/pivot/trunk/wtk/src/org/apache/pivot/wtk/Component.java?rev=1795471&r1=1795470&r2=1795471&view=diff
==============================================================================
--- pivot/trunk/wtk/src/org/apache/pivot/wtk/Component.java (original)
+++ pivot/trunk/wtk/src/org/apache/pivot/wtk/Component.java Thu May 18 01:50:14 2017
@@ -2772,6 +2772,17 @@ public abstract class Component implemen
         return consumed;
     }
 
+    /**
+     * Returns the input method listener for this component,
+     * which will reside in the skin, so defer to the skin class.
+     *
+     * @return The input method listener (if any) for this
+     * component.
+     */
+    public TextInputMethodListener getTextInputMethodListener() {
+        return ((ComponentSkin)getSkin()).getTextInputMethodListener();
+    }
+
     @Override
     public String toString() {
         String s;

Modified: pivot/trunk/wtk/src/org/apache/pivot/wtk/GraphicsUtilities.java
URL: http://svn.apache.org/viewvc/pivot/trunk/wtk/src/org/apache/pivot/wtk/GraphicsUtilities.java?rev=1795471&r1=1795470&r2=1795471&view=diff
==============================================================================
--- pivot/trunk/wtk/src/org/apache/pivot/wtk/GraphicsUtilities.java (original)
+++ pivot/trunk/wtk/src/org/apache/pivot/wtk/GraphicsUtilities.java Thu May 18 01:50:14 2017
@@ -17,13 +17,17 @@
 package org.apache.pivot.wtk;
 
 import java.awt.Color;
+import java.awt.Font;
 import java.awt.GradientPaint;
 import java.awt.Graphics2D;
 import java.awt.LinearGradientPaint;
 import java.awt.Paint;
 import java.awt.RadialGradientPaint;
 import java.awt.RenderingHints;
+import java.awt.font.FontRenderContext;
+import java.awt.font.GlyphVector;
 import java.awt.geom.AffineTransform;
+import java.awt.geom.Rectangle2D;
 import java.util.HashMap;
 import java.util.Locale;
 import java.util.Map;
@@ -286,10 +290,8 @@ public final class GraphicsUtilities {
 
         if (width > 0 && height > 0 && thickness > 0) {
             drawLine(rectGraphics, x, y, width, Orientation.HORIZONTAL, thickness);
-            drawLine(rectGraphics, x + width - thickness, y, height, Orientation.VERTICAL,
-                thickness);
-            drawLine(rectGraphics, x, y + height - thickness, width, Orientation.HORIZONTAL,
-                thickness);
+            drawLine(rectGraphics, x + width - thickness, y, height, Orientation.VERTICAL, thickness);
+            drawLine(rectGraphics, x, y + height - thickness, width, Orientation.HORIZONTAL, thickness);
             drawLine(rectGraphics, x, y, height, Orientation.VERTICAL, thickness);
         }
 
@@ -301,13 +303,17 @@ public final class GraphicsUtilities {
     /**
      * Interprets a string as a color value.
      *
-     * @param value One of the following forms: <ul> <li>0xdddddddd - 8
-     * hexadecimal digits, specifying 8 bits each of red, green, and blue,
-     * followed by 8 bits of alpha.</li> <li>#dddddd - 6 hexadecimal digits,
-     * specifying 8 bits each of red, green, and blue. <li>Any of the names of
-     * the static colors in the Java {@link Color} class.</li>
-     * <li>Any of the CSS3/X11 color names from here: http://www.w3.org/TR/css3-color/
-     * (except the Java color names will be accepted first if there is a conflict).</li></ul>
+     * @param value One of the following forms:
+     * <ul>
+     * <li>0xdddddddd - 8 hexadecimal digits, specifying 8 bits each of red,
+     * green, and blue, followed by 8 bits of alpha.</li>
+     * <li>#dddddd - 6 hexadecimal digits, specifying 8 bits each of red,
+     * green, and blue.</li>
+     * <li>Any of the names of the static colors in the Java {@link Color} class.</li>
+     * <li>Any of the CSS3/X11 color names from here:
+     * <a href="http://www.w3.org/TR/css3-color/">http://www.w3.org/TR/css3-color/</a>
+     * (except the Java color names will be accepted first if there is a conflict).</li>
+     * </ul>
      * @return A {@link Color} on successful decoding
      * @throws NumberFormatException if the value in the first two cases
      * contains illegal hexadecimal digits.
@@ -371,6 +377,14 @@ public final class GraphicsUtilities {
         return color;
     }
 
+    /**
+     * Generate a full color value given the RGB value along with the alpha
+     * (opacity) value.
+     *
+     * @param rgb The 24-bit red, green, and blue value.
+     * @param alpha The opacity value (0.0 - 1.0).
+     * @return The full color value from these two parts.
+     */
     public static Color getColor(int rgb, float alpha) {
         float red = ((rgb >> 16) & 0xff) / 255f;
         float green = ((rgb >> 8) & 0xff) / 255f;
@@ -516,4 +530,45 @@ public final class GraphicsUtilities {
 
         return paint;
     }
+
+    /**
+     * Set the context in the given graphics environment for subsequent font drawing.
+     *
+     * @param graphics          The graphics context.
+     * @param fontRenderContext The font rendering context used to get the font drawing hints.
+     * @param font              The font to use.
+     * @param color             The foreground color for the text.
+     */
+    public static void prepareForText(Graphics2D graphics, FontRenderContext fontRenderContext, Font font, Color color) {
+        graphics.setFont(font);
+        graphics.setColor(color);
+        graphics.setRenderingHint(RenderingHints.KEY_TEXT_ANTIALIASING, fontRenderContext.getAntiAliasingHint());
+        graphics.setRenderingHint(RenderingHints.KEY_FRACTIONALMETRICS, fontRenderContext.getFractionalMetricsHint());
+    }
+
+    /**
+     * Calculate the average character bounds for the given font.
+     * <p> This bounds is the width of the "missing glyph code" and
+     * the maximum character height of any glyph in the font.
+     *
+     * @param font The font in question.
+     * @return The bounding rectangle to use for the average character size.
+     * @see Platform#getFontRenderContext
+     */
+    public static Dimensions getAverageCharacterSize(Font font) {
+        int missingGlyphCode = font.getMissingGlyphCode();
+        FontRenderContext fontRenderContext = Platform.getFontRenderContext();
+
+        GlyphVector missingGlyphVector = font.createGlyphVector(fontRenderContext,
+            new int[] { missingGlyphCode });
+        Rectangle2D textBounds = missingGlyphVector.getLogicalBounds();
+
+        Rectangle2D maxCharBounds = font.getMaxCharBounds(fontRenderContext);
+        return new Dimensions(
+            (int) Math.ceil(textBounds.getWidth()),
+            (int) Math.ceil(maxCharBounds.getHeight())
+          );
+    }
+
 }
+

Modified: pivot/trunk/wtk/src/org/apache/pivot/wtk/TextInput.java
URL: http://svn.apache.org/viewvc/pivot/trunk/wtk/src/org/apache/pivot/wtk/TextInput.java?rev=1795471&r1=1795470&r2=1795471&view=diff
==============================================================================
--- pivot/trunk/wtk/src/org/apache/pivot/wtk/TextInput.java (original)
+++ pivot/trunk/wtk/src/org/apache/pivot/wtk/TextInput.java Thu May 18 01:50:14 2017
@@ -22,6 +22,7 @@ import java.io.IOException;
 import org.apache.pivot.collections.LinkedList;
 import org.apache.pivot.json.JSON;
 import org.apache.pivot.util.ListenerList;
+import org.apache.pivot.util.Utils;
 import org.apache.pivot.util.Vote;
 import org.apache.pivot.wtk.validation.Validator;
 
@@ -327,9 +328,7 @@ public class TextInput extends Component
     }
 
     public void setText(String text) {
-        if (text == null) {
-            throw new IllegalArgumentException();
-        }
+        Utils.checkNull(text, "text");
 
         if (text.length() > maximumLength) {
             throw new IllegalArgumentException("Text length is greater than maximum length.");
@@ -367,9 +366,7 @@ public class TextInput extends Component
     }
 
     private void insertText(CharSequence text, int index, boolean addToEditHistory) {
-        if (text == null) {
-            throw new IllegalArgumentException();
-        }
+        Utils.checkNull(text, "text");
 
         if (characters.length() + text.length() > maximumLength) {
             throw new IllegalArgumentException("Insertion of text would exceed maximum length.");
@@ -470,6 +467,16 @@ public class TextInput extends Component
     }
 
     /**
+     * @return A (sub) character sequence representing the contents between
+     * the given indices.
+     * @param start The start of the sequence (inclusive).
+     * @param end The end of the sequence (exclusive).
+     */
+    public CharSequence getCharacters(int start, int end) {
+        return characters.subSequence(start, end);
+    }
+
+    /**
      * @return The character at a given index.
      *
      * @param index Location of the character to retrieve.
@@ -612,9 +619,7 @@ public class TextInput extends Component
      * @throws IllegalArgumentException if the selection span is {@code null}.
      */
     public final void setSelection(Span selection) {
-        if (selection == null) {
-            throw new IllegalArgumentException("selection is null.");
-        }
+        Utils.checkNull(selection, "selection");
 
         setSelection(Math.min(selection.start, selection.end), (int) selection.getLength());
     }
@@ -783,9 +788,7 @@ public class TextInput extends Component
     }
 
     public void setTextBindType(BindType textBindType) {
-        if (textBindType == null) {
-            throw new IllegalArgumentException();
-        }
+        Utils.checkNull(textBindType, "textBindType");
 
         BindType previousTextBindType = this.textBindType;
 

Added: pivot/trunk/wtk/src/org/apache/pivot/wtk/TextInputMethodListener.java
URL: http://svn.apache.org/viewvc/pivot/trunk/wtk/src/org/apache/pivot/wtk/TextInputMethodListener.java?rev=1795471&view=auto
==============================================================================
--- pivot/trunk/wtk/src/org/apache/pivot/wtk/TextInputMethodListener.java (added)
+++ pivot/trunk/wtk/src/org/apache/pivot/wtk/TextInputMethodListener.java Thu May 18 01:50:14 2017
@@ -0,0 +1,106 @@
+/*
+ * 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;
+
+import java.awt.Rectangle;
+import java.awt.event.InputMethodEvent;
+import java.awt.event.InputMethodListener;
+import java.awt.font.TextHitInfo;
+import java.awt.im.InputMethodRequests;
+import java.text.AttributedCharacterIterator;
+
+
+/**
+ * An interface expected to be implemented by every (text) component
+ * that can interace with the Input Method Editors for on-the-spot
+ * editing, esp. of Far Eastern languages (Chinese, Japanese, etc.).
+ * <p> This interface encapsulates both the {@link InputMethodRequests}
+ * and {@link InputMethodListener} standard interfaces to reduce the
+ * number of objects needed.
+ */
+public interface TextInputMethodListener extends InputMethodRequests, InputMethodListener {
+
+    /**
+     * A default implementation of the {@link TextInputMethodListener} interface that can be used
+     * to provide the minimum necessary functionality.
+     */
+    public static class Adapter implements TextInputMethodListener {
+        @Override
+        public AttributedCharacterIterator cancelLatestCommittedText(AttributedCharacterIterator.Attribute[] attributes) {
+            return null;
+        }
+
+        @Override
+        public AttributedCharacterIterator getCommittedText(int beginIndex, int endIndex, AttributedCharacterIterator.Attribute[] attributes) {
+            return null;
+        }
+
+        @Override
+        public int getCommittedTextLength() {
+            return 0;
+        }
+
+        @Override
+        public int getInsertPositionOffset() {
+            return 0;
+        }
+
+        @Override
+        public TextHitInfo getLocationOffset(int x, int y) {
+            return null;
+        }
+
+        @Override
+        public AttributedCharacterIterator getSelectedText(AttributedCharacterIterator.Attribute[] attributes) {
+            return null;
+        }
+
+        @Override
+        public Rectangle getTextLocation(TextHitInfo offset) {
+            return new Rectangle();
+        }
+
+        @Override
+        public void inputMethodTextChanged(InputMethodEvent event) {
+            // empty block
+        }
+
+        @Override
+        public void caretPositionChanged(InputMethodEvent event) {
+            // empty block
+        }
+    }
+
+    AttributedCharacterIterator cancelLatestCommittedText(AttributedCharacterIterator.Attribute[] attributes);
+
+    AttributedCharacterIterator getCommittedText(int beginIndex, int endIndex, AttributedCharacterIterator.Attribute[] attributes);
+
+    int getCommittedTextLength();
+
+    int getInsertPositionOffset();
+
+    TextHitInfo getLocationOffset(int x, int y);
+
+    AttributedCharacterIterator getSelectedText(AttributedCharacterIterator.Attribute[] attributes);
+
+    Rectangle getTextLocation(TextHitInfo offset);
+
+    void inputMethodTextChanged(InputMethodEvent event);
+
+    void caretPositionChanged(InputMethodEvent event);
+
+}

Modified: pivot/trunk/wtk/src/org/apache/pivot/wtk/skin/ComponentSkin.java
URL: http://svn.apache.org/viewvc/pivot/trunk/wtk/src/org/apache/pivot/wtk/skin/ComponentSkin.java?rev=1795471&r1=1795470&r2=1795471&view=diff
==============================================================================
--- pivot/trunk/wtk/src/org/apache/pivot/wtk/skin/ComponentSkin.java (original)
+++ pivot/trunk/wtk/src/org/apache/pivot/wtk/skin/ComponentSkin.java Thu May 18 01:50:14 2017
@@ -45,6 +45,7 @@ import org.apache.pivot.wtk.MenuHandler;
 import org.apache.pivot.wtk.Mouse;
 import org.apache.pivot.wtk.Point;
 import org.apache.pivot.wtk.Skin;
+import org.apache.pivot.wtk.TextInputMethodListener;
 import org.apache.pivot.wtk.Theme;
 import org.apache.pivot.wtk.Tooltip;
 
@@ -464,4 +465,16 @@ public abstract class ComponentSkin impl
         return currentTheme().getDefaultForegroundColor();
     }
 
+    /**
+     * Returns the input method listener for this component.
+     * <p> Should be overridden by any component's skin that wants
+     * to handle Input Method events (such as <tt>TextInput</tt>).
+     *
+     * @return The input method listener (if any) for this
+     * component.
+     */
+    public TextInputMethodListener getTextInputMethodListener() {
+        return null;
+    }
+
 }

Modified: pivot/trunk/wtk/src/org/apache/pivot/wtk/skin/TextAreaSkin.java
URL: http://svn.apache.org/viewvc/pivot/trunk/wtk/src/org/apache/pivot/wtk/skin/TextAreaSkin.java?rev=1795471&r1=1795470&r2=1795471&view=diff
==============================================================================
--- pivot/trunk/wtk/src/org/apache/pivot/wtk/skin/TextAreaSkin.java (original)
+++ pivot/trunk/wtk/src/org/apache/pivot/wtk/skin/TextAreaSkin.java Thu May 18 01:50:14 2017
@@ -31,6 +31,7 @@ import java.awt.geom.Rectangle2D;
 import org.apache.pivot.collections.ArrayList;
 import org.apache.pivot.collections.Dictionary;
 import org.apache.pivot.collections.Sequence;
+import org.apache.pivot.util.Utils;
 import org.apache.pivot.wtk.ApplicationContext;
 import org.apache.pivot.wtk.Bounds;
 import org.apache.pivot.wtk.Component;
@@ -513,22 +514,11 @@ public class TextAreaSkin extends Compon
      * @param font The new font for the text.
      */
     public void setFont(Font font) {
-        if (font == null) {
-            throw new IllegalArgumentException("font is null.");
-        }
+        Utils.checkNull(font, "font");
 
         this.font = font;
 
-        int missingGlyphCode = font.getMissingGlyphCode();
-        FontRenderContext fontRenderContext = Platform.getFontRenderContext();
-
-        GlyphVector missingGlyphVector = font.createGlyphVector(fontRenderContext,
-            new int[] { missingGlyphCode });
-        Rectangle2D textBounds = missingGlyphVector.getLogicalBounds();
-
-        Rectangle2D maxCharBounds = font.getMaxCharBounds(fontRenderContext);
-        averageCharacterSize = new Dimensions((int) Math.ceil(textBounds.getWidth()),
-            (int) Math.ceil(maxCharBounds.getHeight()));
+        averageCharacterSize = GraphicsUtilities.getAverageCharacterSize(font);
 
         invalidateComponent();
     }
@@ -539,9 +529,7 @@ public class TextAreaSkin extends Compon
      * @param font A {@link ComponentSkin#decodeFont(String) font specification}
      */
     public final void setFont(String font) {
-        if (font == null) {
-            throw new IllegalArgumentException("font is null.");
-        }
+        Utils.checkNull(font, "font");
 
         setFont(decodeFont(font));
     }
@@ -552,9 +540,7 @@ public class TextAreaSkin extends Compon
      * @param font A dictionary {@link Theme#deriveFont describing a font}
      */
     public final void setFont(Dictionary<String, ?> font) {
-        if (font == null) {
-            throw new IllegalArgumentException("font is null.");
-        }
+        Utils.checkNull(font, "font");
 
         setFont(Theme.deriveFont(font));
     }