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

Commit 28b4da5b authored by Justin Ghan's avatar Justin Ghan Committed by Android (Google) Code Review
Browse files

Merge changes from topics "insert-impl", "select-delete-impl"

* changes:
  TextView implementation of insert gesture
  TextView implementation of select and delete gestures
parents 35ab6eca 3ebab3f7
Loading
Loading
Loading
Loading
+78 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2022 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;

import android.annotation.IntRange;
import android.annotation.NonNull;
import android.graphics.Paint;

/**
 * Implementation of {@code SegmentIterator} using grapheme clusters as the text segment. Whitespace
 * characters are included as segments.
 *
 * @hide
 */
public class GraphemeClusterSegmentIterator extends SegmentIterator {
    private final CharSequence mText;
    private final TextPaint mTextPaint;

    public GraphemeClusterSegmentIterator(
            @NonNull CharSequence text, @NonNull TextPaint textPaint) {
        mText = text;
        mTextPaint = textPaint;
    }

    @Override
    public int previousStartBoundary(@IntRange(from = 0) int offset) {
        int boundary = mTextPaint.getTextRunCursor(
                mText, 0, mText.length(), false, offset, Paint.CURSOR_BEFORE);
        return boundary == -1 ? DONE : boundary;
    }

    @Override
    public int previousEndBoundary(@IntRange(from = 0) int offset) {
        int boundary = mTextPaint.getTextRunCursor(
                mText, 0, mText.length(), false, offset, Paint.CURSOR_BEFORE);
        // Check that there is another cursor position before, otherwise this is not a valid
        // end boundary.
        if (mTextPaint.getTextRunCursor(
                mText, 0, mText.length(), false, boundary, Paint.CURSOR_BEFORE) == -1) {
            return DONE;
        }
        return boundary == -1 ? DONE : boundary;
    }

    @Override
    public int nextStartBoundary(@IntRange(from = 0) int offset) {
        int boundary = mTextPaint.getTextRunCursor(
                mText, 0, mText.length(), false, offset, Paint.CURSOR_AFTER);
        // Check that there is another cursor position after, otherwise this is not a valid
        // start boundary.
        if (mTextPaint.getTextRunCursor(
                mText, 0, mText.length(), false, boundary, Paint.CURSOR_AFTER) == -1) {
            return DONE;
        }
        return boundary == -1 ? DONE : boundary;
    }

    @Override
    public int nextEndBoundary(@IntRange(from = 0) int offset) {
        int boundary = mTextPaint.getTextRunCursor(
                mText, 0, mText.length(), false, offset, Paint.CURSOR_AFTER);
        return boundary == -1 ? DONE : boundary;
    }
}
+410 −24

File changed.

Preview size limit exceeded, changes collapsed.

+111 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2022 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;

import android.annotation.IntRange;

/**
 * Finds text segment boundaries within text. Subclasses can implement different types of text
 * segments. Grapheme clusters and words are examples of possible text segments.
 *
 * <p>Granular units may not overlap, so every character belongs to at most one text segment. A
 * character may belong to no text segments.
 *
 * <p>For example, a word level text segment iterator may subdivide the text "Hello, World!" into
 * four text segments: "Hello", ",", "World", "!". The space character does not belong to any text
 * segments.
 *
 * @hide
 */
public abstract class SegmentIterator {
    public static final int DONE = -1;

    private int mRunStartOffset;
    private int mRunEndOffset;

    /**
     * Returns the character offset of the previous text segment start boundary before the specified
     * character offset, or {@code DONE} if there are none.
     */
    public abstract int previousStartBoundary(@IntRange(from = 0) int offset);

    /**
     * Returns the character offset of the previous text segment end boundary before the specified
     * character offset, or {@code DONE} if there are none.
     */
    public abstract int previousEndBoundary(@IntRange(from = 0) int offset);

    /**
     * Returns the character offset of the next text segment start boundary after the specified
     * character offset, or {@code DONE} if there are none.
     */
    public abstract int nextStartBoundary(@IntRange(from = 0) int offset);

    /**
     * Returns the character offset of the next text segment end boundary after the specified
     * character offset, or {@code DONE} if there are none.
     */
    public abstract int nextEndBoundary(@IntRange(from = 0) int offset);

    /**
     * Sets the start and end of a run which can be used to constrain the scope of the iterator's
     * search.
     *
     * @hide
     */
    void setRunLimits(
            @IntRange(from = 0) int runStartOffset, @IntRange(from = 0) int runEndOffset) {
        mRunStartOffset = runStartOffset;
        mRunEndOffset = runEndOffset;
    }

    /** @hide */
    int previousStartBoundaryOrRunStart(@IntRange(from = 0) int offset) {
        int start = previousStartBoundary(offset);
        if (start == DONE) {
            return DONE;
        }
        return Math.max(start, mRunStartOffset);
    }

    /** @hide */
    int previousEndBoundaryWithinRunLimits(@IntRange(from = 0) int offset) {
        int end = previousEndBoundary(offset);
        if (end <= mRunStartOffset) {
            return DONE;
        }
        return end;
    }

    /** @hide */
    int nextStartBoundaryWithinRunLimits(@IntRange(from = 0) int offset) {
        int start = nextStartBoundary(offset);
        if (start >= mRunEndOffset) {
            return DONE;
        }
        return start;
    }

    /** @hide */
    int nextEndBoundaryOrRunEnd(@IntRange(from = 0) int offset) {
        int end = nextEndBoundary(offset);
        if (end == DONE) {
            return DONE;
        }
        return Math.min(end, mRunEndOffset);
    }
}
+87 −0
Original line number Diff line number Diff line
/*
 * Copyright (C) 2022 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;

import android.annotation.IntRange;
import android.annotation.NonNull;
import android.icu.text.BreakIterator;
import android.text.method.WordIterator;

/**
 * Implementation of {@code SegmentIterator} using words as the text segment. Word boundaries are
 * found using {@code WordIterator}. Whitespace characters are excluded, so they are not included in
 * any text segments.
 *
 * @hide
 */
public class WordSegmentIterator extends SegmentIterator {
    private final CharSequence mText;
    private final WordIterator mWordIterator;

    public WordSegmentIterator(@NonNull CharSequence text, @NonNull WordIterator wordIterator) {
        mText = text;
        mWordIterator = wordIterator;
    }

    @Override
    public int previousStartBoundary(@IntRange(from = 0) int offset) {
        int boundary = offset;
        do {
            boundary = mWordIterator.prevBoundary(boundary);
            if (boundary == BreakIterator.DONE) {
                return DONE;
            }
        } while (Character.isWhitespace(mText.charAt(boundary)));
        return boundary;
    }

    @Override
    public int previousEndBoundary(@IntRange(from = 0) int offset) {
        int boundary = offset;
        do {
            boundary = mWordIterator.prevBoundary(boundary);
            if (boundary == BreakIterator.DONE || boundary == 0) {
                return DONE;
            }
        } while (Character.isWhitespace(mText.charAt(boundary - 1)));
        return boundary;
    }

    @Override
    public int nextStartBoundary(@IntRange(from = 0) int offset) {
        int boundary = offset;
        do {
            boundary = mWordIterator.nextBoundary(boundary);
            if (boundary == BreakIterator.DONE || boundary == mText.length()) {
                return DONE;
            }
        } while (Character.isWhitespace(mText.charAt(boundary)));
        return boundary;
    }

    @Override
    public int nextEndBoundary(@IntRange(from = 0) int offset) {
        int boundary = offset;
        do {
            boundary = mWordIterator.nextBoundary(boundary);
            if (boundary == BreakIterator.DONE) {
                return DONE;
            }
        } while (Character.isWhitespace(mText.charAt(boundary - 1)));
        return boundary;
    }
}
+85 −0
Original line number Diff line number Diff line
@@ -72,6 +72,7 @@ import android.graphics.Insets;
import android.graphics.Paint;
import android.graphics.Paint.FontMetricsInt;
import android.graphics.Path;
import android.graphics.PointF;
import android.graphics.PorterDuff;
import android.graphics.Rect;
import android.graphics.RectF;
@@ -98,12 +99,14 @@ import android.text.BoringLayout;
import android.text.DynamicLayout;
import android.text.Editable;
import android.text.GetChars;
import android.text.GraphemeClusterSegmentIterator;
import android.text.GraphicsOperations;
import android.text.InputFilter;
import android.text.InputType;
import android.text.Layout;
import android.text.ParcelableSpan;
import android.text.PrecomputedText;
import android.text.SegmentIterator;
import android.text.Selection;
import android.text.SpanWatcher;
import android.text.Spannable;
@@ -117,6 +120,7 @@ import android.text.TextPaint;
import android.text.TextUtils;
import android.text.TextUtils.TruncateAt;
import android.text.TextWatcher;
import android.text.WordSegmentIterator;
import android.text.method.AllCapsTransformationMethod;
import android.text.method.ArrowKeyMovementMethod;
import android.text.method.DateKeyListener;
@@ -183,11 +187,15 @@ import android.view.inputmethod.BaseInputConnection;
import android.view.inputmethod.CompletionInfo;
import android.view.inputmethod.CorrectionInfo;
import android.view.inputmethod.CursorAnchorInfo;
import android.view.inputmethod.DeleteGesture;
import android.view.inputmethod.EditorInfo;
import android.view.inputmethod.ExtractedText;
import android.view.inputmethod.ExtractedTextRequest;
import android.view.inputmethod.HandwritingGesture;
import android.view.inputmethod.InputConnection;
import android.view.inputmethod.InputMethodManager;
import android.view.inputmethod.InsertGesture;
import android.view.inputmethod.SelectGesture;
import android.view.inspector.InspectableProperty;
import android.view.inspector.InspectableProperty.EnumEntry;
import android.view.inspector.InspectableProperty.FlagEntry;
@@ -9282,6 +9290,83 @@ public class TextView extends View implements ViewTreeObserver.OnPreDrawListener
        return false;
    }
    /** @hide */
    public int performHandwritingSelectGesture(@NonNull SelectGesture gesture) {
        int[] range = getRangeForRect(gesture.getSelectionArea(), gesture.getGranularity());
        if (range == null) {
            return handleGestureFailure(gesture);
        }
        Selection.setSelection(getEditableText(), range[0], range[1]);
        mEditor.startSelectionActionModeAsync(/* adjustSelection= */ false);
        return 0;
    }
    /** @hide */
    public int performHandwritingDeleteGesture(@NonNull DeleteGesture gesture) {
        int[] range = getRangeForRect(gesture.getDeletionArea(), gesture.getGranularity());
        if (range == null) {
            return handleGestureFailure(gesture);
        }
        getEditableText().delete(range[0], range[1]);
        Selection.setSelection(getEditableText(), range[0]);
        // TODO: Delete extra spaces.
        return 0;
    }
    /** @hide */
    public int performHandwritingInsertGesture(@NonNull InsertGesture gesture) {
        PointF point = gesture.getInsertionPoint();
        // The coordinates provided are screen coordinates - transform to content coordinates.
        int[] screenToViewport = getLocationOnScreen();
        point.offset(
                -(screenToViewport[0] + viewportToContentHorizontalOffset()),
                -(screenToViewport[1] + viewportToContentVerticalOffset()));
        int line = mLayout.getLineForVertical((int) point.y);
        if (point.y < mLayout.getLineTop(line)
                || point.y > mLayout.getLineBottomWithoutSpacing(line)) {
            return handleGestureFailure(gesture);
        }
        if (point.x < mLayout.getLineLeft(line) || point.x > mLayout.getLineRight(line)) {
            return handleGestureFailure(gesture);
        }
        int offset = mLayout.getOffsetForHorizontal(line, point.x);
        String textToInsert = gesture.getTextToInsert();
        getEditableText().insert(offset, textToInsert);
        Selection.setSelection(getEditableText(), offset + textToInsert.length());
        // TODO: Insert extra spaces if necessary.
        return 0;
    }
    private int handleGestureFailure(HandwritingGesture gesture) {
        if (!TextUtils.isEmpty(gesture.getFallbackText())) {
            getEditableText()
                    .replace(getSelectionStart(), getSelectionEnd(), gesture.getFallbackText());
        }
        return 0;
    }
    @Nullable
    private int[] getRangeForRect(@NonNull RectF area, int granularity) {
        // The coordinates provided are screen coordinates - transform to content coordinates.
        int[] screenToViewport = getLocationOnScreen();
        area = new RectF(area);
        area.offset(
                -(screenToViewport[0] + viewportToContentHorizontalOffset()),
                -(screenToViewport[1] + viewportToContentVerticalOffset()));
        SegmentIterator segmentIterator;
        if (granularity == HandwritingGesture.GRANULARITY_WORD) {
            WordIterator wordIterator = getWordIterator();
            wordIterator.setCharSequence(mText, 0, mText.length());
            segmentIterator = new WordSegmentIterator(mText, wordIterator);
        } else {
            segmentIterator = new GraphemeClusterSegmentIterator(mText, mTextPaint);
        }
        return mLayout.getRangeForRect(area, segmentIterator);
    }
    /** @hide */
    @VisibleForTesting
    @UnsupportedAppUsage
Loading