diff options
author | Jeff Sharkey <jsharkey@android.com> | 2011-03-21 16:40:23 -0700 |
---|---|---|
committer | Jeff Sharkey <jsharkey@android.com> | 2011-03-21 18:20:49 -0700 |
commit | e982dfc1bae36620f67371efc7b0a0f8adc9450d (patch) | |
tree | eacbafbdd7542c0d8bcce9bfafb4ae69bad0dc15 /core/java/android/text | |
parent | b57816957a1105ca42e2abca7dbc2203f24b4e2a (diff) | |
download | frameworks_base-e982dfc1bae36620f67371efc7b0a0f8adc9450d.zip frameworks_base-e982dfc1bae36620f67371efc7b0a0f8adc9450d.tar.gz frameworks_base-e982dfc1bae36620f67371efc7b0a0f8adc9450d.tar.bz2 |
Support Ctrl-based EditText movement.
EditText now listens for Ctrl+left/right to jump through text at word
boundaries. It also listens for Ctrl+home/end to move to start/end of
the full text. This emulates behavior found in desktop text editors.
Bug: 4081964
Change-Id: I98bd19c0d8707357847db3466648a83fd774dbaf
Diffstat (limited to 'core/java/android/text')
-rw-r--r-- | core/java/android/text/CharSequenceIterator.java | 95 | ||||
-rw-r--r-- | core/java/android/text/Selection.java | 49 | ||||
-rw-r--r-- | core/java/android/text/method/ArrowKeyMovementMethod.java | 125 | ||||
-rw-r--r-- | core/java/android/text/method/BaseMovementMethod.java | 22 |
4 files changed, 285 insertions, 6 deletions
diff --git a/core/java/android/text/CharSequenceIterator.java b/core/java/android/text/CharSequenceIterator.java new file mode 100644 index 0000000..4946406 --- /dev/null +++ b/core/java/android/text/CharSequenceIterator.java @@ -0,0 +1,95 @@ +/* + * Copyright (C) 2011 The Android Open Source Project + * + * Licensed 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 android.text; + +import android.util.MathUtils; + +import java.text.CharacterIterator; + +/** {@hide} */ +public class CharSequenceIterator implements CharacterIterator { + private final CharSequence mValue; + + private final int mStart; + private final int mEnd; + private int mIndex; + + public CharSequenceIterator(CharSequence value) { + mValue = value; + mStart = 0; + mEnd = value.length(); + mIndex = 0; + } + + @Override + public Object clone() { + try { + return super.clone(); + } catch (CloneNotSupportedException e) { + throw new AssertionError(e); + } + } + + /** {@inheritDoc} */ + public char current() { + if (mIndex == mEnd) { + return DONE; + } + return mValue.charAt(mIndex); + } + + /** {@inheritDoc} */ + public int getBeginIndex() { + return mStart; + } + + /** {@inheritDoc} */ + public int getEndIndex() { + return mEnd; + } + + /** {@inheritDoc} */ + public int getIndex() { + return mIndex; + } + + /** {@inheritDoc} */ + public char first() { + return setIndex(mStart); + } + + /** {@inheritDoc} */ + public char last() { + return setIndex(mEnd - 1); + } + + /** {@inheritDoc} */ + public char next() { + return setIndex(mIndex + 1); + } + + /** {@inheritDoc} */ + public char previous() { + return setIndex(mIndex - 1); + } + + /** {@inheritDoc} */ + public char setIndex(int index) { + mIndex = MathUtils.constrain(index, mStart, mEnd); + return current(); + } +} diff --git a/core/java/android/text/Selection.java b/core/java/android/text/Selection.java index 13cb5e6..b18570a 100644 --- a/core/java/android/text/Selection.java +++ b/core/java/android/text/Selection.java @@ -16,6 +16,11 @@ package android.text; +import android.util.Log; + +import java.text.BreakIterator; +import java.text.CharacterIterator; + /** * Utility class for manipulating cursors and selections in CharSequences. @@ -38,7 +43,7 @@ public class Selection { else return -1; } - + /** * Return the offset of the selection edge or cursor, or -1 if * there is no selection or cursor. @@ -57,7 +62,7 @@ public class Selection { // private static int pin(int value, int min, int max) { // return value < min ? 0 : (value > max ? max : value); // } - + /** * Set the selection anchor to <code>start</code> and the selection edge * to <code>stop</code>. @@ -69,7 +74,7 @@ public class Selection { int ostart = getSelectionStart(text); int oend = getSelectionEnd(text); - + if (ostart != start || oend != stop) { text.setSpan(SELECTION_START, start, start, Spanned.SPAN_POINT_POINT|Spanned.SPAN_INTERMEDIATE); @@ -357,6 +362,42 @@ public class Selection { return true; } + /** {@hide} */ + public static interface PositionIterator { + public static final int DONE = BreakIterator.DONE; + + public int preceding(int position); + public int following(int position); + } + + /** {@hide} */ + public static boolean moveToPreceding( + Spannable text, PositionIterator iter, boolean extendSelection) { + final int offset = iter.preceding(getSelectionEnd(text)); + if (offset != PositionIterator.DONE) { + if (extendSelection) { + extendSelection(text, offset); + } else { + setSelection(text, offset); + } + } + return true; + } + + /** {@hide} */ + public static boolean moveToFollowing( + Spannable text, PositionIterator iter, boolean extendSelection) { + final int offset = iter.following(getSelectionEnd(text)); + if (offset != PositionIterator.DONE) { + if (extendSelection) { + extendSelection(text, offset); + } else { + setSelection(text, offset); + } + } + return true; + } + private static int findEdge(Spannable text, Layout layout, int dir) { int pt = getSelectionEnd(text); int line = layout.getLineForOffset(pt); @@ -419,7 +460,7 @@ public class Selection { private static final class START implements NoCopySpan { } private static final class END implements NoCopySpan { } - + /* * Public constants */ diff --git a/core/java/android/text/method/ArrowKeyMovementMethod.java b/core/java/android/text/method/ArrowKeyMovementMethod.java index a61ff13..80c0106 100644 --- a/core/java/android/text/method/ArrowKeyMovementMethod.java +++ b/core/java/android/text/method/ArrowKeyMovementMethod.java @@ -17,14 +17,23 @@ package android.text.method; import android.graphics.Rect; +import android.text.CharSequenceIterator; +import android.text.Editable; import android.text.Layout; import android.text.Selection; import android.text.Spannable; +import android.text.Spanned; +import android.text.TextWatcher; +import android.util.Log; +import android.util.MathUtils; import android.view.KeyEvent; import android.view.MotionEvent; import android.view.View; import android.widget.TextView; +import java.text.BreakIterator; +import java.text.CharacterIterator; + /** * A movement method that provides cursor movement and selection. * Supports displaying the context menu on DPad Center. @@ -193,6 +202,20 @@ public class ArrowKeyMovementMethod extends BaseMovementMethod implements Moveme } } + /** {@hide} */ + @Override + protected boolean leftWord(TextView widget, Spannable buffer) { + mWordIterator.setCharSequence(buffer); + return Selection.moveToPreceding(buffer, mWordIterator, isSelecting(buffer)); + } + + /** {@hide} */ + @Override + protected boolean rightWord(TextView widget, Spannable buffer) { + mWordIterator.setCharSequence(buffer); + return Selection.moveToFollowing(buffer, mWordIterator, isSelecting(buffer)); + } + @Override protected boolean home(TextView widget, Spannable buffer) { return lineStart(widget, buffer); @@ -205,7 +228,8 @@ public class ArrowKeyMovementMethod extends BaseMovementMethod implements Moveme @Override public boolean onTouchEvent(TextView widget, Spannable buffer, MotionEvent event) { - int initialScrollX = -1, initialScrollY = -1; + int initialScrollX = -1; + int initialScrollY = -1; final int action = event.getAction(); if (action == MotionEvent.ACTION_UP) { @@ -220,7 +244,7 @@ public class ArrowKeyMovementMethod extends BaseMovementMethod implements Moveme boolean cap = isSelecting(buffer); if (cap) { int offset = widget.getOffset((int) event.getX(), (int) event.getY()); - + buffer.setSpan(LAST_TAP_DOWN, offset, offset, Spannable.SPAN_POINT_POINT); // Disallow intercepting of the touch events, so that @@ -308,6 +332,103 @@ public class ArrowKeyMovementMethod extends BaseMovementMethod implements Moveme return sInstance; } + /** + * Walks through cursor positions at word boundaries. Internally uses + * {@link BreakIterator#getWordInstance()}, and caches {@link CharSequence} + * for performance reasons. + */ + private static class WordIterator implements Selection.PositionIterator { + private CharSequence mCurrent; + private boolean mCurrentDirty = false; + + private BreakIterator mIterator; + + private TextWatcher mWatcher = new TextWatcher() { + /** {@inheritDoc} */ + public void beforeTextChanged(CharSequence s, int start, int count, int after) { + // ignored + } + + /** {@inheritDoc} */ + public void onTextChanged(CharSequence s, int start, int before, int count) { + mCurrentDirty = true; + } + + /** {@inheritDoc} */ + public void afterTextChanged(Editable s) { + // ignored + } + }; + + public void setCharSequence(CharSequence incoming) { + if (mIterator == null) { + mIterator = BreakIterator.getWordInstance(); + } + + // when incoming is different object, move listeners to new sequence + // and mark as dirty so we reload contents. + if (mCurrent != incoming) { + if (mCurrent instanceof Editable) { + ((Editable) mCurrent).removeSpan(mWatcher); + } + + if (incoming instanceof Editable) { + ((Editable) incoming).setSpan( + mWatcher, 0, incoming.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE); + } + + mCurrent = incoming; + mCurrentDirty = true; + } + + if (mCurrentDirty) { + final CharacterIterator charIterator = new CharSequenceIterator(mCurrent); + mIterator.setText(charIterator); + + mCurrentDirty = false; + } + } + + private boolean isValidOffset(int offset) { + return offset >= 0 && offset < mCurrent.length(); + } + + private boolean isLetterOrDigit(int offset) { + if (isValidOffset(offset)) { + return Character.isLetterOrDigit(mCurrent.charAt(offset)); + } else { + return false; + } + } + + /** {@inheritDoc} */ + public int preceding(int offset) { + // always round cursor index into valid string index + offset = MathUtils.constrain(offset, 0, mCurrent.length() - 1); + + do { + offset = mIterator.preceding(offset); + if (isLetterOrDigit(offset)) break; + } while (isValidOffset(offset)); + + return offset; + } + + /** {@inheritDoc} */ + public int following(int offset) { + // always round cursor index into valid string index + offset = MathUtils.constrain(offset, 0, mCurrent.length() - 1); + + do { + offset = mIterator.following(offset); + if (isLetterOrDigit(offset - 1)) break; + } while (isValidOffset(offset)); + + return offset; + } + } + + private WordIterator mWordIterator = new WordIterator(); private static final Object LAST_TAP_DOWN = new Object(); private static ArrowKeyMovementMethod sInstance; diff --git a/core/java/android/text/method/BaseMovementMethod.java b/core/java/android/text/method/BaseMovementMethod.java index 94c6ed0..f554b90 100644 --- a/core/java/android/text/method/BaseMovementMethod.java +++ b/core/java/android/text/method/BaseMovementMethod.java @@ -164,6 +164,9 @@ public class BaseMovementMethod implements MovementMethod { if (KeyEvent.metaStateHasNoModifiers(movementMetaState)) { return left(widget, buffer); } else if (KeyEvent.metaStateHasModifiers(movementMetaState, + KeyEvent.META_CTRL_ON)) { + return leftWord(widget, buffer); + } else if (KeyEvent.metaStateHasModifiers(movementMetaState, KeyEvent.META_ALT_ON)) { return lineStart(widget, buffer); } @@ -173,6 +176,9 @@ public class BaseMovementMethod implements MovementMethod { if (KeyEvent.metaStateHasNoModifiers(movementMetaState)) { return right(widget, buffer); } else if (KeyEvent.metaStateHasModifiers(movementMetaState, + KeyEvent.META_CTRL_ON)) { + return rightWord(widget, buffer); + } else if (KeyEvent.metaStateHasModifiers(movementMetaState, KeyEvent.META_ALT_ON)) { return lineEnd(widget, buffer); } @@ -217,12 +223,18 @@ public class BaseMovementMethod implements MovementMethod { case KeyEvent.KEYCODE_MOVE_HOME: if (KeyEvent.metaStateHasNoModifiers(movementMetaState)) { return home(widget, buffer); + } else if (KeyEvent.metaStateHasModifiers(movementMetaState, + KeyEvent.META_CTRL_ON)) { + return top(widget, buffer); } break; case KeyEvent.KEYCODE_MOVE_END: if (KeyEvent.metaStateHasNoModifiers(movementMetaState)) { return end(widget, buffer); + } else if (KeyEvent.metaStateHasModifiers(movementMetaState, + KeyEvent.META_CTRL_ON)) { + return bottom(widget, buffer); } break; } @@ -349,6 +361,16 @@ public class BaseMovementMethod implements MovementMethod { return false; } + /** {@hide} */ + protected boolean leftWord(TextView widget, Spannable buffer) { + return false; + } + + /** {@hide} */ + protected boolean rightWord(TextView widget, Spannable buffer) { + return false; + } + /** * Performs a home movement action. * Moves the cursor or scrolls to the start of the line or to the top of the |