Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit 6435a56a authored by Gilles Debunne's avatar Gilles Debunne
Browse files

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
parent defa12e9
Loading
Loading
Loading
Loading
+58 −59

File changed.

Preview size limit exceeded, changes collapsed.

+44 −81
Original line number Diff line number Diff line
@@ -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++) {
+4 −0
Original line number Diff line number Diff line
@@ -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.
+41 −0
Original line number Diff line number Diff line
/*
 * 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;
    }
}
+10 −7
Original line number Diff line number Diff line
@@ -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 =
Loading