diff options
author | Justin Klaassen <justinklaassen@google.com> | 2017-09-15 17:58:39 -0400 |
---|---|---|
committer | Justin Klaassen <justinklaassen@google.com> | 2017-09-15 17:58:39 -0400 |
commit | 10d07c88d69cc64f73a069163e7ea5ba2519a099 (patch) | |
tree | 8dbd149eb350320a29c3d10e7ad3201de1c5cbee /android/text/method | |
parent | 677516fb6b6f207d373984757d3d9450474b6b00 (diff) | |
download | android-28-10d07c88d69cc64f73a069163e7ea5ba2519a099.tar.gz |
Import Android SDK Platform PI [4335822]
/google/data/ro/projects/android/fetch_artifact \
--bid 4335822 \
--target sdk_phone_armv7-win_sdk \
sdk-repo-linux-sources-4335822.zip
AndroidVersion.ApiLevel has been modified to appear as 28
Change-Id: Ic8f04be005a71c2b9abeaac754d8da8d6f9a2c32
Diffstat (limited to 'android/text/method')
27 files changed, 6538 insertions, 0 deletions
diff --git a/android/text/method/AllCapsTransformationMethod.java b/android/text/method/AllCapsTransformationMethod.java new file mode 100644 index 00000000..c807e7da --- /dev/null +++ b/android/text/method/AllCapsTransformationMethod.java @@ -0,0 +1,77 @@ +/* + * 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.method; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.content.Context; +import android.graphics.Rect; +import android.text.Spanned; +import android.text.TextUtils; +import android.util.Log; +import android.view.View; +import android.widget.TextView; + +import java.util.Locale; + +/** + * Transforms source text into an ALL CAPS string, locale-aware. + * + * @hide + */ +public class AllCapsTransformationMethod implements TransformationMethod2 { + private static final String TAG = "AllCapsTransformationMethod"; + + private boolean mEnabled; + private Locale mLocale; + + public AllCapsTransformationMethod(@NonNull Context context) { + mLocale = context.getResources().getConfiguration().getLocales().get(0); + } + + @Override + public CharSequence getTransformation(@Nullable CharSequence source, View view) { + if (!mEnabled) { + Log.w(TAG, "Caller did not enable length changes; not transforming text"); + return source; + } + + if (source == null) { + return null; + } + + Locale locale = null; + if (view instanceof TextView) { + locale = ((TextView)view).getTextLocale(); + } + if (locale == null) { + locale = mLocale; + } + final boolean copySpans = source instanceof Spanned; + return TextUtils.toUpperCase(locale, source, copySpans); + } + + @Override + public void onFocusChanged(View view, CharSequence sourceText, boolean focused, int direction, + Rect previouslyFocusedRect) { + } + + @Override + public void setLengthChangesAllowed(boolean allowLengthChanges) { + mEnabled = allowLengthChanges; + } + +} diff --git a/android/text/method/ArrowKeyMovementMethod.java b/android/text/method/ArrowKeyMovementMethod.java new file mode 100644 index 00000000..57fe1315 --- /dev/null +++ b/android/text/method/ArrowKeyMovementMethod.java @@ -0,0 +1,337 @@ +/* + * Copyright (C) 2006 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.method; + +import android.graphics.Rect; +import android.text.Layout; +import android.text.Selection; +import android.text.Spannable; +import android.view.KeyEvent; +import android.view.MotionEvent; +import android.view.View; +import android.widget.TextView; + +/** + * A movement method that provides cursor movement and selection. + * Supports displaying the context menu on DPad Center. + */ +public class ArrowKeyMovementMethod extends BaseMovementMethod implements MovementMethod { + private static boolean isSelecting(Spannable buffer) { + return ((MetaKeyKeyListener.getMetaState(buffer, MetaKeyKeyListener.META_SHIFT_ON) == 1) || + (MetaKeyKeyListener.getMetaState(buffer, MetaKeyKeyListener.META_SELECTING) != 0)); + } + + private static int getCurrentLineTop(Spannable buffer, Layout layout) { + return layout.getLineTop(layout.getLineForOffset(Selection.getSelectionEnd(buffer))); + } + + private static int getPageHeight(TextView widget) { + // This calculation does not take into account the view transformations that + // may have been applied to the child or its containers. In case of scaling or + // rotation, the calculated page height may be incorrect. + final Rect rect = new Rect(); + return widget.getGlobalVisibleRect(rect) ? rect.height() : 0; + } + + @Override + protected boolean handleMovementKey(TextView widget, Spannable buffer, int keyCode, + int movementMetaState, KeyEvent event) { + switch (keyCode) { + case KeyEvent.KEYCODE_DPAD_CENTER: + if (KeyEvent.metaStateHasNoModifiers(movementMetaState)) { + if (event.getAction() == KeyEvent.ACTION_DOWN + && event.getRepeatCount() == 0 + && MetaKeyKeyListener.getMetaState(buffer, + MetaKeyKeyListener.META_SELECTING, event) != 0) { + return widget.showContextMenu(); + } + } + break; + } + return super.handleMovementKey(widget, buffer, keyCode, movementMetaState, event); + } + + @Override + protected boolean left(TextView widget, Spannable buffer) { + final Layout layout = widget.getLayout(); + if (isSelecting(buffer)) { + return Selection.extendLeft(buffer, layout); + } else { + return Selection.moveLeft(buffer, layout); + } + } + + @Override + protected boolean right(TextView widget, Spannable buffer) { + final Layout layout = widget.getLayout(); + if (isSelecting(buffer)) { + return Selection.extendRight(buffer, layout); + } else { + return Selection.moveRight(buffer, layout); + } + } + + @Override + protected boolean up(TextView widget, Spannable buffer) { + final Layout layout = widget.getLayout(); + if (isSelecting(buffer)) { + return Selection.extendUp(buffer, layout); + } else { + return Selection.moveUp(buffer, layout); + } + } + + @Override + protected boolean down(TextView widget, Spannable buffer) { + final Layout layout = widget.getLayout(); + if (isSelecting(buffer)) { + return Selection.extendDown(buffer, layout); + } else { + return Selection.moveDown(buffer, layout); + } + } + + @Override + protected boolean pageUp(TextView widget, Spannable buffer) { + final Layout layout = widget.getLayout(); + final boolean selecting = isSelecting(buffer); + final int targetY = getCurrentLineTop(buffer, layout) - getPageHeight(widget); + boolean handled = false; + for (;;) { + final int previousSelectionEnd = Selection.getSelectionEnd(buffer); + if (selecting) { + Selection.extendUp(buffer, layout); + } else { + Selection.moveUp(buffer, layout); + } + if (Selection.getSelectionEnd(buffer) == previousSelectionEnd) { + break; + } + handled = true; + if (getCurrentLineTop(buffer, layout) <= targetY) { + break; + } + } + return handled; + } + + @Override + protected boolean pageDown(TextView widget, Spannable buffer) { + final Layout layout = widget.getLayout(); + final boolean selecting = isSelecting(buffer); + final int targetY = getCurrentLineTop(buffer, layout) + getPageHeight(widget); + boolean handled = false; + for (;;) { + final int previousSelectionEnd = Selection.getSelectionEnd(buffer); + if (selecting) { + Selection.extendDown(buffer, layout); + } else { + Selection.moveDown(buffer, layout); + } + if (Selection.getSelectionEnd(buffer) == previousSelectionEnd) { + break; + } + handled = true; + if (getCurrentLineTop(buffer, layout) >= targetY) { + break; + } + } + return handled; + } + + @Override + protected boolean top(TextView widget, Spannable buffer) { + if (isSelecting(buffer)) { + Selection.extendSelection(buffer, 0); + } else { + Selection.setSelection(buffer, 0); + } + return true; + } + + @Override + protected boolean bottom(TextView widget, Spannable buffer) { + if (isSelecting(buffer)) { + Selection.extendSelection(buffer, buffer.length()); + } else { + Selection.setSelection(buffer, buffer.length()); + } + return true; + } + + @Override + protected boolean lineStart(TextView widget, Spannable buffer) { + final Layout layout = widget.getLayout(); + if (isSelecting(buffer)) { + return Selection.extendToLeftEdge(buffer, layout); + } else { + return Selection.moveToLeftEdge(buffer, layout); + } + } + + @Override + protected boolean lineEnd(TextView widget, Spannable buffer) { + final Layout layout = widget.getLayout(); + if (isSelecting(buffer)) { + return Selection.extendToRightEdge(buffer, layout); + } else { + return Selection.moveToRightEdge(buffer, layout); + } + } + + /** {@hide} */ + @Override + protected boolean leftWord(TextView widget, Spannable buffer) { + final int selectionEnd = widget.getSelectionEnd(); + final WordIterator wordIterator = widget.getWordIterator(); + wordIterator.setCharSequence(buffer, selectionEnd, selectionEnd); + return Selection.moveToPreceding(buffer, wordIterator, isSelecting(buffer)); + } + + /** {@hide} */ + @Override + protected boolean rightWord(TextView widget, Spannable buffer) { + final int selectionEnd = widget.getSelectionEnd(); + final WordIterator wordIterator = widget.getWordIterator(); + wordIterator.setCharSequence(buffer, selectionEnd, selectionEnd); + return Selection.moveToFollowing(buffer, wordIterator, isSelecting(buffer)); + } + + @Override + protected boolean home(TextView widget, Spannable buffer) { + return lineStart(widget, buffer); + } + + @Override + protected boolean end(TextView widget, Spannable buffer) { + return lineEnd(widget, buffer); + } + + @Override + public boolean onTouchEvent(TextView widget, Spannable buffer, MotionEvent event) { + int initialScrollX = -1; + int initialScrollY = -1; + final int action = event.getAction(); + + if (action == MotionEvent.ACTION_UP) { + initialScrollX = Touch.getInitialScrollX(widget, buffer); + initialScrollY = Touch.getInitialScrollY(widget, buffer); + } + + boolean wasTouchSelecting = isSelecting(buffer); + boolean handled = Touch.onTouchEvent(widget, buffer, event); + + if (widget.didTouchFocusSelect()) { + return handled; + } + if (action == MotionEvent.ACTION_DOWN) { + // For touch events, the code should run only when selection is active. + if (isSelecting(buffer)) { + if (!widget.isFocused()) { + if (!widget.requestFocus()) { + return handled; + } + } + int offset = widget.getOffsetForPosition(event.getX(), event.getY()); + buffer.setSpan(LAST_TAP_DOWN, offset, offset, Spannable.SPAN_POINT_POINT); + // Disallow intercepting of the touch events, so that + // users can scroll and select at the same time. + // without this, users would get booted out of select + // mode once the view detected it needed to scroll. + widget.getParent().requestDisallowInterceptTouchEvent(true); + } + } else if (widget.isFocused()) { + if (action == MotionEvent.ACTION_MOVE) { + if (isSelecting(buffer) && handled) { + final int startOffset = buffer.getSpanStart(LAST_TAP_DOWN); + // Before selecting, make sure we've moved out of the "slop". + // handled will be true, if we're in select mode AND we're + // OUT of the slop + + // Turn long press off while we're selecting. User needs to + // re-tap on the selection to enable long press + widget.cancelLongPress(); + + // Update selection as we're moving the selection area. + + // Get the current touch position + final int offset = widget.getOffsetForPosition(event.getX(), event.getY()); + Selection.setSelection(buffer, Math.min(startOffset, offset), + Math.max(startOffset, offset)); + return true; + } + } else if (action == MotionEvent.ACTION_UP) { + // If we have scrolled, then the up shouldn't move the cursor, + // but we do need to make sure the cursor is still visible at + // the current scroll offset to avoid the scroll jumping later + // to show it. + if ((initialScrollY >= 0 && initialScrollY != widget.getScrollY()) || + (initialScrollX >= 0 && initialScrollX != widget.getScrollX())) { + widget.moveCursorToVisibleOffset(); + return true; + } + + if (wasTouchSelecting) { + final int startOffset = buffer.getSpanStart(LAST_TAP_DOWN); + final int endOffset = widget.getOffsetForPosition(event.getX(), event.getY()); + Selection.setSelection(buffer, Math.min(startOffset, endOffset), + Math.max(startOffset, endOffset)); + buffer.removeSpan(LAST_TAP_DOWN); + } + + MetaKeyKeyListener.adjustMetaAfterKeypress(buffer); + MetaKeyKeyListener.resetLockedMeta(buffer); + + return true; + } + } + return handled; + } + + @Override + public boolean canSelectArbitrarily() { + return true; + } + + @Override + public void initialize(TextView widget, Spannable text) { + Selection.setSelection(text, 0); + } + + @Override + public void onTakeFocus(TextView view, Spannable text, int dir) { + if ((dir & (View.FOCUS_FORWARD | View.FOCUS_DOWN)) != 0) { + if (view.getLayout() == null) { + // This shouldn't be null, but do something sensible if it is. + Selection.setSelection(text, text.length()); + } + } else { + Selection.setSelection(text, text.length()); + } + } + + public static MovementMethod getInstance() { + if (sInstance == null) { + sInstance = new ArrowKeyMovementMethod(); + } + + return sInstance; + } + + private static final Object LAST_TAP_DOWN = new Object(); + private static ArrowKeyMovementMethod sInstance; +} diff --git a/android/text/method/BaseKeyListener.java b/android/text/method/BaseKeyListener.java new file mode 100644 index 00000000..5f0a46d8 --- /dev/null +++ b/android/text/method/BaseKeyListener.java @@ -0,0 +1,524 @@ +/* + * Copyright (C) 2006 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.method; + +import android.graphics.Paint; +import android.icu.lang.UCharacter; +import android.icu.lang.UProperty; +import android.text.Editable; +import android.text.Emoji; +import android.text.InputType; +import android.text.Layout; +import android.text.NoCopySpan; +import android.text.Selection; +import android.text.Spanned; +import android.text.method.TextKeyListener.Capitalize; +import android.text.style.ReplacementSpan; +import android.view.KeyEvent; +import android.view.View; +import android.widget.TextView; + +import com.android.internal.annotations.GuardedBy; + +import java.text.BreakIterator; + +/** + * Abstract base class for key listeners. + * + * Provides a basic foundation for entering and editing text. + * Subclasses should override {@link #onKeyDown} and {@link #onKeyUp} to insert + * characters as keys are pressed. + * <p></p> + * As for all implementations of {@link KeyListener}, this class is only concerned + * with hardware keyboards. Software input methods have no obligation to trigger + * the methods in this class. + */ +public abstract class BaseKeyListener extends MetaKeyKeyListener + implements KeyListener { + /* package */ static final Object OLD_SEL_START = new NoCopySpan.Concrete(); + + private static final int LINE_FEED = 0x0A; + private static final int CARRIAGE_RETURN = 0x0D; + + private final Object mLock = new Object(); + + @GuardedBy("mLock") + static Paint sCachedPaint = null; + + /** + * Performs the action that happens when you press the {@link KeyEvent#KEYCODE_DEL} key in + * a {@link TextView}. If there is a selection, deletes the selection; otherwise, + * deletes the character before the cursor, if any; ALT+DEL deletes everything on + * the line the cursor is on. + * + * @return true if anything was deleted; false otherwise. + */ + public boolean backspace(View view, Editable content, int keyCode, KeyEvent event) { + return backspaceOrForwardDelete(view, content, keyCode, event, false); + } + + /** + * Performs the action that happens when you press the {@link KeyEvent#KEYCODE_FORWARD_DEL} + * key in a {@link TextView}. If there is a selection, deletes the selection; otherwise, + * deletes the character before the cursor, if any; ALT+FORWARD_DEL deletes everything on + * the line the cursor is on. + * + * @return true if anything was deleted; false otherwise. + */ + public boolean forwardDelete(View view, Editable content, int keyCode, KeyEvent event) { + return backspaceOrForwardDelete(view, content, keyCode, event, true); + } + + // Returns true if the given code point is a variation selector. + private static boolean isVariationSelector(int codepoint) { + return UCharacter.hasBinaryProperty(codepoint, UProperty.VARIATION_SELECTOR); + } + + // Returns the offset of the replacement span edge if the offset is inside of the replacement + // span. Otherwise, does nothing and returns the input offset value. + private static int adjustReplacementSpan(CharSequence text, int offset, boolean moveToStart) { + if (!(text instanceof Spanned)) { + return offset; + } + + ReplacementSpan[] spans = ((Spanned) text).getSpans(offset, offset, ReplacementSpan.class); + for (int i = 0; i < spans.length; i++) { + final int start = ((Spanned) text).getSpanStart(spans[i]); + final int end = ((Spanned) text).getSpanEnd(spans[i]); + + if (start < offset && end > offset) { + offset = moveToStart ? start : end; + } + } + return offset; + } + + // Returns the start offset to be deleted by a backspace key from the given offset. + private static int getOffsetForBackspaceKey(CharSequence text, int offset) { + if (offset <= 1) { + return 0; + } + + // Initial state + final int STATE_START = 0; + + // The offset is immediately before line feed. + final int STATE_LF = 1; + + // The offset is immediately before a KEYCAP. + final int STATE_BEFORE_KEYCAP = 2; + // The offset is immediately before a variation selector and a KEYCAP. + final int STATE_BEFORE_VS_AND_KEYCAP = 3; + + // The offset is immediately before an emoji modifier. + final int STATE_BEFORE_EMOJI_MODIFIER = 4; + // The offset is immediately before a variation selector and an emoji modifier. + final int STATE_BEFORE_VS_AND_EMOJI_MODIFIER = 5; + + // The offset is immediately before a variation selector. + final int STATE_BEFORE_VS = 6; + + // The offset is immediately before an emoji. + final int STATE_BEFORE_EMOJI = 7; + // The offset is immediately before a ZWJ that were seen before a ZWJ emoji. + final int STATE_BEFORE_ZWJ = 8; + // The offset is immediately before a variation selector and a ZWJ that were seen before a + // ZWJ emoji. + final int STATE_BEFORE_VS_AND_ZWJ = 9; + + // The number of following RIS code points is odd. + final int STATE_ODD_NUMBERED_RIS = 10; + // The number of following RIS code points is even. + final int STATE_EVEN_NUMBERED_RIS = 11; + + // The offset is in emoji tag sequence. + final int STATE_IN_TAG_SEQUENCE = 12; + + // The state machine has been stopped. + final int STATE_FINISHED = 13; + + int deleteCharCount = 0; // Char count to be deleted by backspace. + int lastSeenVSCharCount = 0; // Char count of previous variation selector. + + int state = STATE_START; + + int tmpOffset = offset; + do { + final int codePoint = Character.codePointBefore(text, tmpOffset); + tmpOffset -= Character.charCount(codePoint); + + switch (state) { + case STATE_START: + deleteCharCount = Character.charCount(codePoint); + if (codePoint == LINE_FEED) { + state = STATE_LF; + } else if (isVariationSelector(codePoint)) { + state = STATE_BEFORE_VS; + } else if (Emoji.isRegionalIndicatorSymbol(codePoint)) { + state = STATE_ODD_NUMBERED_RIS; + } else if (Emoji.isEmojiModifier(codePoint)) { + state = STATE_BEFORE_EMOJI_MODIFIER; + } else if (codePoint == Emoji.COMBINING_ENCLOSING_KEYCAP) { + state = STATE_BEFORE_KEYCAP; + } else if (Emoji.isEmoji(codePoint)) { + state = STATE_BEFORE_EMOJI; + } else if (codePoint == Emoji.CANCEL_TAG) { + state = STATE_IN_TAG_SEQUENCE; + } else { + state = STATE_FINISHED; + } + break; + case STATE_LF: + if (codePoint == CARRIAGE_RETURN) { + ++deleteCharCount; + } + state = STATE_FINISHED; + break; + case STATE_ODD_NUMBERED_RIS: + if (Emoji.isRegionalIndicatorSymbol(codePoint)) { + deleteCharCount += 2; /* Char count of RIS */ + state = STATE_EVEN_NUMBERED_RIS; + } else { + state = STATE_FINISHED; + } + break; + case STATE_EVEN_NUMBERED_RIS: + if (Emoji.isRegionalIndicatorSymbol(codePoint)) { + deleteCharCount -= 2; /* Char count of RIS */ + state = STATE_ODD_NUMBERED_RIS; + } else { + state = STATE_FINISHED; + } + break; + case STATE_BEFORE_KEYCAP: + if (isVariationSelector(codePoint)) { + lastSeenVSCharCount = Character.charCount(codePoint); + state = STATE_BEFORE_VS_AND_KEYCAP; + break; + } + + if (Emoji.isKeycapBase(codePoint)) { + deleteCharCount += Character.charCount(codePoint); + } + state = STATE_FINISHED; + break; + case STATE_BEFORE_VS_AND_KEYCAP: + if (Emoji.isKeycapBase(codePoint)) { + deleteCharCount += lastSeenVSCharCount + Character.charCount(codePoint); + } + state = STATE_FINISHED; + break; + case STATE_BEFORE_EMOJI_MODIFIER: + if (isVariationSelector(codePoint)) { + lastSeenVSCharCount = Character.charCount(codePoint); + state = STATE_BEFORE_VS_AND_EMOJI_MODIFIER; + break; + } else if (Emoji.isEmojiModifierBase(codePoint)) { + deleteCharCount += Character.charCount(codePoint); + } + state = STATE_FINISHED; + break; + case STATE_BEFORE_VS_AND_EMOJI_MODIFIER: + if (Emoji.isEmojiModifierBase(codePoint)) { + deleteCharCount += lastSeenVSCharCount + Character.charCount(codePoint); + } + state = STATE_FINISHED; + break; + case STATE_BEFORE_VS: + if (Emoji.isEmoji(codePoint)) { + deleteCharCount += Character.charCount(codePoint); + state = STATE_BEFORE_EMOJI; + break; + } + + if (!isVariationSelector(codePoint) && + UCharacter.getCombiningClass(codePoint) == 0) { + deleteCharCount += Character.charCount(codePoint); + } + state = STATE_FINISHED; + break; + case STATE_BEFORE_EMOJI: + if (codePoint == Emoji.ZERO_WIDTH_JOINER) { + state = STATE_BEFORE_ZWJ; + } else { + state = STATE_FINISHED; + } + break; + case STATE_BEFORE_ZWJ: + if (Emoji.isEmoji(codePoint)) { + deleteCharCount += Character.charCount(codePoint) + 1; // +1 for ZWJ. + state = Emoji.isEmojiModifier(codePoint) ? + STATE_BEFORE_EMOJI_MODIFIER : STATE_BEFORE_EMOJI; + } else if (isVariationSelector(codePoint)) { + lastSeenVSCharCount = Character.charCount(codePoint); + state = STATE_BEFORE_VS_AND_ZWJ; + } else { + state = STATE_FINISHED; + } + break; + case STATE_BEFORE_VS_AND_ZWJ: + if (Emoji.isEmoji(codePoint)) { + // +1 for ZWJ. + deleteCharCount += lastSeenVSCharCount + 1 + Character.charCount(codePoint); + lastSeenVSCharCount = 0; + state = STATE_BEFORE_EMOJI; + } else { + state = STATE_FINISHED; + } + break; + case STATE_IN_TAG_SEQUENCE: + if (Emoji.isTagSpecChar(codePoint)) { + deleteCharCount += 2; /* Char count of emoji tag spec character. */ + // Keep the same state. + } else if (Emoji.isEmoji(codePoint)) { + deleteCharCount += Character.charCount(codePoint); + state = STATE_FINISHED; + } else { + // Couldn't find tag_base character. Delete the last tag_term character. + deleteCharCount = 2; // for U+E007F + state = STATE_FINISHED; + } + // TODO: Need handle emoji variation selectors. Issue 35224297 + break; + default: + throw new IllegalArgumentException("state " + state + " is unknown"); + } + } while (tmpOffset > 0 && state != STATE_FINISHED); + + return adjustReplacementSpan(text, offset - deleteCharCount, true /* move to the start */); + } + + // Returns the end offset to be deleted by a forward delete key from the given offset. + private static int getOffsetForForwardDeleteKey(CharSequence text, int offset, Paint paint) { + final int len = text.length(); + + if (offset >= len - 1) { + return len; + } + + offset = paint.getTextRunCursor(text, offset, len, Paint.DIRECTION_LTR /* not used */, + offset, Paint.CURSOR_AFTER); + + return adjustReplacementSpan(text, offset, false /* move to the end */); + } + + private boolean backspaceOrForwardDelete(View view, Editable content, int keyCode, + KeyEvent event, boolean isForwardDelete) { + // Ensure the key event does not have modifiers except ALT or SHIFT or CTRL. + if (!KeyEvent.metaStateHasNoModifiers(event.getMetaState() + & ~(KeyEvent.META_SHIFT_MASK | KeyEvent.META_ALT_MASK | KeyEvent.META_CTRL_MASK))) { + return false; + } + + // If there is a current selection, delete it. + if (deleteSelection(view, content)) { + return true; + } + + // MetaKeyKeyListener doesn't track control key state. Need to check the KeyEvent instead. + boolean isCtrlActive = ((event.getMetaState() & KeyEvent.META_CTRL_ON) != 0); + boolean isShiftActive = (getMetaState(content, META_SHIFT_ON, event) == 1); + boolean isAltActive = (getMetaState(content, META_ALT_ON, event) == 1); + + if (isCtrlActive) { + if (isAltActive || isShiftActive) { + // Ctrl+Alt, Ctrl+Shift, Ctrl+Alt+Shift should not delete any characters. + return false; + } + return deleteUntilWordBoundary(view, content, isForwardDelete); + } + + // Alt+Backspace or Alt+ForwardDelete deletes the current line, if possible. + if (isAltActive && deleteLine(view, content)) { + return true; + } + + // Delete a character. + final int start = Selection.getSelectionEnd(content); + final int end; + if (isForwardDelete) { + final Paint paint; + if (view instanceof TextView) { + paint = ((TextView)view).getPaint(); + } else { + synchronized (mLock) { + if (sCachedPaint == null) { + sCachedPaint = new Paint(); + } + paint = sCachedPaint; + } + } + end = getOffsetForForwardDeleteKey(content, start, paint); + } else { + end = getOffsetForBackspaceKey(content, start); + } + if (start != end) { + content.delete(Math.min(start, end), Math.max(start, end)); + return true; + } + return false; + } + + private boolean deleteUntilWordBoundary(View view, Editable content, boolean isForwardDelete) { + int currentCursorOffset = Selection.getSelectionStart(content); + + // If there is a selection, do nothing. + if (currentCursorOffset != Selection.getSelectionEnd(content)) { + return false; + } + + // Early exit if there is no contents to delete. + if ((!isForwardDelete && currentCursorOffset == 0) || + (isForwardDelete && currentCursorOffset == content.length())) { + return false; + } + + WordIterator wordIterator = null; + if (view instanceof TextView) { + wordIterator = ((TextView)view).getWordIterator(); + } + + if (wordIterator == null) { + // Default locale is used for WordIterator since the appropriate locale is not clear + // here. + // TODO: Use appropriate locale for WordIterator. + wordIterator = new WordIterator(); + } + + int deleteFrom; + int deleteTo; + + if (isForwardDelete) { + deleteFrom = currentCursorOffset; + wordIterator.setCharSequence(content, deleteFrom, content.length()); + deleteTo = wordIterator.following(currentCursorOffset); + if (deleteTo == BreakIterator.DONE) { + deleteTo = content.length(); + } + } else { + deleteTo = currentCursorOffset; + wordIterator.setCharSequence(content, 0, deleteTo); + deleteFrom = wordIterator.preceding(currentCursorOffset); + if (deleteFrom == BreakIterator.DONE) { + deleteFrom = 0; + } + } + content.delete(deleteFrom, deleteTo); + return true; + } + + private boolean deleteSelection(View view, Editable content) { + int selectionStart = Selection.getSelectionStart(content); + int selectionEnd = Selection.getSelectionEnd(content); + if (selectionEnd < selectionStart) { + int temp = selectionEnd; + selectionEnd = selectionStart; + selectionStart = temp; + } + if (selectionStart != selectionEnd) { + content.delete(selectionStart, selectionEnd); + return true; + } + return false; + } + + private boolean deleteLine(View view, Editable content) { + if (view instanceof TextView) { + final Layout layout = ((TextView) view).getLayout(); + if (layout != null) { + final int line = layout.getLineForOffset(Selection.getSelectionStart(content)); + final int start = layout.getLineStart(line); + final int end = layout.getLineEnd(line); + if (end != start) { + content.delete(start, end); + return true; + } + } + } + return false; + } + + static int makeTextContentType(Capitalize caps, boolean autoText) { + int contentType = InputType.TYPE_CLASS_TEXT; + switch (caps) { + case CHARACTERS: + contentType |= InputType.TYPE_TEXT_FLAG_CAP_CHARACTERS; + break; + case WORDS: + contentType |= InputType.TYPE_TEXT_FLAG_CAP_WORDS; + break; + case SENTENCES: + contentType |= InputType.TYPE_TEXT_FLAG_CAP_SENTENCES; + break; + } + if (autoText) { + contentType |= InputType.TYPE_TEXT_FLAG_AUTO_CORRECT; + } + return contentType; + } + + public boolean onKeyDown(View view, Editable content, + int keyCode, KeyEvent event) { + boolean handled; + switch (keyCode) { + case KeyEvent.KEYCODE_DEL: + handled = backspace(view, content, keyCode, event); + break; + case KeyEvent.KEYCODE_FORWARD_DEL: + handled = forwardDelete(view, content, keyCode, event); + break; + default: + handled = false; + break; + } + + if (handled) { + adjustMetaAfterKeypress(content); + return true; + } + + return super.onKeyDown(view, content, keyCode, event); + } + + /** + * Base implementation handles ACTION_MULTIPLE KEYCODE_UNKNOWN by inserting + * the event's text into the content. + */ + public boolean onKeyOther(View view, Editable content, KeyEvent event) { + if (event.getAction() != KeyEvent.ACTION_MULTIPLE + || event.getKeyCode() != KeyEvent.KEYCODE_UNKNOWN) { + // Not something we are interested in. + return false; + } + + int selectionStart = Selection.getSelectionStart(content); + int selectionEnd = Selection.getSelectionEnd(content); + if (selectionEnd < selectionStart) { + int temp = selectionEnd; + selectionEnd = selectionStart; + selectionStart = temp; + } + + CharSequence text = event.getCharacters(); + if (text == null) { + return false; + } + + content.replace(selectionStart, selectionEnd, text); + return true; + } +} diff --git a/android/text/method/BaseMovementMethod.java b/android/text/method/BaseMovementMethod.java new file mode 100644 index 00000000..155a2c4f --- /dev/null +++ b/android/text/method/BaseMovementMethod.java @@ -0,0 +1,673 @@ +/* + * Copyright (C) 2010 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.method; + +import android.text.Layout; +import android.text.Spannable; +import android.view.InputDevice; +import android.view.KeyEvent; +import android.view.MotionEvent; +import android.widget.TextView; + +/** + * Base classes for movement methods. + */ +public class BaseMovementMethod implements MovementMethod { + @Override + public boolean canSelectArbitrarily() { + return false; + } + + @Override + public void initialize(TextView widget, Spannable text) { + } + + @Override + public boolean onKeyDown(TextView widget, Spannable text, int keyCode, KeyEvent event) { + final int movementMetaState = getMovementMetaState(text, event); + boolean handled = handleMovementKey(widget, text, keyCode, movementMetaState, event); + if (handled) { + MetaKeyKeyListener.adjustMetaAfterKeypress(text); + MetaKeyKeyListener.resetLockedMeta(text); + } + return handled; + } + + @Override + public boolean onKeyOther(TextView widget, Spannable text, KeyEvent event) { + final int movementMetaState = getMovementMetaState(text, event); + final int keyCode = event.getKeyCode(); + if (keyCode != KeyEvent.KEYCODE_UNKNOWN + && event.getAction() == KeyEvent.ACTION_MULTIPLE) { + final int repeat = event.getRepeatCount(); + boolean handled = false; + for (int i = 0; i < repeat; i++) { + if (!handleMovementKey(widget, text, keyCode, movementMetaState, event)) { + break; + } + handled = true; + } + if (handled) { + MetaKeyKeyListener.adjustMetaAfterKeypress(text); + MetaKeyKeyListener.resetLockedMeta(text); + } + return handled; + } + return false; + } + + @Override + public boolean onKeyUp(TextView widget, Spannable text, int keyCode, KeyEvent event) { + return false; + } + + @Override + public void onTakeFocus(TextView widget, Spannable text, int direction) { + } + + @Override + public boolean onTouchEvent(TextView widget, Spannable text, MotionEvent event) { + return false; + } + + @Override + public boolean onTrackballEvent(TextView widget, Spannable text, MotionEvent event) { + return false; + } + + @Override + public boolean onGenericMotionEvent(TextView widget, Spannable text, MotionEvent event) { + if ((event.getSource() & InputDevice.SOURCE_CLASS_POINTER) != 0) { + switch (event.getAction()) { + case MotionEvent.ACTION_SCROLL: { + final float vscroll; + final float hscroll; + if ((event.getMetaState() & KeyEvent.META_SHIFT_ON) != 0) { + vscroll = 0; + hscroll = event.getAxisValue(MotionEvent.AXIS_VSCROLL); + } else { + vscroll = -event.getAxisValue(MotionEvent.AXIS_VSCROLL); + hscroll = event.getAxisValue(MotionEvent.AXIS_HSCROLL); + } + + boolean handled = false; + if (hscroll < 0) { + handled |= scrollLeft(widget, text, (int)Math.ceil(-hscroll)); + } else if (hscroll > 0) { + handled |= scrollRight(widget, text, (int)Math.ceil(hscroll)); + } + if (vscroll < 0) { + handled |= scrollUp(widget, text, (int)Math.ceil(-vscroll)); + } else if (vscroll > 0) { + handled |= scrollDown(widget, text, (int)Math.ceil(vscroll)); + } + return handled; + } + } + } + return false; + } + + /** + * Gets the meta state used for movement using the modifiers tracked by the text + * buffer as well as those present in the key event. + * + * The movement meta state excludes the state of locked modifiers or the SHIFT key + * since they are not used by movement actions (but they may be used for selection). + * + * @param buffer The text buffer. + * @param event The key event. + * @return The keyboard meta states used for movement. + */ + protected int getMovementMetaState(Spannable buffer, KeyEvent event) { + // We ignore locked modifiers and SHIFT. + int metaState = MetaKeyKeyListener.getMetaState(buffer, event) + & ~(MetaKeyKeyListener.META_ALT_LOCKED | MetaKeyKeyListener.META_SYM_LOCKED); + return KeyEvent.normalizeMetaState(metaState) & ~KeyEvent.META_SHIFT_MASK; + } + + /** + * Performs a movement key action. + * The default implementation decodes the key down and invokes movement actions + * such as {@link #down} and {@link #up}. + * {@link #onKeyDown(TextView, Spannable, int, KeyEvent)} calls this method once + * to handle an {@link KeyEvent#ACTION_DOWN}. + * {@link #onKeyOther(TextView, Spannable, KeyEvent)} calls this method repeatedly + * to handle each repetition of an {@link KeyEvent#ACTION_MULTIPLE}. + * + * @param widget The text view. + * @param buffer The text buffer. + * @param event The key event. + * @param keyCode The key code. + * @param movementMetaState The keyboard meta states used for movement. + * @param event The key event. + * @return True if the event was handled. + */ + protected boolean handleMovementKey(TextView widget, Spannable buffer, + int keyCode, int movementMetaState, KeyEvent event) { + switch (keyCode) { + case KeyEvent.KEYCODE_DPAD_LEFT: + 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); + } + break; + + case KeyEvent.KEYCODE_DPAD_RIGHT: + 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); + } + break; + + case KeyEvent.KEYCODE_DPAD_UP: + if (KeyEvent.metaStateHasNoModifiers(movementMetaState)) { + return up(widget, buffer); + } else if (KeyEvent.metaStateHasModifiers(movementMetaState, + KeyEvent.META_ALT_ON)) { + return top(widget, buffer); + } + break; + + case KeyEvent.KEYCODE_DPAD_DOWN: + if (KeyEvent.metaStateHasNoModifiers(movementMetaState)) { + return down(widget, buffer); + } else if (KeyEvent.metaStateHasModifiers(movementMetaState, + KeyEvent.META_ALT_ON)) { + return bottom(widget, buffer); + } + break; + + case KeyEvent.KEYCODE_PAGE_UP: + if (KeyEvent.metaStateHasNoModifiers(movementMetaState)) { + return pageUp(widget, buffer); + } else if (KeyEvent.metaStateHasModifiers(movementMetaState, + KeyEvent.META_ALT_ON)) { + return top(widget, buffer); + } + break; + + case KeyEvent.KEYCODE_PAGE_DOWN: + if (KeyEvent.metaStateHasNoModifiers(movementMetaState)) { + return pageDown(widget, buffer); + } else if (KeyEvent.metaStateHasModifiers(movementMetaState, + KeyEvent.META_ALT_ON)) { + return bottom(widget, buffer); + } + break; + + 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; + } + return false; + } + + /** + * Performs a left movement action. + * Moves the cursor or scrolls left by one character. + * + * @param widget The text view. + * @param buffer The text buffer. + * @return True if the event was handled. + */ + protected boolean left(TextView widget, Spannable buffer) { + return false; + } + + /** + * Performs a right movement action. + * Moves the cursor or scrolls right by one character. + * + * @param widget The text view. + * @param buffer The text buffer. + * @return True if the event was handled. + */ + protected boolean right(TextView widget, Spannable buffer) { + return false; + } + + /** + * Performs an up movement action. + * Moves the cursor or scrolls up by one line. + * + * @param widget The text view. + * @param buffer The text buffer. + * @return True if the event was handled. + */ + protected boolean up(TextView widget, Spannable buffer) { + return false; + } + + /** + * Performs a down movement action. + * Moves the cursor or scrolls down by one line. + * + * @param widget The text view. + * @param buffer The text buffer. + * @return True if the event was handled. + */ + protected boolean down(TextView widget, Spannable buffer) { + return false; + } + + /** + * Performs a page-up movement action. + * Moves the cursor or scrolls up by one page. + * + * @param widget The text view. + * @param buffer The text buffer. + * @return True if the event was handled. + */ + protected boolean pageUp(TextView widget, Spannable buffer) { + return false; + } + + /** + * Performs a page-down movement action. + * Moves the cursor or scrolls down by one page. + * + * @param widget The text view. + * @param buffer The text buffer. + * @return True if the event was handled. + */ + protected boolean pageDown(TextView widget, Spannable buffer) { + return false; + } + + /** + * Performs a top movement action. + * Moves the cursor or scrolls to the top of the buffer. + * + * @param widget The text view. + * @param buffer The text buffer. + * @return True if the event was handled. + */ + protected boolean top(TextView widget, Spannable buffer) { + return false; + } + + /** + * Performs a bottom movement action. + * Moves the cursor or scrolls to the bottom of the buffer. + * + * @param widget The text view. + * @param buffer The text buffer. + * @return True if the event was handled. + */ + protected boolean bottom(TextView widget, Spannable buffer) { + return false; + } + + /** + * Performs a line-start movement action. + * Moves the cursor or scrolls to the start of the line. + * + * @param widget The text view. + * @param buffer The text buffer. + * @return True if the event was handled. + */ + protected boolean lineStart(TextView widget, Spannable buffer) { + return false; + } + + /** + * Performs a line-end movement action. + * Moves the cursor or scrolls to the end of the line. + * + * @param widget The text view. + * @param buffer The text buffer. + * @return True if the event was handled. + */ + protected boolean lineEnd(TextView widget, Spannable buffer) { + 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 + * document depending on whether the insertion point is being moved or + * the document is being scrolled. + * + * @param widget The text view. + * @param buffer The text buffer. + * @return True if the event was handled. + */ + protected boolean home(TextView widget, Spannable buffer) { + return false; + } + + /** + * Performs an end movement action. + * Moves the cursor or scrolls to the start of the line or to the top of the + * document depending on whether the insertion point is being moved or + * the document is being scrolled. + * + * @param widget The text view. + * @param buffer The text buffer. + * @return True if the event was handled. + */ + protected boolean end(TextView widget, Spannable buffer) { + return false; + } + + private int getTopLine(TextView widget) { + return widget.getLayout().getLineForVertical(widget.getScrollY()); + } + + private int getBottomLine(TextView widget) { + return widget.getLayout().getLineForVertical(widget.getScrollY() + getInnerHeight(widget)); + } + + private int getInnerWidth(TextView widget) { + return widget.getWidth() - widget.getTotalPaddingLeft() - widget.getTotalPaddingRight(); + } + + private int getInnerHeight(TextView widget) { + return widget.getHeight() - widget.getTotalPaddingTop() - widget.getTotalPaddingBottom(); + } + + private int getCharacterWidth(TextView widget) { + return (int) Math.ceil(widget.getPaint().getFontSpacing()); + } + + private int getScrollBoundsLeft(TextView widget) { + final Layout layout = widget.getLayout(); + final int topLine = getTopLine(widget); + final int bottomLine = getBottomLine(widget); + if (topLine > bottomLine) { + return 0; + } + int left = Integer.MAX_VALUE; + for (int line = topLine; line <= bottomLine; line++) { + final int lineLeft = (int) Math.floor(layout.getLineLeft(line)); + if (lineLeft < left) { + left = lineLeft; + } + } + return left; + } + + private int getScrollBoundsRight(TextView widget) { + final Layout layout = widget.getLayout(); + final int topLine = getTopLine(widget); + final int bottomLine = getBottomLine(widget); + if (topLine > bottomLine) { + return 0; + } + int right = Integer.MIN_VALUE; + for (int line = topLine; line <= bottomLine; line++) { + final int lineRight = (int) Math.ceil(layout.getLineRight(line)); + if (lineRight > right) { + right = lineRight; + } + } + return right; + } + + /** + * Performs a scroll left action. + * Scrolls left by the specified number of characters. + * + * @param widget The text view. + * @param buffer The text buffer. + * @param amount The number of characters to scroll by. Must be at least 1. + * @return True if the event was handled. + * @hide + */ + protected boolean scrollLeft(TextView widget, Spannable buffer, int amount) { + final int minScrollX = getScrollBoundsLeft(widget); + int scrollX = widget.getScrollX(); + if (scrollX > minScrollX) { + scrollX = Math.max(scrollX - getCharacterWidth(widget) * amount, minScrollX); + widget.scrollTo(scrollX, widget.getScrollY()); + return true; + } + return false; + } + + /** + * Performs a scroll right action. + * Scrolls right by the specified number of characters. + * + * @param widget The text view. + * @param buffer The text buffer. + * @param amount The number of characters to scroll by. Must be at least 1. + * @return True if the event was handled. + * @hide + */ + protected boolean scrollRight(TextView widget, Spannable buffer, int amount) { + final int maxScrollX = getScrollBoundsRight(widget) - getInnerWidth(widget); + int scrollX = widget.getScrollX(); + if (scrollX < maxScrollX) { + scrollX = Math.min(scrollX + getCharacterWidth(widget) * amount, maxScrollX); + widget.scrollTo(scrollX, widget.getScrollY()); + return true; + } + return false; + } + + /** + * Performs a scroll up action. + * Scrolls up by the specified number of lines. + * + * @param widget The text view. + * @param buffer The text buffer. + * @param amount The number of lines to scroll by. Must be at least 1. + * @return True if the event was handled. + * @hide + */ + protected boolean scrollUp(TextView widget, Spannable buffer, int amount) { + final Layout layout = widget.getLayout(); + final int top = widget.getScrollY(); + int topLine = layout.getLineForVertical(top); + if (layout.getLineTop(topLine) == top) { + // If the top line is partially visible, bring it all the way + // into view; otherwise, bring the previous line into view. + topLine -= 1; + } + if (topLine >= 0) { + topLine = Math.max(topLine - amount + 1, 0); + Touch.scrollTo(widget, layout, widget.getScrollX(), layout.getLineTop(topLine)); + return true; + } + return false; + } + + /** + * Performs a scroll down action. + * Scrolls down by the specified number of lines. + * + * @param widget The text view. + * @param buffer The text buffer. + * @param amount The number of lines to scroll by. Must be at least 1. + * @return True if the event was handled. + * @hide + */ + protected boolean scrollDown(TextView widget, Spannable buffer, int amount) { + final Layout layout = widget.getLayout(); + final int innerHeight = getInnerHeight(widget); + final int bottom = widget.getScrollY() + innerHeight; + int bottomLine = layout.getLineForVertical(bottom); + if (layout.getLineTop(bottomLine + 1) < bottom + 1) { + // Less than a pixel of this line is out of view, + // so we must have tried to make it entirely in view + // and now want the next line to be in view instead. + bottomLine += 1; + } + final int limit = layout.getLineCount() - 1; + if (bottomLine <= limit) { + bottomLine = Math.min(bottomLine + amount - 1, limit); + Touch.scrollTo(widget, layout, widget.getScrollX(), + layout.getLineTop(bottomLine + 1) - innerHeight); + return true; + } + return false; + } + + /** + * Performs a scroll page up action. + * Scrolls up by one page. + * + * @param widget The text view. + * @param buffer The text buffer. + * @return True if the event was handled. + * @hide + */ + protected boolean scrollPageUp(TextView widget, Spannable buffer) { + final Layout layout = widget.getLayout(); + final int top = widget.getScrollY() - getInnerHeight(widget); + int topLine = layout.getLineForVertical(top); + if (topLine >= 0) { + Touch.scrollTo(widget, layout, widget.getScrollX(), layout.getLineTop(topLine)); + return true; + } + return false; + } + + /** + * Performs a scroll page up action. + * Scrolls down by one page. + * + * @param widget The text view. + * @param buffer The text buffer. + * @return True if the event was handled. + * @hide + */ + protected boolean scrollPageDown(TextView widget, Spannable buffer) { + final Layout layout = widget.getLayout(); + final int innerHeight = getInnerHeight(widget); + final int bottom = widget.getScrollY() + innerHeight + innerHeight; + int bottomLine = layout.getLineForVertical(bottom); + if (bottomLine <= layout.getLineCount() - 1) { + Touch.scrollTo(widget, layout, widget.getScrollX(), + layout.getLineTop(bottomLine + 1) - innerHeight); + return true; + } + return false; + } + + /** + * Performs a scroll to top action. + * Scrolls to the top of the document. + * + * @param widget The text view. + * @param buffer The text buffer. + * @return True if the event was handled. + * @hide + */ + protected boolean scrollTop(TextView widget, Spannable buffer) { + final Layout layout = widget.getLayout(); + if (getTopLine(widget) >= 0) { + Touch.scrollTo(widget, layout, widget.getScrollX(), layout.getLineTop(0)); + return true; + } + return false; + } + + /** + * Performs a scroll to bottom action. + * Scrolls to the bottom of the document. + * + * @param widget The text view. + * @param buffer The text buffer. + * @return True if the event was handled. + * @hide + */ + protected boolean scrollBottom(TextView widget, Spannable buffer) { + final Layout layout = widget.getLayout(); + final int lineCount = layout.getLineCount(); + if (getBottomLine(widget) <= lineCount - 1) { + Touch.scrollTo(widget, layout, widget.getScrollX(), + layout.getLineTop(lineCount) - getInnerHeight(widget)); + return true; + } + return false; + } + + /** + * Performs a scroll to line start action. + * Scrolls to the start of the line. + * + * @param widget The text view. + * @param buffer The text buffer. + * @return True if the event was handled. + * @hide + */ + protected boolean scrollLineStart(TextView widget, Spannable buffer) { + final int minScrollX = getScrollBoundsLeft(widget); + int scrollX = widget.getScrollX(); + if (scrollX > minScrollX) { + widget.scrollTo(minScrollX, widget.getScrollY()); + return true; + } + return false; + } + + /** + * Performs a scroll to line end action. + * Scrolls to the end of the line. + * + * @param widget The text view. + * @param buffer The text buffer. + * @return True if the event was handled. + * @hide + */ + protected boolean scrollLineEnd(TextView widget, Spannable buffer) { + final int maxScrollX = getScrollBoundsRight(widget) - getInnerWidth(widget); + int scrollX = widget.getScrollX(); + if (scrollX < maxScrollX) { + widget.scrollTo(maxScrollX, widget.getScrollY()); + return true; + } + return false; + } +} diff --git a/android/text/method/CharacterPickerDialog.java b/android/text/method/CharacterPickerDialog.java new file mode 100644 index 00000000..7d838e06 --- /dev/null +++ b/android/text/method/CharacterPickerDialog.java @@ -0,0 +1,143 @@ +/* + * Copyright (C) 2008 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.method; + +import android.app.Dialog; +import android.content.Context; +import android.os.Bundle; +import android.text.Editable; +import android.text.Selection; +import android.view.LayoutInflater; +import android.view.View; +import android.view.View.OnClickListener; +import android.view.ViewGroup; +import android.view.Window; +import android.view.WindowManager; +import android.widget.AdapterView; +import android.widget.AdapterView.OnItemClickListener; +import android.widget.BaseAdapter; +import android.widget.Button; +import android.widget.GridView; + +import com.android.internal.R; + +/** + * Dialog for choosing accented characters related to a base character. + */ +public class CharacterPickerDialog extends Dialog + implements OnItemClickListener, OnClickListener { + private View mView; + private Editable mText; + private String mOptions; + private boolean mInsert; + private LayoutInflater mInflater; + private Button mCancelButton; + + /** + * Creates a new CharacterPickerDialog that presents the specified + * <code>options</code> for insertion or replacement (depending on + * the sense of <code>insert</code>) into <code>text</code>. + */ + public CharacterPickerDialog(Context context, View view, + Editable text, String options, + boolean insert) { + super(context, com.android.internal.R.style.Theme_Panel); + + mView = view; + mText = text; + mOptions = options; + mInsert = insert; + mInflater = LayoutInflater.from(context); + } + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + + WindowManager.LayoutParams params = getWindow().getAttributes(); + params.token = mView.getApplicationWindowToken(); + params.type = params.TYPE_APPLICATION_ATTACHED_DIALOG; + params.flags = params.flags | Window.FEATURE_NO_TITLE; + + setContentView(R.layout.character_picker); + + GridView grid = (GridView) findViewById(R.id.characterPicker); + grid.setAdapter(new OptionsAdapter(getContext())); + grid.setOnItemClickListener(this); + + mCancelButton = (Button) findViewById(R.id.cancel); + mCancelButton.setOnClickListener(this); + } + + /** + * Handles clicks on the character buttons. + */ + public void onItemClick(AdapterView parent, View view, int position, long id) { + String result = String.valueOf(mOptions.charAt(position)); + replaceCharacterAndClose(result); + } + + private void replaceCharacterAndClose(CharSequence replace) { + int selEnd = Selection.getSelectionEnd(mText); + if (mInsert || selEnd == 0) { + mText.insert(selEnd, replace); + } else { + mText.replace(selEnd - 1, selEnd, replace); + } + + dismiss(); + } + + /** + * Handles clicks on the Cancel button. + */ + public void onClick(View v) { + if (v == mCancelButton) { + dismiss(); + } else if (v instanceof Button) { + CharSequence result = ((Button) v).getText(); + replaceCharacterAndClose(result); + } + } + + private class OptionsAdapter extends BaseAdapter { + + public OptionsAdapter(Context context) { + super(); + } + + public View getView(int position, View convertView, ViewGroup parent) { + Button b = (Button) + mInflater.inflate(R.layout.character_picker_button, null); + b.setText(String.valueOf(mOptions.charAt(position))); + b.setOnClickListener(CharacterPickerDialog.this); + return b; + } + + public final int getCount() { + return mOptions.length(); + } + + public final Object getItem(int position) { + return String.valueOf(mOptions.charAt(position)); + } + + public final long getItemId(int position) { + return position; + } + } +} diff --git a/android/text/method/DateKeyListener.java b/android/text/method/DateKeyListener.java new file mode 100644 index 00000000..0accbf6c --- /dev/null +++ b/android/text/method/DateKeyListener.java @@ -0,0 +1,128 @@ +/* + * Copyright (C) 2006 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.method; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.text.InputType; +import android.view.KeyEvent; + +import com.android.internal.annotations.GuardedBy; +import com.android.internal.util.ArrayUtils; + +import java.util.HashMap; +import java.util.LinkedHashSet; +import java.util.Locale; + +/** + * For entering dates in a text field. + * <p></p> + * As for all implementations of {@link KeyListener}, this class is only concerned + * with hardware keyboards. Software input methods have no obligation to trigger + * the methods in this class. + */ +public class DateKeyListener extends NumberKeyListener +{ + public int getInputType() { + if (mNeedsAdvancedInput) { + return InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_NORMAL; + } else { + return InputType.TYPE_CLASS_DATETIME | InputType.TYPE_DATETIME_VARIATION_DATE; + } + } + + @Override + @NonNull + protected char[] getAcceptedChars() { + return mCharacters; + } + + /** + * @deprecated Use {@link #DateKeyListener(Locale)} instead. + */ + @Deprecated + public DateKeyListener() { + this(null); + } + + private static final String SYMBOLS_TO_IGNORE = "yMLd"; + private static final String[] SKELETONS = {"yMd", "yM", "Md"}; + + public DateKeyListener(@Nullable Locale locale) { + final LinkedHashSet<Character> chars = new LinkedHashSet<>(); + // First add the digits, then add all the non-pattern characters seen in the pattern for + // "yMd", which is supposed to only have numerical fields. + final boolean success = NumberKeyListener.addDigits(chars, locale) + && NumberKeyListener.addFormatCharsFromSkeletons( + chars, locale, SKELETONS, SYMBOLS_TO_IGNORE); + if (success) { + mCharacters = NumberKeyListener.collectionToArray(chars); + mNeedsAdvancedInput = !ArrayUtils.containsAll(CHARACTERS, mCharacters); + } else { + mCharacters = CHARACTERS; + mNeedsAdvancedInput = false; + } + } + + /** + * @deprecated Use {@link #getInstance(Locale)} instead. + */ + @Deprecated + @NonNull + public static DateKeyListener getInstance() { + return getInstance(null); + } + + /** + * Returns an instance of DateKeyListener appropriate for the given locale. + */ + @NonNull + public static DateKeyListener getInstance(@Nullable Locale locale) { + DateKeyListener instance; + synchronized (sLock) { + instance = sInstanceCache.get(locale); + if (instance == null) { + instance = new DateKeyListener(locale); + sInstanceCache.put(locale, instance); + } + } + return instance; + } + + /** + * This field used to list the characters that were used. But is now a fixed data + * field that is the list of code units used for the deprecated case where the class + * is instantiated with null or no input parameter. + * + * @see KeyEvent#getMatch + * @see #getAcceptedChars + * + * @deprecated Use {@link #getAcceptedChars()} instead. + */ + @Deprecated + public static final char[] CHARACTERS = new char[] { + '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', + '/', '-', '.' + }; + + private final char[] mCharacters; + private final boolean mNeedsAdvancedInput; + + private static final Object sLock = new Object(); + @GuardedBy("sLock") + private static final HashMap<Locale, DateKeyListener> sInstanceCache = new HashMap<>(); +} diff --git a/android/text/method/DateTimeKeyListener.java b/android/text/method/DateTimeKeyListener.java new file mode 100644 index 00000000..1593db5d --- /dev/null +++ b/android/text/method/DateTimeKeyListener.java @@ -0,0 +1,139 @@ +/* + * Copyright (C) 2006 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.method; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.text.InputType; +import android.view.KeyEvent; + +import com.android.internal.annotations.GuardedBy; +import com.android.internal.util.ArrayUtils; + +import java.util.HashMap; +import java.util.LinkedHashSet; +import java.util.Locale; + +/** + * For entering dates and times in the same text field. + * <p></p> + * As for all implementations of {@link KeyListener}, this class is only concerned + * with hardware keyboards. Software input methods have no obligation to trigger + * the methods in this class. + */ +public class DateTimeKeyListener extends NumberKeyListener +{ + public int getInputType() { + if (mNeedsAdvancedInput) { + return InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_NORMAL; + } else { + return InputType.TYPE_CLASS_DATETIME | InputType.TYPE_DATETIME_VARIATION_NORMAL; + } + } + + @Override + @NonNull + protected char[] getAcceptedChars() + { + return mCharacters; + } + + /** + * @deprecated Use {@link #DateTimeKeyListener(Locale)} instead. + */ + @Deprecated + public DateTimeKeyListener() { + this(null); + } + + private static final String SYMBOLS_TO_IGNORE = "yMLdahHKkms"; + private static final String SKELETON_12HOUR = "yMdhms"; + private static final String SKELETON_24HOUR = "yMdHms"; + + public DateTimeKeyListener(@Nullable Locale locale) { + final LinkedHashSet<Character> chars = new LinkedHashSet<>(); + // First add the digits. Then, add all the character in AM and PM markers. Finally, add all + // the non-pattern characters seen in the patterns for "yMdhms" and "yMdHms". + final boolean success = NumberKeyListener.addDigits(chars, locale) + && NumberKeyListener.addAmPmChars(chars, locale) + && NumberKeyListener.addFormatCharsFromSkeleton( + chars, locale, SKELETON_12HOUR, SYMBOLS_TO_IGNORE) + && NumberKeyListener.addFormatCharsFromSkeleton( + chars, locale, SKELETON_24HOUR, SYMBOLS_TO_IGNORE); + if (success) { + mCharacters = NumberKeyListener.collectionToArray(chars); + if (locale != null && "en".equals(locale.getLanguage())) { + // For backward compatibility reasons, assume we don't need advanced input for + // English locales, although English locales literally also need a comma and perhaps + // uppercase letters for AM and PM. + mNeedsAdvancedInput = false; + } else { + mNeedsAdvancedInput = !ArrayUtils.containsAll(CHARACTERS, mCharacters); + } + } else { + mCharacters = CHARACTERS; + mNeedsAdvancedInput = false; + } + } + + /** + * @deprecated Use {@link #getInstance(Locale)} instead. + */ + @Deprecated + @NonNull + public static DateTimeKeyListener getInstance() { + return getInstance(null); + } + + /** + * Returns an instance of DateTimeKeyListener appropriate for the given locale. + */ + @NonNull + public static DateTimeKeyListener getInstance(@Nullable Locale locale) { + DateTimeKeyListener instance; + synchronized (sLock) { + instance = sInstanceCache.get(locale); + if (instance == null) { + instance = new DateTimeKeyListener(locale); + sInstanceCache.put(locale, instance); + } + } + return instance; + } + + /** + * This field used to list the characters that were used. But is now a fixed data + * field that is the list of code units used for the deprecated case where the class + * is instantiated with null or no input parameter. + * + * @see KeyEvent#getMatch + * @see #getAcceptedChars + * + * @deprecated Use {@link #getAcceptedChars()} instead. + */ + public static final char[] CHARACTERS = new char[] { + '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'm', + 'p', ':', '/', '-', ' ' + }; + + private final char[] mCharacters; + private final boolean mNeedsAdvancedInput; + + private static final Object sLock = new Object(); + @GuardedBy("sLock") + private static final HashMap<Locale, DateTimeKeyListener> sInstanceCache = new HashMap<>(); +} diff --git a/android/text/method/DialerKeyListener.java b/android/text/method/DialerKeyListener.java new file mode 100644 index 00000000..17abed6c --- /dev/null +++ b/android/text/method/DialerKeyListener.java @@ -0,0 +1,117 @@ +/* + * Copyright (C) 2006 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.method; + +import android.text.InputType; +import android.text.Spannable; +import android.view.KeyCharacterMap.KeyData; +import android.view.KeyEvent; + +/** + * For dialing-only text entry + * <p></p> + * As for all implementations of {@link KeyListener}, this class is only concerned + * with hardware keyboards. Software input methods have no obligation to trigger + * the methods in this class. + */ +public class DialerKeyListener extends NumberKeyListener +{ + @Override + protected char[] getAcceptedChars() + { + return CHARACTERS; + } + + public static DialerKeyListener getInstance() { + if (sInstance != null) + return sInstance; + + sInstance = new DialerKeyListener(); + return sInstance; + } + + public int getInputType() { + return InputType.TYPE_CLASS_PHONE; + } + + /** + * Overrides the superclass's lookup method to prefer the number field + * from the KeyEvent. + */ + protected int lookup(KeyEvent event, Spannable content) { + int meta = getMetaState(content, event); + int number = event.getNumber(); + + /* + * Prefer number if no meta key is active, or if it produces something + * valid and the meta lookup does not. + */ + if ((meta & (MetaKeyKeyListener.META_ALT_ON | MetaKeyKeyListener.META_SHIFT_ON)) == 0) { + if (number != 0) { + return number; + } + } + + int match = super.lookup(event, content); + + if (match != 0) { + return match; + } else { + /* + * If a meta key is active but the lookup with the meta key + * did not produce anything, try some other meta keys, because + * the user might have pressed SHIFT when they meant ALT, + * or vice versa. + */ + + if (meta != 0) { + KeyData kd = new KeyData(); + char[] accepted = getAcceptedChars(); + + if (event.getKeyData(kd)) { + for (int i = 1; i < kd.meta.length; i++) { + if (ok(accepted, kd.meta[i])) { + return kd.meta[i]; + } + } + } + } + + /* + * Otherwise, use the number associated with the key, since + * whatever they wanted to do with the meta key does not + * seem to be valid here. + */ + + return number; + } + } + + + /** + * The characters that are used. + * + * @see KeyEvent#getMatch + * @see #getAcceptedChars + */ + public static final char[] CHARACTERS = new char[] { + '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '#', '*', + '+', '-', '(', ')', ',', '/', 'N', '.', ' ', ';' + }; + + private static DialerKeyListener sInstance; +} diff --git a/android/text/method/DigitsKeyListener.java b/android/text/method/DigitsKeyListener.java new file mode 100644 index 00000000..d9f2dcf2 --- /dev/null +++ b/android/text/method/DigitsKeyListener.java @@ -0,0 +1,430 @@ +/* + * Copyright (C) 2006 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.method; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.icu.lang.UCharacter; +import android.icu.lang.UProperty; +import android.icu.text.DecimalFormatSymbols; +import android.text.InputType; +import android.text.SpannableStringBuilder; +import android.text.Spanned; +import android.view.KeyEvent; + +import com.android.internal.annotations.GuardedBy; +import com.android.internal.util.ArrayUtils; + +import java.util.HashMap; +import java.util.LinkedHashSet; +import java.util.Locale; + +/** + * For digits-only text entry + * <p></p> + * As for all implementations of {@link KeyListener}, this class is only concerned + * with hardware keyboards. Software input methods have no obligation to trigger + * the methods in this class. + */ +public class DigitsKeyListener extends NumberKeyListener +{ + private char[] mAccepted; + private boolean mNeedsAdvancedInput; + private final boolean mSign; + private final boolean mDecimal; + private final boolean mStringMode; + @Nullable + private final Locale mLocale; + + private static final String DEFAULT_DECIMAL_POINT_CHARS = "."; + private static final String DEFAULT_SIGN_CHARS = "-+"; + + private static final char HYPHEN_MINUS = '-'; + // Various locales use this as minus sign + private static final char MINUS_SIGN = '\u2212'; + // Slovenian uses this as minus sign (a bug?): http://unicode.org/cldr/trac/ticket/10050 + private static final char EN_DASH = '\u2013'; + + private String mDecimalPointChars = DEFAULT_DECIMAL_POINT_CHARS; + private String mSignChars = DEFAULT_SIGN_CHARS; + + private static final int SIGN = 1; + private static final int DECIMAL = 2; + + @Override + protected char[] getAcceptedChars() { + return mAccepted; + } + + /** + * The characters that are used in compatibility mode. + * + * @see KeyEvent#getMatch + * @see #getAcceptedChars + */ + private static final char[][] COMPATIBILITY_CHARACTERS = { + { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9' }, + { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '-', '+' }, + { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '.' }, + { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '-', '+', '.' }, + }; + + private boolean isSignChar(final char c) { + return mSignChars.indexOf(c) != -1; + } + + private boolean isDecimalPointChar(final char c) { + return mDecimalPointChars.indexOf(c) != -1; + } + + /** + * Allocates a DigitsKeyListener that accepts the ASCII digits 0 through 9. + * + * @deprecated Use {@link #DigitsKeyListener(Locale)} instead. + */ + @Deprecated + public DigitsKeyListener() { + this(null, false, false); + } + + /** + * Allocates a DigitsKeyListener that accepts the ASCII digits 0 through 9, plus the ASCII plus + * or minus sign (only at the beginning) and/or the ASCII period ('.') as the decimal point + * (only one per field) if specified. + * + * @deprecated Use {@link #DigitsKeyListener(Locale, boolean, boolean)} instead. + */ + @Deprecated + public DigitsKeyListener(boolean sign, boolean decimal) { + this(null, sign, decimal); + } + + public DigitsKeyListener(@Nullable Locale locale) { + this(locale, false, false); + } + + private void setToCompat() { + mDecimalPointChars = DEFAULT_DECIMAL_POINT_CHARS; + mSignChars = DEFAULT_SIGN_CHARS; + final int kind = (mSign ? SIGN : 0) | (mDecimal ? DECIMAL : 0); + mAccepted = COMPATIBILITY_CHARACTERS[kind]; + mNeedsAdvancedInput = false; + } + + private void calculateNeedForAdvancedInput() { + final int kind = (mSign ? SIGN : 0) | (mDecimal ? DECIMAL : 0); + mNeedsAdvancedInput = !ArrayUtils.containsAll(COMPATIBILITY_CHARACTERS[kind], mAccepted); + } + + // Takes a sign string and strips off its bidi controls, if any. + @NonNull + private static String stripBidiControls(@NonNull String sign) { + // For the sake of simplicity, we operate on code units, since all bidi controls are + // in the BMP. We also expect the string to be very short (almost always 1 character), so we + // don't need to use StringBuilder. + String result = ""; + for (int i = 0; i < sign.length(); i++) { + final char c = sign.charAt(i); + if (!UCharacter.hasBinaryProperty(c, UProperty.BIDI_CONTROL)) { + if (result.isEmpty()) { + result = String.valueOf(c); + } else { + // This should happen very rarely, only if we have a multi-character sign, + // or a sign outside BMP. + result += c; + } + } + } + return result; + } + + public DigitsKeyListener(@Nullable Locale locale, boolean sign, boolean decimal) { + mSign = sign; + mDecimal = decimal; + mStringMode = false; + mLocale = locale; + if (locale == null) { + setToCompat(); + return; + } + LinkedHashSet<Character> chars = new LinkedHashSet<>(); + final boolean success = NumberKeyListener.addDigits(chars, locale); + if (!success) { + setToCompat(); + return; + } + if (sign || decimal) { + final DecimalFormatSymbols symbols = DecimalFormatSymbols.getInstance(locale); + if (sign) { + final String minusString = stripBidiControls(symbols.getMinusSignString()); + final String plusString = stripBidiControls(symbols.getPlusSignString()); + if (minusString.length() > 1 || plusString.length() > 1) { + // non-BMP and multi-character signs are not supported. + setToCompat(); + return; + } + final char minus = minusString.charAt(0); + final char plus = plusString.charAt(0); + chars.add(Character.valueOf(minus)); + chars.add(Character.valueOf(plus)); + mSignChars = "" + minus + plus; + + if (minus == MINUS_SIGN || minus == EN_DASH) { + // If the minus sign is U+2212 MINUS SIGN or U+2013 EN DASH, we also need to + // accept the ASCII hyphen-minus. + chars.add(HYPHEN_MINUS); + mSignChars += HYPHEN_MINUS; + } + } + if (decimal) { + final String separatorString = symbols.getDecimalSeparatorString(); + if (separatorString.length() > 1) { + // non-BMP and multi-character decimal separators are not supported. + setToCompat(); + return; + } + final Character separatorChar = Character.valueOf(separatorString.charAt(0)); + chars.add(separatorChar); + mDecimalPointChars = separatorChar.toString(); + } + } + mAccepted = NumberKeyListener.collectionToArray(chars); + calculateNeedForAdvancedInput(); + } + + private DigitsKeyListener(@NonNull final String accepted) { + mSign = false; + mDecimal = false; + mStringMode = true; + mLocale = null; + mAccepted = new char[accepted.length()]; + accepted.getChars(0, accepted.length(), mAccepted, 0); + // Theoretically we may need advanced input, but for backward compatibility, we don't change + // the input type. + mNeedsAdvancedInput = false; + } + + /** + * Returns a DigitsKeyListener that accepts the ASCII digits 0 through 9. + * + * @deprecated Use {@link #getInstance(Locale)} instead. + */ + @Deprecated + @NonNull + public static DigitsKeyListener getInstance() { + return getInstance(false, false); + } + + /** + * Returns a DigitsKeyListener that accepts the ASCII digits 0 through 9, plus the ASCII plus + * or minus sign (only at the beginning) and/or the ASCII period ('.') as the decimal point + * (only one per field) if specified. + * + * @deprecated Use {@link #getInstance(Locale, boolean, boolean)} instead. + */ + @Deprecated + @NonNull + public static DigitsKeyListener getInstance(boolean sign, boolean decimal) { + return getInstance(null, sign, decimal); + } + + /** + * Returns a DigitsKeyListener that accepts the locale-appropriate digits. + */ + @NonNull + public static DigitsKeyListener getInstance(@Nullable Locale locale) { + return getInstance(locale, false, false); + } + + private static final Object sLocaleCacheLock = new Object(); + @GuardedBy("sLocaleCacheLock") + private static final HashMap<Locale, DigitsKeyListener[]> sLocaleInstanceCache = + new HashMap<>(); + + /** + * Returns a DigitsKeyListener that accepts the locale-appropriate digits, plus the + * locale-appropriate plus or minus sign (only at the beginning) and/or the locale-appropriate + * decimal separator (only one per field) if specified. + */ + @NonNull + public static DigitsKeyListener getInstance( + @Nullable Locale locale, boolean sign, boolean decimal) { + final int kind = (sign ? SIGN : 0) | (decimal ? DECIMAL : 0); + synchronized (sLocaleCacheLock) { + DigitsKeyListener[] cachedValue = sLocaleInstanceCache.get(locale); + if (cachedValue != null && cachedValue[kind] != null) { + return cachedValue[kind]; + } + if (cachedValue == null) { + cachedValue = new DigitsKeyListener[4]; + sLocaleInstanceCache.put(locale, cachedValue); + } + return cachedValue[kind] = new DigitsKeyListener(locale, sign, decimal); + } + } + + private static final Object sStringCacheLock = new Object(); + @GuardedBy("sStringCacheLock") + private static final HashMap<String, DigitsKeyListener> sStringInstanceCache = new HashMap<>(); + + /** + * Returns a DigitsKeyListener that accepts only the characters + * that appear in the specified String. Note that not all characters + * may be available on every keyboard. + */ + @NonNull + public static DigitsKeyListener getInstance(@NonNull String accepted) { + DigitsKeyListener result; + synchronized (sStringCacheLock) { + result = sStringInstanceCache.get(accepted); + if (result == null) { + result = new DigitsKeyListener(accepted); + sStringInstanceCache.put(accepted, result); + } + } + return result; + } + + /** + * Returns a DigitsKeyListener based on an the settings of a existing DigitsKeyListener, with + * the locale modified. + * + * @hide + */ + @NonNull + public static DigitsKeyListener getInstance( + @Nullable Locale locale, + @NonNull DigitsKeyListener listener) { + if (listener.mStringMode) { + return listener; // string-mode DigitsKeyListeners have no locale. + } else { + return getInstance(locale, listener.mSign, listener.mDecimal); + } + } + + /** + * Returns the input type for the listener. + */ + public int getInputType() { + int contentType; + if (mNeedsAdvancedInput) { + contentType = InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_NORMAL; + } else { + contentType = InputType.TYPE_CLASS_NUMBER; + if (mSign) { + contentType |= InputType.TYPE_NUMBER_FLAG_SIGNED; + } + if (mDecimal) { + contentType |= InputType.TYPE_NUMBER_FLAG_DECIMAL; + } + } + return contentType; + } + + @Override + public CharSequence filter(CharSequence source, int start, int end, + Spanned dest, int dstart, int dend) { + CharSequence out = super.filter(source, start, end, dest, dstart, dend); + + if (mSign == false && mDecimal == false) { + return out; + } + + if (out != null) { + source = out; + start = 0; + end = out.length(); + } + + int sign = -1; + int decimal = -1; + int dlen = dest.length(); + + /* + * Find out if the existing text has a sign or decimal point characters. + */ + + for (int i = 0; i < dstart; i++) { + char c = dest.charAt(i); + + if (isSignChar(c)) { + sign = i; + } else if (isDecimalPointChar(c)) { + decimal = i; + } + } + for (int i = dend; i < dlen; i++) { + char c = dest.charAt(i); + + if (isSignChar(c)) { + return ""; // Nothing can be inserted in front of a sign character. + } else if (isDecimalPointChar(c)) { + decimal = i; + } + } + + /* + * If it does, we must strip them out from the source. + * In addition, a sign character must be the very first character, + * and nothing can be inserted before an existing sign character. + * Go in reverse order so the offsets are stable. + */ + + SpannableStringBuilder stripped = null; + + for (int i = end - 1; i >= start; i--) { + char c = source.charAt(i); + boolean strip = false; + + if (isSignChar(c)) { + if (i != start || dstart != 0) { + strip = true; + } else if (sign >= 0) { + strip = true; + } else { + sign = i; + } + } else if (isDecimalPointChar(c)) { + if (decimal >= 0) { + strip = true; + } else { + decimal = i; + } + } + + if (strip) { + if (end == start + 1) { + return ""; // Only one character, and it was stripped. + } + + if (stripped == null) { + stripped = new SpannableStringBuilder(source, start, end); + } + + stripped.delete(i - start, i + 1 - start); + } + } + + if (stripped != null) { + return stripped; + } else if (out != null) { + return out; + } else { + return null; + } + } +} diff --git a/android/text/method/HideReturnsTransformationMethod.java b/android/text/method/HideReturnsTransformationMethod.java new file mode 100644 index 00000000..c6a90ca9 --- /dev/null +++ b/android/text/method/HideReturnsTransformationMethod.java @@ -0,0 +1,52 @@ +/* + * Copyright (C) 2006 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.method; + +/** + * This transformation method causes any carriage return characters (\r) + * to be hidden by displaying them as zero-width non-breaking space + * characters (\uFEFF). + */ +public class HideReturnsTransformationMethod +extends ReplacementTransformationMethod { + private static char[] ORIGINAL = new char[] { '\r' }; + private static char[] REPLACEMENT = new char[] { '\uFEFF' }; + + /** + * The character to be replaced is \r. + */ + protected char[] getOriginal() { + return ORIGINAL; + } + + /** + * The character that \r is replaced with is \uFEFF. + */ + protected char[] getReplacement() { + return REPLACEMENT; + } + + public static HideReturnsTransformationMethod getInstance() { + if (sInstance != null) + return sInstance; + + sInstance = new HideReturnsTransformationMethod(); + return sInstance; + } + + private static HideReturnsTransformationMethod sInstance; +} diff --git a/android/text/method/KeyListener.java b/android/text/method/KeyListener.java new file mode 100644 index 00000000..ce7054c4 --- /dev/null +++ b/android/text/method/KeyListener.java @@ -0,0 +1,85 @@ +/* + * Copyright (C) 2006 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.method; + +import android.text.Editable; +import android.view.KeyEvent; +import android.view.View; + +/** + * Interface for converting text key events into edit operations on an + * Editable class. Note that for most cases this interface has been + * superceded by general soft input methods as defined by + * {@link android.view.inputmethod.InputMethod}; it should only be used + * for cases where an application has its own on-screen keypad and also wants + * to process hard keyboard events to match it. + * <p></p> + * Key presses on soft input methods are not required to trigger the methods + * in this listener, and are in fact discouraged to do so. The default + * android keyboard will not trigger these for any key to any application + * targetting Jelly Bean or later, and will only deliver it for some + * key presses to applications targetting Ice Cream Sandwich or earlier. + */ +public interface KeyListener { + /** + * Return the type of text that this key listener is manipulating, + * as per {@link android.text.InputType}. This is used to + * determine the mode of the soft keyboard that is shown for the editor. + * + * <p>If you return + * {@link android.text.InputType#TYPE_NULL} + * then <em>no</em> soft keyboard will provided. In other words, you + * must be providing your own key pad for on-screen input and the key + * listener will be used to handle input from a hard keyboard. + * + * <p>If you + * return any other value, a soft input method will be created when the + * user puts focus in the editor, which will provide a keypad and also + * consume hard key events. This means that the key listener will generally + * not be used, instead the soft input method will take care of managing + * key input as per the content type returned here. + */ + public int getInputType(); + + /** + * If the key listener wants to handle this key, return true, + * otherwise return false and the caller (i.e. the widget host) + * will handle the key. + */ + public boolean onKeyDown(View view, Editable text, + int keyCode, KeyEvent event); + + /** + * If the key listener wants to handle this key release, return true, + * otherwise return false and the caller (i.e. the widget host) + * will handle the key. + */ + public boolean onKeyUp(View view, Editable text, + int keyCode, KeyEvent event); + + /** + * If the key listener wants to other kinds of key events, return true, + * otherwise return false and the caller (i.e. the widget host) + * will handle the key. + */ + public boolean onKeyOther(View view, Editable text, KeyEvent event); + + /** + * Remove the given shift states from the edited text. + */ + public void clearMetaKeyState(View view, Editable content, int states); +} diff --git a/android/text/method/LinkMovementMethod.java b/android/text/method/LinkMovementMethod.java new file mode 100644 index 00000000..31ed5492 --- /dev/null +++ b/android/text/method/LinkMovementMethod.java @@ -0,0 +1,257 @@ +/* + * Copyright (C) 2006 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.method; + +import android.text.Layout; +import android.text.NoCopySpan; +import android.text.Selection; +import android.text.Spannable; +import android.text.style.ClickableSpan; +import android.view.KeyEvent; +import android.view.MotionEvent; +import android.view.View; +import android.widget.TextView; + +/** + * A movement method that traverses links in the text buffer and scrolls if necessary. + * Supports clicking on links with DPad Center or Enter. + */ +public class LinkMovementMethod extends ScrollingMovementMethod { + private static final int CLICK = 1; + private static final int UP = 2; + private static final int DOWN = 3; + + @Override + public boolean canSelectArbitrarily() { + return true; + } + + @Override + protected boolean handleMovementKey(TextView widget, Spannable buffer, int keyCode, + int movementMetaState, KeyEvent event) { + switch (keyCode) { + case KeyEvent.KEYCODE_DPAD_CENTER: + case KeyEvent.KEYCODE_ENTER: + if (KeyEvent.metaStateHasNoModifiers(movementMetaState)) { + if (event.getAction() == KeyEvent.ACTION_DOWN && + event.getRepeatCount() == 0 && action(CLICK, widget, buffer)) { + return true; + } + } + break; + } + return super.handleMovementKey(widget, buffer, keyCode, movementMetaState, event); + } + + @Override + protected boolean up(TextView widget, Spannable buffer) { + if (action(UP, widget, buffer)) { + return true; + } + + return super.up(widget, buffer); + } + + @Override + protected boolean down(TextView widget, Spannable buffer) { + if (action(DOWN, widget, buffer)) { + return true; + } + + return super.down(widget, buffer); + } + + @Override + protected boolean left(TextView widget, Spannable buffer) { + if (action(UP, widget, buffer)) { + return true; + } + + return super.left(widget, buffer); + } + + @Override + protected boolean right(TextView widget, Spannable buffer) { + if (action(DOWN, widget, buffer)) { + return true; + } + + return super.right(widget, buffer); + } + + private boolean action(int what, TextView widget, Spannable buffer) { + Layout layout = widget.getLayout(); + + int padding = widget.getTotalPaddingTop() + + widget.getTotalPaddingBottom(); + int areaTop = widget.getScrollY(); + int areaBot = areaTop + widget.getHeight() - padding; + + int lineTop = layout.getLineForVertical(areaTop); + int lineBot = layout.getLineForVertical(areaBot); + + int first = layout.getLineStart(lineTop); + int last = layout.getLineEnd(lineBot); + + ClickableSpan[] candidates = buffer.getSpans(first, last, ClickableSpan.class); + + int a = Selection.getSelectionStart(buffer); + int b = Selection.getSelectionEnd(buffer); + + int selStart = Math.min(a, b); + int selEnd = Math.max(a, b); + + if (selStart < 0) { + if (buffer.getSpanStart(FROM_BELOW) >= 0) { + selStart = selEnd = buffer.length(); + } + } + + if (selStart > last) + selStart = selEnd = Integer.MAX_VALUE; + if (selEnd < first) + selStart = selEnd = -1; + + switch (what) { + case CLICK: + if (selStart == selEnd) { + return false; + } + + ClickableSpan[] link = buffer.getSpans(selStart, selEnd, ClickableSpan.class); + + if (link.length != 1) + return false; + + link[0].onClick(widget); + break; + + case UP: + int bestStart, bestEnd; + + bestStart = -1; + bestEnd = -1; + + for (int i = 0; i < candidates.length; i++) { + int end = buffer.getSpanEnd(candidates[i]); + + if (end < selEnd || selStart == selEnd) { + if (end > bestEnd) { + bestStart = buffer.getSpanStart(candidates[i]); + bestEnd = end; + } + } + } + + if (bestStart >= 0) { + Selection.setSelection(buffer, bestEnd, bestStart); + return true; + } + + break; + + case DOWN: + bestStart = Integer.MAX_VALUE; + bestEnd = Integer.MAX_VALUE; + + for (int i = 0; i < candidates.length; i++) { + int start = buffer.getSpanStart(candidates[i]); + + if (start > selStart || selStart == selEnd) { + if (start < bestStart) { + bestStart = start; + bestEnd = buffer.getSpanEnd(candidates[i]); + } + } + } + + if (bestEnd < Integer.MAX_VALUE) { + Selection.setSelection(buffer, bestStart, bestEnd); + return true; + } + + break; + } + + return false; + } + + @Override + public boolean onTouchEvent(TextView widget, Spannable buffer, + MotionEvent event) { + int action = event.getAction(); + + if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_DOWN) { + int x = (int) event.getX(); + int y = (int) event.getY(); + + x -= widget.getTotalPaddingLeft(); + y -= widget.getTotalPaddingTop(); + + x += widget.getScrollX(); + y += widget.getScrollY(); + + Layout layout = widget.getLayout(); + int line = layout.getLineForVertical(y); + int off = layout.getOffsetForHorizontal(line, x); + + ClickableSpan[] links = buffer.getSpans(off, off, ClickableSpan.class); + + if (links.length != 0) { + if (action == MotionEvent.ACTION_UP) { + links[0].onClick(widget); + } else if (action == MotionEvent.ACTION_DOWN) { + Selection.setSelection(buffer, + buffer.getSpanStart(links[0]), + buffer.getSpanEnd(links[0])); + } + return true; + } else { + Selection.removeSelection(buffer); + } + } + + return super.onTouchEvent(widget, buffer, event); + } + + @Override + public void initialize(TextView widget, Spannable text) { + Selection.removeSelection(text); + text.removeSpan(FROM_BELOW); + } + + @Override + public void onTakeFocus(TextView view, Spannable text, int dir) { + Selection.removeSelection(text); + + if ((dir & View.FOCUS_BACKWARD) != 0) { + text.setSpan(FROM_BELOW, 0, 0, Spannable.SPAN_POINT_POINT); + } else { + text.removeSpan(FROM_BELOW); + } + } + + public static MovementMethod getInstance() { + if (sInstance == null) + sInstance = new LinkMovementMethod(); + + return sInstance; + } + + private static LinkMovementMethod sInstance; + private static Object FROM_BELOW = new NoCopySpan.Concrete(); +} diff --git a/android/text/method/MetaKeyKeyListener.java b/android/text/method/MetaKeyKeyListener.java new file mode 100644 index 00000000..c3c7302c --- /dev/null +++ b/android/text/method/MetaKeyKeyListener.java @@ -0,0 +1,660 @@ +/* + * Copyright (C) 2006 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.method; + +import android.text.Editable; +import android.text.NoCopySpan; +import android.text.Spannable; +import android.text.Spanned; +import android.view.KeyCharacterMap; +import android.view.KeyEvent; +import android.view.View; + +/** + * This base class encapsulates the behavior for tracking the state of + * meta keys such as SHIFT, ALT and SYM as well as the pseudo-meta state of selecting text. + * <p> + * Key listeners that care about meta state should inherit from this class; + * you should not instantiate this class directly in a client. + * </p><p> + * This class provides two mechanisms for tracking meta state that can be used + * together or independently. + * </p> + * <ul> + * <li>Methods such as {@link #handleKeyDown(long, int, KeyEvent)} and + * {@link #getMetaState(long)} operate on a meta key state bit mask.</li> + * <li>Methods such as {@link #onKeyDown(View, Editable, int, KeyEvent)} and + * {@link #getMetaState(CharSequence, int)} operate on meta key state flags stored + * as spans in an {@link Editable} text buffer. The spans only describe the current + * meta key state of the text editor; they do not carry any positional information.</li> + * </ul> + * <p> + * The behavior of this class varies according to the keyboard capabilities + * described by the {@link KeyCharacterMap} of the keyboard device such as + * the {@link KeyCharacterMap#getModifierBehavior() key modifier behavior}. + * </p><p> + * {@link MetaKeyKeyListener} implements chorded and toggled key modifiers. + * When key modifiers are toggled into a latched or locked state, the state + * of the modifier is stored in the {@link Editable} text buffer or in a + * meta state integer managed by the client. These latched or locked modifiers + * should be considered to be held <b>in addition to</b> those that the + * keyboard already reported as being pressed in {@link KeyEvent#getMetaState()}. + * In other words, the {@link MetaKeyKeyListener} augments the meta state + * provided by the keyboard; it does not replace it. This distinction is important + * to ensure that meta keys not handled by {@link MetaKeyKeyListener} such as + * {@link KeyEvent#KEYCODE_CAPS_LOCK} or {@link KeyEvent#KEYCODE_NUM_LOCK} are + * taken into consideration. + * </p><p> + * To ensure correct meta key behavior, the following pattern should be used + * when mapping key codes to characters: + * </p> + * <code> + * private char getUnicodeChar(TextKeyListener listener, KeyEvent event, Editable textBuffer) { + * // Use the combined meta states from the event and the key listener. + * int metaState = event.getMetaState() | listener.getMetaState(textBuffer); + * return event.getUnicodeChar(metaState); + * } + * </code> + */ +public abstract class MetaKeyKeyListener { + /** + * Flag that indicates that the SHIFT key is on. + * Value equals {@link KeyEvent#META_SHIFT_ON}. + */ + public static final int META_SHIFT_ON = KeyEvent.META_SHIFT_ON; + /** + * Flag that indicates that the ALT key is on. + * Value equals {@link KeyEvent#META_ALT_ON}. + */ + public static final int META_ALT_ON = KeyEvent.META_ALT_ON; + /** + * Flag that indicates that the SYM key is on. + * Value equals {@link KeyEvent#META_SYM_ON}. + */ + public static final int META_SYM_ON = KeyEvent.META_SYM_ON; + + /** + * Flag that indicates that the SHIFT key is locked in CAPS mode. + */ + public static final int META_CAP_LOCKED = KeyEvent.META_CAP_LOCKED; + /** + * Flag that indicates that the ALT key is locked. + */ + public static final int META_ALT_LOCKED = KeyEvent.META_ALT_LOCKED; + /** + * Flag that indicates that the SYM key is locked. + */ + public static final int META_SYM_LOCKED = KeyEvent.META_SYM_LOCKED; + + /** + * @hide pending API review + */ + public static final int META_SELECTING = KeyEvent.META_SELECTING; + + // These bits are privately used by the meta key key listener. + // They are deliberately assigned values outside of the representable range of an 'int' + // so as not to conflict with any meta key states publicly defined by KeyEvent. + private static final long META_CAP_USED = 1L << 32; + private static final long META_ALT_USED = 1L << 33; + private static final long META_SYM_USED = 1L << 34; + + private static final long META_CAP_PRESSED = 1L << 40; + private static final long META_ALT_PRESSED = 1L << 41; + private static final long META_SYM_PRESSED = 1L << 42; + + private static final long META_CAP_RELEASED = 1L << 48; + private static final long META_ALT_RELEASED = 1L << 49; + private static final long META_SYM_RELEASED = 1L << 50; + + private static final long META_SHIFT_MASK = META_SHIFT_ON + | META_CAP_LOCKED | META_CAP_USED + | META_CAP_PRESSED | META_CAP_RELEASED; + private static final long META_ALT_MASK = META_ALT_ON + | META_ALT_LOCKED | META_ALT_USED + | META_ALT_PRESSED | META_ALT_RELEASED; + private static final long META_SYM_MASK = META_SYM_ON + | META_SYM_LOCKED | META_SYM_USED + | META_SYM_PRESSED | META_SYM_RELEASED; + + private static final Object CAP = new NoCopySpan.Concrete(); + private static final Object ALT = new NoCopySpan.Concrete(); + private static final Object SYM = new NoCopySpan.Concrete(); + private static final Object SELECTING = new NoCopySpan.Concrete(); + + private static final int PRESSED_RETURN_VALUE = 1; + private static final int LOCKED_RETURN_VALUE = 2; + + /** + * Resets all meta state to inactive. + */ + public static void resetMetaState(Spannable text) { + text.removeSpan(CAP); + text.removeSpan(ALT); + text.removeSpan(SYM); + text.removeSpan(SELECTING); + } + + /** + * Gets the state of the meta keys. + * + * @param text the buffer in which the meta key would have been pressed. + * + * @return an integer in which each bit set to one represents a pressed + * or locked meta key. + */ + public static final int getMetaState(CharSequence text) { + return getActive(text, CAP, META_SHIFT_ON, META_CAP_LOCKED) | + getActive(text, ALT, META_ALT_ON, META_ALT_LOCKED) | + getActive(text, SYM, META_SYM_ON, META_SYM_LOCKED) | + getActive(text, SELECTING, META_SELECTING, META_SELECTING); + } + + /** + * Gets the state of the meta keys for a specific key event. + * + * For input devices that use toggled key modifiers, the `toggled' state + * is stored into the text buffer. This method retrieves the meta state + * for this event, accounting for the stored state. If the event has been + * created by a device that does not support toggled key modifiers, like + * a virtual device for example, the stored state is ignored. + * + * @param text the buffer in which the meta key would have been pressed. + * @param event the event for which to evaluate the meta state. + * @return an integer in which each bit set to one represents a pressed + * or locked meta key. + */ + public static final int getMetaState(final CharSequence text, final KeyEvent event) { + int metaState = event.getMetaState(); + if (event.getKeyCharacterMap().getModifierBehavior() + == KeyCharacterMap.MODIFIER_BEHAVIOR_CHORDED_OR_TOGGLED) { + metaState |= getMetaState(text); + } + return metaState; + } + + // As META_SELECTING is @hide we should not mention it in public comments, hence the + // omission in @param meta + /** + * Gets the state of a particular meta key. + * + * @param meta META_SHIFT_ON, META_ALT_ON, META_SYM_ON + * @param text the buffer in which the meta key would have been pressed. + * + * @return 0 if inactive, 1 if active, 2 if locked. + */ + public static final int getMetaState(CharSequence text, int meta) { + switch (meta) { + case META_SHIFT_ON: + return getActive(text, CAP, PRESSED_RETURN_VALUE, LOCKED_RETURN_VALUE); + + case META_ALT_ON: + return getActive(text, ALT, PRESSED_RETURN_VALUE, LOCKED_RETURN_VALUE); + + case META_SYM_ON: + return getActive(text, SYM, PRESSED_RETURN_VALUE, LOCKED_RETURN_VALUE); + + case META_SELECTING: + return getActive(text, SELECTING, PRESSED_RETURN_VALUE, LOCKED_RETURN_VALUE); + + default: + return 0; + } + } + + /** + * Gets the state of a particular meta key to use with a particular key event. + * + * If the key event has been created by a device that does not support toggled + * key modifiers, like a virtual keyboard for example, only the meta state in + * the key event is considered. + * + * @param meta META_SHIFT_ON, META_ALT_ON, META_SYM_ON + * @param text the buffer in which the meta key would have been pressed. + * @param event the event for which to evaluate the meta state. + * @return 0 if inactive, 1 if active, 2 if locked. + */ + public static final int getMetaState(final CharSequence text, final int meta, + final KeyEvent event) { + int metaState = event.getMetaState(); + if (event.getKeyCharacterMap().getModifierBehavior() + == KeyCharacterMap.MODIFIER_BEHAVIOR_CHORDED_OR_TOGGLED) { + metaState |= getMetaState(text); + } + if (META_SELECTING == meta) { + // #getMetaState(long, int) does not support META_SELECTING, but we want the same + // behavior as #getMetaState(CharSequence, int) so we need to do it here + if ((metaState & META_SELECTING) != 0) { + // META_SELECTING is only ever set to PRESSED and can't be LOCKED, so return 1 + return 1; + } + return 0; + } + return getMetaState(metaState, meta); + } + + private static int getActive(CharSequence text, Object meta, + int on, int lock) { + if (!(text instanceof Spanned)) { + return 0; + } + + Spanned sp = (Spanned) text; + int flag = sp.getSpanFlags(meta); + + if (flag == LOCKED) { + return lock; + } else if (flag != 0) { + return on; + } else { + return 0; + } + } + + /** + * Call this method after you handle a keypress so that the meta + * state will be reset to unshifted (if it is not still down) + * or primed to be reset to unshifted (once it is released). + */ + public static void adjustMetaAfterKeypress(Spannable content) { + adjust(content, CAP); + adjust(content, ALT); + adjust(content, SYM); + } + + /** + * Returns true if this object is one that this class would use to + * keep track of any meta state in the specified text. + */ + public static boolean isMetaTracker(CharSequence text, Object what) { + return what == CAP || what == ALT || what == SYM || + what == SELECTING; + } + + /** + * Returns true if this object is one that this class would use to + * keep track of the selecting meta state in the specified text. + */ + public static boolean isSelectingMetaTracker(CharSequence text, Object what) { + return what == SELECTING; + } + + private static void adjust(Spannable content, Object what) { + int current = content.getSpanFlags(what); + + if (current == PRESSED) + content.setSpan(what, 0, 0, USED); + else if (current == RELEASED) + content.removeSpan(what); + } + + /** + * Call this if you are a method that ignores the locked meta state + * (arrow keys, for example) and you handle a key. + */ + protected static void resetLockedMeta(Spannable content) { + resetLock(content, CAP); + resetLock(content, ALT); + resetLock(content, SYM); + resetLock(content, SELECTING); + } + + private static void resetLock(Spannable content, Object what) { + int current = content.getSpanFlags(what); + + if (current == LOCKED) + content.removeSpan(what); + } + + /** + * Handles presses of the meta keys. + */ + public boolean onKeyDown(View view, Editable content, int keyCode, KeyEvent event) { + if (keyCode == KeyEvent.KEYCODE_SHIFT_LEFT || keyCode == KeyEvent.KEYCODE_SHIFT_RIGHT) { + press(content, CAP); + return true; + } + + if (keyCode == KeyEvent.KEYCODE_ALT_LEFT || keyCode == KeyEvent.KEYCODE_ALT_RIGHT + || keyCode == KeyEvent.KEYCODE_NUM) { + press(content, ALT); + return true; + } + + if (keyCode == KeyEvent.KEYCODE_SYM) { + press(content, SYM); + return true; + } + + return false; // no super to call through to + } + + private void press(Editable content, Object what) { + int state = content.getSpanFlags(what); + + if (state == PRESSED) + ; // repeat before use + else if (state == RELEASED) + content.setSpan(what, 0, 0, LOCKED); + else if (state == USED) + ; // repeat after use + else if (state == LOCKED) + content.removeSpan(what); + else + content.setSpan(what, 0, 0, PRESSED); + } + + /** + * Start selecting text. + * @hide pending API review + */ + public static void startSelecting(View view, Spannable content) { + content.setSpan(SELECTING, 0, 0, PRESSED); + } + + /** + * Stop selecting text. This does not actually collapse the selection; + * call {@link android.text.Selection#setSelection} too. + * @hide pending API review + */ + public static void stopSelecting(View view, Spannable content) { + content.removeSpan(SELECTING); + } + + /** + * Handles release of the meta keys. + */ + public boolean onKeyUp(View view, Editable content, int keyCode, KeyEvent event) { + if (keyCode == KeyEvent.KEYCODE_SHIFT_LEFT || keyCode == KeyEvent.KEYCODE_SHIFT_RIGHT) { + release(content, CAP, event); + return true; + } + + if (keyCode == KeyEvent.KEYCODE_ALT_LEFT || keyCode == KeyEvent.KEYCODE_ALT_RIGHT + || keyCode == KeyEvent.KEYCODE_NUM) { + release(content, ALT, event); + return true; + } + + if (keyCode == KeyEvent.KEYCODE_SYM) { + release(content, SYM, event); + return true; + } + + return false; // no super to call through to + } + + private void release(Editable content, Object what, KeyEvent event) { + int current = content.getSpanFlags(what); + + switch (event.getKeyCharacterMap().getModifierBehavior()) { + case KeyCharacterMap.MODIFIER_BEHAVIOR_CHORDED_OR_TOGGLED: + if (current == USED) + content.removeSpan(what); + else if (current == PRESSED) + content.setSpan(what, 0, 0, RELEASED); + break; + + default: + content.removeSpan(what); + break; + } + } + + public void clearMetaKeyState(View view, Editable content, int states) { + clearMetaKeyState(content, states); + } + + public static void clearMetaKeyState(Editable content, int states) { + if ((states&META_SHIFT_ON) != 0) content.removeSpan(CAP); + if ((states&META_ALT_ON) != 0) content.removeSpan(ALT); + if ((states&META_SYM_ON) != 0) content.removeSpan(SYM); + if ((states&META_SELECTING) != 0) content.removeSpan(SELECTING); + } + + /** + * Call this if you are a method that ignores the locked meta state + * (arrow keys, for example) and you handle a key. + */ + public static long resetLockedMeta(long state) { + if ((state & META_CAP_LOCKED) != 0) { + state &= ~META_SHIFT_MASK; + } + if ((state & META_ALT_LOCKED) != 0) { + state &= ~META_ALT_MASK; + } + if ((state & META_SYM_LOCKED) != 0) { + state &= ~META_SYM_MASK; + } + return state; + } + + // --------------------------------------------------------------------- + // Version of API that operates on a state bit mask + // --------------------------------------------------------------------- + + /** + * Gets the state of the meta keys. + * + * @param state the current meta state bits. + * + * @return an integer in which each bit set to one represents a pressed + * or locked meta key. + */ + public static final int getMetaState(long state) { + int result = 0; + + if ((state & META_CAP_LOCKED) != 0) { + result |= META_CAP_LOCKED; + } else if ((state & META_SHIFT_ON) != 0) { + result |= META_SHIFT_ON; + } + + if ((state & META_ALT_LOCKED) != 0) { + result |= META_ALT_LOCKED; + } else if ((state & META_ALT_ON) != 0) { + result |= META_ALT_ON; + } + + if ((state & META_SYM_LOCKED) != 0) { + result |= META_SYM_LOCKED; + } else if ((state & META_SYM_ON) != 0) { + result |= META_SYM_ON; + } + + return result; + } + + /** + * Gets the state of a particular meta key. + * + * @param state the current state bits. + * @param meta META_SHIFT_ON, META_ALT_ON, or META_SYM_ON + * + * @return 0 if inactive, 1 if active, 2 if locked. + */ + public static final int getMetaState(long state, int meta) { + switch (meta) { + case META_SHIFT_ON: + if ((state & META_CAP_LOCKED) != 0) return LOCKED_RETURN_VALUE; + if ((state & META_SHIFT_ON) != 0) return PRESSED_RETURN_VALUE; + return 0; + + case META_ALT_ON: + if ((state & META_ALT_LOCKED) != 0) return LOCKED_RETURN_VALUE; + if ((state & META_ALT_ON) != 0) return PRESSED_RETURN_VALUE; + return 0; + + case META_SYM_ON: + if ((state & META_SYM_LOCKED) != 0) return LOCKED_RETURN_VALUE; + if ((state & META_SYM_ON) != 0) return PRESSED_RETURN_VALUE; + return 0; + + default: + return 0; + } + } + + /** + * Call this method after you handle a keypress so that the meta + * state will be reset to unshifted (if it is not still down) + * or primed to be reset to unshifted (once it is released). Takes + * the current state, returns the new state. + */ + public static long adjustMetaAfterKeypress(long state) { + if ((state & META_CAP_PRESSED) != 0) { + state = (state & ~META_SHIFT_MASK) | META_SHIFT_ON | META_CAP_USED; + } else if ((state & META_CAP_RELEASED) != 0) { + state &= ~META_SHIFT_MASK; + } + + if ((state & META_ALT_PRESSED) != 0) { + state = (state & ~META_ALT_MASK) | META_ALT_ON | META_ALT_USED; + } else if ((state & META_ALT_RELEASED) != 0) { + state &= ~META_ALT_MASK; + } + + if ((state & META_SYM_PRESSED) != 0) { + state = (state & ~META_SYM_MASK) | META_SYM_ON | META_SYM_USED; + } else if ((state & META_SYM_RELEASED) != 0) { + state &= ~META_SYM_MASK; + } + return state; + } + + /** + * Handles presses of the meta keys. + */ + public static long handleKeyDown(long state, int keyCode, KeyEvent event) { + if (keyCode == KeyEvent.KEYCODE_SHIFT_LEFT || keyCode == KeyEvent.KEYCODE_SHIFT_RIGHT) { + return press(state, META_SHIFT_ON, META_SHIFT_MASK, + META_CAP_LOCKED, META_CAP_PRESSED, META_CAP_RELEASED, META_CAP_USED); + } + + if (keyCode == KeyEvent.KEYCODE_ALT_LEFT || keyCode == KeyEvent.KEYCODE_ALT_RIGHT + || keyCode == KeyEvent.KEYCODE_NUM) { + return press(state, META_ALT_ON, META_ALT_MASK, + META_ALT_LOCKED, META_ALT_PRESSED, META_ALT_RELEASED, META_ALT_USED); + } + + if (keyCode == KeyEvent.KEYCODE_SYM) { + return press(state, META_SYM_ON, META_SYM_MASK, + META_SYM_LOCKED, META_SYM_PRESSED, META_SYM_RELEASED, META_SYM_USED); + } + return state; + } + + private static long press(long state, int what, long mask, + long locked, long pressed, long released, long used) { + if ((state & pressed) != 0) { + // repeat before use + } else if ((state & released) != 0) { + state = (state &~ mask) | what | locked; + } else if ((state & used) != 0) { + // repeat after use + } else if ((state & locked) != 0) { + state &= ~mask; + } else { + state |= what | pressed; + } + return state; + } + + /** + * Handles release of the meta keys. + */ + public static long handleKeyUp(long state, int keyCode, KeyEvent event) { + if (keyCode == KeyEvent.KEYCODE_SHIFT_LEFT || keyCode == KeyEvent.KEYCODE_SHIFT_RIGHT) { + return release(state, META_SHIFT_ON, META_SHIFT_MASK, + META_CAP_PRESSED, META_CAP_RELEASED, META_CAP_USED, event); + } + + if (keyCode == KeyEvent.KEYCODE_ALT_LEFT || keyCode == KeyEvent.KEYCODE_ALT_RIGHT + || keyCode == KeyEvent.KEYCODE_NUM) { + return release(state, META_ALT_ON, META_ALT_MASK, + META_ALT_PRESSED, META_ALT_RELEASED, META_ALT_USED, event); + } + + if (keyCode == KeyEvent.KEYCODE_SYM) { + return release(state, META_SYM_ON, META_SYM_MASK, + META_SYM_PRESSED, META_SYM_RELEASED, META_SYM_USED, event); + } + return state; + } + + private static long release(long state, int what, long mask, + long pressed, long released, long used, KeyEvent event) { + switch (event.getKeyCharacterMap().getModifierBehavior()) { + case KeyCharacterMap.MODIFIER_BEHAVIOR_CHORDED_OR_TOGGLED: + if ((state & used) != 0) { + state &= ~mask; + } else if ((state & pressed) != 0) { + state |= what | released; + } + break; + + default: + state &= ~mask; + break; + } + return state; + } + + /** + * Clears the state of the specified meta key if it is locked. + * @param state the meta key state + * @param which meta keys to clear, may be a combination of {@link #META_SHIFT_ON}, + * {@link #META_ALT_ON} or {@link #META_SYM_ON}. + */ + public long clearMetaKeyState(long state, int which) { + if ((which & META_SHIFT_ON) != 0 && (state & META_CAP_LOCKED) != 0) { + state &= ~META_SHIFT_MASK; + } + if ((which & META_ALT_ON) != 0 && (state & META_ALT_LOCKED) != 0) { + state &= ~META_ALT_MASK; + } + if ((which & META_SYM_ON) != 0 && (state & META_SYM_LOCKED) != 0) { + state &= ~META_SYM_MASK; + } + return state; + } + + /** + * The meta key has been pressed but has not yet been used. + */ + private static final int PRESSED = + Spannable.SPAN_MARK_MARK | (1 << Spannable.SPAN_USER_SHIFT); + + /** + * The meta key has been pressed and released but has still + * not yet been used. + */ + private static final int RELEASED = + Spannable.SPAN_MARK_MARK | (2 << Spannable.SPAN_USER_SHIFT); + + /** + * The meta key has been pressed and used but has not yet been released. + */ + private static final int USED = + Spannable.SPAN_MARK_MARK | (3 << Spannable.SPAN_USER_SHIFT); + + /** + * The meta key has been pressed and released without use, and then + * pressed again; it may also have been released again. + */ + private static final int LOCKED = + Spannable.SPAN_MARK_MARK | (4 << Spannable.SPAN_USER_SHIFT); +} diff --git a/android/text/method/MovementMethod.java b/android/text/method/MovementMethod.java new file mode 100644 index 00000000..f6fe575a --- /dev/null +++ b/android/text/method/MovementMethod.java @@ -0,0 +1,60 @@ +/* + * Copyright (C) 2006 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.method; + +import android.text.Spannable; +import android.view.KeyEvent; +import android.view.MotionEvent; +import android.widget.TextView; + +/** + * Provides cursor positioning, scrolling and text selection functionality in a {@link TextView}. + * <p> + * The {@link TextView} delegates handling of key events, trackball motions and touches to + * the movement method for purposes of content navigation. The framework automatically + * selects an appropriate movement method based on the content of the {@link TextView}. + * </p><p> + * This interface is intended for use by the framework; it should not be implemented + * directly by applications. + * </p> + */ +public interface MovementMethod { + public void initialize(TextView widget, Spannable text); + public boolean onKeyDown(TextView widget, Spannable text, int keyCode, KeyEvent event); + public boolean onKeyUp(TextView widget, Spannable text, int keyCode, KeyEvent event); + + /** + * If the key listener wants to other kinds of key events, return true, + * otherwise return false and the caller (i.e. the widget host) + * will handle the key. + */ + public boolean onKeyOther(TextView view, Spannable text, KeyEvent event); + + public void onTakeFocus(TextView widget, Spannable text, int direction); + public boolean onTrackballEvent(TextView widget, Spannable text, MotionEvent event); + public boolean onTouchEvent(TextView widget, Spannable text, MotionEvent event); + public boolean onGenericMotionEvent(TextView widget, Spannable text, MotionEvent event); + + /** + * Returns true if this movement method allows arbitrary selection + * of any text; false if it has no selection (like a movement method + * that only scrolls) or a constrained selection (for example + * limited to links. The "Select All" menu item is disabled + * if arbitrary selection is not allowed. + */ + public boolean canSelectArbitrarily(); +} diff --git a/android/text/method/MultiTapKeyListener.java b/android/text/method/MultiTapKeyListener.java new file mode 100644 index 00000000..5770482b --- /dev/null +++ b/android/text/method/MultiTapKeyListener.java @@ -0,0 +1,293 @@ +/* + * Copyright (C) 2006 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.method; + +import android.os.Handler; +import android.os.SystemClock; +import android.text.Editable; +import android.text.Selection; +import android.text.SpanWatcher; +import android.text.Spannable; +import android.text.method.TextKeyListener.Capitalize; +import android.util.SparseArray; +import android.view.KeyEvent; +import android.view.View; + +/** + * This is the standard key listener for alphabetic input on 12-key + * keyboards. You should generally not need to instantiate this yourself; + * TextKeyListener will do it for you. + * <p></p> + * As for all implementations of {@link KeyListener}, this class is only concerned + * with hardware keyboards. Software input methods have no obligation to trigger + * the methods in this class. + */ +public class MultiTapKeyListener extends BaseKeyListener + implements SpanWatcher { + private static MultiTapKeyListener[] sInstance = + new MultiTapKeyListener[Capitalize.values().length * 2]; + + private static final SparseArray<String> sRecs = new SparseArray<String>(); + + private Capitalize mCapitalize; + private boolean mAutoText; + + static { + sRecs.put(KeyEvent.KEYCODE_1, ".,1!@#$%^&*:/?'=()"); + sRecs.put(KeyEvent.KEYCODE_2, "abc2ABC"); + sRecs.put(KeyEvent.KEYCODE_3, "def3DEF"); + sRecs.put(KeyEvent.KEYCODE_4, "ghi4GHI"); + sRecs.put(KeyEvent.KEYCODE_5, "jkl5JKL"); + sRecs.put(KeyEvent.KEYCODE_6, "mno6MNO"); + sRecs.put(KeyEvent.KEYCODE_7, "pqrs7PQRS"); + sRecs.put(KeyEvent.KEYCODE_8, "tuv8TUV"); + sRecs.put(KeyEvent.KEYCODE_9, "wxyz9WXYZ"); + sRecs.put(KeyEvent.KEYCODE_0, "0+"); + sRecs.put(KeyEvent.KEYCODE_POUND, " "); + }; + + public MultiTapKeyListener(Capitalize cap, + boolean autotext) { + mCapitalize = cap; + mAutoText = autotext; + } + + /** + * Returns a new or existing instance with the specified capitalization + * and correction properties. + */ + public static MultiTapKeyListener getInstance(boolean autotext, + Capitalize cap) { + int off = cap.ordinal() * 2 + (autotext ? 1 : 0); + + if (sInstance[off] == null) { + sInstance[off] = new MultiTapKeyListener(cap, autotext); + } + + return sInstance[off]; + } + + public int getInputType() { + return makeTextContentType(mCapitalize, mAutoText); + } + + public boolean onKeyDown(View view, Editable content, + int keyCode, KeyEvent event) { + int selStart, selEnd; + int pref = 0; + + if (view != null) { + pref = TextKeyListener.getInstance().getPrefs(view.getContext()); + } + + { + int a = Selection.getSelectionStart(content); + int b = Selection.getSelectionEnd(content); + + selStart = Math.min(a, b); + selEnd = Math.max(a, b); + } + + int activeStart = content.getSpanStart(TextKeyListener.ACTIVE); + int activeEnd = content.getSpanEnd(TextKeyListener.ACTIVE); + + // now for the multitap cases... + + // Try to increment the character we were working on before + // if we have one and it's still the same key. + + int rec = (content.getSpanFlags(TextKeyListener.ACTIVE) + & Spannable.SPAN_USER) >>> Spannable.SPAN_USER_SHIFT; + + if (activeStart == selStart && activeEnd == selEnd && + selEnd - selStart == 1 && + rec >= 0 && rec < sRecs.size()) { + if (keyCode == KeyEvent.KEYCODE_STAR) { + char current = content.charAt(selStart); + + if (Character.isLowerCase(current)) { + content.replace(selStart, selEnd, + String.valueOf(current).toUpperCase()); + removeTimeouts(content); + new Timeout(content); // for its side effects + + return true; + } + if (Character.isUpperCase(current)) { + content.replace(selStart, selEnd, + String.valueOf(current).toLowerCase()); + removeTimeouts(content); + new Timeout(content); // for its side effects + + return true; + } + } + + if (sRecs.indexOfKey(keyCode) == rec) { + String val = sRecs.valueAt(rec); + char ch = content.charAt(selStart); + int ix = val.indexOf(ch); + + if (ix >= 0) { + ix = (ix + 1) % (val.length()); + + content.replace(selStart, selEnd, val, ix, ix + 1); + removeTimeouts(content); + new Timeout(content); // for its side effects + + return true; + } + } + + // Is this key one we know about at all? If so, acknowledge + // that the selection is our fault but the key has changed + // or the text no longer matches, so move the selection over + // so that it inserts instead of replaces. + + rec = sRecs.indexOfKey(keyCode); + + if (rec >= 0) { + Selection.setSelection(content, selEnd, selEnd); + selStart = selEnd; + } + } else { + rec = sRecs.indexOfKey(keyCode); + } + + if (rec >= 0) { + // We have a valid key. Replace the selection or insertion point + // with the first character for that key, and remember what + // record it came from for next time. + + String val = sRecs.valueAt(rec); + + int off = 0; + if ((pref & TextKeyListener.AUTO_CAP) != 0 && + TextKeyListener.shouldCap(mCapitalize, content, selStart)) { + for (int i = 0; i < val.length(); i++) { + if (Character.isUpperCase(val.charAt(i))) { + off = i; + break; + } + } + } + + if (selStart != selEnd) { + Selection.setSelection(content, selEnd); + } + + content.setSpan(OLD_SEL_START, selStart, selStart, + Spannable.SPAN_MARK_MARK); + + content.replace(selStart, selEnd, val, off, off + 1); + + int oldStart = content.getSpanStart(OLD_SEL_START); + selEnd = Selection.getSelectionEnd(content); + + if (selEnd != oldStart) { + Selection.setSelection(content, oldStart, selEnd); + + content.setSpan(TextKeyListener.LAST_TYPED, + oldStart, selEnd, + Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + + content.setSpan(TextKeyListener.ACTIVE, + oldStart, selEnd, + Spannable.SPAN_EXCLUSIVE_EXCLUSIVE | + (rec << Spannable.SPAN_USER_SHIFT)); + + } + + removeTimeouts(content); + new Timeout(content); // for its side effects + + // Set up the callback so we can remove the timeout if the + // cursor moves. + + if (content.getSpanStart(this) < 0) { + KeyListener[] methods = content.getSpans(0, content.length(), + KeyListener.class); + for (Object method : methods) { + content.removeSpan(method); + } + content.setSpan(this, 0, content.length(), + Spannable.SPAN_INCLUSIVE_INCLUSIVE); + } + + return true; + } + + return super.onKeyDown(view, content, keyCode, event); + } + + public void onSpanChanged(Spannable buf, + Object what, int s, int e, int start, int stop) { + if (what == Selection.SELECTION_END) { + buf.removeSpan(TextKeyListener.ACTIVE); + removeTimeouts(buf); + } + } + + private static void removeTimeouts(Spannable buf) { + Timeout[] timeout = buf.getSpans(0, buf.length(), Timeout.class); + + for (int i = 0; i < timeout.length; i++) { + Timeout t = timeout[i]; + + t.removeCallbacks(t); + t.mBuffer = null; + buf.removeSpan(t); + } + } + + private class Timeout + extends Handler + implements Runnable + { + public Timeout(Editable buffer) { + mBuffer = buffer; + mBuffer.setSpan(Timeout.this, 0, mBuffer.length(), + Spannable.SPAN_INCLUSIVE_INCLUSIVE); + + postAtTime(this, SystemClock.uptimeMillis() + 2000); + } + + public void run() { + Spannable buf = mBuffer; + + if (buf != null) { + int st = Selection.getSelectionStart(buf); + int en = Selection.getSelectionEnd(buf); + + int start = buf.getSpanStart(TextKeyListener.ACTIVE); + int end = buf.getSpanEnd(TextKeyListener.ACTIVE); + + if (st == start && en == end) { + Selection.setSelection(buf, Selection.getSelectionEnd(buf)); + } + + buf.removeSpan(Timeout.this); + } + } + + private Editable mBuffer; + } + + public void onSpanAdded(Spannable s, Object what, int start, int end) { } + public void onSpanRemoved(Spannable s, Object what, int start, int end) { } +} + diff --git a/android/text/method/NumberKeyListener.java b/android/text/method/NumberKeyListener.java new file mode 100644 index 00000000..d40015ee --- /dev/null +++ b/android/text/method/NumberKeyListener.java @@ -0,0 +1,255 @@ +/* + * Copyright (C) 2006 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.method; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.icu.text.DecimalFormatSymbols; +import android.text.Editable; +import android.text.InputFilter; +import android.text.Selection; +import android.text.Spannable; +import android.text.SpannableStringBuilder; +import android.text.Spanned; +import android.text.format.DateFormat; +import android.view.KeyEvent; +import android.view.View; + +import libcore.icu.LocaleData; + +import java.util.Collection; +import java.util.Locale; + +/** + * For numeric text entry + * <p></p> + * As for all implementations of {@link KeyListener}, this class is only concerned + * with hardware keyboards. Software input methods have no obligation to trigger + * the methods in this class. + */ +public abstract class NumberKeyListener extends BaseKeyListener + implements InputFilter +{ + /** + * You can say which characters you can accept. + */ + @NonNull + protected abstract char[] getAcceptedChars(); + + protected int lookup(KeyEvent event, Spannable content) { + return event.getMatch(getAcceptedChars(), getMetaState(content, event)); + } + + public CharSequence filter(CharSequence source, int start, int end, + Spanned dest, int dstart, int dend) { + char[] accept = getAcceptedChars(); + boolean filter = false; + + int i; + for (i = start; i < end; i++) { + if (!ok(accept, source.charAt(i))) { + break; + } + } + + if (i == end) { + // It was all OK. + return null; + } + + if (end - start == 1) { + // It was not OK, and there is only one char, so nothing remains. + return ""; + } + + SpannableStringBuilder filtered = + new SpannableStringBuilder(source, start, end); + i -= start; + end -= start; + + int len = end - start; + // Only count down to i because the chars before that were all OK. + for (int j = end - 1; j >= i; j--) { + if (!ok(accept, source.charAt(j))) { + filtered.delete(j, j + 1); + } + } + + return filtered; + } + + protected static boolean ok(char[] accept, char c) { + for (int i = accept.length - 1; i >= 0; i--) { + if (accept[i] == c) { + return true; + } + } + + return false; + } + + @Override + public boolean onKeyDown(View view, Editable content, + int keyCode, KeyEvent event) { + int selStart, selEnd; + + { + int a = Selection.getSelectionStart(content); + int b = Selection.getSelectionEnd(content); + + selStart = Math.min(a, b); + selEnd = Math.max(a, b); + } + + if (selStart < 0 || selEnd < 0) { + selStart = selEnd = 0; + Selection.setSelection(content, 0); + } + + int i = event != null ? lookup(event, content) : 0; + int repeatCount = event != null ? event.getRepeatCount() : 0; + if (repeatCount == 0) { + if (i != 0) { + if (selStart != selEnd) { + Selection.setSelection(content, selEnd); + } + + content.replace(selStart, selEnd, String.valueOf((char) i)); + + adjustMetaAfterKeypress(content); + return true; + } + } else if (i == '0' && repeatCount == 1) { + // Pretty hackish, it replaces the 0 with the + + + if (selStart == selEnd && selEnd > 0 && + content.charAt(selStart - 1) == '0') { + content.replace(selStart - 1, selEnd, String.valueOf('+')); + adjustMetaAfterKeypress(content); + return true; + } + } + + adjustMetaAfterKeypress(content); + return super.onKeyDown(view, content, keyCode, event); + } + + /* package */ + @Nullable + static boolean addDigits(@NonNull Collection<Character> collection, @Nullable Locale locale) { + if (locale == null) { + return false; + } + final String[] digits = DecimalFormatSymbols.getInstance(locale).getDigitStrings(); + for (int i = 0; i < 10; i++) { + if (digits[i].length() > 1) { // multi-codeunit digits. Not supported. + return false; + } + collection.add(Character.valueOf(digits[i].charAt(0))); + } + return true; + } + + // From http://unicode.org/reports/tr35/tr35-dates.html#Date_Format_Patterns + private static final String DATE_TIME_FORMAT_SYMBOLS = + "GyYuUrQqMLlwWdDFgEecabBhHKkjJCmsSAzZOvVXx"; + private static final char SINGLE_QUOTE = '\''; + + /* package */ + static boolean addFormatCharsFromSkeleton( + @NonNull Collection<Character> collection, @Nullable Locale locale, + @NonNull String skeleton, @NonNull String symbolsToIgnore) { + if (locale == null) { + return false; + } + final String pattern = DateFormat.getBestDateTimePattern(locale, skeleton); + boolean outsideQuotes = true; + for (int i = 0; i < pattern.length(); i++) { + final char ch = pattern.charAt(i); + if (Character.isSurrogate(ch)) { // characters outside BMP are not supported. + return false; + } else if (ch == SINGLE_QUOTE) { + outsideQuotes = !outsideQuotes; + // Single quote characters should be considered if and only if they follow + // another single quote. + if (i == 0 || pattern.charAt(i - 1) != SINGLE_QUOTE) { + continue; + } + } + + if (outsideQuotes) { + if (symbolsToIgnore.indexOf(ch) != -1) { + // Skip expected pattern characters. + continue; + } else if (DATE_TIME_FORMAT_SYMBOLS.indexOf(ch) != -1) { + // An unexpected symbols is seen. We've failed. + return false; + } + } + // If we are here, we are either inside quotes, or we have seen a non-pattern + // character outside quotes. So ch is a valid character in a date. + collection.add(Character.valueOf(ch)); + } + return true; + } + + /* package */ + static boolean addFormatCharsFromSkeletons( + @NonNull Collection<Character> collection, @Nullable Locale locale, + @NonNull String[] skeletons, @NonNull String symbolsToIgnore) { + for (int i = 0; i < skeletons.length; i++) { + final boolean success = addFormatCharsFromSkeleton( + collection, locale, skeletons[i], symbolsToIgnore); + if (!success) { + return false; + } + } + return true; + } + + + /* package */ + static boolean addAmPmChars(@NonNull Collection<Character> collection, + @Nullable Locale locale) { + if (locale == null) { + return false; + } + final String[] amPm = LocaleData.get(locale).amPm; + for (int i = 0; i < amPm.length; i++) { + for (int j = 0; j < amPm[i].length(); j++) { + final char ch = amPm[i].charAt(j); + if (Character.isBmpCodePoint(ch)) { + collection.add(Character.valueOf(ch)); + } else { // We don't support non-BMP characters. + return false; + } + } + } + return true; + } + + /* package */ + @NonNull + static char[] collectionToArray(@NonNull Collection<Character> chars) { + final char[] result = new char[chars.size()]; + int i = 0; + for (Character ch : chars) { + result[i++] = ch; + } + return result; + } +} diff --git a/android/text/method/PasswordTransformationMethod.java b/android/text/method/PasswordTransformationMethod.java new file mode 100644 index 00000000..4485e385 --- /dev/null +++ b/android/text/method/PasswordTransformationMethod.java @@ -0,0 +1,266 @@ +/* + * Copyright (C) 2006 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.method; + +import android.graphics.Rect; +import android.os.Handler; +import android.os.SystemClock; +import android.text.Editable; +import android.text.GetChars; +import android.text.NoCopySpan; +import android.text.Spannable; +import android.text.Spanned; +import android.text.TextUtils; +import android.text.TextWatcher; +import android.text.style.UpdateLayout; +import android.view.View; + +import java.lang.ref.WeakReference; + +public class PasswordTransformationMethod +implements TransformationMethod, TextWatcher +{ + public CharSequence getTransformation(CharSequence source, View view) { + if (source instanceof Spannable) { + Spannable sp = (Spannable) source; + + /* + * Remove any references to other views that may still be + * attached. This will happen when you flip the screen + * while a password field is showing; there will still + * be references to the old EditText in the text. + */ + ViewReference[] vr = sp.getSpans(0, sp.length(), + ViewReference.class); + for (int i = 0; i < vr.length; i++) { + sp.removeSpan(vr[i]); + } + + removeVisibleSpans(sp); + + sp.setSpan(new ViewReference(view), 0, 0, + Spannable.SPAN_POINT_POINT); + } + + return new PasswordCharSequence(source); + } + + public static PasswordTransformationMethod getInstance() { + if (sInstance != null) + return sInstance; + + sInstance = new PasswordTransformationMethod(); + return sInstance; + } + + public void beforeTextChanged(CharSequence s, int start, + int count, int after) { + // This callback isn't used. + } + + public void onTextChanged(CharSequence s, int start, + int before, int count) { + if (s instanceof Spannable) { + Spannable sp = (Spannable) s; + ViewReference[] vr = sp.getSpans(0, s.length(), + ViewReference.class); + if (vr.length == 0) { + return; + } + + /* + * There should generally only be one ViewReference in the text, + * but make sure to look through all of them if necessary in case + * something strange is going on. (We might still end up with + * multiple ViewReferences if someone moves text from one password + * field to another.) + */ + View v = null; + for (int i = 0; v == null && i < vr.length; i++) { + v = vr[i].get(); + } + + if (v == null) { + return; + } + + int pref = TextKeyListener.getInstance().getPrefs(v.getContext()); + if ((pref & TextKeyListener.SHOW_PASSWORD) != 0) { + if (count > 0) { + removeVisibleSpans(sp); + + if (count == 1) { + sp.setSpan(new Visible(sp, this), start, start + count, + Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + } + } + } + } + } + + public void afterTextChanged(Editable s) { + // This callback isn't used. + } + + public void onFocusChanged(View view, CharSequence sourceText, + boolean focused, int direction, + Rect previouslyFocusedRect) { + if (!focused) { + if (sourceText instanceof Spannable) { + Spannable sp = (Spannable) sourceText; + + removeVisibleSpans(sp); + } + } + } + + private static void removeVisibleSpans(Spannable sp) { + Visible[] old = sp.getSpans(0, sp.length(), Visible.class); + for (int i = 0; i < old.length; i++) { + sp.removeSpan(old[i]); + } + } + + private static class PasswordCharSequence + implements CharSequence, GetChars + { + public PasswordCharSequence(CharSequence source) { + mSource = source; + } + + public int length() { + return mSource.length(); + } + + public char charAt(int i) { + if (mSource instanceof Spanned) { + Spanned sp = (Spanned) mSource; + + int st = sp.getSpanStart(TextKeyListener.ACTIVE); + int en = sp.getSpanEnd(TextKeyListener.ACTIVE); + + if (i >= st && i < en) { + return mSource.charAt(i); + } + + Visible[] visible = sp.getSpans(0, sp.length(), Visible.class); + + for (int a = 0; a < visible.length; a++) { + if (sp.getSpanStart(visible[a].mTransformer) >= 0) { + st = sp.getSpanStart(visible[a]); + en = sp.getSpanEnd(visible[a]); + + if (i >= st && i < en) { + return mSource.charAt(i); + } + } + } + } + + return DOT; + } + + public CharSequence subSequence(int start, int end) { + char[] buf = new char[end - start]; + + getChars(start, end, buf, 0); + return new String(buf); + } + + public String toString() { + return subSequence(0, length()).toString(); + } + + public void getChars(int start, int end, char[] dest, int off) { + TextUtils.getChars(mSource, start, end, dest, off); + + int st = -1, en = -1; + int nvisible = 0; + int[] starts = null, ends = null; + + if (mSource instanceof Spanned) { + Spanned sp = (Spanned) mSource; + + st = sp.getSpanStart(TextKeyListener.ACTIVE); + en = sp.getSpanEnd(TextKeyListener.ACTIVE); + + Visible[] visible = sp.getSpans(0, sp.length(), Visible.class); + nvisible = visible.length; + starts = new int[nvisible]; + ends = new int[nvisible]; + + for (int i = 0; i < nvisible; i++) { + if (sp.getSpanStart(visible[i].mTransformer) >= 0) { + starts[i] = sp.getSpanStart(visible[i]); + ends[i] = sp.getSpanEnd(visible[i]); + } + } + } + + for (int i = start; i < end; i++) { + if (! (i >= st && i < en)) { + boolean visible = false; + + for (int a = 0; a < nvisible; a++) { + if (i >= starts[a] && i < ends[a]) { + visible = true; + break; + } + } + + if (!visible) { + dest[i - start + off] = DOT; + } + } + } + } + + private CharSequence mSource; + } + + private static class Visible + extends Handler + implements UpdateLayout, Runnable + { + public Visible(Spannable sp, PasswordTransformationMethod ptm) { + mText = sp; + mTransformer = ptm; + postAtTime(this, SystemClock.uptimeMillis() + 1500); + } + + public void run() { + mText.removeSpan(this); + } + + private Spannable mText; + private PasswordTransformationMethod mTransformer; + } + + /** + * Used to stash a reference back to the View in the Editable so we + * can use it to check the settings. + */ + private static class ViewReference extends WeakReference<View> + implements NoCopySpan { + public ViewReference(View v) { + super(v); + } + } + + private static PasswordTransformationMethod sInstance; + private static char DOT = '\u2022'; +} diff --git a/android/text/method/QwertyKeyListener.java b/android/text/method/QwertyKeyListener.java new file mode 100644 index 00000000..bea68b12 --- /dev/null +++ b/android/text/method/QwertyKeyListener.java @@ -0,0 +1,529 @@ +/* + * Copyright (C) 2006 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.method; + +import android.text.AutoText; +import android.text.Editable; +import android.text.NoCopySpan; +import android.text.Selection; +import android.text.Spannable; +import android.text.TextUtils; +import android.text.method.TextKeyListener.Capitalize; +import android.util.SparseArray; +import android.view.KeyCharacterMap; +import android.view.KeyEvent; +import android.view.View; + +/** + * This is the standard key listener for alphabetic input on qwerty + * keyboards. You should generally not need to instantiate this yourself; + * TextKeyListener will do it for you. + * <p></p> + * As for all implementations of {@link KeyListener}, this class is only concerned + * with hardware keyboards. Software input methods have no obligation to trigger + * the methods in this class. + */ +public class QwertyKeyListener extends BaseKeyListener { + private static QwertyKeyListener[] sInstance = + new QwertyKeyListener[Capitalize.values().length * 2]; + private static QwertyKeyListener sFullKeyboardInstance; + + private Capitalize mAutoCap; + private boolean mAutoText; + private boolean mFullKeyboard; + + private QwertyKeyListener(Capitalize cap, boolean autoText, boolean fullKeyboard) { + mAutoCap = cap; + mAutoText = autoText; + mFullKeyboard = fullKeyboard; + } + + public QwertyKeyListener(Capitalize cap, boolean autoText) { + this(cap, autoText, false); + } + + /** + * Returns a new or existing instance with the specified capitalization + * and correction properties. + */ + public static QwertyKeyListener getInstance(boolean autoText, Capitalize cap) { + int off = cap.ordinal() * 2 + (autoText ? 1 : 0); + + if (sInstance[off] == null) { + sInstance[off] = new QwertyKeyListener(cap, autoText); + } + + return sInstance[off]; + } + + /** + * Gets an instance of the listener suitable for use with full keyboards. + * Disables auto-capitalization, auto-text and long-press initiated on-screen + * character pickers. + */ + public static QwertyKeyListener getInstanceForFullKeyboard() { + if (sFullKeyboardInstance == null) { + sFullKeyboardInstance = new QwertyKeyListener(Capitalize.NONE, false, true); + } + return sFullKeyboardInstance; + } + + public int getInputType() { + return makeTextContentType(mAutoCap, mAutoText); + } + + public boolean onKeyDown(View view, Editable content, + int keyCode, KeyEvent event) { + int selStart, selEnd; + int pref = 0; + + if (view != null) { + pref = TextKeyListener.getInstance().getPrefs(view.getContext()); + } + + { + int a = Selection.getSelectionStart(content); + int b = Selection.getSelectionEnd(content); + + selStart = Math.min(a, b); + selEnd = Math.max(a, b); + + if (selStart < 0 || selEnd < 0) { + selStart = selEnd = 0; + Selection.setSelection(content, 0, 0); + } + } + + int activeStart = content.getSpanStart(TextKeyListener.ACTIVE); + int activeEnd = content.getSpanEnd(TextKeyListener.ACTIVE); + + // QWERTY keyboard normal case + + int i = event.getUnicodeChar(getMetaState(content, event)); + + if (!mFullKeyboard) { + int count = event.getRepeatCount(); + if (count > 0 && selStart == selEnd && selStart > 0) { + char c = content.charAt(selStart - 1); + + if ((c == i || c == Character.toUpperCase(i)) && view != null) { + if (showCharacterPicker(view, content, c, false, count)) { + resetMetaState(content); + return true; + } + } + } + } + + if (i == KeyCharacterMap.PICKER_DIALOG_INPUT) { + if (view != null) { + showCharacterPicker(view, content, + KeyCharacterMap.PICKER_DIALOG_INPUT, true, 1); + } + resetMetaState(content); + return true; + } + + if (i == KeyCharacterMap.HEX_INPUT) { + int start; + + if (selStart == selEnd) { + start = selEnd; + + while (start > 0 && selEnd - start < 4 && + Character.digit(content.charAt(start - 1), 16) >= 0) { + start--; + } + } else { + start = selStart; + } + + int ch = -1; + try { + String hex = TextUtils.substring(content, start, selEnd); + ch = Integer.parseInt(hex, 16); + } catch (NumberFormatException nfe) { } + + if (ch >= 0) { + selStart = start; + Selection.setSelection(content, selStart, selEnd); + i = ch; + } else { + i = 0; + } + } + + if (i != 0) { + boolean dead = false; + + if ((i & KeyCharacterMap.COMBINING_ACCENT) != 0) { + dead = true; + i = i & KeyCharacterMap.COMBINING_ACCENT_MASK; + } + + if (activeStart == selStart && activeEnd == selEnd) { + boolean replace = false; + + if (selEnd - selStart - 1 == 0) { + char accent = content.charAt(selStart); + int composed = event.getDeadChar(accent, i); + + if (composed != 0) { + i = composed; + replace = true; + dead = false; + } + } + + if (!replace) { + Selection.setSelection(content, selEnd); + content.removeSpan(TextKeyListener.ACTIVE); + selStart = selEnd; + } + } + + if ((pref & TextKeyListener.AUTO_CAP) != 0 + && Character.isLowerCase(i) + && TextKeyListener.shouldCap(mAutoCap, content, selStart)) { + int where = content.getSpanEnd(TextKeyListener.CAPPED); + int flags = content.getSpanFlags(TextKeyListener.CAPPED); + + if (where == selStart && (((flags >> 16) & 0xFFFF) == i)) { + content.removeSpan(TextKeyListener.CAPPED); + } else { + flags = i << 16; + i = Character.toUpperCase(i); + + if (selStart == 0) + content.setSpan(TextKeyListener.CAPPED, 0, 0, + Spannable.SPAN_MARK_MARK | flags); + else + content.setSpan(TextKeyListener.CAPPED, + selStart - 1, selStart, + Spannable.SPAN_EXCLUSIVE_EXCLUSIVE | + flags); + } + } + + if (selStart != selEnd) { + Selection.setSelection(content, selEnd); + } + content.setSpan(OLD_SEL_START, selStart, selStart, + Spannable.SPAN_MARK_MARK); + + content.replace(selStart, selEnd, String.valueOf((char) i)); + + int oldStart = content.getSpanStart(OLD_SEL_START); + selEnd = Selection.getSelectionEnd(content); + + if (oldStart < selEnd) { + content.setSpan(TextKeyListener.LAST_TYPED, + oldStart, selEnd, + Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + + if (dead) { + Selection.setSelection(content, oldStart, selEnd); + content.setSpan(TextKeyListener.ACTIVE, oldStart, selEnd, + Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + } + } + + adjustMetaAfterKeypress(content); + + // potentially do autotext replacement if the character + // that was typed was an autotext terminator + + if ((pref & TextKeyListener.AUTO_TEXT) != 0 && mAutoText && + (i == ' ' || i == '\t' || i == '\n' || + i == ',' || i == '.' || i == '!' || i == '?' || + i == '"' || Character.getType(i) == Character.END_PUNCTUATION) && + content.getSpanEnd(TextKeyListener.INHIBIT_REPLACEMENT) + != oldStart) { + int x; + + for (x = oldStart; x > 0; x--) { + char c = content.charAt(x - 1); + if (c != '\'' && !Character.isLetter(c)) { + break; + } + } + + String rep = getReplacement(content, x, oldStart, view); + + if (rep != null) { + Replaced[] repl = content.getSpans(0, content.length(), + Replaced.class); + for (int a = 0; a < repl.length; a++) + content.removeSpan(repl[a]); + + char[] orig = new char[oldStart - x]; + TextUtils.getChars(content, x, oldStart, orig, 0); + + content.setSpan(new Replaced(orig), x, oldStart, + Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + content.replace(x, oldStart, rep); + } + } + + // Replace two spaces by a period and a space. + + if ((pref & TextKeyListener.AUTO_PERIOD) != 0 && mAutoText) { + selEnd = Selection.getSelectionEnd(content); + if (selEnd - 3 >= 0) { + if (content.charAt(selEnd - 1) == ' ' && + content.charAt(selEnd - 2) == ' ') { + char c = content.charAt(selEnd - 3); + + for (int j = selEnd - 3; j > 0; j--) { + if (c == '"' || + Character.getType(c) == Character.END_PUNCTUATION) { + c = content.charAt(j - 1); + } else { + break; + } + } + + if (Character.isLetter(c) || Character.isDigit(c)) { + content.replace(selEnd - 2, selEnd - 1, "."); + } + } + } + } + + return true; + } else if (keyCode == KeyEvent.KEYCODE_DEL + && (event.hasNoModifiers() || event.hasModifiers(KeyEvent.META_ALT_ON)) + && selStart == selEnd) { + // special backspace case for undoing autotext + + int consider = 1; + + // if backspacing over the last typed character, + // it undoes the autotext prior to that character + // (unless the character typed was newline, in which + // case this behavior would be confusing) + + if (content.getSpanEnd(TextKeyListener.LAST_TYPED) == selStart) { + if (content.charAt(selStart - 1) != '\n') + consider = 2; + } + + Replaced[] repl = content.getSpans(selStart - consider, selStart, + Replaced.class); + + if (repl.length > 0) { + int st = content.getSpanStart(repl[0]); + int en = content.getSpanEnd(repl[0]); + String old = new String(repl[0].mText); + + content.removeSpan(repl[0]); + + // only cancel the autocomplete if the cursor is at the end of + // the replaced span (or after it, because the user is + // backspacing over the space after the word, not the word + // itself). + if (selStart >= en) { + content.setSpan(TextKeyListener.INHIBIT_REPLACEMENT, + en, en, Spannable.SPAN_POINT_POINT); + content.replace(st, en, old); + + en = content.getSpanStart(TextKeyListener.INHIBIT_REPLACEMENT); + if (en - 1 >= 0) { + content.setSpan(TextKeyListener.INHIBIT_REPLACEMENT, + en - 1, en, + Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + } else { + content.removeSpan(TextKeyListener.INHIBIT_REPLACEMENT); + } + adjustMetaAfterKeypress(content); + } else { + adjustMetaAfterKeypress(content); + return super.onKeyDown(view, content, keyCode, event); + } + + return true; + } + } + + return super.onKeyDown(view, content, keyCode, event); + } + + private String getReplacement(CharSequence src, int start, int end, + View view) { + int len = end - start; + boolean changecase = false; + + String replacement = AutoText.get(src, start, end, view); + + if (replacement == null) { + String key = TextUtils.substring(src, start, end).toLowerCase(); + replacement = AutoText.get(key, 0, end - start, view); + changecase = true; + + if (replacement == null) + return null; + } + + int caps = 0; + + if (changecase) { + for (int j = start; j < end; j++) { + if (Character.isUpperCase(src.charAt(j))) + caps++; + } + } + + String out; + + if (caps == 0) + out = replacement; + else if (caps == 1) + out = toTitleCase(replacement); + else if (caps == len) + out = replacement.toUpperCase(); + else + out = toTitleCase(replacement); + + if (out.length() == len && + TextUtils.regionMatches(src, start, out, 0, len)) + return null; + + return out; + } + + /** + * Marks the specified region of <code>content</code> as having + * contained <code>original</code> prior to AutoText replacement. + * Call this method when you have done or are about to do an + * AutoText-style replacement on a region of text and want to let + * the same mechanism (the user pressing DEL immediately after the + * change) undo the replacement. + * + * @param content the Editable text where the replacement was made + * @param start the start of the replaced region + * @param end the end of the replaced region; the location of the cursor + * @param original the text to be restored if the user presses DEL + */ + public static void markAsReplaced(Spannable content, int start, int end, + String original) { + Replaced[] repl = content.getSpans(0, content.length(), Replaced.class); + for (int a = 0; a < repl.length; a++) { + content.removeSpan(repl[a]); + } + + int len = original.length(); + char[] orig = new char[len]; + original.getChars(0, len, orig, 0); + + content.setSpan(new Replaced(orig), start, end, + Spannable.SPAN_EXCLUSIVE_EXCLUSIVE); + } + + private static SparseArray<String> PICKER_SETS = + new SparseArray<String>(); + static { + PICKER_SETS.put('A', "\u00C0\u00C1\u00C2\u00C4\u00C6\u00C3\u00C5\u0104\u0100"); + PICKER_SETS.put('C', "\u00C7\u0106\u010C"); + PICKER_SETS.put('D', "\u010E"); + PICKER_SETS.put('E', "\u00C8\u00C9\u00CA\u00CB\u0118\u011A\u0112"); + PICKER_SETS.put('G', "\u011E"); + PICKER_SETS.put('L', "\u0141"); + PICKER_SETS.put('I', "\u00CC\u00CD\u00CE\u00CF\u012A\u0130"); + PICKER_SETS.put('N', "\u00D1\u0143\u0147"); + PICKER_SETS.put('O', "\u00D8\u0152\u00D5\u00D2\u00D3\u00D4\u00D6\u014C"); + PICKER_SETS.put('R', "\u0158"); + PICKER_SETS.put('S', "\u015A\u0160\u015E"); + PICKER_SETS.put('T', "\u0164"); + PICKER_SETS.put('U', "\u00D9\u00DA\u00DB\u00DC\u016E\u016A"); + PICKER_SETS.put('Y', "\u00DD\u0178"); + PICKER_SETS.put('Z', "\u0179\u017B\u017D"); + PICKER_SETS.put('a', "\u00E0\u00E1\u00E2\u00E4\u00E6\u00E3\u00E5\u0105\u0101"); + PICKER_SETS.put('c', "\u00E7\u0107\u010D"); + PICKER_SETS.put('d', "\u010F"); + PICKER_SETS.put('e', "\u00E8\u00E9\u00EA\u00EB\u0119\u011B\u0113"); + PICKER_SETS.put('g', "\u011F"); + PICKER_SETS.put('i', "\u00EC\u00ED\u00EE\u00EF\u012B\u0131"); + PICKER_SETS.put('l', "\u0142"); + PICKER_SETS.put('n', "\u00F1\u0144\u0148"); + PICKER_SETS.put('o', "\u00F8\u0153\u00F5\u00F2\u00F3\u00F4\u00F6\u014D"); + PICKER_SETS.put('r', "\u0159"); + PICKER_SETS.put('s', "\u00A7\u00DF\u015B\u0161\u015F"); + PICKER_SETS.put('t', "\u0165"); + PICKER_SETS.put('u', "\u00F9\u00FA\u00FB\u00FC\u016F\u016B"); + PICKER_SETS.put('y', "\u00FD\u00FF"); + PICKER_SETS.put('z', "\u017A\u017C\u017E"); + PICKER_SETS.put(KeyCharacterMap.PICKER_DIALOG_INPUT, + "\u2026\u00A5\u2022\u00AE\u00A9\u00B1[]{}\\|"); + PICKER_SETS.put('/', "\\"); + + // From packages/inputmethods/LatinIME/res/xml/kbd_symbols.xml + + PICKER_SETS.put('1', "\u00b9\u00bd\u2153\u00bc\u215b"); + PICKER_SETS.put('2', "\u00b2\u2154"); + PICKER_SETS.put('3', "\u00b3\u00be\u215c"); + PICKER_SETS.put('4', "\u2074"); + PICKER_SETS.put('5', "\u215d"); + PICKER_SETS.put('7', "\u215e"); + PICKER_SETS.put('0', "\u207f\u2205"); + PICKER_SETS.put('$', "\u00a2\u00a3\u20ac\u00a5\u20a3\u20a4\u20b1"); + PICKER_SETS.put('%', "\u2030"); + PICKER_SETS.put('*', "\u2020\u2021"); + PICKER_SETS.put('-', "\u2013\u2014"); + PICKER_SETS.put('+', "\u00b1"); + PICKER_SETS.put('(', "[{<"); + PICKER_SETS.put(')', "]}>"); + PICKER_SETS.put('!', "\u00a1"); + PICKER_SETS.put('"', "\u201c\u201d\u00ab\u00bb\u02dd"); + PICKER_SETS.put('?', "\u00bf"); + PICKER_SETS.put(',', "\u201a\u201e"); + + // From packages/inputmethods/LatinIME/res/xml/kbd_symbols_shift.xml + + PICKER_SETS.put('=', "\u2260\u2248\u221e"); + PICKER_SETS.put('<', "\u2264\u00ab\u2039"); + PICKER_SETS.put('>', "\u2265\u00bb\u203a"); + }; + + private boolean showCharacterPicker(View view, Editable content, char c, + boolean insert, int count) { + String set = PICKER_SETS.get(c); + if (set == null) { + return false; + } + + if (count == 1) { + new CharacterPickerDialog(view.getContext(), + view, content, set, insert).show(); + } + + return true; + } + + private static String toTitleCase(String src) { + return Character.toUpperCase(src.charAt(0)) + src.substring(1); + } + + /* package */ static class Replaced implements NoCopySpan + { + public Replaced(char[] text) { + mText = text; + } + + private char[] mText; + } +} + diff --git a/android/text/method/ReplacementTransformationMethod.java b/android/text/method/ReplacementTransformationMethod.java new file mode 100644 index 00000000..d6f879aa --- /dev/null +++ b/android/text/method/ReplacementTransformationMethod.java @@ -0,0 +1,205 @@ +/* + * Copyright (C) 2006 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.method; + +import android.graphics.Rect; +import android.text.Editable; +import android.text.GetChars; +import android.text.Spannable; +import android.text.Spanned; +import android.text.SpannedString; +import android.text.TextUtils; +import android.view.View; + +/** + * This transformation method causes the characters in the {@link #getOriginal} + * array to be replaced by the corresponding characters in the + * {@link #getReplacement} array. + */ +public abstract class ReplacementTransformationMethod +implements TransformationMethod +{ + /** + * Returns the list of characters that are to be replaced by other + * characters when displayed. + */ + protected abstract char[] getOriginal(); + /** + * Returns a parallel array of replacement characters for the ones + * that are to be replaced. + */ + protected abstract char[] getReplacement(); + + /** + * Returns a CharSequence that will mirror the contents of the + * source CharSequence but with the characters in {@link #getOriginal} + * replaced by ones from {@link #getReplacement}. + */ + public CharSequence getTransformation(CharSequence source, View v) { + char[] original = getOriginal(); + char[] replacement = getReplacement(); + + /* + * Short circuit for faster display if the text will never change. + */ + if (!(source instanceof Editable)) { + /* + * Check whether the text does not contain any of the + * source characters so can be used unchanged. + */ + boolean doNothing = true; + int n = original.length; + for (int i = 0; i < n; i++) { + if (TextUtils.indexOf(source, original[i]) >= 0) { + doNothing = false; + break; + } + } + if (doNothing) { + return source; + } + + if (!(source instanceof Spannable)) { + /* + * The text contains some of the source characters, + * but they can be flattened out now instead of + * at display time. + */ + if (source instanceof Spanned) { + return new SpannedString(new SpannedReplacementCharSequence( + (Spanned) source, + original, replacement)); + } else { + return new ReplacementCharSequence(source, + original, + replacement).toString(); + } + } + } + + if (source instanceof Spanned) { + return new SpannedReplacementCharSequence((Spanned) source, + original, replacement); + } else { + return new ReplacementCharSequence(source, original, replacement); + } + } + + public void onFocusChanged(View view, CharSequence sourceText, + boolean focused, int direction, + Rect previouslyFocusedRect) { + // This callback isn't used. + } + + private static class ReplacementCharSequence + implements CharSequence, GetChars { + private char[] mOriginal, mReplacement; + + public ReplacementCharSequence(CharSequence source, char[] original, + char[] replacement) { + mSource = source; + mOriginal = original; + mReplacement = replacement; + } + + public int length() { + return mSource.length(); + } + + public char charAt(int i) { + char c = mSource.charAt(i); + + int n = mOriginal.length; + for (int j = 0; j < n; j++) { + if (c == mOriginal[j]) { + c = mReplacement[j]; + } + } + + return c; + } + + public CharSequence subSequence(int start, int end) { + char[] c = new char[end - start]; + + getChars(start, end, c, 0); + return new String(c); + } + + public String toString() { + char[] c = new char[length()]; + + getChars(0, length(), c, 0); + return new String(c); + } + + public void getChars(int start, int end, char[] dest, int off) { + TextUtils.getChars(mSource, start, end, dest, off); + int offend = end - start + off; + int n = mOriginal.length; + + for (int i = off; i < offend; i++) { + char c = dest[i]; + + for (int j = 0; j < n; j++) { + if (c == mOriginal[j]) { + dest[i] = mReplacement[j]; + } + } + } + } + + private CharSequence mSource; + } + + private static class SpannedReplacementCharSequence + extends ReplacementCharSequence + implements Spanned + { + public SpannedReplacementCharSequence(Spanned source, char[] original, + char[] replacement) { + super(source, original, replacement); + mSpanned = source; + } + + public CharSequence subSequence(int start, int end) { + return new SpannedString(this).subSequence(start, end); + } + + public <T> T[] getSpans(int start, int end, Class<T> type) { + return mSpanned.getSpans(start, end, type); + } + + public int getSpanStart(Object tag) { + return mSpanned.getSpanStart(tag); + } + + public int getSpanEnd(Object tag) { + return mSpanned.getSpanEnd(tag); + } + + public int getSpanFlags(Object tag) { + return mSpanned.getSpanFlags(tag); + } + + public int nextSpanTransition(int start, int end, Class type) { + return mSpanned.nextSpanTransition(start, end, type); + } + + private Spanned mSpanned; + } +} diff --git a/android/text/method/ScrollingMovementMethod.java b/android/text/method/ScrollingMovementMethod.java new file mode 100644 index 00000000..4f422cbf --- /dev/null +++ b/android/text/method/ScrollingMovementMethod.java @@ -0,0 +1,121 @@ +/* + * Copyright (C) 2006 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.method; + +import android.text.Layout; +import android.text.Spannable; +import android.view.MotionEvent; +import android.view.View; +import android.widget.TextView; + +/** + * A movement method that interprets movement keys by scrolling the text buffer. + */ +public class ScrollingMovementMethod extends BaseMovementMethod implements MovementMethod { + @Override + protected boolean left(TextView widget, Spannable buffer) { + return scrollLeft(widget, buffer, 1); + } + + @Override + protected boolean right(TextView widget, Spannable buffer) { + return scrollRight(widget, buffer, 1); + } + + @Override + protected boolean up(TextView widget, Spannable buffer) { + return scrollUp(widget, buffer, 1); + } + + @Override + protected boolean down(TextView widget, Spannable buffer) { + return scrollDown(widget, buffer, 1); + } + + @Override + protected boolean pageUp(TextView widget, Spannable buffer) { + return scrollPageUp(widget, buffer); + } + + @Override + protected boolean pageDown(TextView widget, Spannable buffer) { + return scrollPageDown(widget, buffer); + } + + @Override + protected boolean top(TextView widget, Spannable buffer) { + return scrollTop(widget, buffer); + } + + @Override + protected boolean bottom(TextView widget, Spannable buffer) { + return scrollBottom(widget, buffer); + } + + @Override + protected boolean lineStart(TextView widget, Spannable buffer) { + return scrollLineStart(widget, buffer); + } + + @Override + protected boolean lineEnd(TextView widget, Spannable buffer) { + return scrollLineEnd(widget, buffer); + } + + @Override + protected boolean home(TextView widget, Spannable buffer) { + return top(widget, buffer); + } + + @Override + protected boolean end(TextView widget, Spannable buffer) { + return bottom(widget, buffer); + } + + @Override + public boolean onTouchEvent(TextView widget, Spannable buffer, MotionEvent event) { + return Touch.onTouchEvent(widget, buffer, event); + } + + @Override + public void onTakeFocus(TextView widget, Spannable text, int dir) { + Layout layout = widget.getLayout(); + + if (layout != null && (dir & View.FOCUS_FORWARD) != 0) { + widget.scrollTo(widget.getScrollX(), + layout.getLineTop(0)); + } + if (layout != null && (dir & View.FOCUS_BACKWARD) != 0) { + int padding = widget.getTotalPaddingTop() + + widget.getTotalPaddingBottom(); + int line = layout.getLineCount() - 1; + + widget.scrollTo(widget.getScrollX(), + layout.getLineTop(line+1) - + (widget.getHeight() - padding)); + } + } + + public static MovementMethod getInstance() { + if (sInstance == null) + sInstance = new ScrollingMovementMethod(); + + return sInstance; + } + + private static ScrollingMovementMethod sInstance; +} diff --git a/android/text/method/SingleLineTransformationMethod.java b/android/text/method/SingleLineTransformationMethod.java new file mode 100644 index 00000000..818526a7 --- /dev/null +++ b/android/text/method/SingleLineTransformationMethod.java @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2006 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.method; + +/** + * This transformation method causes any newline characters (\n) to be + * displayed as spaces instead of causing line breaks, and causes + * carriage return characters (\r) to have no appearance. + */ +public class SingleLineTransformationMethod +extends ReplacementTransformationMethod { + private static char[] ORIGINAL = new char[] { '\n', '\r' }; + private static char[] REPLACEMENT = new char[] { ' ', '\uFEFF' }; + + /** + * The characters to be replaced are \n and \r. + */ + protected char[] getOriginal() { + return ORIGINAL; + } + + /** + * The character \n is replaced with is space; + * the character \r is replaced with is FEFF (zero width space). + */ + protected char[] getReplacement() { + return REPLACEMENT; + } + + public static SingleLineTransformationMethod getInstance() { + if (sInstance != null) + return sInstance; + + sInstance = new SingleLineTransformationMethod(); + return sInstance; + } + + private static SingleLineTransformationMethod sInstance; +} diff --git a/android/text/method/TextKeyListener.java b/android/text/method/TextKeyListener.java new file mode 100644 index 00000000..9cbda9c0 --- /dev/null +++ b/android/text/method/TextKeyListener.java @@ -0,0 +1,316 @@ +/* + * Copyright (C) 2007 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.method; + +import android.content.ContentResolver; +import android.content.Context; +import android.database.ContentObserver; +import android.os.Handler; +import android.provider.Settings; +import android.provider.Settings.System; +import android.text.Editable; +import android.text.InputType; +import android.text.NoCopySpan; +import android.text.Selection; +import android.text.SpanWatcher; +import android.text.Spannable; +import android.text.TextUtils; +import android.view.KeyCharacterMap; +import android.view.KeyEvent; +import android.view.View; + +import java.lang.ref.WeakReference; + +/** + * This is the key listener for typing normal text. It delegates to + * other key listeners appropriate to the current keyboard and language. + * <p></p> + * As for all implementations of {@link KeyListener}, this class is only concerned + * with hardware keyboards. Software input methods have no obligation to trigger + * the methods in this class. + */ +public class TextKeyListener extends BaseKeyListener implements SpanWatcher { + private static TextKeyListener[] sInstance = + new TextKeyListener[Capitalize.values().length * 2]; + + /* package */ static final Object ACTIVE = new NoCopySpan.Concrete(); + /* package */ static final Object CAPPED = new NoCopySpan.Concrete(); + /* package */ static final Object INHIBIT_REPLACEMENT = new NoCopySpan.Concrete(); + /* package */ static final Object LAST_TYPED = new NoCopySpan.Concrete(); + + private Capitalize mAutoCap; + private boolean mAutoText; + + private int mPrefs; + private boolean mPrefsInited; + + /* package */ static final int AUTO_CAP = 1; + /* package */ static final int AUTO_TEXT = 2; + /* package */ static final int AUTO_PERIOD = 4; + /* package */ static final int SHOW_PASSWORD = 8; + private WeakReference<ContentResolver> mResolver; + private TextKeyListener.SettingsObserver mObserver; + + /** + * Creates a new TextKeyListener with the specified capitalization + * and correction properties. + * + * @param cap when, if ever, to automatically capitalize. + * @param autotext whether to automatically do spelling corrections. + */ + public TextKeyListener(Capitalize cap, boolean autotext) { + mAutoCap = cap; + mAutoText = autotext; + } + + /** + * Returns a new or existing instance with the specified capitalization + * and correction properties. + * + * @param cap when, if ever, to automatically capitalize. + * @param autotext whether to automatically do spelling corrections. + */ + public static TextKeyListener getInstance(boolean autotext, + Capitalize cap) { + int off = cap.ordinal() * 2 + (autotext ? 1 : 0); + + if (sInstance[off] == null) { + sInstance[off] = new TextKeyListener(cap, autotext); + } + + return sInstance[off]; + } + + /** + * Returns a new or existing instance with no automatic capitalization + * or correction. + */ + public static TextKeyListener getInstance() { + return getInstance(false, Capitalize.NONE); + } + + /** + * Returns whether it makes sense to automatically capitalize at the + * specified position in the specified text, with the specified rules. + * + * @param cap the capitalization rules to consider. + * @param cs the text in which an insertion is being made. + * @param off the offset into that text where the insertion is being made. + * + * @return whether the character being inserted should be capitalized. + */ + public static boolean shouldCap(Capitalize cap, CharSequence cs, int off) { + int i; + char c; + + if (cap == Capitalize.NONE) { + return false; + } + if (cap == Capitalize.CHARACTERS) { + return true; + } + + return TextUtils.getCapsMode(cs, off, cap == Capitalize.WORDS + ? TextUtils.CAP_MODE_WORDS : TextUtils.CAP_MODE_SENTENCES) + != 0; + } + + public int getInputType() { + return makeTextContentType(mAutoCap, mAutoText); + } + + @Override + public boolean onKeyDown(View view, Editable content, + int keyCode, KeyEvent event) { + KeyListener im = getKeyListener(event); + + return im.onKeyDown(view, content, keyCode, event); + } + + @Override + public boolean onKeyUp(View view, Editable content, + int keyCode, KeyEvent event) { + KeyListener im = getKeyListener(event); + + return im.onKeyUp(view, content, keyCode, event); + } + + @Override + public boolean onKeyOther(View view, Editable content, KeyEvent event) { + KeyListener im = getKeyListener(event); + + return im.onKeyOther(view, content, event); + } + + /** + * Clear all the input state (autotext, autocap, multitap, undo) + * from the specified Editable, going beyond Editable.clear(), which + * just clears the text but not the input state. + * + * @param e the buffer whose text and state are to be cleared. + */ + public static void clear(Editable e) { + e.clear(); + e.removeSpan(ACTIVE); + e.removeSpan(CAPPED); + e.removeSpan(INHIBIT_REPLACEMENT); + e.removeSpan(LAST_TYPED); + + QwertyKeyListener.Replaced[] repl = e.getSpans(0, e.length(), + QwertyKeyListener.Replaced.class); + final int count = repl.length; + for (int i = 0; i < count; i++) { + e.removeSpan(repl[i]); + } + } + + public void onSpanAdded(Spannable s, Object what, int start, int end) { } + public void onSpanRemoved(Spannable s, Object what, int start, int end) { } + + public void onSpanChanged(Spannable s, Object what, int start, int end, + int st, int en) { + if (what == Selection.SELECTION_END) { + s.removeSpan(ACTIVE); + } + } + + private KeyListener getKeyListener(KeyEvent event) { + KeyCharacterMap kmap = event.getKeyCharacterMap(); + int kind = kmap.getKeyboardType(); + + if (kind == KeyCharacterMap.ALPHA) { + return QwertyKeyListener.getInstance(mAutoText, mAutoCap); + } else if (kind == KeyCharacterMap.NUMERIC) { + return MultiTapKeyListener.getInstance(mAutoText, mAutoCap); + } else if (kind == KeyCharacterMap.FULL + || kind == KeyCharacterMap.SPECIAL_FUNCTION) { + // We consider special function keyboards full keyboards as a workaround for + // devices that do not have built-in keyboards. Applications may try to inject + // key events using the built-in keyboard device id which may be configured as + // a special function keyboard using a default key map. Ideally, as of Honeycomb, + // these applications should be modified to use KeyCharacterMap.VIRTUAL_KEYBOARD. + return QwertyKeyListener.getInstanceForFullKeyboard(); + } + + return NullKeyListener.getInstance(); + } + + public enum Capitalize { + NONE, SENTENCES, WORDS, CHARACTERS, + } + + private static class NullKeyListener implements KeyListener + { + public int getInputType() { + return InputType.TYPE_NULL; + } + + public boolean onKeyDown(View view, Editable content, + int keyCode, KeyEvent event) { + return false; + } + + public boolean onKeyUp(View view, Editable content, int keyCode, + KeyEvent event) { + return false; + } + + public boolean onKeyOther(View view, Editable content, KeyEvent event) { + return false; + } + + public void clearMetaKeyState(View view, Editable content, int states) { + } + + public static NullKeyListener getInstance() { + if (sInstance != null) + return sInstance; + + sInstance = new NullKeyListener(); + return sInstance; + } + + private static NullKeyListener sInstance; + } + + public void release() { + if (mResolver != null) { + final ContentResolver contentResolver = mResolver.get(); + if (contentResolver != null) { + contentResolver.unregisterContentObserver(mObserver); + mResolver.clear(); + } + mObserver = null; + mResolver = null; + mPrefsInited = false; + } + } + + private void initPrefs(Context context) { + final ContentResolver contentResolver = context.getContentResolver(); + mResolver = new WeakReference<ContentResolver>(contentResolver); + if (mObserver == null) { + mObserver = new SettingsObserver(); + contentResolver.registerContentObserver(Settings.System.CONTENT_URI, true, mObserver); + } + + updatePrefs(contentResolver); + mPrefsInited = true; + } + + private class SettingsObserver extends ContentObserver { + public SettingsObserver() { + super(new Handler()); + } + + @Override + public void onChange(boolean selfChange) { + if (mResolver != null) { + final ContentResolver contentResolver = mResolver.get(); + if (contentResolver == null) { + mPrefsInited = false; + } else { + updatePrefs(contentResolver); + } + } else { + mPrefsInited = false; + } + } + } + + private void updatePrefs(ContentResolver resolver) { + boolean cap = System.getInt(resolver, System.TEXT_AUTO_CAPS, 1) > 0; + boolean text = System.getInt(resolver, System.TEXT_AUTO_REPLACE, 1) > 0; + boolean period = System.getInt(resolver, System.TEXT_AUTO_PUNCTUATE, 1) > 0; + boolean pw = System.getInt(resolver, System.TEXT_SHOW_PASSWORD, 1) > 0; + + mPrefs = (cap ? AUTO_CAP : 0) | + (text ? AUTO_TEXT : 0) | + (period ? AUTO_PERIOD : 0) | + (pw ? SHOW_PASSWORD : 0); + } + + /* package */ int getPrefs(Context context) { + synchronized (this) { + if (!mPrefsInited || mResolver.get() == null) { + initPrefs(context); + } + } + + return mPrefs; + } +} diff --git a/android/text/method/TimeKeyListener.java b/android/text/method/TimeKeyListener.java new file mode 100644 index 00000000..f11f4009 --- /dev/null +++ b/android/text/method/TimeKeyListener.java @@ -0,0 +1,139 @@ +/* + * Copyright (C) 2006 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.method; + +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.text.InputType; +import android.view.KeyEvent; + +import com.android.internal.annotations.GuardedBy; +import com.android.internal.util.ArrayUtils; + +import java.util.HashMap; +import java.util.LinkedHashSet; +import java.util.Locale; + +/** + * For entering times in a text field. + * <p></p> + * As for all implementations of {@link KeyListener}, this class is only concerned + * with hardware keyboards. Software input methods have no obligation to trigger + * the methods in this class. + */ +public class TimeKeyListener extends NumberKeyListener +{ + public int getInputType() { + if (mNeedsAdvancedInput) { + return InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_VARIATION_NORMAL; + } else { + return InputType.TYPE_CLASS_DATETIME | InputType.TYPE_DATETIME_VARIATION_TIME; + } + } + + @Override + @NonNull + protected char[] getAcceptedChars() + { + return mCharacters; + } + + /** + * @deprecated Use {@link #TimeKeyListener(Locale)} instead. + */ + @Deprecated + public TimeKeyListener() { + this(null); + } + + private static final String SYMBOLS_TO_IGNORE = "ahHKkms"; + private static final String SKELETON_12HOUR = "hms"; + private static final String SKELETON_24HOUR = "Hms"; + + public TimeKeyListener(@Nullable Locale locale) { + final LinkedHashSet<Character> chars = new LinkedHashSet<>(); + // First add the digits. Then, add all the character in AM and PM markers. Finally, add all + // the non-pattern characters seen in the patterns for "hms" and "Hms". + final boolean success = NumberKeyListener.addDigits(chars, locale) + && NumberKeyListener.addAmPmChars(chars, locale) + && NumberKeyListener.addFormatCharsFromSkeleton( + chars, locale, SKELETON_12HOUR, SYMBOLS_TO_IGNORE) + && NumberKeyListener.addFormatCharsFromSkeleton( + chars, locale, SKELETON_24HOUR, SYMBOLS_TO_IGNORE); + if (success) { + mCharacters = NumberKeyListener.collectionToArray(chars); + if (locale != null && "en".equals(locale.getLanguage())) { + // For backward compatibility reasons, assume we don't need advanced input for + // English locales, although English locales may need uppercase letters for + // AM and PM. + mNeedsAdvancedInput = false; + } else { + mNeedsAdvancedInput = !ArrayUtils.containsAll(CHARACTERS, mCharacters); + } + } else { + mCharacters = CHARACTERS; + mNeedsAdvancedInput = false; + } + } + + /** + * @deprecated Use {@link #getInstance(Locale)} instead. + */ + @Deprecated + @NonNull + public static TimeKeyListener getInstance() { + return getInstance(null); + } + + /** + * Returns an instance of TimeKeyListener appropriate for the given locale. + */ + @NonNull + public static TimeKeyListener getInstance(@Nullable Locale locale) { + TimeKeyListener instance; + synchronized (sLock) { + instance = sInstanceCache.get(locale); + if (instance == null) { + instance = new TimeKeyListener(locale); + sInstanceCache.put(locale, instance); + } + } + return instance; + } + + /** + * This field used to list the characters that were used. But is now a fixed data + * field that is the list of code units used for the deprecated case where the class + * is instantiated with null or no input parameter. + * + * @see KeyEvent#getMatch + * @see #getAcceptedChars + * + * @deprecated Use {@link #getAcceptedChars()} instead. + */ + public static final char[] CHARACTERS = new char[] { + '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'm', + 'p', ':' + }; + + private final char[] mCharacters; + private final boolean mNeedsAdvancedInput; + + private static final Object sLock = new Object(); + @GuardedBy("sLock") + private static final HashMap<Locale, TimeKeyListener> sInstanceCache = new HashMap<>(); +} diff --git a/android/text/method/Touch.java b/android/text/method/Touch.java new file mode 100644 index 00000000..44811cb3 --- /dev/null +++ b/android/text/method/Touch.java @@ -0,0 +1,213 @@ +/* + * Copyright (C) 2008 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.method; + +import android.text.Layout; +import android.text.Layout.Alignment; +import android.text.NoCopySpan; +import android.text.Spannable; +import android.view.KeyEvent; +import android.view.MotionEvent; +import android.view.ViewConfiguration; +import android.widget.TextView; + +public class Touch { + private Touch() { } + + /** + * Scrolls the specified widget to the specified coordinates, except + * constrains the X scrolling position to the horizontal regions of + * the text that will be visible after scrolling to the specified + * Y position. + */ + public static void scrollTo(TextView widget, Layout layout, int x, int y) { + final int horizontalPadding = widget.getTotalPaddingLeft() + widget.getTotalPaddingRight(); + final int availableWidth = widget.getWidth() - horizontalPadding; + + final int top = layout.getLineForVertical(y); + Alignment a = layout.getParagraphAlignment(top); + boolean ltr = layout.getParagraphDirection(top) > 0; + + int left, right; + if (widget.getHorizontallyScrolling()) { + final int verticalPadding = widget.getTotalPaddingTop() + widget.getTotalPaddingBottom(); + final int bottom = layout.getLineForVertical(y + widget.getHeight() - verticalPadding); + + left = Integer.MAX_VALUE; + right = 0; + + for (int i = top; i <= bottom; i++) { + left = (int) Math.min(left, layout.getLineLeft(i)); + right = (int) Math.max(right, layout.getLineRight(i)); + } + } else { + left = 0; + right = availableWidth; + } + + final int actualWidth = right - left; + + if (actualWidth < availableWidth) { + if (a == Alignment.ALIGN_CENTER) { + x = left - ((availableWidth - actualWidth) / 2); + } else if ((ltr && (a == Alignment.ALIGN_OPPOSITE)) || + (!ltr && (a == Alignment.ALIGN_NORMAL)) || + (a == Alignment.ALIGN_RIGHT)) { + // align_opposite does NOT mean align_right, we need the paragraph + // direction to resolve it to left or right + x = left - (availableWidth - actualWidth); + } else { + x = left; + } + } else { + x = Math.min(x, right - availableWidth); + x = Math.max(x, left); + } + + widget.scrollTo(x, y); + } + + /** + * Handles touch events for dragging. You may want to do other actions + * like moving the cursor on touch as well. + */ + public static boolean onTouchEvent(TextView widget, Spannable buffer, + MotionEvent event) { + DragState[] ds; + + switch (event.getActionMasked()) { + case MotionEvent.ACTION_DOWN: + ds = buffer.getSpans(0, buffer.length(), DragState.class); + + for (int i = 0; i < ds.length; i++) { + buffer.removeSpan(ds[i]); + } + + buffer.setSpan(new DragState(event.getX(), event.getY(), + widget.getScrollX(), widget.getScrollY()), + 0, 0, Spannable.SPAN_MARK_MARK); + return true; + + case MotionEvent.ACTION_UP: + ds = buffer.getSpans(0, buffer.length(), DragState.class); + + for (int i = 0; i < ds.length; i++) { + buffer.removeSpan(ds[i]); + } + + if (ds.length > 0 && ds[0].mUsed) { + return true; + } else { + return false; + } + + case MotionEvent.ACTION_MOVE: + ds = buffer.getSpans(0, buffer.length(), DragState.class); + + if (ds.length > 0) { + if (ds[0].mFarEnough == false) { + int slop = ViewConfiguration.get(widget.getContext()).getScaledTouchSlop(); + + if (Math.abs(event.getX() - ds[0].mX) >= slop || + Math.abs(event.getY() - ds[0].mY) >= slop) { + ds[0].mFarEnough = true; + } + } + + if (ds[0].mFarEnough) { + ds[0].mUsed = true; + boolean cap = (event.getMetaState() & KeyEvent.META_SHIFT_ON) != 0 + || MetaKeyKeyListener.getMetaState(buffer, + MetaKeyKeyListener.META_SHIFT_ON) == 1 + || MetaKeyKeyListener.getMetaState(buffer, + MetaKeyKeyListener.META_SELECTING) != 0; + + float dx; + float dy; + if (cap) { + // if we're selecting, we want the scroll to go in + // the direction of the drag + dx = event.getX() - ds[0].mX; + dy = event.getY() - ds[0].mY; + } else { + dx = ds[0].mX - event.getX(); + dy = ds[0].mY - event.getY(); + } + ds[0].mX = event.getX(); + ds[0].mY = event.getY(); + + int nx = widget.getScrollX() + (int) dx; + int ny = widget.getScrollY() + (int) dy; + + int padding = widget.getTotalPaddingTop() + widget.getTotalPaddingBottom(); + Layout layout = widget.getLayout(); + + ny = Math.min(ny, layout.getHeight() - (widget.getHeight() - padding)); + ny = Math.max(ny, 0); + + int oldX = widget.getScrollX(); + int oldY = widget.getScrollY(); + + scrollTo(widget, layout, nx, ny); + + // If we actually scrolled, then cancel the up action. + if (oldX != widget.getScrollX() || oldY != widget.getScrollY()) { + widget.cancelLongPress(); + } + + return true; + } + } + } + + return false; + } + + /** + * @param widget The text view. + * @param buffer The text buffer. + */ + public static int getInitialScrollX(TextView widget, Spannable buffer) { + DragState[] ds = buffer.getSpans(0, buffer.length(), DragState.class); + return ds.length > 0 ? ds[0].mScrollX : -1; + } + + /** + * @param widget The text view. + * @param buffer The text buffer. + */ + public static int getInitialScrollY(TextView widget, Spannable buffer) { + DragState[] ds = buffer.getSpans(0, buffer.length(), DragState.class); + return ds.length > 0 ? ds[0].mScrollY : -1; + } + + private static class DragState implements NoCopySpan { + public float mX; + public float mY; + public int mScrollX; + public int mScrollY; + public boolean mFarEnough; + public boolean mUsed; + + public DragState(float x, float y, int scrollX, int scrollY) { + mX = x; + mY = y; + mScrollX = scrollX; + mScrollY = scrollY; + } + } +} diff --git a/android/text/method/TransformationMethod.java b/android/text/method/TransformationMethod.java new file mode 100644 index 00000000..b542109d --- /dev/null +++ b/android/text/method/TransformationMethod.java @@ -0,0 +1,45 @@ +/* + * Copyright (C) 2006 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.method; + +import android.graphics.Rect; +import android.view.View; + +/** + * TextView uses TransformationMethods to do things like replacing the + * characters of passwords with dots, or keeping the newline characters + * from causing line breaks in single-line text fields. + */ +public interface TransformationMethod +{ + /** + * Returns a CharSequence that is a transformation of the source text -- + * for example, replacing each character with a dot in a password field. + * Beware that the returned text must be exactly the same length as + * the source text, and that if the source text is Editable, the returned + * text must mirror it dynamically instead of doing a one-time copy. + */ + public CharSequence getTransformation(CharSequence source, View view); + + /** + * This method is called when the TextView that uses this + * TransformationMethod gains or loses focus. + */ + public void onFocusChanged(View view, CharSequence sourceText, + boolean focused, int direction, + Rect previouslyFocusedRect); +} diff --git a/android/text/method/TransformationMethod2.java b/android/text/method/TransformationMethod2.java new file mode 100644 index 00000000..ef00ecdb --- /dev/null +++ b/android/text/method/TransformationMethod2.java @@ -0,0 +1,33 @@ +/* + * 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.method; + +/** + * TransformationMethod2 extends the TransformationMethod interface + * and adds the ability to relax restrictions of TransformationMethod. + * + * @hide + */ +public interface TransformationMethod2 extends TransformationMethod { + /** + * Relax the contract of TransformationMethod to allow length changes, + * or revert to the length-restricted behavior. + * + * @param allowLengthChanges true to allow the transformation to change the length + * of the input string. + */ + public void setLengthChangesAllowed(boolean allowLengthChanges); +} diff --git a/android/text/method/WordIterator.java b/android/text/method/WordIterator.java new file mode 100644 index 00000000..33e96a86 --- /dev/null +++ b/android/text/method/WordIterator.java @@ -0,0 +1,388 @@ +/* + * 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.method; + +import android.annotation.NonNull; +import android.icu.lang.UCharacter; +import android.icu.lang.UProperty; +import android.icu.text.BreakIterator; +import android.text.CharSequenceCharacterIterator; +import android.text.Selection; + +import java.util.Locale; + +/** + * Walks through cursor positions at word boundaries. Internally uses + * {@link BreakIterator#getWordInstance()}, and caches {@link CharSequence} + * for performance reasons. + * + * Also provides methods to determine word boundaries. + * {@hide} + */ +public class WordIterator implements Selection.PositionIterator { + // Size of the window for the word iterator, should be greater than the longest word's length + private static final int WINDOW_WIDTH = 50; + + private int mStart, mEnd; + private CharSequence mCharSeq; + private final BreakIterator mIterator; + + /** + * Constructs a WordIterator using the default locale. + */ + public WordIterator() { + this(Locale.getDefault()); + } + + /** + * Constructs a new WordIterator for the specified locale. + * @param locale The locale to be used for analyzing the text. + */ + public WordIterator(Locale locale) { + mIterator = BreakIterator.getWordInstance(locale); + } + + public void setCharSequence(@NonNull CharSequence charSequence, int start, int end) { + if (0 <= start && end <= charSequence.length()) { + mCharSeq = charSequence; + mStart = Math.max(0, start - WINDOW_WIDTH); + mEnd = Math.min(charSequence.length(), end + WINDOW_WIDTH); + mIterator.setText(new CharSequenceCharacterIterator(charSequence, mStart, mEnd)); + } else { + throw new IndexOutOfBoundsException("input indexes are outside the CharSequence"); + } + } + + /** {@inheritDoc} */ + public int preceding(int offset) { + checkOffsetIsValid(offset); + while (true) { + offset = mIterator.preceding(offset); + if (offset == BreakIterator.DONE || isOnLetterOrDigit(offset)) { + return offset; + } + } + } + + /** {@inheritDoc} */ + public int following(int offset) { + checkOffsetIsValid(offset); + while (true) { + offset = mIterator.following(offset); + if (offset == BreakIterator.DONE || isAfterLetterOrDigit(offset)) { + return offset; + } + } + } + + /** {@inheritDoc} */ + public boolean isBoundary(int offset) { + checkOffsetIsValid(offset); + return mIterator.isBoundary(offset); + } + + /** + * Returns the position of next boundary after the given offset. Returns + * {@code DONE} if there is no boundary after the given offset. + * + * @param offset the given start position to search from. + * @return the position of the last boundary preceding the given offset. + */ + public int nextBoundary(int offset) { + checkOffsetIsValid(offset); + return mIterator.following(offset); + } + + /** + * Returns the position of boundary preceding the given offset or + * {@code DONE} if the given offset specifies the starting position. + * + * @param offset the given start position to search from. + * @return the position of the last boundary preceding the given offset. + */ + public int prevBoundary(int offset) { + checkOffsetIsValid(offset); + return mIterator.preceding(offset); + } + + /** If <code>offset</code> is within a word, returns the index of the first character of that + * word, otherwise returns BreakIterator.DONE. + * + * The offsets that are considered to be part of a word are the indexes of its characters, + * <i>as well as</i> the index of its last character plus one. + * If offset is the index of a low surrogate character, BreakIterator.DONE will be returned. + * + * Valid range for offset is [0..textLength] (note the inclusive upper bound). + * The returned value is within [0..offset] or BreakIterator.DONE. + * + * @throws IllegalArgumentException is offset is not valid. + */ + public int getBeginning(int offset) { + // TODO: Check if usage of this can be updated to getBeginning(offset, true) if + // so this method can be removed. + return getBeginning(offset, false); + } + + /** + * If <code>offset</code> is within a word, returns the index of the last character of that + * word plus one, otherwise returns BreakIterator.DONE. + * + * The offsets that are considered to be part of a word are the indexes of its characters, + * <i>as well as</i> the index of its last character plus one. + * If offset is the index of a low surrogate character, BreakIterator.DONE will be returned. + * + * Valid range for offset is [0..textLength] (note the inclusive upper bound). + * The returned value is within [offset..textLength] or BreakIterator.DONE. + * + * @throws IllegalArgumentException is offset is not valid. + */ + public int getEnd(int offset) { + // TODO: Check if usage of this can be updated to getEnd(offset, true), if + // so this method can be removed. + return getEnd(offset, false); + } + + /** + * If the <code>offset</code> is within a word or on a word boundary that can only be + * considered the start of a word (e.g. _word where "_" is any character that would not + * be considered part of the word) then this returns the index of the first character of + * that word. + * + * If the offset is on a word boundary that can be considered the start and end of a + * word, e.g. AABB (where AA and BB are both words) and the offset is the boundary + * between AA and BB, this would return the start of the previous word, AA. + * + * Returns BreakIterator.DONE if there is no previous boundary. + * + * @throws IllegalArgumentException is offset is not valid. + */ + public int getPrevWordBeginningOnTwoWordsBoundary(int offset) { + return getBeginning(offset, true); + } + + /** + * If the <code>offset</code> is within a word or on a word boundary that can only be + * considered the end of a word (e.g. word_ where "_" is any character that would not + * be considered part of the word) then this returns the index of the last character + * plus one of that word. + * + * If the offset is on a word boundary that can be considered the start and end of a + * word, e.g. AABB (where AA and BB are both words) and the offset is the boundary + * between AA and BB, this would return the end of the next word, BB. + * + * Returns BreakIterator.DONE if there is no next boundary. + * + * @throws IllegalArgumentException is offset is not valid. + */ + public int getNextWordEndOnTwoWordBoundary(int offset) { + return getEnd(offset, true); + } + + /** + * If the <code>offset</code> is within a word or on a word boundary that can only be + * considered the start of a word (e.g. _word where "_" is any character that would not + * be considered part of the word) then this returns the index of the first character of + * that word. + * + * If the offset is on a word boundary that can be considered the start and end of a + * word, e.g. AABB (where AA and BB are both words) and the offset is the boundary + * between AA and BB, and getPrevWordBeginningOnTwoWordsBoundary is true then this would + * return the start of the previous word, AA. Otherwise it would return the current offset, + * the start of BB. + * + * Returns BreakIterator.DONE if there is no previous boundary. + * + * @throws IllegalArgumentException is offset is not valid. + */ + private int getBeginning(int offset, boolean getPrevWordBeginningOnTwoWordsBoundary) { + checkOffsetIsValid(offset); + + if (isOnLetterOrDigit(offset)) { + if (mIterator.isBoundary(offset) + && (!isAfterLetterOrDigit(offset) + || !getPrevWordBeginningOnTwoWordsBoundary)) { + return offset; + } else { + return mIterator.preceding(offset); + } + } else { + if (isAfterLetterOrDigit(offset)) { + return mIterator.preceding(offset); + } + } + return BreakIterator.DONE; + } + + /** + * If the <code>offset</code> is within a word or on a word boundary that can only be + * considered the end of a word (e.g. word_ where "_" is any character that would not be + * considered part of the word) then this returns the index of the last character plus one + * of that word. + * + * If the offset is on a word boundary that can be considered the start and end of a + * word, e.g. AABB (where AA and BB are both words) and the offset is the boundary + * between AA and BB, and getNextWordEndOnTwoWordBoundary is true then this would return + * the end of the next word, BB. Otherwise it would return the current offset, the end + * of AA. + * + * Returns BreakIterator.DONE if there is no next boundary. + * + * @throws IllegalArgumentException is offset is not valid. + */ + private int getEnd(int offset, boolean getNextWordEndOnTwoWordBoundary) { + checkOffsetIsValid(offset); + + if (isAfterLetterOrDigit(offset)) { + if (mIterator.isBoundary(offset) + && (!isOnLetterOrDigit(offset) || !getNextWordEndOnTwoWordBoundary)) { + return offset; + } else { + return mIterator.following(offset); + } + } else { + if (isOnLetterOrDigit(offset)) { + return mIterator.following(offset); + } + } + return BreakIterator.DONE; + } + + /** + * If <code>offset</code> is within a group of punctuation as defined + * by {@link #isPunctuation(int)}, returns the index of the first character + * of that group, otherwise returns BreakIterator.DONE. + * + * @param offset the offset to search from. + */ + public int getPunctuationBeginning(int offset) { + checkOffsetIsValid(offset); + while (offset != BreakIterator.DONE && !isPunctuationStartBoundary(offset)) { + offset = prevBoundary(offset); + } + // No need to shift offset, prevBoundary handles that. + return offset; + } + + /** + * If <code>offset</code> is within a group of punctuation as defined + * by {@link #isPunctuation(int)}, returns the index of the last character + * of that group plus one, otherwise returns BreakIterator.DONE. + * + * @param offset the offset to search from. + */ + public int getPunctuationEnd(int offset) { + checkOffsetIsValid(offset); + while (offset != BreakIterator.DONE && !isPunctuationEndBoundary(offset)) { + offset = nextBoundary(offset); + } + // No need to shift offset, nextBoundary handles that. + return offset; + } + + /** + * Indicates if the provided offset is after a punctuation character + * as defined by {@link #isPunctuation(int)}. + * + * @param offset the offset to check from. + * @return Whether the offset is after a punctuation character. + */ + public boolean isAfterPunctuation(int offset) { + if (mStart < offset && offset <= mEnd) { + final int codePoint = Character.codePointBefore(mCharSeq, offset); + return isPunctuation(codePoint); + } + return false; + } + + /** + * Indicates if the provided offset is at a punctuation character + * as defined by {@link #isPunctuation(int)}. + * + * @param offset the offset to check from. + * @return Whether the offset is at a punctuation character. + */ + public boolean isOnPunctuation(int offset) { + if (mStart <= offset && offset < mEnd) { + final int codePoint = Character.codePointAt(mCharSeq, offset); + return isPunctuation(codePoint); + } + return false; + } + + /** + * Indicates if the codepoint is a mid-word-only punctuation. + * + * At the moment, this is locale-independent, and includes all the characters in + * the MidLetter, MidNumLet, and Single_Quote class of Unicode word breaking algorithm (see + * UAX #29 "Unicode Text Segmentation" at http://unicode.org/reports/tr29/). These are all the + * characters that according to the rules WB6 and WB7 of UAX #29 prevent word breaks if they are + * in the middle of a word, but they become word breaks if they happen at the end of a word + * (accroding to rule WB999 that breaks word in any place that is not prohibited otherwise). + * + * @param locale the locale to consider the codepoint in. Presently ignored. + * @param codePoint the codepoint to check. + * @return True if the codepoint is a mid-word punctuation. + */ + public static boolean isMidWordPunctuation(Locale locale, int codePoint) { + final int wb = UCharacter.getIntPropertyValue(codePoint, UProperty.WORD_BREAK); + return (wb == UCharacter.WordBreak.MIDLETTER + || wb == UCharacter.WordBreak.MIDNUMLET + || wb == UCharacter.WordBreak.SINGLE_QUOTE); + } + + private boolean isPunctuationStartBoundary(int offset) { + return isOnPunctuation(offset) && !isAfterPunctuation(offset); + } + + private boolean isPunctuationEndBoundary(int offset) { + return !isOnPunctuation(offset) && isAfterPunctuation(offset); + } + + private static boolean isPunctuation(int cp) { + final int type = Character.getType(cp); + return (type == Character.CONNECTOR_PUNCTUATION + || type == Character.DASH_PUNCTUATION + || type == Character.END_PUNCTUATION + || type == Character.FINAL_QUOTE_PUNCTUATION + || type == Character.INITIAL_QUOTE_PUNCTUATION + || type == Character.OTHER_PUNCTUATION + || type == Character.START_PUNCTUATION); + } + + private boolean isAfterLetterOrDigit(int offset) { + if (mStart < offset && offset <= mEnd) { + final int codePoint = Character.codePointBefore(mCharSeq, offset); + if (Character.isLetterOrDigit(codePoint)) return true; + } + return false; + } + + private boolean isOnLetterOrDigit(int offset) { + if (mStart <= offset && offset < mEnd) { + final int codePoint = Character.codePointAt(mCharSeq, offset); + if (Character.isLetterOrDigit(codePoint)) return true; + } + return false; + } + + private void checkOffsetIsValid(int offset) { + if (!(mStart <= offset && offset <= mEnd)) { + throw new IllegalArgumentException("Invalid offset: " + (offset) + + ". Valid range is [" + mStart + ", " + mEnd + "]"); + } + } +} |