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

Commit f2560e62 authored by James Cook's avatar James Cook Committed by Android (Google) Code Review
Browse files

Merge "Improve undo support for text entered with IME"

parents 43c410ea d2026686
Loading
Loading
Loading
Loading
+1 −0
Original line number Original line Diff line number Diff line
@@ -30802,6 +30802,7 @@ package android.text {
    method public int getSpanStart(java.lang.Object);
    method public int getSpanStart(java.lang.Object);
    method public T[] getSpans(int, int, java.lang.Class<T>);
    method public T[] getSpans(int, int, java.lang.Class<T>);
    method public deprecated int getTextRunCursor(int, int, int, int, int, android.graphics.Paint);
    method public deprecated int getTextRunCursor(int, int, int, int, int, android.graphics.Paint);
    method public int getTextWatcherDepth();
    method public android.text.SpannableStringBuilder insert(int, java.lang.CharSequence, int, int);
    method public android.text.SpannableStringBuilder insert(int, java.lang.CharSequence, int, int);
    method public android.text.SpannableStringBuilder insert(int, java.lang.CharSequence);
    method public android.text.SpannableStringBuilder insert(int, java.lang.CharSequence);
    method public int length();
    method public int length();
+1 −0
Original line number Original line Diff line number Diff line
@@ -33158,6 +33158,7 @@ package android.text {
    method public int getSpanStart(java.lang.Object);
    method public int getSpanStart(java.lang.Object);
    method public T[] getSpans(int, int, java.lang.Class<T>);
    method public T[] getSpans(int, int, java.lang.Class<T>);
    method public deprecated int getTextRunCursor(int, int, int, int, int, android.graphics.Paint);
    method public deprecated int getTextRunCursor(int, int, int, int, int, android.graphics.Paint);
    method public int getTextWatcherDepth();
    method public android.text.SpannableStringBuilder insert(int, java.lang.CharSequence, int, int);
    method public android.text.SpannableStringBuilder insert(int, java.lang.CharSequence, int, int);
    method public android.text.SpannableStringBuilder insert(int, java.lang.CharSequence);
    method public android.text.SpannableStringBuilder insert(int, java.lang.CharSequence);
    method public int length();
    method public int length();
+15 −14
Original line number Original line Diff line number Diff line
@@ -119,15 +119,13 @@ public class UndoManager {
    }
    }


    /**
    /**
     * Flatten the current undo state into a Parcelable object, which can later be restored
     * Flatten the current undo state into a Parcel object, which can later be restored
     * with {@link #restoreInstanceState(android.os.Parcelable)}.
     * with {@link #restoreInstanceState(android.os.Parcel, java.lang.ClassLoader)}.
     */
     */
    public Parcelable saveInstanceState() {
    public void saveInstanceState(Parcel p) {
        if (mUpdateCount > 0) {
        if (mUpdateCount > 0) {
            throw new IllegalStateException("Can't save state while updating");
            throw new IllegalStateException("Can't save state while updating");
        }
        }
        ParcelableParcel pp = new ParcelableParcel(getClass().getClassLoader());
        Parcel p = pp.getParcel();
        mStateSeq++;
        mStateSeq++;
        if (mStateSeq <= 0) {
        if (mStateSeq <= 0) {
            mStateSeq = 0;
            mStateSeq = 0;
@@ -151,7 +149,6 @@ public class UndoManager {
            mRedos.get(i).writeToParcel(p);
            mRedos.get(i).writeToParcel(p);
        }
        }
        p.writeInt(0);
        p.writeInt(0);
        return pp;
    }
    }


    void saveOwner(UndoOwner owner, Parcel out) {
    void saveOwner(UndoOwner owner, Parcel out) {
@@ -168,26 +165,24 @@ public class UndoManager {
    }
    }


    /**
    /**
     * Restore an undo state previously created with {@link #saveInstanceState()}.  This will
     * Restore an undo state previously created with {@link #saveInstanceState(Parcel)}.  This
     * restore the UndoManager's state to almost exactly what it was at the point it had
     * will restore the UndoManager's state to almost exactly what it was at the point it had
     * been previously saved; the only information not restored is the data object
     * been previously saved; the only information not restored is the data object
     * associated with each {@link UndoOwner}, which requires separate calls to
     * associated with each {@link UndoOwner}, which requires separate calls to
     * {@link #getOwner(String, Object)} to re-associate the owner with its data.
     * {@link #getOwner(String, Object)} to re-associate the owner with its data.
     */
     */
    public void restoreInstanceState(Parcelable state) {
    public void restoreInstanceState(Parcel p, ClassLoader loader) {
        if (mUpdateCount > 0) {
        if (mUpdateCount > 0) {
            throw new IllegalStateException("Can't save state while updating");
            throw new IllegalStateException("Can't save state while updating");
        }
        }
        forgetUndos(null, -1);
        forgetUndos(null, -1);
        forgetRedos(null, -1);
        forgetRedos(null, -1);
        ParcelableParcel pp = (ParcelableParcel)state;
        Parcel p = pp.getParcel();
        mHistorySize = p.readInt();
        mHistorySize = p.readInt();
        mStateOwners = new UndoOwner[p.readInt()];
        mStateOwners = new UndoOwner[p.readInt()];


        int stype;
        int stype;
        while ((stype=p.readInt()) != 0) {
        while ((stype=p.readInt()) != 0) {
            UndoState ustate = new UndoState(this, p, pp.getClassLoader());
            UndoState ustate = new UndoState(this, p, loader);
            if (stype == 1) {
            if (stype == 1) {
                mUndos.add(0, ustate);
                mUndos.add(0, ustate);
            } else {
            } else {
@@ -311,12 +306,15 @@ public class UndoManager {
        }
        }


        int removed = 0;
        int removed = 0;
        for (int i=0; i<mUndos.size() && removed < count; i++) {
        int i = 0;
        while (i < mUndos.size() && removed < count) {
            UndoState state = mUndos.get(i);
            UndoState state = mUndos.get(i);
            if (count > 0 && matchOwners(state, owners)) {
            if (count > 0 && matchOwners(state, owners)) {
                state.destroy();
                state.destroy();
                mUndos.remove(i);
                mUndos.remove(i);
                removed++;
                removed++;
            } else {
                i++;
            }
            }
        }
        }


@@ -329,12 +327,15 @@ public class UndoManager {
        }
        }


        int removed = 0;
        int removed = 0;
        for (int i=0; i<mRedos.size() && removed < count; i++) {
        int i = 0;
        while (i < mRedos.size() && removed < count) {
            UndoState state = mRedos.get(i);
            UndoState state = mRedos.get(i);
            if (count > 0 && matchOwners(state, owners)) {
            if (count > 0 && matchOwners(state, owners)) {
                state.destroy();
                state.destroy();
                mRedos.remove(i);
                mRedos.remove(i);
                removed++;
                removed++;
            } else {
                i++;
            }
            }
        }
        }


+19 −0
Original line number Original line Diff line number Diff line
@@ -1006,28 +1006,43 @@ public class SpannableStringBuilder implements CharSequence, GetChars, Spannable
        return new String(buf);
        return new String(buf);
    }
    }


    /**
     * Returns the depth of TextWatcher callbacks. Returns 0 when the object is not handling
     * TextWatchers. A return value greater than 1 implies that a TextWatcher caused a change that
     * recursively triggered a TextWatcher.
     */
    public int getTextWatcherDepth() {
        return mTextWatcherDepth;
    }

    private void sendBeforeTextChanged(TextWatcher[] watchers, int start, int before, int after) {
    private void sendBeforeTextChanged(TextWatcher[] watchers, int start, int before, int after) {
        int n = watchers.length;
        int n = watchers.length;


        mTextWatcherDepth++;
        for (int i = 0; i < n; i++) {
        for (int i = 0; i < n; i++) {
            watchers[i].beforeTextChanged(this, start, before, after);
            watchers[i].beforeTextChanged(this, start, before, after);
        }
        }
        mTextWatcherDepth--;
    }
    }


    private void sendTextChanged(TextWatcher[] watchers, int start, int before, int after) {
    private void sendTextChanged(TextWatcher[] watchers, int start, int before, int after) {
        int n = watchers.length;
        int n = watchers.length;


        mTextWatcherDepth++;
        for (int i = 0; i < n; i++) {
        for (int i = 0; i < n; i++) {
            watchers[i].onTextChanged(this, start, before, after);
            watchers[i].onTextChanged(this, start, before, after);
        }
        }
        mTextWatcherDepth--;
    }
    }


    private void sendAfterTextChanged(TextWatcher[] watchers) {
    private void sendAfterTextChanged(TextWatcher[] watchers) {
        int n = watchers.length;
        int n = watchers.length;


        mTextWatcherDepth++;
        for (int i = 0; i < n; i++) {
        for (int i = 0; i < n; i++) {
            watchers[i].afterTextChanged(this);
            watchers[i].afterTextChanged(this);
        }
        }
        mTextWatcherDepth--;
    }
    }


    private void sendSpanAdded(Object what, int start, int end) {
    private void sendSpanAdded(Object what, int start, int end) {
@@ -1524,6 +1539,10 @@ public class SpannableStringBuilder implements CharSequence, GetChars, Spannable
    private IdentityHashMap<Object, Integer> mIndexOfSpan;
    private IdentityHashMap<Object, Integer> mIndexOfSpan;
    private int mLowWaterMark;  // indices below this have not been touched
    private int mLowWaterMark;  // indices below this have not been touched


    // TextWatcher callbacks may trigger changes that trigger more callbacks. This keeps track of
    // how deep the callbacks go.
    private int mTextWatcherDepth;

    // TODO These value are tightly related to the public SPAN_MARK/POINT values in {@link Spanned}
    // TODO These value are tightly related to the public SPAN_MARK/POINT values in {@link Spanned}
    private static final int MARK = 1;
    private static final int MARK = 1;
    private static final int POINT = 2;
    private static final int POINT = 2;
+96 −20
Original line number Original line Diff line number Diff line
@@ -236,12 +236,17 @@ public class Editor {
    }
    }


    ParcelableParcel saveInstanceState() {
    ParcelableParcel saveInstanceState() {
        // For now there is only undo state.
        ParcelableParcel state = new ParcelableParcel(getClass().getClassLoader());
        return (ParcelableParcel) mUndoManager.saveInstanceState();
        Parcel parcel = state.getParcel();
        mUndoManager.saveInstanceState(parcel);
        mUndoInputFilter.saveInstanceState(parcel);
        return state;
    }
    }


    void restoreInstanceState(ParcelableParcel state) {
    void restoreInstanceState(ParcelableParcel state) {
        mUndoManager.restoreInstanceState(state);
        Parcel parcel = state.getParcel();
        mUndoManager.restoreInstanceState(parcel, state.getClassLoader());
        mUndoInputFilter.restoreInstanceState(parcel);
        // Re-associate this object as the owner of undo state.
        // Re-associate this object as the owner of undo state.
        mUndoOwner = mUndoManager.getOwner(UNDO_OWNER_TAG, this);
        mUndoOwner = mUndoManager.getOwner(UNDO_OWNER_TAG, this);
    }
    }
@@ -4590,20 +4595,30 @@ public class Editor {
        // Whether the current filter pass is directly caused by an end-user text edit.
        // Whether the current filter pass is directly caused by an end-user text edit.
        private boolean mIsUserEdit;
        private boolean mIsUserEdit;


        // Whether this is the first pass through the filter for a given end-user text edit.
        // Whether the text field is handling an IME composition. Must be parceled in case the user
        private boolean mFirstFilterPass;
        // rotates the screen during composition.
        private boolean mHasComposition;


        public UndoInputFilter(Editor editor) {
        public UndoInputFilter(Editor editor) {
            mEditor = editor;
            mEditor = editor;
        }
        }


        public void saveInstanceState(Parcel parcel) {
            parcel.writeInt(mIsUserEdit ? 1 : 0);
            parcel.writeInt(mHasComposition ? 1 : 0);
        }

        public void restoreInstanceState(Parcel parcel) {
            mIsUserEdit = parcel.readInt() != 0;
            mHasComposition = parcel.readInt() != 0;
        }

        /**
        /**
         * Signals that a user-triggered edit is starting.
         * Signals that a user-triggered edit is starting.
         */
         */
        public void beginBatchEdit() {
        public void beginBatchEdit() {
            if (DEBUG_UNDO) Log.d(TAG, "beginBatchEdit");
            if (DEBUG_UNDO) Log.d(TAG, "beginBatchEdit");
            mIsUserEdit = true;
            mIsUserEdit = true;
            mFirstFilterPass = true;
        }
        }


        public void endBatchEdit() {
        public void endBatchEdit() {
@@ -4624,17 +4639,63 @@ public class Editor {
                return null;
                return null;
            }
            }


            // Check for and handle IME composition edits.
            if (handleCompositionEdit(source, start, end, dstart)) {
                return null;
            }

            // Handle keyboard edits.
            handleKeyboardEdit(source, start, end, dest, dstart, dend);
            return null;
        }

        /**
         * Returns true iff the edit was handled, either because it should be ignored or because
         * this function created an undo operation for it.
         */
        private boolean handleCompositionEdit(CharSequence source, int start, int end, int dstart) {
            // Ignore edits while the user is composing.
            if (isComposition(source)) {
                mHasComposition = true;
                return true;
            }
            final boolean hadComposition = mHasComposition;
            mHasComposition = false;

            // Check for the transition out of the composing state.
            if (hadComposition) {
                // If there was no text the user canceled composition. Ignore the edit.
                if (start == end) {
                    return true;
                }

                // Otherwise the user inserted the composition.
                String newText = TextUtils.substring(source, start, end);
                EditOperation edit = new EditOperation(mEditor, false, "", dstart, newText);
                recordEdit(edit);
                return true;
            }

            // This was neither a composition event nor a transition out of composing.
            return false;
        }

        private void handleKeyboardEdit(CharSequence source, int start, int end,
                Spanned dest, int dstart, int dend) {
            // An application may install a TextWatcher to provide additional modifications after
            // An application may install a TextWatcher to provide additional modifications after
            // the initial input filters run (e.g. a credit card formatter that adds spaces to a
            // the initial input filters run (e.g. a credit card formatter that adds spaces to a
            // string). This results in multiple filter() calls for what the user considers to be
            // string). This results in multiple filter() calls for what the user considers to be
            // a single operation. Always undo the whole set of changes in one step.
            // a single operation. Always undo the whole set of changes in one step.
            final boolean forceMerge = !mFirstFilterPass;
            final boolean forceMerge = isInTextWatcher();
            mFirstFilterPass = false;


            // Build a new operation with all the information from this edit.
            // Build a new operation with all the information from this edit.
            EditOperation edit = new EditOperation(mEditor, forceMerge,
            String newText = TextUtils.substring(source, start, end);
                    source, start, end, dest, dstart, dend);
            String oldText = TextUtils.substring(dest, dstart, dend);
            EditOperation edit = new EditOperation(mEditor, forceMerge, oldText, dstart, newText);
            recordEdit(edit);
        }


        private void recordEdit(EditOperation edit) {
            // Fetch the last edit operation and attempt to merge in the new edit.
            // Fetch the last edit operation and attempt to merge in the new edit.
            final UndoManager um = mEditor.mUndoManager;
            final UndoManager um = mEditor.mUndoManager;
            um.beginUpdate("Edit text");
            um.beginUpdate("Edit text");
@@ -4660,7 +4721,6 @@ public class Editor {
                um.addOperation(edit, UndoManager.MERGE_MODE_NONE);
                um.addOperation(edit, UndoManager.MERGE_MODE_NONE);
            }
            }
            um.endUpdate();
            um.endUpdate();
            return null;  // Text not changed.
        }
        }


        private boolean canUndoEdit(CharSequence source, int start, int end,
        private boolean canUndoEdit(CharSequence source, int start, int end,
@@ -4692,6 +4752,23 @@ public class Editor {


            return true;
            return true;
        }
        }

        private boolean isComposition(CharSequence source) {
            if (!(source instanceof Spannable)) {
                return false;
            }
            // This is a composition edit if the source has a non-zero-length composing span.
            Spannable text = (Spannable) source;
            int composeBegin = EditableInputConnection.getComposingSpanStart(text);
            int composeEnd = EditableInputConnection.getComposingSpanEnd(text);
            return composeBegin < composeEnd;
        }

        private boolean isInTextWatcher() {
            CharSequence text = mEditor.mTextView.getText();
            return (text instanceof SpannableStringBuilder)
                    && ((SpannableStringBuilder) text).getTextWatcherDepth() > 0;
        }
    }
    }


    /**
    /**
@@ -4713,17 +4790,16 @@ public class Editor {
        private int mNewCursorPos;
        private int mNewCursorPos;


        /**
        /**
         * Constructs an edit operation from a text input operation that replaces the range
         * Constructs an edit operation from a text input operation on editor that replaces the
         * (dstart, dend) of dest with (start, end) of source. See {@link InputFilter#filter}.
         * oldText starting at dstart with newText. If forceMerge is true then always forcibly
         * If forceMerge is true then always forcibly merge this operation with any previous one.
         * merge this operation with any previous one.
         */
         */
        public EditOperation(Editor editor, boolean forceMerge,
        public EditOperation(Editor editor, boolean forceMerge, String oldText, int dstart,
                CharSequence source, int start, int end, Spanned dest, int dstart, int dend) {
                String newText) {
            super(editor.mUndoOwner);
            super(editor.mUndoOwner);
            mForceMerge = forceMerge;
            mForceMerge = forceMerge;

            mOldText = oldText;
            mOldText = dest.subSequence(dstart, dend).toString();
            mNewText = newText;
            mNewText = source.subSequence(start, end).toString();


            // Determine the type of the edit and store where it occurred. Avoid storing
            // Determine the type of the edit and store where it occurred. Avoid storing
            // irrevelant data (e.g. mNewTextStart for a delete) because that makes the
            // irrevelant data (e.g. mNewTextStart for a delete) because that makes the
@@ -4742,7 +4818,7 @@ public class Editor {


            // Store cursor data.
            // Store cursor data.
            mOldCursorPos = editor.mTextView.getSelectionStart();
            mOldCursorPos = editor.mTextView.getSelectionStart();
            mNewCursorPos = dstart + (end - start);
            mNewCursorPos = dstart + mNewText.length();
        }
        }


        public EditOperation(Parcel src, ClassLoader loader) {
        public EditOperation(Parcel src, ClassLoader loader) {
Loading