diff options
author | Gilles Debunne <debunne@google.com> | 2011-08-04 21:22:30 -0700 |
---|---|---|
committer | Gilles Debunne <debunne@google.com> | 2011-08-23 13:13:54 -0700 |
commit | 6435a56a8c02de98befcc8cd743b2b638cffb327 (patch) | |
tree | 62a5678b5531f53c7b25e920f8f9c6b65f8b510c /core | |
parent | defa12e95b8d25db5f3e9a044e83d6fe680b67a3 (diff) | |
download | frameworks_base-6435a56a8c02de98befcc8cd743b2b638cffb327.zip frameworks_base-6435a56a8c02de98befcc8cd743b2b638cffb327.tar.gz frameworks_base-6435a56a8c02de98befcc8cd743b2b638cffb327.tar.bz2 |
Spell checking in TextViews
New UX interactions (the Paste action is no longer displayed after a delay)
suggestionEnabled flag replaced by existing input type flag.
removeSpans fixed in SpannableStringBuilder to always send notifications
SuggestionSpan handled by TextView instead of SpannableStringBuilder
New span update algorithm to correctly handle edition around word boundaries.
Change-Id: I52c01172f19e595fa512e285a565a3fd97c3c50e
Diffstat (limited to 'core')
-rw-r--r-- | core/java/android/text/SpannableStringBuilder.java | 125 | ||||
-rw-r--r-- | core/java/android/text/method/WordIterator.java | 4 | ||||
-rw-r--r-- | core/java/android/text/style/SpellCheckSpan.java | 41 | ||||
-rw-r--r-- | core/java/android/text/style/SuggestionSpan.java | 17 | ||||
-rw-r--r-- | core/java/android/view/textservice/SuggestionsInfo.java | 4 | ||||
-rw-r--r-- | core/java/android/widget/SpellChecker.java | 226 | ||||
-rw-r--r-- | core/java/android/widget/TextView.java | 376 | ||||
-rw-r--r-- | core/res/res/drawable-hdpi/ic_menu_selectall_holo_dark.png | bin | 709 -> 626 bytes | |||
-rw-r--r-- | core/res/res/drawable-hdpi/ic_menu_selectall_holo_light.png | bin | 945 -> 435 bytes | |||
-rw-r--r-- | core/res/res/layout/keyguard_screen_password_landscape.xml | 1 | ||||
-rw-r--r-- | core/res/res/layout/keyguard_screen_password_portrait.xml | 2 | ||||
-rwxr-xr-x | core/res/res/values/attrs.xml | 6 | ||||
-rw-r--r-- | core/res/res/values/public.xml | 2 | ||||
-rwxr-xr-x | core/res/res/values/strings.xml | 2 |
14 files changed, 599 insertions, 207 deletions
diff --git a/core/java/android/text/SpannableStringBuilder.java b/core/java/android/text/SpannableStringBuilder.java index 6bde802..5fed775 100644 --- a/core/java/android/text/SpannableStringBuilder.java +++ b/core/java/android/text/SpannableStringBuilder.java @@ -16,21 +16,18 @@ package android.text; -import com.android.internal.util.ArrayUtils; - import android.graphics.Canvas; import android.graphics.Paint; -import android.text.style.SuggestionSpan; + +import com.android.internal.util.ArrayUtils; import java.lang.reflect.Array; /** * This is the class for text whose content and markup can both be changed. */ -public class SpannableStringBuilder -implements CharSequence, GetChars, Spannable, Editable, Appendable, - GraphicsOperations -{ +public class SpannableStringBuilder implements CharSequence, GetChars, Spannable, Editable, + Appendable, GraphicsOperations { /** * Create a new SpannableStringBuilder with empty contents */ @@ -111,8 +108,7 @@ implements CharSequence, GetChars, Spannable, Editable, Appendable, if (where < 0) { throw new IndexOutOfBoundsException("charAt: " + where + " < 0"); } else if (where >= len) { - throw new IndexOutOfBoundsException("charAt: " + where + - " >= length " + len); + throw new IndexOutOfBoundsException("charAt: " + where + " >= length " + len); } if (where >= mGapStart) @@ -266,8 +262,7 @@ implements CharSequence, GetChars, Spannable, Editable, Appendable, return append(String.valueOf(text)); } - private int change(int start, int end, - CharSequence tb, int tbstart, int tbend) { + private int change(int start, int end, CharSequence tb, int tbstart, int tbend) { return change(true, start, end, tb, tbstart, tbend); } @@ -277,8 +272,9 @@ implements CharSequence, GetChars, Spannable, Editable, Appendable, int ret = tbend - tbstart; TextWatcher[] recipients = null; - if (notify) + if (notify) { recipients = sendTextWillChange(start, end - start, tbend - tbstart); + } for (int i = mSpanCount - 1; i >= 0; i--) { if ((mSpanFlags[i] & SPAN_PARAGRAPH) == SPAN_PARAGRAPH) { @@ -353,7 +349,6 @@ implements CharSequence, GetChars, Spannable, Editable, Appendable, // no need for span fixup on pure insertion if (tbend > tbstart && end - start == 0) { if (notify) { - removeSuggestionSpans(start, end); sendTextChange(recipients, start, end - start, tbend - tbstart); sendTextHasChanged(recipients); } @@ -388,7 +383,6 @@ implements CharSequence, GetChars, Spannable, Editable, Appendable, if (mSpanEnds[i] < mSpanStarts[i]) { removeSpan(i); } - removeSuggestionSpans(start, end); } if (notify) { @@ -399,30 +393,26 @@ implements CharSequence, GetChars, Spannable, Editable, Appendable, return ret; } - /** - * Removes the SuggestionSpan that overlap the [start, end] range, and that would - * not make sense anymore after the change. - */ - private void removeSuggestionSpans(int start, int end) { - for (int i = mSpanCount - 1; i >= 0; i--) { - final int spanEnd = mSpanEnds[i]; - final int spanSpart = mSpanStarts[i]; - if ((mSpans[i] instanceof SuggestionSpan) && ( - (spanSpart < start && spanEnd > start) || - (spanSpart < end && spanEnd > end))) { - removeSpan(i); - } - } - } - private void removeSpan(int i) { - // XXX send notification on removal - System.arraycopy(mSpans, i + 1, mSpans, i, mSpanCount - (i + 1)); - System.arraycopy(mSpanStarts, i + 1, mSpanStarts, i, mSpanCount - (i + 1)); - System.arraycopy(mSpanEnds, i + 1, mSpanEnds, i, mSpanCount - (i + 1)); - System.arraycopy(mSpanFlags, i + 1, mSpanFlags, i, mSpanCount - (i + 1)); + Object object = mSpans[i]; + + int start = mSpanStarts[i]; + int end = mSpanEnds[i]; + + if (start > mGapStart) start -= mGapLength; + if (end > mGapStart) end -= mGapLength; + + int count = mSpanCount - (i + 1); + System.arraycopy(mSpans, i + 1, mSpans, i, count); + System.arraycopy(mSpanStarts, i + 1, mSpanStarts, i, count); + System.arraycopy(mSpanEnds, i + 1, mSpanEnds, i, count); + System.arraycopy(mSpanFlags, i + 1, mSpanFlags, i, count); mSpanCount--; + + mSpans[mSpanCount] = null; + + sendSpanRemoved(object, start, end); } // Documentation from interface @@ -462,11 +452,10 @@ implements CharSequence, GetChars, Spannable, Editable, Appendable, moveGapTo(end); TextWatcher[] recipients; - recipients = sendTextWillChange(start, end - start, - tbend - tbstart); - int origlen = end - start; + recipients = sendTextWillChange(start, origlen, tbend - tbstart); + if (mGapLength < 2) resizeFor(length() + 1); @@ -486,11 +475,9 @@ implements CharSequence, GetChars, Spannable, Editable, Appendable, new Exception("mGapLength < 1").printStackTrace(); } - int oldlen = (end + 1) - start; - int inserted = change(false, start + 1, start + 1, tb, tbstart, tbend); change(false, start, start + 1, "", 0, 0); - change(false, start + inserted, start + inserted + oldlen - 1, "", 0, 0); + change(false, start + inserted, start + inserted + origlen, "", 0, 0); /* * Special case to keep the cursor in the same position @@ -515,13 +502,12 @@ implements CharSequence, GetChars, Spannable, Editable, Appendable, off = off * inserted / (end - start); selend = (int) off + start; - setSpan(false, Selection.SELECTION_END, selend, selend, - Spanned.SPAN_POINT_POINT); + setSpan(false, Selection.SELECTION_END, selend, selend, Spanned.SPAN_POINT_POINT); } - sendTextChange(recipients, start, origlen, inserted); sendTextHasChanged(recipients); } + return this; } @@ -534,8 +520,7 @@ implements CharSequence, GetChars, Spannable, Editable, Appendable, setSpan(true, what, start, end, flags); } - private void setSpan(boolean send, - Object what, int start, int end, int flags) { + private void setSpan(boolean send, Object what, int start, int end, int flags) { int nstart = start; int nend = end; @@ -546,8 +531,7 @@ implements CharSequence, GetChars, Spannable, Editable, Appendable, char c = charAt(start - 1); if (c != '\n') - throw new RuntimeException( - "PARAGRAPH span must start at paragraph boundary"); + throw new RuntimeException("PARAGRAPH span must start at paragraph boundary"); } } @@ -556,23 +540,22 @@ implements CharSequence, GetChars, Spannable, Editable, Appendable, char c = charAt(end - 1); if (c != '\n') - throw new RuntimeException( - "PARAGRAPH span must end at paragraph boundary"); + throw new RuntimeException("PARAGRAPH span must end at paragraph boundary"); } } - if (start > mGapStart) + if (start > mGapStart) { start += mGapLength; - else if (start == mGapStart) { + } else if (start == mGapStart) { int flag = (flags & START_MASK) >> START_SHIFT; if (flag == POINT || (flag == PARAGRAPH && start == length())) start += mGapLength; } - if (end > mGapStart) + if (end > mGapStart) { end += mGapLength; - else if (end == mGapStart) { + } else if (end == mGapStart) { int flag = (flags & END_MASK); if (flag == POINT || (flag == PARAGRAPH && end == length())) @@ -637,25 +620,7 @@ implements CharSequence, GetChars, Spannable, Editable, Appendable, public void removeSpan(Object what) { for (int i = mSpanCount - 1; i >= 0; i--) { if (mSpans[i] == what) { - int ostart = mSpanStarts[i]; - int oend = mSpanEnds[i]; - - if (ostart > mGapStart) - ostart -= mGapLength; - if (oend > mGapStart) - oend -= mGapLength; - - int count = mSpanCount - (i + 1); - - System.arraycopy(mSpans, i + 1, mSpans, i, count); - System.arraycopy(mSpanStarts, i + 1, mSpanStarts, i, count); - System.arraycopy(mSpanEnds, i + 1, mSpanEnds, i, count); - System.arraycopy(mSpanFlags, i + 1, mSpanFlags, i, count); - - mSpanCount--; - mSpans[mSpanCount] = null; - - sendSpanRemoved(what, ostart, oend); + removeSpan(i); return; } } @@ -729,6 +694,8 @@ implements CharSequence, GetChars, Spannable, Editable, Appendable, */ @SuppressWarnings("unchecked") public <T> T[] getSpans(int queryStart, int queryEnd, Class<T> kind) { + if (kind == null) return ArrayUtils.emptyArray(kind); + int spanCount = mSpanCount; Object[] spans = mSpans; int[] starts = mSpanStarts; @@ -742,6 +709,8 @@ implements CharSequence, GetChars, Spannable, Editable, Appendable, T ret1 = null; for (int i = 0; i < spanCount; i++) { + if (!kind.isInstance(spans[i])) continue; + int spanStart = starts[i]; int spanEnd = ends[i]; @@ -766,10 +735,6 @@ implements CharSequence, GetChars, Spannable, Editable, Appendable, continue; } - if (kind != null && !kind.isInstance(spans[i])) { - continue; - } - if (count == 0) { // Safe conversion thanks to the isInstance test above ret1 = (T) spans[i]; @@ -909,8 +874,7 @@ implements CharSequence, GetChars, Spannable, Editable, Appendable, return recip; } - private void sendTextChange(TextWatcher[] recip, int start, int before, - int after) { + private void sendTextChange(TextWatcher[] recip, int start, int before, int after) { int n = recip.length; for (int i = 0; i < n; i++) { @@ -945,8 +909,7 @@ implements CharSequence, GetChars, Spannable, Editable, Appendable, } private void sendSpanChanged(Object what, int s, int e, int st, int en) { - SpanWatcher[] recip = getSpans(Math.min(s, st), Math.max(e, en), - SpanWatcher.class); + SpanWatcher[] recip = getSpans(Math.min(s, st), Math.max(e, en), SpanWatcher.class); int n = recip.length; for (int i = 0; i < n; i++) { diff --git a/core/java/android/text/method/WordIterator.java b/core/java/android/text/method/WordIterator.java index af524ee..0433ec4 100644 --- a/core/java/android/text/method/WordIterator.java +++ b/core/java/android/text/method/WordIterator.java @@ -73,6 +73,10 @@ public class WordIterator implements Selection.PositionIterator { } }; + public void forceUpdate() { + mCurrentDirty = true; + } + public void setCharSequence(CharSequence incoming) { // When incoming is different object, move listeners to new sequence // and mark as dirty so we reload contents. diff --git a/core/java/android/text/style/SpellCheckSpan.java b/core/java/android/text/style/SpellCheckSpan.java new file mode 100644 index 0000000..9b23177 --- /dev/null +++ b/core/java/android/text/style/SpellCheckSpan.java @@ -0,0 +1,41 @@ +/* + * 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.style; + +/** + * A SpellCheckSpan is an internal data structure created by the TextView's SpellChecker to + * annotate portions of the text that are about to or currently being spell checked. They are + * automatically removed once the spell check is completed. + * + * @hide + */ +public class SpellCheckSpan { + + private boolean mSpellCheckInProgress; + + public SpellCheckSpan() { + mSpellCheckInProgress = false; + } + + public void setSpellCheckInProgress() { + mSpellCheckInProgress = true; + } + + public boolean isSpellCheckInProgress() { + return mSpellCheckInProgress; + } +} diff --git a/core/java/android/text/style/SuggestionSpan.java b/core/java/android/text/style/SuggestionSpan.java index fb94bc7..ea57f917 100644 --- a/core/java/android/text/style/SuggestionSpan.java +++ b/core/java/android/text/style/SuggestionSpan.java @@ -40,7 +40,7 @@ import java.util.Locale; * These spans should typically be created by the input method to provide correction and alternates * for the text. * - * @see TextView#setSuggestionsEnabled(boolean) + * @see TextView#isSuggestionsEnabled() */ public class SuggestionSpan extends CharacterStyle implements ParcelableSpan { @@ -76,7 +76,7 @@ public class SuggestionSpan extends CharacterStyle implements ParcelableSpan { * And the current IME might want to specify any IME as the target IME including other IMEs. */ - private final int mFlags; + private int mFlags; private final String[] mSuggestions; private final String mLocaleString; private final String mNotificationTargetClassName; @@ -134,8 +134,7 @@ public class SuggestionSpan extends CharacterStyle implements ParcelableSpan { } else { mNotificationTargetClassName = ""; } - mHashCode = hashCodeInternal( - mFlags, mSuggestions, mLocaleString, mNotificationTargetClassName); + mHashCode = hashCodeInternal(mSuggestions, mLocaleString, mNotificationTargetClassName); initStyle(context); } @@ -211,6 +210,10 @@ public class SuggestionSpan extends CharacterStyle implements ParcelableSpan { return mFlags; } + public void setFlags(int flags) { + mFlags = flags; + } + @Override public int describeContents() { return 0; @@ -247,10 +250,10 @@ public class SuggestionSpan extends CharacterStyle implements ParcelableSpan { return mHashCode; } - private static int hashCodeInternal(int flags, String[] suggestions,String locale, + private static int hashCodeInternal(String[] suggestions, String locale, String notificationTargetClassName) { - return Arrays.hashCode(new Object[] {SystemClock.uptimeMillis(), flags, suggestions, locale, - notificationTargetClassName}); + return Arrays.hashCode(new Object[] {Long.valueOf(SystemClock.uptimeMillis()), suggestions, + locale, notificationTargetClassName}); } public static final Parcelable.Creator<SuggestionSpan> CREATOR = diff --git a/core/java/android/view/textservice/SuggestionsInfo.java b/core/java/android/view/textservice/SuggestionsInfo.java index ed0f89d..62a06b9 100644 --- a/core/java/android/view/textservice/SuggestionsInfo.java +++ b/core/java/android/view/textservice/SuggestionsInfo.java @@ -16,6 +16,8 @@ package android.view.textservice; +import com.android.internal.util.ArrayUtils; + import android.os.Parcel; import android.os.Parcelable; @@ -23,7 +25,7 @@ import android.os.Parcelable; * This class contains a metadata of suggestions from the text service */ public final class SuggestionsInfo implements Parcelable { - private static final String[] EMPTY = new String[0]; + private static final String[] EMPTY = ArrayUtils.emptyArray(String.class); /** * Flag of the attributes of the suggestions that can be obtained by diff --git a/core/java/android/widget/SpellChecker.java b/core/java/android/widget/SpellChecker.java new file mode 100644 index 0000000..5e3b956 --- /dev/null +++ b/core/java/android/widget/SpellChecker.java @@ -0,0 +1,226 @@ +// Copyright 2011 Google Inc. All Rights Reserved. + +package android.widget; + +import android.content.Context; +import android.text.Editable; +import android.text.Selection; +import android.text.Spanned; +import android.text.style.SpellCheckSpan; +import android.text.style.SuggestionSpan; +import android.util.Log; +import android.view.textservice.SpellCheckerSession; +import android.view.textservice.SpellCheckerSession.SpellCheckerSessionListener; +import android.view.textservice.SuggestionsInfo; +import android.view.textservice.TextInfo; +import android.view.textservice.TextServicesManager; + +import com.android.internal.util.ArrayUtils; + +import java.util.Locale; + + +/** + * Helper class for TextView. Bridge between the TextView and the Dictionnary service. + * + * @hide + */ +public class SpellChecker implements SpellCheckerSessionListener { + private static final String LOG_TAG = "SpellChecker"; + private static final boolean DEBUG_SPELL_CHECK = false; + private static final int DELAY_BEFORE_SPELL_CHECK = 400; // milliseconds + + private final TextView mTextView; + + final SpellCheckerSession spellCheckerSession; + final int mCookie; + + // Paired arrays for the (id, spellCheckSpan) pair. mIndex is the next available position + private int[] mIds; + private SpellCheckSpan[] mSpellCheckSpans; + // The actual current number of used slots in the above arrays + private int mLength; + + private int mSpanSequenceCounter = 0; + private Runnable mChecker; + + public SpellChecker(TextView textView) { + mTextView = textView; + + final TextServicesManager textServicesManager = (TextServicesManager) textView.getContext(). + getSystemService(Context.TEXT_SERVICES_MANAGER_SERVICE); + spellCheckerSession = textServicesManager.newSpellCheckerSession( + null /* not currently used by the textServicesManager */, Locale.getDefault(), + this, true /* means use the languages defined in Settings */); + mCookie = hashCode(); + + // Arbitrary: 4 simultaneous spell check spans. Will automatically double size on demand + final int size = ArrayUtils.idealObjectArraySize(4); + mIds = new int[size]; + mSpellCheckSpans = new SpellCheckSpan[size]; + mLength = 0; + } + + public void addSpellCheckSpan(SpellCheckSpan spellCheckSpan) { + int length = mIds.length; + if (mLength >= length) { + final int newSize = length * 2; + int[] newIds = new int[newSize]; + SpellCheckSpan[] newSpellCheckSpans = new SpellCheckSpan[newSize]; + System.arraycopy(mIds, 0, newIds, 0, length); + System.arraycopy(mSpellCheckSpans, 0, newSpellCheckSpans, 0, length); + mIds = newIds; + mSpellCheckSpans = newSpellCheckSpans; + } + + mIds[mLength] = mSpanSequenceCounter++; + mSpellCheckSpans[mLength] = spellCheckSpan; + mLength++; + + if (DEBUG_SPELL_CHECK) { + final Editable mText = (Editable) mTextView.getText(); + int start = mText.getSpanStart(spellCheckSpan); + int end = mText.getSpanEnd(spellCheckSpan); + if (start >= 0 && end >= 0) { + Log.d(LOG_TAG, "Schedule check " + mText.subSequence(start, end)); + } else { + Log.d(LOG_TAG, "Schedule check EMPTY!"); + } + } + + scheduleSpellCheck(); + } + + public void removeSpellCheckSpan(SpellCheckSpan spellCheckSpan) { + for (int i = 0; i < mLength; i++) { + if (mSpellCheckSpans[i] == spellCheckSpan) { + removeAtIndex(i); + return; + } + } + } + + private void removeAtIndex(int i) { + System.arraycopy(mIds, i + 1, mIds, i, mLength - i - 1); + System.arraycopy(mSpellCheckSpans, i + 1, mSpellCheckSpans, i, mLength - i - 1); + mLength--; + } + + public void onSelectionChanged() { + scheduleSpellCheck(); + } + + private void scheduleSpellCheck() { + if (mLength == 0) return; + if (mChecker != null) { + mTextView.removeCallbacks(mChecker); + } + if (mChecker == null) { + mChecker = new Runnable() { + public void run() { + spellCheck(); + } + }; + } + mTextView.postDelayed(mChecker, DELAY_BEFORE_SPELL_CHECK); + } + + private void spellCheck() { + final Editable editable = (Editable) mTextView.getText(); + final int selectionStart = Selection.getSelectionStart(editable); + final int selectionEnd = Selection.getSelectionEnd(editable); + + TextInfo[] textInfos = new TextInfo[mLength]; + int textInfosCount = 0; + + for (int i = 0; i < mLength; i++) { + SpellCheckSpan spellCheckSpan = mSpellCheckSpans[i]; + + if (spellCheckSpan.isSpellCheckInProgress()) continue; + + final int start = editable.getSpanStart(spellCheckSpan); + final int end = editable.getSpanEnd(spellCheckSpan); + + // Do not check this word if the user is currently editing it + if (start >= 0 && end >= 0 && (selectionEnd < start || selectionStart > end)) { + final String word = editable.subSequence(start, end).toString(); + spellCheckSpan.setSpellCheckInProgress(); + textInfos[textInfosCount++] = new TextInfo(word, mCookie, mIds[i]); + } + } + + if (textInfosCount > 0) { + if (textInfosCount < mLength) { + TextInfo[] textInfosCopy = new TextInfo[textInfosCount]; + System.arraycopy(textInfos, 0, textInfosCopy, 0, textInfosCount); + textInfos = textInfosCopy; + } + spellCheckerSession.getSuggestions(textInfos, SuggestionSpan.SUGGESTIONS_MAX_SIZE, + false /* TODO Set sequentialWords to true for initial spell check */); + } + } + + @Override + public void onGetSuggestions(SuggestionsInfo[] results) { + final Editable editable = (Editable) mTextView.getText(); + for (int i = 0; i < results.length; i++) { + SuggestionsInfo suggestionsInfo = results[i]; + if (suggestionsInfo.getCookie() != mCookie) continue; + + final int sequenceNumber = suggestionsInfo.getSequence(); + // Starting from the end, to limit the number of array copy while removing + for (int j = mLength - 1; j >= 0; j--) { + if (sequenceNumber == mIds[j]) { + SpellCheckSpan spellCheckSpan = mSpellCheckSpans[j]; + final int attributes = suggestionsInfo.getSuggestionsAttributes(); + boolean isInDictionary = + ((attributes & SuggestionsInfo.RESULT_ATTR_IN_THE_DICTIONARY) > 0); + boolean looksLikeTypo = + ((attributes & SuggestionsInfo.RESULT_ATTR_LOOKS_LIKE_TYPO) > 0); + + if (DEBUG_SPELL_CHECK) { + final int start = editable.getSpanStart(spellCheckSpan); + final int end = editable.getSpanEnd(spellCheckSpan); + Log.d(LOG_TAG, "Result sequence=" + suggestionsInfo.getSequence() + " " + + editable.subSequence(start, end) + + "\t" + (isInDictionary?"IN_DICT" : "NOT_DICT") + + "\t" + (looksLikeTypo?"TYPO" : "NOT_TYPO")); + } + + if (!isInDictionary && looksLikeTypo) { + String[] suggestions = getSuggestions(suggestionsInfo); + if (suggestions.length > 0) { + SuggestionSpan suggestionSpan = new SuggestionSpan( + mTextView.getContext(), suggestions, + SuggestionSpan.FLAG_EASY_CORRECT | + SuggestionSpan.FLAG_MISSPELLED); + final int start = editable.getSpanStart(spellCheckSpan); + final int end = editable.getSpanEnd(spellCheckSpan); + editable.setSpan(suggestionSpan, start, end, + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + // TODO limit to the word rectangle region + mTextView.invalidate(); + + if (DEBUG_SPELL_CHECK) { + String suggestionsString = ""; + for (String s : suggestions) { suggestionsString += s + "|"; } + Log.d(LOG_TAG, " Suggestions for " + sequenceNumber + " " + + editable.subSequence(start, end)+ " " + suggestionsString); + } + } + } + editable.removeSpan(spellCheckSpan); + } + } + } + } + + private static String[] getSuggestions(SuggestionsInfo suggestionsInfo) { + final int len = Math.max(0, suggestionsInfo.getSuggestionsCount()); + String[] suggestions = new String[len]; + for (int j = 0; j < len; ++j) { + suggestions[j] = suggestionsInfo.getSuggestionAt(j); + } + return suggestions; + } +} diff --git a/core/java/android/widget/TextView.java b/core/java/android/widget/TextView.java index 662b964..683a984 100644 --- a/core/java/android/widget/TextView.java +++ b/core/java/android/widget/TextView.java @@ -61,11 +61,6 @@ import android.text.SpannedString; import android.text.StaticLayout; import android.text.TextDirectionHeuristic; import android.text.TextDirectionHeuristics; -import android.text.TextDirectionHeuristics.AnyStrong; -import android.text.TextDirectionHeuristics.CharCount; -import android.text.TextDirectionHeuristics.FirstStrong; -import android.text.TextDirectionHeuristics.TextDirectionAlgorithm; -import android.text.TextDirectionHeuristics.TextDirectionHeuristicImpl; import android.text.TextPaint; import android.text.TextUtils; import android.text.TextWatcher; @@ -88,6 +83,7 @@ import android.text.method.TransformationMethod2; import android.text.method.WordIterator; import android.text.style.ClickableSpan; import android.text.style.ParagraphStyle; +import android.text.style.SpellCheckSpan; import android.text.style.SuggestionSpan; import android.text.style.TextAppearanceSpan; import android.text.style.URLSpan; @@ -220,7 +216,6 @@ import java.util.HashMap; * @attr ref android.R.styleable#TextView_imeActionLabel * @attr ref android.R.styleable#TextView_imeActionId * @attr ref android.R.styleable#TextView_editorExtras - * @attr ref android.R.styleable#TextView_suggestionsEnabled */ @RemoteView public class TextView extends View implements ViewTreeObserver.OnPreDrawListener { @@ -334,7 +329,6 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener private int mTextEditSuggestionItemLayout; private SuggestionsPopupWindow mSuggestionsPopupWindow; private SuggestionRangeSpan mSuggestionRangeSpan; - private boolean mSuggestionsEnabled = true; private int mCursorDrawableRes; private final Drawable[] mCursorDrawable = new Drawable[2]; @@ -356,6 +350,8 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener private WordIterator mWordIterator; + private SpellChecker mSpellChecker; + // The alignment to pass to Layout, or null if not resolved. private Layout.Alignment mLayoutAlignment; @@ -826,10 +822,6 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener mTextIsSelectable = a.getBoolean(attr, false); break; - case com.android.internal.R.styleable.TextView_suggestionsEnabled: - mSuggestionsEnabled = a.getBoolean(attr, true); - break; - case com.android.internal.R.styleable.TextView_textAllCaps: allCaps = a.getBoolean(attr, false); break; @@ -3100,18 +3092,19 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener } boolean needEditableForNotification = false; + boolean startSpellCheck = false; if (mListeners != null && mListeners.size() != 0) { needEditableForNotification = true; } - if (type == BufferType.EDITABLE || mInput != null || - needEditableForNotification) { + if (type == BufferType.EDITABLE || mInput != null || needEditableForNotification) { Editable t = mEditableFactory.newEditable(text); text = t; setFilters(t, mFilters); InputMethodManager imm = InputMethodManager.peekInstance(); if (imm != null) imm.restartInput(this); + startSpellCheck = true; } else if (type == BufferType.SPANNABLE || mMovement != null) { text = mSpannableFactory.newSpannable(text); } else if (!(text instanceof CharWrapper)) { @@ -3200,6 +3193,10 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener sendOnTextChanged(text, 0, oldlen, textLength); onTextChanged(text, 0, oldlen, textLength); + if (startSpellCheck) { + updateSpellCheckSpans(0, textLength); + } + if (needEditableForNotification) { sendAfterTextChanged((Editable) text); } @@ -7113,8 +7110,9 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener * to turn off ellipsizing. * * If {@link #setMaxLines} has been used to set two or more lines, - * {@link TextUtils.TruncateAt#END} and {@link TextUtils.TruncateAt#MARQUEE} - * are only supported (other ellipsizing types will not do anything). + * {@link android.text.TextUtils.TruncateAt#END} and + * {@link android.text.TextUtils.TruncateAt#MARQUEE}* are only supported + * (other ellipsizing types will not do anything). * * @attr ref android.R.styleable#TextView_ellipsize */ @@ -7376,7 +7374,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener * @param lengthAfter The length of the replacement modified text */ protected void onTextChanged(CharSequence text, int start, int lengthBefore, int lengthAfter) { - // intentionally empty + // intentionally empty, template pattern method can be overridden by subclasses } /** @@ -7388,6 +7386,9 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener */ protected void onSelectionChanged(int selStart, int selEnd) { sendAccessibilityEvent(AccessibilityEvent.TYPE_VIEW_TEXT_SELECTION_CHANGED); + if (mSpellChecker != null) { + mSpellChecker.onSelectionChanged(); + } } /** @@ -7422,8 +7423,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener } } - private void sendBeforeTextChanged(CharSequence text, int start, int before, - int after) { + private void sendBeforeTextChanged(CharSequence text, int start, int before, int after) { if (mListeners != null) { final ArrayList<TextWatcher> list = mListeners; final int count = list.size(); @@ -7431,14 +7431,32 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener list.get(i).beforeTextChanged(text, start, before, after); } } + + // The spans that are inside or intersect the modified region no longer make sense + removeIntersectingSpans(start, start + before, SpellCheckSpan.class); + removeIntersectingSpans(start, start + before, SuggestionSpan.class); + } + + // Removes all spans that are inside or actually overlap the start..end range + private <T> void removeIntersectingSpans(int start, int end, Class<T> type) { + if (!(mText instanceof Editable)) return; + Editable text = (Editable) mText; + + T[] spans = text.getSpans(start, end, type); + final int length = spans.length; + for (int i = 0; i < length; i++) { + final int s = text.getSpanStart(spans[i]); + final int e = text.getSpanEnd(spans[i]); + if (e == start || s == end) break; + text.removeSpan(spans[i]); + } } /** * Not private so it can be called from an inner class without going * through a thunk. */ - void sendOnTextChanged(CharSequence text, int start, int before, - int after) { + void sendOnTextChanged(CharSequence text, int start, int before, int after) { if (mListeners != null) { final ArrayList<TextWatcher> list = mListeners; final int count = list.size(); @@ -7486,6 +7504,11 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener sendOnTextChanged(buffer, start, before, after); onTextChanged(buffer, start, before, after); + // The WordIterator text change listener may be called after this one. + // Make sure this changed text is rescanned before the iterator is used on it. + getWordIterator().forceUpdate(); + updateSpellCheckSpans(start, start + after); + // Hide the controllers if the amount of content changed if (before != after) { hideControllers(); @@ -7573,7 +7596,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener } } } - + if (what instanceof ParcelableSpan) { // If this is a span that can be sent to a remote process, // the current extract editor would be interested in it. @@ -7603,10 +7626,102 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener } } } + + if (what instanceof SpellCheckSpan) { + if (newStart < 0) { + getSpellChecker().removeSpellCheckSpan((SpellCheckSpan) what); + } else if (oldStart < 0) { + getSpellChecker().addSpellCheckSpan((SpellCheckSpan) what); + } + } + + if (what instanceof SuggestionSpan) { + if (newStart < 0) { + Log.d("spellcheck", "REMOVE suggspan " + mText.subSequence(oldStart, oldEnd)); + } + } + } + + /** + * Create new SpellCheckSpans on the modified region. + */ + private void updateSpellCheckSpans(int start, int end) { + if (!(mText instanceof Editable) || !isSuggestionsEnabled()) return; + Editable text = (Editable) mText; + + WordIterator wordIterator = getWordIterator(); + wordIterator.setCharSequence(text); + + // Move back to the beginning of the current word, if any + int wordStart = wordIterator.preceding(start); + int wordEnd; + if (wordStart == BreakIterator.DONE) { + wordEnd = wordIterator.following(start); + if (wordEnd != BreakIterator.DONE) { + wordStart = wordIterator.getBeginning(wordEnd); + } + } else { + wordEnd = wordIterator.getEnd(wordStart); + } + if (wordEnd == BreakIterator.DONE) { + return; + } + + // Iterate over the newly added text and schedule new SpellCheckSpans + while (wordStart <= end) { + if (wordEnd >= start) { + // A word across the interval boundaries must remove boundary edition spans + if (wordStart < start && wordEnd > start) { + removeEditionSpansAt(start, text); + } + + if (wordStart < end && wordEnd > end) { + removeEditionSpansAt(end, text); + } + + // Do not create new boundary spans if they already exist + boolean createSpellCheckSpan = true; + if (wordEnd == start) { + SpellCheckSpan[] spellCheckSpans = text.getSpans(start, start, + SpellCheckSpan.class); + if (spellCheckSpans.length > 0) createSpellCheckSpan = false; + } + + if (wordStart == end) { + SpellCheckSpan[] spellCheckSpans = text.getSpans(end, end, + SpellCheckSpan.class); + if (spellCheckSpans.length > 0) createSpellCheckSpan = false; + } + + if (createSpellCheckSpan) { + text.setSpan(new SpellCheckSpan(), wordStart, wordEnd, + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + } + } + + // iterate word by word + wordEnd = wordIterator.following(wordEnd); + if (wordEnd == BreakIterator.DONE) return; + wordStart = wordIterator.getBeginning(wordEnd); + if (wordStart == BreakIterator.DONE) { + Log.e(LOG_TAG, "Unable to find word beginning from " + wordEnd + "in " + mText); + return; + } + } + } + + private static void removeEditionSpansAt(int offset, Editable text) { + SuggestionSpan[] suggestionSpans = text.getSpans(offset, offset, SuggestionSpan.class); + for (int i = 0; i < suggestionSpans.length; i++) { + text.removeSpan(suggestionSpans[i]); + } + SpellCheckSpan[] spellCheckSpans = text.getSpans(offset, offset, SpellCheckSpan.class); + for (int i = 0; i < spellCheckSpans.length; i++) { + text.removeSpan(spellCheckSpans[i]); + } } - private class ChangeWatcher - implements TextWatcher, SpanWatcher { + private class ChangeWatcher implements TextWatcher, SpanWatcher { private CharSequence mBeforeText; @@ -7631,8 +7746,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener TextView.this.handleTextChanged(buffer, start, before, after); if (AccessibilityManager.getInstance(mContext).isEnabled() && - (isFocused() || isSelected() && - isShown())) { + (isFocused() || isSelected() && isShown())) { sendAccessibilityEventTypeViewTextChanged(mBeforeText, start, before, after); mBeforeText = null; } @@ -7642,8 +7756,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener if (DEBUG_EXTRACT) Log.v(LOG_TAG, "afterTextChanged: " + buffer); TextView.this.sendAfterTextChanged(buffer); - if (MetaKeyKeyListener.getMetaState(buffer, - MetaKeyKeyListener.META_SELECTING) != 0) { + if (MetaKeyKeyListener.getMetaState(buffer, MetaKeyKeyListener.META_SELECTING) != 0) { MetaKeyKeyListener.stopSelecting(TextView.this, buffer); } } @@ -7841,17 +7954,20 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener if (mInputContentType != null) { mInputContentType.enterDown = false; } + hideControllers(); - removeAllSuggestionSpans(); + + removeSpans(0, mText.length(), SuggestionSpan.class); + removeSpans(0, mText.length(), SpellCheckSpan.class); } startStopMarquee(hasWindowFocus); } - private void removeAllSuggestionSpans() { + private void removeSpans(int start, int end, Class<?> type) { if (mText instanceof Editable) { Editable editable = ((Editable) mText); - SuggestionSpan[] spans = editable.getSpans(0, mText.length(), SuggestionSpan.class); + Object[] spans = editable.getSpans(start, end, type); final int length = spans.length; for (int i = 0; i < length; i++) { editable.removeSpan(spans[i]); @@ -7969,6 +8085,8 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener } } } + + handled = true; } if (handled) { @@ -7980,11 +8098,22 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener } /** + * @return <code>true</code> if the cursor/current selection overlaps a {@link SuggestionSpan}. + */ + private boolean isCursorInsideSuggestionSpan() { + if (!(mText instanceof Spannable)) return false; + + SuggestionSpan[] suggestionSpans = ((Spannable) mText).getSpans(getSelectionStart(), + getSelectionEnd(), SuggestionSpan.class); + return (suggestionSpans.length > 0); + } + + /** * @return <code>true</code> if the cursor is inside an {@link SuggestionSpan} with * {@link SuggestionSpan#FLAG_EASY_CORRECT} set. */ private boolean isCursorInsideEasyCorrectionSpan() { - Spannable spannable = (Spannable) TextView.this.mText; + Spannable spannable = (Spannable) mText; SuggestionSpan[] suggestionSpans = spannable.getSpans(getSelectionStart(), getSelectionEnd(), SuggestionSpan.class); for (int i = 0; i < suggestionSpans.length; i++) { @@ -8445,16 +8574,14 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener selectionStart = ((Spanned) mText).getSpanStart(url); selectionEnd = ((Spanned) mText).getSpanEnd(url); } else { - if (mWordIterator == null) { - mWordIterator = new WordIterator(); - } - // WordIerator handles text changes, this is a no-op if text in unchanged. - mWordIterator.setCharSequence(mText); + WordIterator wordIterator = getWordIterator(); + // WordIterator handles text changes, this is a no-op if text in unchanged. + wordIterator.setCharSequence(mText); - selectionStart = mWordIterator.getBeginning(minOffset); + selectionStart = wordIterator.getBeginning(minOffset); if (selectionStart == BreakIterator.DONE) return false; - selectionEnd = mWordIterator.getEnd(maxOffset); + selectionEnd = wordIterator.getEnd(maxOffset); if (selectionEnd == BreakIterator.DONE) return false; } @@ -8462,6 +8589,20 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener return true; } + WordIterator getWordIterator() { + if (mWordIterator == null) { + mWordIterator = new WordIterator(); + } + return mWordIterator; + } + + private SpellChecker getSpellChecker() { + if (mSpellChecker == null) { + mSpellChecker = new SpellChecker(this); + } + return mSpellChecker; + } + private long getLastTouchOffsets() { int minOffset, maxOffset; @@ -8790,7 +8931,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener final int offset = getOffsetForPosition(mLastDownPositionX, mLastDownPositionY); stopSelectionActionMode(); Selection.setSelection((Spannable) mText, offset); - getInsertionController().showImmediately(); + getInsertionController().showWithActionPopup(); handled = true; } @@ -9067,10 +9208,12 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener } private class SuggestionsPopupWindow extends PinnedPopupWindow implements OnClickListener { - private static final int MAX_NUMBER_SUGGESTIONS = 5; + private static final int MAX_NUMBER_SUGGESTIONS = SuggestionSpan.SUGGESTIONS_MAX_SIZE; private static final int NO_SUGGESTIONS = -1; + private static final float AVERAGE_HIGHLIGHTS_PER_SUGGESTION = 1.4f; private WordIterator mSuggestionWordIterator; - private TextAppearanceSpan[] mHighlightSpans = new TextAppearanceSpan[0]; + private TextAppearanceSpan[] mHighlightSpans = new TextAppearanceSpan + [(int) (AVERAGE_HIGHLIGHTS_PER_SUGGESTION * MAX_NUMBER_SUGGESTIONS)]; @Override protected void createPopupWindow() { @@ -9149,9 +9292,10 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener @Override public void show() { if (!(mText instanceof Editable)) return; - updateSuggestions(); - super.show(); + if (updateSuggestions()) { + super.show(); + } } @Override @@ -9179,7 +9323,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener } } - private void updateSuggestions() { + private boolean updateSuggestions() { Spannable spannable = (Spannable)TextView.this.mText; SuggestionSpan[] suggestionSpans = getSuggestionSpans(); @@ -9217,22 +9361,15 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener } } - if (totalNbSuggestions == 0) { - // TODO Replace by final text, use a dedicated layout, add a fade out timer... - TextView textView = (TextView) mContentView.getChildAt(0); - textView.setText("No suggestions available"); - SuggestionInfo suggestionInfo = (SuggestionInfo) textView.getTag(); - suggestionInfo.spanStart = NO_SUGGESTIONS; - totalNbSuggestions++; - } else { - if (mSuggestionRangeSpan == null) mSuggestionRangeSpan = new SuggestionRangeSpan(); - ((Editable) mText).setSpan(mSuggestionRangeSpan, spanUnionStart, spanUnionEnd, - Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + if (totalNbSuggestions == 0) return false; - for (int i = 0; i < totalNbSuggestions; i++) { - final TextView textView = (TextView) mContentView.getChildAt(i); - highlightTextDifferences(textView, spanUnionStart, spanUnionEnd); - } + if (mSuggestionRangeSpan == null) mSuggestionRangeSpan = new SuggestionRangeSpan(); + ((Editable) mText).setSpan(mSuggestionRangeSpan, spanUnionStart, spanUnionEnd, + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + + for (int i = 0; i < totalNbSuggestions; i++) { + final TextView textView = (TextView) mContentView.getChildAt(i); + highlightTextDifferences(textView, spanUnionStart, spanUnionEnd); } for (int i = 0; i < totalNbSuggestions; i++) { @@ -9241,6 +9378,27 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener for (int i = totalNbSuggestions; i < MAX_NUMBER_SUGGESTIONS; i++) { mContentView.getChildAt(i).setVisibility(GONE); } + + return true; + } + + private void onDictionarySuggestionsReceived(String[] suggestions) { + if (suggestions.length == 0) { + // TODO Actual implementation of this feature + suggestions = new String[] {"Add to dictionary"}; + } + + WordIterator wordIterator = getWordIterator(); + wordIterator.setCharSequence(mText); + + final int pos = getSelectionStart(); + int wordStart = wordIterator.getBeginning(pos); + int wordEnd = wordIterator.getEnd(pos); + + SuggestionSpan suggestionSpan = new SuggestionSpan(getContext(), suggestions, 0); + ((Editable) mText).setSpan(suggestionSpan, wordStart, wordEnd, + Spanned.SPAN_EXCLUSIVE_EXCLUSIVE); + show(); } private long[] getWordLimits(CharSequence text) { @@ -9422,6 +9580,15 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener final String originalText = mText.subSequence(spanStart, spanEnd).toString(); ((Editable) mText).replace(spanStart, spanEnd, suggestion); + // A replacement on a misspelled text removes the misspelled flag. + // TODO restore the flag if the misspelled word is selected back? + int suggestionSpanFlags = suggestionInfo.suggestionSpan.getFlags(); + if ((suggestionSpanFlags & SuggestionSpan.FLAG_MISSPELLED) > 0) { + suggestionSpanFlags &= ~(SuggestionSpan.FLAG_MISSPELLED); + suggestionSpanFlags &= ~(SuggestionSpan.FLAG_EASY_CORRECT); + suggestionInfo.suggestionSpan.setFlags(suggestionSpanFlags); + } + // Notify source IME of the suggestion pick. Do this before swaping texts. if (!TextUtils.isEmpty( suggestionInfo.suggestionSpan.getNotificationTargetClassName())) { @@ -9471,53 +9638,46 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener boolean areSuggestionsShown() { return mSuggestionsPopupWindow != null && mSuggestionsPopupWindow.isShowing(); - } + } + + void onDictionarySuggestionsReceived(String[] suggestions) { + if (mSuggestionsPopupWindow != null) { + mSuggestionsPopupWindow.onDictionarySuggestionsReceived(suggestions); + } + } /** - * Some parts of the text can have alternate suggestion text attached. This is typically done by - * the IME by adding {@link SuggestionSpan}s to the text. + * Return whether or not suggestions are enabled on this TextView. The suggestions are generated + * by the IME or by the spell checker as the user types. This is done by adding + * {@link SuggestionSpan}s to the text. * * When suggestions are enabled (default), this list of suggestions will be displayed when the - * user double taps on these parts of the text. No suggestions are displayed when this value is - * false. Use {@link #setSuggestionsEnabled(boolean)} to change this value. - * - * Note that suggestions are only enabled for a subset of input types. In addition to setting - * this flag to <code>true</code> using {@link #setSuggestionsEnabled(boolean)} or the - * <code>android:suggestionsEnabled</code> xml attribute, this method will return - * <code>true</code> only if the class of your input type is {@link InputType#TYPE_CLASS_TEXT}. - * In addition, the type variation must also be one of + * user asks for them on these parts of the text. This value depends on the inputType of this + * TextView. + * + * The class of the input type must be {@link InputType#TYPE_CLASS_TEXT}. + * + * In addition, the type variation must be one of * {@link InputType#TYPE_TEXT_VARIATION_NORMAL}, * {@link InputType#TYPE_TEXT_VARIATION_EMAIL_SUBJECT}, * {@link InputType#TYPE_TEXT_VARIATION_LONG_MESSAGE}, * {@link InputType#TYPE_TEXT_VARIATION_SHORT_MESSAGE} or * {@link InputType#TYPE_TEXT_VARIATION_WEB_EDIT_TEXT}. * - * @return true if the suggestions popup window is enabled. + * And finally, the {@link InputType#TYPE_TEXT_FLAG_NO_SUGGESTIONS} flag must <i>not</i> be set. * - * @attr ref android.R.styleable#TextView_suggestionsEnabled + * @return true if the suggestions popup window is enabled, based on the inputType. */ public boolean isSuggestionsEnabled() { - if (!mSuggestionsEnabled) return false; if ((mInputType & InputType.TYPE_MASK_CLASS) != InputType.TYPE_CLASS_TEXT) return false; + if ((mInputType & InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS) > 0) return false; + final int variation = mInputType & EditorInfo.TYPE_MASK_VARIATION; - if (variation == EditorInfo.TYPE_TEXT_VARIATION_NORMAL || + return (variation == EditorInfo.TYPE_TEXT_VARIATION_NORMAL || variation == EditorInfo.TYPE_TEXT_VARIATION_EMAIL_SUBJECT || variation == EditorInfo.TYPE_TEXT_VARIATION_LONG_MESSAGE || variation == EditorInfo.TYPE_TEXT_VARIATION_SHORT_MESSAGE || - variation == EditorInfo.TYPE_TEXT_VARIATION_WEB_EDIT_TEXT) return true; - - return false; - } - - /** - * Enables or disables the suggestion popup. See {@link #isSuggestionsEnabled()}. - * - * @param enabled Whether or not suggestions are enabled. - * - * @attr ref android.R.styleable#TextView_suggestionsEnabled - */ - public void setSuggestionsEnabled(boolean enabled) { - mSuggestionsEnabled = enabled; + variation == EditorInfo.TYPE_TEXT_VARIATION_WEB_EDIT_TEXT); } /** @@ -9787,11 +9947,11 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener @Override public void show() { boolean canPaste = canPaste(); - boolean suggestionsEnabled = isSuggestionsEnabled(); + boolean canSuggest = isSuggestionsEnabled() && isCursorInsideSuggestionSpan(); mPasteTextView.setVisibility(canPaste ? View.VISIBLE : View.GONE); - mReplaceTextView.setVisibility(suggestionsEnabled ? View.VISIBLE : View.GONE); + mReplaceTextView.setVisibility(canSuggest ? View.VISIBLE : View.GONE); - if (!canPaste && !suggestionsEnabled) return; + if (!canPaste && !canSuggest) return; super.show(); } @@ -9802,6 +9962,9 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener onTextContextMenuItem(ID_PASTE); hide(); } else if (view == mReplaceTextView) { + final int middle = (getSelectionStart() + getSelectionEnd()) / 2; + stopSelectionActionMode(); + Selection.setSelection((Spannable) mText, middle); showSuggestions(); } } @@ -10133,17 +10296,18 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener @Override public void show() { super.show(); - hideAfterDelay(); - } - - public void show(int delayBeforeShowActionPopup) { - show(); final long durationSinceCutOrCopy = SystemClock.uptimeMillis() - sLastCutOrCopyTime; if (durationSinceCutOrCopy < RECENT_CUT_COPY_DURATION) { - delayBeforeShowActionPopup = 0; + showActionPopupWindow(0); } - showActionPopupWindow(delayBeforeShowActionPopup); + + hideAfterDelay(); + } + + public void showWithActionPopup() { + show(); + showActionPopupWindow(0); } private void hideAfterDelay() { @@ -10194,7 +10358,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener // Tapping on the handle dismisses the displayed action popup mActionPopupWindow.hide(); } else { - show(0); + showWithActionPopup(); } } } @@ -10349,16 +10513,14 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener } private class InsertionPointCursorController implements CursorController { - private static final int DELAY_BEFORE_PASTE_ACTION = 1600; - private InsertionHandleView mHandle; public void show() { - getHandle().show(DELAY_BEFORE_PASTE_ACTION); + getHandle().show(); } - public void showImmediately() { - getHandle().show(0); + public void showWithActionPopup() { + getHandle().showWithActionPopup(); } public void hide() { @@ -10390,7 +10552,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener } private class SelectionModifierCursorController implements CursorController { - private static final int DELAY_BEFORE_REPLACE_ACTION = 1200; + private static final int DELAY_BEFORE_REPLACE_ACTION = 200; // milliseconds // The cursor controller handles, lazily created when shown. private SelectionStartHandleView mStartHandle; private SelectionEndHandleView mEndHandle; @@ -10879,8 +11041,8 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener private int mAutoLinkMask; private boolean mLinksClickable = true; - private float mSpacingMult = 1; - private float mSpacingAdd = 0; + private float mSpacingMult = 1.0f; + private float mSpacingAdd = 0.0f; private boolean mTextIsSelectable = false; private static final int LINES = 1; diff --git a/core/res/res/drawable-hdpi/ic_menu_selectall_holo_dark.png b/core/res/res/drawable-hdpi/ic_menu_selectall_holo_dark.png Binary files differindex 5579443..b161361 100644 --- a/core/res/res/drawable-hdpi/ic_menu_selectall_holo_dark.png +++ b/core/res/res/drawable-hdpi/ic_menu_selectall_holo_dark.png diff --git a/core/res/res/drawable-hdpi/ic_menu_selectall_holo_light.png b/core/res/res/drawable-hdpi/ic_menu_selectall_holo_light.png Binary files differindex 6674914..0a7b364 100644 --- a/core/res/res/drawable-hdpi/ic_menu_selectall_holo_light.png +++ b/core/res/res/drawable-hdpi/ic_menu_selectall_holo_light.png diff --git a/core/res/res/layout/keyguard_screen_password_landscape.xml b/core/res/res/layout/keyguard_screen_password_landscape.xml index bc86ab7..12df99e 100644 --- a/core/res/res/layout/keyguard_screen_password_landscape.xml +++ b/core/res/res/layout/keyguard_screen_password_landscape.xml @@ -151,7 +151,6 @@ android:background="@drawable/lockscreen_password_field_dark" android:textColor="?android:attr/textColorPrimary" android:imeOptions="flagNoFullscreen|actionDone" - android:suggestionsEnabled="false" /> <ImageView android:id="@+id/switch_ime_button" diff --git a/core/res/res/layout/keyguard_screen_password_portrait.xml b/core/res/res/layout/keyguard_screen_password_portrait.xml index 994c439..6145e47 100644 --- a/core/res/res/layout/keyguard_screen_password_portrait.xml +++ b/core/res/res/layout/keyguard_screen_password_portrait.xml @@ -114,7 +114,7 @@ android:textAppearance="?android:attr/textAppearanceMedium" android:textColor="#ffffffff" android:imeOptions="actionDone" - android:suggestionsEnabled="false"/> + /> <ImageView android:id="@+id/switch_ime_button" android:layout_width="wrap_content" diff --git a/core/res/res/values/attrs.xml b/core/res/res/values/attrs.xml index 8db6b4f..7bb5e06 100755 --- a/core/res/res/values/attrs.xml +++ b/core/res/res/values/attrs.xml @@ -881,10 +881,6 @@ Default value is false. EditText content is always selectable. --> <attr name="textIsSelectable" format="boolean" /> - <!-- When true, IME suggestions will be displayed when the user double taps on editable text. - The default value is true. --> - <attr name="suggestionsEnabled" format="boolean" /> - <!-- Where to ellipsize text. --> <attr name="ellipsize"> <enum name="none" value="0" /> @@ -3148,8 +3144,6 @@ <!-- Indicates that the content of a non-editable text can be selected. --> <attr name="textIsSelectable" /> - <!-- Suggestions will be displayed when the user double taps on editable text. --> - <attr name="suggestionsEnabled" /> <!-- Present the text in ALL CAPS. This may use a small-caps form when available. --> <attr name="textAllCaps" /> </declare-styleable> diff --git a/core/res/res/values/public.xml b/core/res/res/values/public.xml index a6bf1e0..b9d05fd 100644 --- a/core/res/res/values/public.xml +++ b/core/res/res/values/public.xml @@ -1717,8 +1717,6 @@ <public type="attr" name="textSuggestionsWindowStyle" /> <public type="attr" name="textEditSuggestionItemLayout" /> - <public type="attr" name="suggestionsEnabled" /> - <public type="attr" name="rowCount" /> <public type="attr" name="rowOrderPreserved" /> <public type="attr" name="columnCount" /> diff --git a/core/res/res/values/strings.xml b/core/res/res/values/strings.xml index c80923d..e31a215 100755 --- a/core/res/res/values/strings.xml +++ b/core/res/res/values/strings.xml @@ -2470,7 +2470,7 @@ <string name="paste">Paste</string> <!-- Item on EditText context menu. This action is used to replace the current word by other suggested words, suggested by the IME or the spell checker --> - <string name="replace">Replace</string> + <string name="replace">Replace\u2026</string> <!-- Item on EditText context menu. This action is used to copy a URL from the edit field into the clipboard. --> <string name="copyUrl">Copy URL</string> |