summaryrefslogtreecommitdiffstats
path: root/core
diff options
context:
space:
mode:
authorGilles Debunne <debunne@google.com>2011-08-04 21:22:30 -0700
committerGilles Debunne <debunne@google.com>2011-08-23 13:13:54 -0700
commit6435a56a8c02de98befcc8cd743b2b638cffb327 (patch)
tree62a5678b5531f53c7b25e920f8f9c6b65f8b510c /core
parentdefa12e95b8d25db5f3e9a044e83d6fe680b67a3 (diff)
downloadframeworks_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.java125
-rw-r--r--core/java/android/text/method/WordIterator.java4
-rw-r--r--core/java/android/text/style/SpellCheckSpan.java41
-rw-r--r--core/java/android/text/style/SuggestionSpan.java17
-rw-r--r--core/java/android/view/textservice/SuggestionsInfo.java4
-rw-r--r--core/java/android/widget/SpellChecker.java226
-rw-r--r--core/java/android/widget/TextView.java376
-rw-r--r--core/res/res/drawable-hdpi/ic_menu_selectall_holo_dark.pngbin709 -> 626 bytes
-rw-r--r--core/res/res/drawable-hdpi/ic_menu_selectall_holo_light.pngbin945 -> 435 bytes
-rw-r--r--core/res/res/layout/keyguard_screen_password_landscape.xml1
-rw-r--r--core/res/res/layout/keyguard_screen_password_portrait.xml2
-rwxr-xr-xcore/res/res/values/attrs.xml6
-rw-r--r--core/res/res/values/public.xml2
-rwxr-xr-xcore/res/res/values/strings.xml2
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
index 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
Binary files differ
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
index 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
Binary files differ
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>