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

Commit 67c1dff2 authored by Gilles Debunne's avatar Gilles Debunne Committed by Android (Google) Code Review
Browse files

Merge "Extracted WordIterator class."

parents 5db13141 e193fd14
Loading
Loading
Loading
Loading
+0 −3
Original line number Diff line number Diff line
@@ -16,10 +16,7 @@

package android.text;

import android.util.Log;

import java.text.BreakIterator;
import java.text.CharacterIterator;


/**
+0 −105
Original line number Diff line number Diff line
@@ -17,23 +17,14 @@
package android.text.method;

import android.graphics.Rect;
import android.text.CharSequenceIterator;
import android.text.Editable;
import android.text.Layout;
import android.text.Selection;
import android.text.Spannable;
import android.text.Spanned;
import android.text.TextWatcher;
import android.util.Log;
import android.util.MathUtils;
import android.view.KeyEvent;
import android.view.MotionEvent;
import android.view.View;
import android.widget.TextView;

import java.text.BreakIterator;
import java.text.CharacterIterator;

/**
 * A movement method that provides cursor movement and selection.
 * Supports displaying the context menu on DPad Center.
@@ -332,102 +323,6 @@ public class ArrowKeyMovementMethod extends BaseMovementMethod implements Moveme
        return sInstance;
    }

    /**
     * Walks through cursor positions at word boundaries. Internally uses
     * {@link BreakIterator#getWordInstance()}, and caches {@link CharSequence}
     * for performance reasons.
     */
    private static class WordIterator implements Selection.PositionIterator {
        private CharSequence mCurrent;
        private boolean mCurrentDirty = false;

        private BreakIterator mIterator;

        private TextWatcher mWatcher = new TextWatcher() {
            /** {@inheritDoc} */
            public void beforeTextChanged(CharSequence s, int start, int count, int after) {
                // ignored
            }

            /** {@inheritDoc} */
            public void onTextChanged(CharSequence s, int start, int before, int count) {
                mCurrentDirty = true;
            }

            /** {@inheritDoc} */
            public void afterTextChanged(Editable s) {
                // ignored
            }
        };

        public void setCharSequence(CharSequence incoming) {
            if (mIterator == null) {
                mIterator = BreakIterator.getWordInstance();
            }

            // when incoming is different object, move listeners to new sequence
            // and mark as dirty so we reload contents.
            if (mCurrent != incoming) {
                if (mCurrent instanceof Editable) {
                    ((Editable) mCurrent).removeSpan(mWatcher);
                }

                if (incoming instanceof Editable) {
                    ((Editable) incoming).setSpan(
                            mWatcher, 0, incoming.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE);
                }

                mCurrent = incoming;
                mCurrentDirty = true;
            }

            if (mCurrentDirty) {
                final CharacterIterator charIterator = new CharSequenceIterator(mCurrent);
                mIterator.setText(charIterator);

                mCurrentDirty = false;
            }
        }

        private boolean isValidOffset(int offset) {
            return offset >= 0 && offset <= mCurrent.length();
        }

        private boolean isLetterOrDigit(int offset) {
            if (isValidOffset(offset)) {
                return Character.isLetterOrDigit(mCurrent.charAt(offset));
            } else {
                return false;
            }
        }

        /** {@inheritDoc} */
        public int preceding(int offset) {
            // always round cursor index into valid string index
            offset = MathUtils.constrain(offset, 0, mCurrent.length());

            do {
                offset = mIterator.preceding(offset);
                if (isLetterOrDigit(offset)) break;
            } while (isValidOffset(offset));

            return offset;
        }

        /** {@inheritDoc} */
        public int following(int offset) {
            // always round cursor index into valid string index
            offset = MathUtils.constrain(offset, 0, mCurrent.length());

            do {
                offset = mIterator.following(offset);
                if (isLetterOrDigit(offset - 1)) break;
            } while (isValidOffset(offset));

            return offset;
        }
    }

    private WordIterator mWordIterator = new WordIterator();

    private static final Object LAST_TAP_DOWN = new Object();
+220 −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.method;

import android.text.CharSequenceIterator;
import android.text.Editable;
import android.text.Selection;
import android.text.Spanned;
import android.text.TextWatcher;

import java.text.BreakIterator;
import java.text.CharacterIterator;
import java.util.Locale;

/**
 * Walks through cursor positions at word boundaries. Internally uses
 * {@link BreakIterator#getWordInstance()}, and caches {@link CharSequence}
 * for performance reasons.
 *
 * Also provides methods to determine word boundaries.
 * {@hide}
 */
public class WordIterator implements Selection.PositionIterator {
    private CharSequence mCurrent;
    private boolean mCurrentDirty = false;

    private BreakIterator mIterator;

    /**
     * Constructs a WordIterator using the default locale.
     */
    public WordIterator() {
        this(Locale.getDefault());
    }

    /**
     * Constructs a new WordIterator for the specified locale.
     * @param locale The locale to be used when analysing the text.
     */
    public WordIterator(Locale locale) {
        mIterator = BreakIterator.getWordInstance(locale);
    }

    private final TextWatcher mWatcher = new TextWatcher() {
        /** {@inheritDoc} */
        public void beforeTextChanged(CharSequence s, int start, int count, int after) {
            // ignored
        }

        /** {@inheritDoc} */
        public void onTextChanged(CharSequence s, int start, int before, int count) {
            mCurrentDirty = true;
        }

        /** {@inheritDoc} */
        public void afterTextChanged(Editable s) {
            // ignored
        }
    };

    public void setCharSequence(CharSequence incoming) {
        // When incoming is different object, move listeners to new sequence
        // and mark as dirty so we reload contents.
        if (mCurrent != incoming) {
            if (mCurrent instanceof Editable) {
                ((Editable) mCurrent).removeSpan(mWatcher);
            }

            if (incoming instanceof Editable) {
                ((Editable) incoming).setSpan(
                        mWatcher, 0, incoming.length(), Spanned.SPAN_INCLUSIVE_INCLUSIVE);
            }

            mCurrent = incoming;
            mCurrentDirty = true;
        }

        if (mCurrentDirty) {
            final CharacterIterator charIterator = new CharSequenceIterator(mCurrent);
            mIterator.setText(charIterator);

            mCurrentDirty = false;
        }
    }

    /** {@inheritDoc} */
    public int preceding(int offset) {
        do {
            offset = mIterator.preceding(offset);
            if (offset == BreakIterator.DONE || isOnLetterOrDigit(offset)) {
                break;
            }
        } while (true);

        return offset;
    }

    /** {@inheritDoc} */
    public int following(int offset) {
        do {
            offset = mIterator.following(offset);
            if (offset == BreakIterator.DONE || isAfterLetterOrDigit(offset)) {
                break;
            }
        } while (true);

        return offset;
    }

    /** If <code>offset</code> is within a word, returns the index of the first character of that
     * word, otherwise returns BreakIterator.DONE.
     *
     * The offsets that are considered to be part of a word are the indexes of its characters,
     * <i>as well as</i> the index of its last character plus one.
     * If offset is the index of a low surrogate character, BreakIterator.DONE will be returned.
     *
     * Valid range for offset is [0..textLength] (note the inclusive upper bound).
     * The returned value is within [0..offset] or BreakIterator.DONE.
     *
     * @throws IllegalArgumentException is offset is not valid.
     */
    public int getBeginning(int offset) {
        checkOffsetIsValid(offset);

        if (isOnLetterOrDigit(offset)) {
            if (mIterator.isBoundary(offset)) {
                return offset;
            } else {
                return mIterator.preceding(offset);
            }
        } else {
            if (isAfterLetterOrDigit(offset)) {
                return mIterator.preceding(offset);
            }
        }
        return BreakIterator.DONE;
    }

    /** If <code>offset</code> is within a word, returns the index of the last character of that
     * word plus one, otherwise returns BreakIterator.DONE.
     *
     * The offsets that are considered to be part of a word are the indexes of its characters,
     * <i>as well as</i> the index of its last character plus one.
     * If offset is the index of a low surrogate character, BreakIterator.DONE will be returned.
     *
     * Valid range for offset is [0..textLength] (note the inclusive upper bound).
     * The returned value is within [offset..textLength] or BreakIterator.DONE.
     *
     * @throws IllegalArgumentException is offset is not valid.
     */
    public int getEnd(int offset) {
        checkOffsetIsValid(offset);

        if (isAfterLetterOrDigit(offset)) {
            if (mIterator.isBoundary(offset)) {
                return offset;
            } else {
                return mIterator.following(offset);
            }
        } else {
            if (isOnLetterOrDigit(offset)) {
                return mIterator.following(offset);
            }
        }
        return BreakIterator.DONE;
    }

    private boolean isAfterLetterOrDigit(int offset) {
        if (offset - 1 >= 0) {
            final char previousChar = mCurrent.charAt(offset - 1);
            if (Character.isLetterOrDigit(previousChar)) return true;
            if (offset - 2 >= 0) {
                final char previousPreviousChar = mCurrent.charAt(offset - 2);
                if (Character.isSurrogatePair(previousPreviousChar, previousChar)) {
                    final int codePoint = Character.toCodePoint(previousPreviousChar, previousChar);
                    return Character.isLetterOrDigit(codePoint);
                }
            }
        }
        return false;
    }

    private boolean isOnLetterOrDigit(int offset) {
        final int length = mCurrent.length();
        if (offset < length) {
            final char currentChar = mCurrent.charAt(offset);
            if (Character.isLetterOrDigit(currentChar)) return true;
            if (offset + 1 < length) {
                final char nextChar = mCurrent.charAt(offset + 1);
                if (Character.isSurrogatePair(currentChar, nextChar)) {
                    final int codePoint = Character.toCodePoint(currentChar, nextChar);
                    return Character.isLetterOrDigit(codePoint);
                }
            }
        }
        return false;
    }

    private void checkOffsetIsValid(int offset) {
        if (offset < 0 || offset > mCurrent.length()) {
            final String message = "Valid range is [0, " + mCurrent.length() + "]";
            throw new IllegalArgumentException(message);
        }
    }
}