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

Commit bec97159 authored by Keisuke Kuroyanagi's avatar Keisuke Kuroyanagi
Browse files

Refresh action mode always when selection is modified.

Text action mode was not correctly updated when the selection
is modified by keyboard, app or IME.
With this CL, Editor#refreshTextActionMode is called always when
selection is updated. This method properly starts, stops or
invalidates text action mode if selection was modified outside of
cursor controllers.

Bug: 10527362
Bug: 22937774

Change-Id: Ic025c109539c3b59638226be4c4c9adf5ea0c38c
parent 4a696ac7
Loading
Loading
Loading
Loading
+104 −69
Original line number Diff line number Diff line
@@ -215,7 +215,7 @@ public class Editor {

    boolean mInBatchEditControllers;
    boolean mShowSoftInputOnFocus = true;
    boolean mPreserveDetachedSelection;
    private boolean mPreserveDetachedSelection;
    boolean mTemporaryDetach;

    boolean mIsBeingLongClicked;
@@ -352,7 +352,6 @@ public class Editor {

    void replace() {
        int middle = (mTextView.getSelectionStart() + mTextView.getSelectionEnd()) / 2;
        stopTextActionMode();
        Selection.setSelection((Spannable) mTextView.getText(), middle);
        showSuggestions();
    }
@@ -429,10 +428,8 @@ public class Editor {
            mSpellChecker = null;
        }

        mPreserveDetachedSelection = true;
        hideCursorAndSpanControllers();
        stopTextActionMode();
        mPreserveDetachedSelection = false;
        stopTextActionModeWithPreservingSelection();
        mTemporaryDetach = false;
    }

@@ -1103,7 +1100,6 @@ public class Editor {
                mInsertionControllerEnabled) {
            final int offset = mTextView.getOffsetForPosition(mLastDownPositionX,
                    mLastDownPositionY);
            stopTextActionMode();
            Selection.setSelection((Spannable) mTextView.getText(), offset);
            getInsertionController().show();
            mIsInsertionActionModeStartPending = true;
@@ -1207,18 +1203,15 @@ public class Editor {
            mTextView.onEndBatchEdit();

            if (mTextView.isInExtractedMode()) {
                // terminateTextSelectionMode removes selection, which we want to keep when
                // ExtractEditText goes out of focus.
                final int selStart = mTextView.getSelectionStart();
                final int selEnd = mTextView.getSelectionEnd();
                hideCursorAndSpanControllers();
                stopTextActionMode();
                Selection.setSelection((Spannable) mTextView.getText(), selStart, selEnd);
                stopTextActionModeWithPreservingSelection();
            } else {
                if (mTemporaryDetach) mPreserveDetachedSelection = true;
                hideCursorAndSpanControllers();
                if (mTemporaryDetach) {
                    stopTextActionModeWithPreservingSelection();
                } else {
                    stopTextActionMode();
                if (mTemporaryDetach) mPreserveDetachedSelection = false;
                }
                downgradeEasyCorrectionSpans();
            }
            // No need to create the controller
@@ -1289,10 +1282,8 @@ public class Editor {
                makeBlink();
            }
            final InputMethodManager imm = InputMethodManager.peekInstance();
            final boolean immFullScreen = (imm != null && imm.isFullscreenMode());
            if (mSelectionModifierCursorController != null && mTextView.hasSelection()
                    && !immFullScreen && mTextActionMode != null) {
                mSelectionModifierCursorController.show();
            if (mTextView.hasSelection() && !extractedTextModeWillBeStarted()) {
                startSelectionActionMode();
            }
        } else {
            if (mBlink != null) {
@@ -1303,9 +1294,7 @@ public class Editor {
            }
            // Order matters! Must be done before onParentLostFocus to rely on isShowingUp
            hideCursorAndSpanControllers();
            if (mSelectionModifierCursorController != null) {
                mSelectionModifierCursorController.hide();
            }
            stopTextActionModeWithPreservingSelection();
            if (mSuggestionsPopupWindow != null) {
                mSuggestionsPopupWindow.onParentLostFocus();
            }
@@ -1855,6 +1844,38 @@ public class Editor {
        }
    }

    void refreshTextActionMode() {
        if (extractedTextModeWillBeStarted()) {
            return;
        }
        final boolean hasSelection = mTextView.hasSelection();
        final SelectionModifierCursorController selectionController = getSelectionController();
        final InsertionPointCursorController insertionController = getInsertionController();
        if ((selectionController != null && selectionController.isCursorBeingModified())
                || (insertionController != null && insertionController.isCursorBeingModified())) {
            // ActionMode should be managed by the currently active cursor controller.
            return;
        }
        if (hasSelection) {
            if (mTextActionMode == null || selectionController == null
                    || !selectionController.isActive()) {
                // Avoid dismissing the selection if it exists.
                stopTextActionModeWithPreservingSelection();
                startSelectionActionMode();
            } else {
                mTextActionMode.invalidateContentRect();
            }
        } else {
            // Insertion action mode is started only when insertion controller is explicitly
            // activated.
            if (insertionController == null || !insertionController.isActive()) {
                stopTextActionMode();
            } else if (mTextActionMode != null) {
                mTextActionMode.invalidateContentRect();
            }
        }
    }

    /**
     * Start an Insertion action mode.
     */
@@ -1878,17 +1899,15 @@ public class Editor {

    /**
     * Starts a Selection Action Mode with the current selection and ensures the selection handles
     * are shown if there is a selection, otherwise the insertion handle is shown. This should be
     * used when the mode is started from a non-touch event.
     * are shown if there is a selection. This should be used when the mode is started from a
     * non-touch event.
     *
     * @return true if the selection mode was actually started.
     */
    boolean startSelectionActionMode() {
    private boolean startSelectionActionMode() {
        boolean selectionStarted = startSelectionActionModeInternal();
        if (selectionStarted) {
            getSelectionController().show();
        } else if (getInsertionController() != null) {
            getInsertionController().show();
        }
        return selectionStarted;
    }
@@ -1906,66 +1925,52 @@ public class Editor {
        if (extractedTextModeWillBeStarted()) {
            return false;
        }
        if (mTextActionMode != null) {
            mTextActionMode.finish();
        if (!checkField()) {
            return false;
        }
        if (!checkFieldAndSelectCurrentWord()) {
        if (!mTextView.hasSelection() && !selectCurrentWord()) {
            // No selection and cannot select a word.
            return false;
        }

        // Avoid dismissing the selection if it exists.
        mPreserveDetachedSelection = true;
        stopTextActionMode();
        mPreserveDetachedSelection = false;

        stopTextActionModeWithPreservingSelection();
        getSelectionController().enterDrag(
                SelectionModifierCursorController.DRAG_ACCELERATOR_MODE_WORD);
        return true;
    }

    /**
     * Checks whether a selection can be performed on the current TextView and if so selects
     * the current word.
     * Checks whether a selection can be performed on the current TextView.
     *
     * @return true if there already was a selection or if the current word was selected.
     * @return true if a selection can be performed
     */
    boolean checkFieldAndSelectCurrentWord() {
    boolean checkField() {
        if (!mTextView.canSelectText() || !mTextView.requestFocus()) {
            Log.w(TextView.LOG_TAG,
                    "TextView does not support text selection. Selection cancelled.");
            return false;
        }

        if (!mTextView.hasSelection()) {
            // There may already be a selection on device rotation
            return selectCurrentWord();
        }
        return true;
    }

    private boolean startSelectionActionModeInternal() {
        if (extractedTextModeWillBeStarted()) {
            return false;
        }
        if (mTextActionMode != null) {
            // Text action mode is already started
            mTextActionMode.invalidate();
            return false;
        }

        if (!checkFieldAndSelectCurrentWord()) {
        if (!checkField() || !mTextView.hasSelection()) {
            return false;
        }

        boolean willExtract = extractedTextModeWillBeStarted();

        // Do not start the action mode when extracted text will show up full screen, which would
        // immediately hide the newly created action bar and would be visually distracting.
        if (!willExtract) {
        ActionMode.Callback actionModeCallback =
                new TextActionModeCallback(true /* hasSelection */);
            mTextActionMode = mTextView.startActionMode(
                    actionModeCallback, ActionMode.TYPE_FLOATING);
        }
        mTextActionMode = mTextView.startActionMode(actionModeCallback, ActionMode.TYPE_FLOATING);

        final boolean selectionStarted = mTextActionMode != null || willExtract;
        final boolean selectionStarted = mTextActionMode != null;
        if (selectionStarted && !mTextView.isTextSelectable() && mShowSoftInputOnFocus) {
            // Show the IME to be able to replace text, except when selecting non editable text.
            final InputMethodManager imm = InputMethodManager.peekInstance();
@@ -2106,6 +2111,12 @@ public class Editor {
        }
    }

    private void stopTextActionModeWithPreservingSelection() {
        mPreserveDetachedSelection = true;
        stopTextActionMode();
        mPreserveDetachedSelection = false;
    }

    /**
     * @return True if this view supports insertion handles.
     */
@@ -2433,16 +2444,14 @@ public class Editor {
        if (offset == -1) {
            return;
        }
        mPreserveDetachedSelection = true;
        stopTextActionMode();
        mPreserveDetachedSelection = false;
        stopTextActionModeWithPreservingSelection();
        final boolean isOnSelection = mTextView.hasSelection()
                && offset >= mTextView.getSelectionStart() && offset <= mTextView.getSelectionEnd();
        if (!isOnSelection) {
            // Right clicked position is not on the selection. Remove the selection and move the
            // cursor to the right clicked position.
            stopTextActionMode();
            Selection.setSelection((Spannable) mTextView.getText(), offset);
            stopTextActionMode();
        }

        if (shouldOfferToShowSuggestions()) {
@@ -3483,7 +3492,6 @@ public class Editor {
        public void onItemClick(AdapterView<?> parent, View view, int position, long id) {
            Editable editable = (Editable) mTextView.getText();
            SuggestionInfo suggestionInfo = mSuggestionInfos[position];

            final int spanStart = editable.getSpanStart(suggestionInfo.mSuggestionSpan);
            final int spanEnd = editable.getSpanEnd(suggestionInfo.mSuggestionSpan);
            if (spanStart < 0 || spanEnd <= spanStart) {
@@ -4384,7 +4392,7 @@ public class Editor {
                        if (distanceSquared < touchSlop * touchSlop) {
                            // Tapping on the handle toggles the insertion action mode.
                            if (mTextActionMode != null) {
                                mTextActionMode.finish();
                                stopTextActionMode();
                            } else {
                                startInsertionActionMode();
                            }
@@ -4801,6 +4809,10 @@ public class Editor {
         * preventing the activity from being recycled.
         */
        public void onDetached();

        public boolean isCursorBeingModified();

        public boolean isActive();
    }

    private class InsertionPointCursorController implements CursorController {
@@ -4844,6 +4856,16 @@ public class Editor {

            if (mHandle != null) mHandle.onDetached();
        }

        @Override
        public boolean isCursorBeingModified() {
            return mHandle != null && mHandle.isDragging();
        }

        @Override
        public boolean isActive() {
            return mHandle != null && mHandle.isShowing();
        }
    }

    class SelectionModifierCursorController implements CursorController {
@@ -5033,9 +5055,7 @@ public class Editor {

                        if (mStartOffset != offset) {
                            // Start character based drag accelerator.
                            if (mTextActionMode != null) {
                                mTextActionMode.finish();
                            }
                            stopTextActionMode();
                            enterDrag(DRAG_ACCELERATOR_MODE_CHARACTER);
                            mDiscardNextActionUp = true;
                            mHaventMovedEnoughToStartDrag = false;
@@ -5109,9 +5129,7 @@ public class Editor {
            if (mInsertionActionModeRunnable != null) {
                mTextView.removeCallbacks(mInsertionActionModeRunnable);
            }
            if (mTextActionMode != null) {
                mTextActionMode.finish();
            }
            stopTextActionMode();
            if (!selectCurrentParagraph()) {
                return false;
            }
@@ -5220,6 +5238,12 @@ public class Editor {
            mStartOffset = -1;
            mDragAcceleratorMode = DRAG_ACCELERATOR_MODE_INACTIVE;
            mSwitchedLines = false;
            final int selectionStart = mTextView.getSelectionStart();
            final int selectionEnd = mTextView.getSelectionEnd();
            if (selectionStart > selectionEnd) {
                Selection.setSelection((Spannable) mTextView.getText(),
                        selectionEnd, selectionStart);
            }
        }

        /**
@@ -5229,6 +5253,12 @@ public class Editor {
            return mStartHandle != null && mStartHandle.isDragging();
        }

        @Override
        public boolean isCursorBeingModified() {
            return isDragAcceleratorActive() || isSelectionStartDragged()
                    || (mEndHandle != null && mEndHandle.isDragging());
        }

        /**
         * @return true if the user is selecting text using the drag accelerator.
         */
@@ -5250,6 +5280,11 @@ public class Editor {
            if (mStartHandle != null) mStartHandle.onDetached();
            if (mEndHandle != null) mEndHandle.onDetached();
        }

        @Override
        public boolean isActive() {
            return mStartHandle != null && mStartHandle.isShowing();
        }
    }

    private class CorrectionHighlighter {
+18 −39
Original line number Diff line number Diff line
@@ -1509,6 +1509,9 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
                if (result != null) {
                    if (isTextEditable()) {
                        replaceSelectionWithText(result);
                        if (mEditor != null) {
                            mEditor.refreshTextActionMode();
                        }
                    } else {
                        if (result.length() > 0) {
                            Toast.makeText(getContext(), String.valueOf(result), Toast.LENGTH_LONG)
@@ -1518,12 +1521,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
                }
            } else if (mText instanceof Spannable) {
                // Reset the selection.
                stopTextActionMode();
                Selection.setSelection((Spannable) mText, getSelectionStart(), getSelectionEnd());
            }

            if (mEditor.hasSelectionController()) {
                mEditor.startSelectionActionMode();
                Selection.setSelection((Spannable) mText, getSelectionEnd());
            }
        }
    }
@@ -5391,11 +5389,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
        // - onFocusChanged cannot start it when focus is given to a view with selected text (after
        //   a screen rotation) since layout is not yet initialized at that point.
        if (mEditor != null && mEditor.mCreatedWithASelection) {
            if (mEditor.extractedTextModeWillBeStarted()) {
                mEditor.checkFieldAndSelectCurrentWord();
            } else {
                mEditor.startSelectionActionMode();
            }
            mEditor.refreshTextActionMode();
            mEditor.mCreatedWithASelection = false;
        }

@@ -6593,6 +6587,9 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
        // in the extracted view.
        mEditor.hideCursorAndSpanControllers();
        stopTextActionMode();
        if (mEditor.mSelectionModifierCursorController != null) {
            mEditor.mSelectionModifierCursorController.resetTouchOffsets();
        }
    }

    /**
@@ -8288,6 +8285,9 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
                if (newSelEnd < 0) {
                    newSelEnd = Selection.getSelectionEnd(buf);
                }
                if (mEditor != null) {
                    mEditor.refreshTextActionMode();
                }
                onSelectionChanged(newSelStart, newSelEnd);
            }
        }
@@ -9217,10 +9217,6 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
                    }
                    if (start >= 0 && start <= end && end <= text.length()) {
                        Selection.setSelection((Spannable) text, start, end);
                        // Make sure selection mode is engaged.
                        if (mEditor != null) {
                            mEditor.startSelectionActionMode();
                        }
                        return true;
                    }
                }
@@ -9411,16 +9407,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener

        switch (id) {
            case ID_SELECT_ALL:
                // This starts an action mode if triggered from another action mode. Text is
                // highlighted, so that it can be bulk edited, like selectAllOnFocus does. Returns
                // true even if text is empty.
                boolean shouldRestartActionMode =
                        mEditor != null && mEditor.mTextActionMode != null;
                stopTextActionMode();
                selectAllText();
                if (shouldRestartActionMode) {
                    mEditor.startSelectionActionMode();
                }
                return true;

            case ID_UNDO:
@@ -9446,7 +9433,6 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
            case ID_CUT:
                setPrimaryClip(ClipData.newPlainText(null, getTransformedText(min, max)));
                deleteText_internal(min, max);
                stopTextActionMode();
                return true;

            case ID_COPY:
@@ -9702,12 +9688,6 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
    }

    boolean selectAllText() {
        // Need to hide insert point cursor controller before settings selection, otherwise insert
        // point cursor controller obtains cursor update event and update cursor with cancelling
        // selection.
        if (mEditor != null) {
            mEditor.hideInsertionPointCursorController();
        }
        final int length = mText.length();
        Selection.setSelection((Spannable) mText, 0, length);
        return length > 0;
@@ -9746,7 +9726,6 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
                    }
                }
            }
            stopTextActionMode();
            sLastCutCopyOrTextChangedTime = 0;
        }
    }
@@ -9759,7 +9738,7 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
            sharingIntent.removeExtra(android.content.Intent.EXTRA_TEXT);
            sharingIntent.putExtra(android.content.Intent.EXTRA_TEXT, selectedText);
            getContext().startActivity(Intent.createChooser(sharingIntent, null));
            stopTextActionMode();
            Selection.setSelection((Spannable) mText, getSelectionEnd());
        }
    }

@@ -10077,6 +10056,12 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
                && getAccessibilitySelectionEnd() == end) {
            return;
        }
        CharSequence text = getIterableTextForAccessibility();
        if (Math.min(start, end) >= 0 && Math.max(start, end) <= text.length()) {
            Selection.setSelection((Spannable) text, start, end);
        } else {
            Selection.removeSelection((Spannable) text);
        }
        // Hide all selection controllers used for adjusting selection
        // since we are doing so explicitlty by other means and these
        // controllers interact with how selection behaves.
@@ -10084,12 +10069,6 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
            mEditor.hideCursorAndSpanControllers();
            mEditor.stopTextActionMode();
        }
        CharSequence text = getIterableTextForAccessibility();
        if (Math.min(start, end) >= 0 && Math.max(start, end) <= text.length()) {
            Selection.setSelection((Spannable) text, start, end);
        } else {
            Selection.removeSelection((Spannable) text);
        }
    }

    /** @hide */