diff options
author | Gilles Debunne <debunne@google.com> | 2011-08-24 17:53:42 -0700 |
---|---|---|
committer | Android (Google) Code Review <android-gerrit@google.com> | 2011-08-24 17:53:42 -0700 |
commit | f874c4f93437cbbef7a915a36d5abb448e1e3209 (patch) | |
tree | 332b93e50437d5dc79be32d63eda55bcc29c29cb /core | |
parent | 7d0dd6addba0f72acb30eea7e9804f56f8c8646f (diff) | |
parent | 6435a56a8c02de98befcc8cd743b2b638cffb327 (diff) | |
download | frameworks_base-f874c4f93437cbbef7a915a36d5abb448e1e3209.zip frameworks_base-f874c4f93437cbbef7a915a36d5abb448e1e3209.tar.gz frameworks_base-f874c4f93437cbbef7a915a36d5abb448e1e3209.tar.bz2 |
Merge "Spell checking in TextViews"
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 bb61c72..a536961 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 8eaac8c..9455124 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> |