aboutsummaryrefslogtreecommitdiffstats
path: root/java/src/org/kelar/inputmethod/latin/WordComposer.java
diff options
context:
space:
mode:
authorAmin Bandali <bandali@kelar.org>2024-12-16 21:45:41 -0500
committerAmin Bandali <bandali@kelar.org>2025-01-11 14:17:35 -0500
commite9a0e66716dab4dd3184d009d8920de1961efdfa (patch)
tree02dcc096643d74645bf28459c2834c3d4a2ad7f2 /java/src/org/kelar/inputmethod/latin/WordComposer.java
parentfb3b9360d70596d7e921de8bf7d3ca99564a077e (diff)
downloadlatinime-e9a0e66716dab4dd3184d009d8920de1961efdfa.tar.gz
latinime-e9a0e66716dab4dd3184d009d8920de1961efdfa.tar.xz
latinime-e9a0e66716dab4dd3184d009d8920de1961efdfa.zip
Rename to Kelar Keyboard (org.kelar.inputmethod.latin)
Diffstat (limited to 'java/src/org/kelar/inputmethod/latin/WordComposer.java')
-rw-r--r--java/src/org/kelar/inputmethod/latin/WordComposer.java481
1 files changed, 481 insertions, 0 deletions
diff --git a/java/src/org/kelar/inputmethod/latin/WordComposer.java b/java/src/org/kelar/inputmethod/latin/WordComposer.java
new file mode 100644
index 000000000..5f05aeab7
--- /dev/null
+++ b/java/src/org/kelar/inputmethod/latin/WordComposer.java
@@ -0,0 +1,481 @@
+/*
+ * Copyright (C) 2008 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 org.kelar.inputmethod.latin;
+
+import org.kelar.inputmethod.annotations.UsedForTesting;
+import org.kelar.inputmethod.event.CombinerChain;
+import org.kelar.inputmethod.event.Event;
+import org.kelar.inputmethod.latin.SuggestedWords.SuggestedWordInfo;
+import org.kelar.inputmethod.latin.common.ComposedData;
+import org.kelar.inputmethod.latin.common.Constants;
+import org.kelar.inputmethod.latin.common.CoordinateUtils;
+import org.kelar.inputmethod.latin.common.InputPointers;
+import org.kelar.inputmethod.latin.common.StringUtils;
+import org.kelar.inputmethod.latin.define.DebugFlags;
+import org.kelar.inputmethod.latin.define.DecoderSpecificConstants;
+
+import java.util.ArrayList;
+import java.util.Collections;
+
+import javax.annotation.Nonnull;
+
+/**
+ * A place to store the currently composing word with information such as adjacent key codes as well
+ */
+public final class WordComposer {
+ private static final int MAX_WORD_LENGTH = DecoderSpecificConstants.DICTIONARY_MAX_WORD_LENGTH;
+ private static final boolean DBG = DebugFlags.DEBUG_ENABLED;
+
+ public static final int CAPS_MODE_OFF = 0;
+ // 1 is shift bit, 2 is caps bit, 4 is auto bit but this is just a convention as these bits
+ // aren't used anywhere in the code
+ public static final int CAPS_MODE_MANUAL_SHIFTED = 0x1;
+ public static final int CAPS_MODE_MANUAL_SHIFT_LOCKED = 0x3;
+ public static final int CAPS_MODE_AUTO_SHIFTED = 0x5;
+ public static final int CAPS_MODE_AUTO_SHIFT_LOCKED = 0x7;
+
+ private CombinerChain mCombinerChain;
+ private String mCombiningSpec; // Memory so that we don't uselessly recreate the combiner chain
+
+ // The list of events that served to compose this string.
+ private final ArrayList<Event> mEvents;
+ private final InputPointers mInputPointers = new InputPointers(MAX_WORD_LENGTH);
+ private SuggestedWordInfo mAutoCorrection;
+ private boolean mIsResumed;
+ private boolean mIsBatchMode;
+ // A memory of the last rejected batch mode suggestion, if any. This goes like this: the user
+ // gestures a word, is displeased with the results and hits backspace, then gestures again.
+ // At the very least we should avoid re-suggesting the same thing, and to do that we memorize
+ // the rejected suggestion in this variable.
+ // TODO: this should be done in a comprehensive way by the User History feature instead of
+ // as an ad-hockery here.
+ private String mRejectedBatchModeSuggestion;
+
+ // Cache these values for performance
+ private CharSequence mTypedWordCache;
+ private int mCapsCount;
+ private int mDigitsCount;
+ private int mCapitalizedMode;
+ // This is the number of code points entered so far. This is not limited to MAX_WORD_LENGTH.
+ // In general, this contains the size of mPrimaryKeyCodes, except when this is greater than
+ // MAX_WORD_LENGTH in which case mPrimaryKeyCodes only contain the first MAX_WORD_LENGTH
+ // code points.
+ private int mCodePointSize;
+ private int mCursorPositionWithinWord;
+
+ /**
+ * Whether the composing word has the only first char capitalized.
+ */
+ private boolean mIsOnlyFirstCharCapitalized;
+
+ public WordComposer() {
+ mCombinerChain = new CombinerChain("");
+ mEvents = new ArrayList<>();
+ mAutoCorrection = null;
+ mIsResumed = false;
+ mIsBatchMode = false;
+ mCursorPositionWithinWord = 0;
+ mRejectedBatchModeSuggestion = null;
+ refreshTypedWordCache();
+ }
+
+ public ComposedData getComposedDataSnapshot() {
+ return new ComposedData(getInputPointers(), isBatchMode(), mTypedWordCache.toString());
+ }
+
+ /**
+ * Restart the combiners, possibly with a new spec.
+ * @param combiningSpec The spec string for combining. This is found in the extra value.
+ */
+ public void restartCombining(final String combiningSpec) {
+ final String nonNullCombiningSpec = null == combiningSpec ? "" : combiningSpec;
+ if (!nonNullCombiningSpec.equals(mCombiningSpec)) {
+ mCombinerChain = new CombinerChain(
+ mCombinerChain.getComposingWordWithCombiningFeedback().toString());
+ mCombiningSpec = nonNullCombiningSpec;
+ }
+ }
+
+ /**
+ * Clear out the keys registered so far.
+ */
+ public void reset() {
+ mCombinerChain.reset();
+ mEvents.clear();
+ mAutoCorrection = null;
+ mCapsCount = 0;
+ mDigitsCount = 0;
+ mIsOnlyFirstCharCapitalized = false;
+ mIsResumed = false;
+ mIsBatchMode = false;
+ mCursorPositionWithinWord = 0;
+ mRejectedBatchModeSuggestion = null;
+ refreshTypedWordCache();
+ }
+
+ private final void refreshTypedWordCache() {
+ mTypedWordCache = mCombinerChain.getComposingWordWithCombiningFeedback();
+ mCodePointSize = Character.codePointCount(mTypedWordCache, 0, mTypedWordCache.length());
+ }
+
+ /**
+ * Number of keystrokes in the composing word.
+ * @return the number of keystrokes
+ */
+ public int size() {
+ return mCodePointSize;
+ }
+
+ public boolean isSingleLetter() {
+ return size() == 1;
+ }
+
+ public final boolean isComposingWord() {
+ return size() > 0;
+ }
+
+ public InputPointers getInputPointers() {
+ return mInputPointers;
+ }
+
+ /**
+ * Process an event and return an event, and return a processed event to apply.
+ * @param event the unprocessed event.
+ * @return the processed event. Never null, but may be marked as consumed.
+ */
+ @Nonnull
+ public Event processEvent(@Nonnull final Event event) {
+ final Event processedEvent = mCombinerChain.processEvent(mEvents, event);
+ // The retained state of the combiner chain may have changed while processing the event,
+ // so we need to update our cache.
+ refreshTypedWordCache();
+ mEvents.add(event);
+ return processedEvent;
+ }
+
+ /**
+ * Apply a processed input event.
+ *
+ * All input events should be supported, including software/hardware events, characters as well
+ * as deletions, multiple inputs and gestures.
+ *
+ * @param event the event to apply. Must not be null.
+ */
+ public void applyProcessedEvent(final Event event) {
+ mCombinerChain.applyProcessedEvent(event);
+ final int primaryCode = event.mCodePoint;
+ final int keyX = event.mX;
+ final int keyY = event.mY;
+ final int newIndex = size();
+ refreshTypedWordCache();
+ mCursorPositionWithinWord = mCodePointSize;
+ // We may have deleted the last one.
+ if (0 == mCodePointSize) {
+ mIsOnlyFirstCharCapitalized = false;
+ }
+ if (Constants.CODE_DELETE != event.mKeyCode) {
+ if (newIndex < MAX_WORD_LENGTH) {
+ // In the batch input mode, the {@code mInputPointers} holds batch input points and
+ // shouldn't be overridden by the "typed key" coordinates
+ // (See {@link #setBatchInputWord}).
+ if (!mIsBatchMode) {
+ // TODO: Set correct pointer id and time
+ mInputPointers.addPointerAt(newIndex, keyX, keyY, 0, 0);
+ }
+ }
+ if (0 == newIndex) {
+ mIsOnlyFirstCharCapitalized = Character.isUpperCase(primaryCode);
+ } else {
+ mIsOnlyFirstCharCapitalized = mIsOnlyFirstCharCapitalized
+ && !Character.isUpperCase(primaryCode);
+ }
+ if (Character.isUpperCase(primaryCode)) mCapsCount++;
+ if (Character.isDigit(primaryCode)) mDigitsCount++;
+ }
+ mAutoCorrection = null;
+ }
+
+ public void setCursorPositionWithinWord(final int posWithinWord) {
+ mCursorPositionWithinWord = posWithinWord;
+ // TODO: compute where that puts us inside the events
+ }
+
+ public boolean isCursorFrontOrMiddleOfComposingWord() {
+ if (DBG && mCursorPositionWithinWord > mCodePointSize) {
+ throw new RuntimeException("Wrong cursor position : " + mCursorPositionWithinWord
+ + "in a word of size " + mCodePointSize);
+ }
+ return mCursorPositionWithinWord != mCodePointSize;
+ }
+
+ /**
+ * When the cursor is moved by the user, we need to update its position.
+ * If it falls inside the currently composing word, we don't reset the composition, and
+ * only update the cursor position.
+ *
+ * @param expectedMoveAmount How many java chars to move the cursor. Negative values move
+ * the cursor backward, positive values move the cursor forward.
+ * @return true if the cursor is still inside the composing word, false otherwise.
+ */
+ public boolean moveCursorByAndReturnIfInsideComposingWord(final int expectedMoveAmount) {
+ int actualMoveAmount = 0;
+ int cursorPos = mCursorPositionWithinWord;
+ // TODO: Don't make that copy. We can do this directly from mTypedWordCache.
+ final int[] codePoints = StringUtils.toCodePointArray(mTypedWordCache);
+ if (expectedMoveAmount >= 0) {
+ // Moving the cursor forward for the expected amount or until the end of the word has
+ // been reached, whichever comes first.
+ while (actualMoveAmount < expectedMoveAmount && cursorPos < codePoints.length) {
+ actualMoveAmount += Character.charCount(codePoints[cursorPos]);
+ ++cursorPos;
+ }
+ } else {
+ // Moving the cursor backward for the expected amount or until the start of the word
+ // has been reached, whichever comes first.
+ while (actualMoveAmount > expectedMoveAmount && cursorPos > 0) {
+ --cursorPos;
+ actualMoveAmount -= Character.charCount(codePoints[cursorPos]);
+ }
+ }
+ // If the actual and expected amounts differ, we crossed the start or the end of the word
+ // so the result would not be inside the composing word.
+ if (actualMoveAmount != expectedMoveAmount) {
+ return false;
+ }
+ mCursorPositionWithinWord = cursorPos;
+ mCombinerChain.applyProcessedEvent(mCombinerChain.processEvent(
+ mEvents, Event.createCursorMovedEvent(cursorPos)));
+ return true;
+ }
+
+ public void setBatchInputPointers(final InputPointers batchPointers) {
+ mInputPointers.set(batchPointers);
+ mIsBatchMode = true;
+ }
+
+ public void setBatchInputWord(final String word) {
+ reset();
+ mIsBatchMode = true;
+ final int length = word.length();
+ for (int i = 0; i < length; i = Character.offsetByCodePoints(word, i, 1)) {
+ final int codePoint = Character.codePointAt(word, i);
+ // We don't want to override the batch input points that are held in mInputPointers
+ // (See {@link #add(int,int,int)}).
+ final Event processedEvent =
+ processEvent(Event.createEventForCodePointFromUnknownSource(codePoint));
+ applyProcessedEvent(processedEvent);
+ }
+ }
+
+ /**
+ * Set the currently composing word to the one passed as an argument.
+ * This will register NOT_A_COORDINATE for X and Ys, and use the passed keyboard for proximity.
+ * @param codePoints the code points to set as the composing word.
+ * @param coordinates the x, y coordinates of the key in the CoordinateUtils format
+ */
+ public void setComposingWord(final int[] codePoints, final int[] coordinates) {
+ reset();
+ final int length = codePoints.length;
+ for (int i = 0; i < length; ++i) {
+ final Event processedEvent =
+ processEvent(Event.createEventForCodePointFromAlreadyTypedText(codePoints[i],
+ CoordinateUtils.xFromArray(coordinates, i),
+ CoordinateUtils.yFromArray(coordinates, i)));
+ applyProcessedEvent(processedEvent);
+ }
+ mIsResumed = true;
+ }
+
+ /**
+ * Returns the word as it was typed, without any correction applied.
+ * @return the word that was typed so far. Never returns null.
+ */
+ public String getTypedWord() {
+ return mTypedWordCache.toString();
+ }
+
+ /**
+ * Whether this composer is composing or about to compose a word in which only the first letter
+ * is a capital.
+ *
+ * If we do have a composing word, we just return whether the word has indeed only its first
+ * character capitalized. If we don't, then we return a value based on the capitalized mode,
+ * which tell us what is likely to happen for the next composing word.
+ *
+ * @return capitalization preference
+ */
+ public boolean isOrWillBeOnlyFirstCharCapitalized() {
+ return isComposingWord() ? mIsOnlyFirstCharCapitalized
+ : (CAPS_MODE_OFF != mCapitalizedMode);
+ }
+
+ /**
+ * Whether or not all of the user typed chars are upper case
+ * @return true if all user typed chars are upper case, false otherwise
+ */
+ public boolean isAllUpperCase() {
+ if (size() <= 1) {
+ return mCapitalizedMode == CAPS_MODE_AUTO_SHIFT_LOCKED
+ || mCapitalizedMode == CAPS_MODE_MANUAL_SHIFT_LOCKED;
+ }
+ return mCapsCount == size();
+ }
+
+ public boolean wasShiftedNoLock() {
+ return mCapitalizedMode == CAPS_MODE_AUTO_SHIFTED
+ || mCapitalizedMode == CAPS_MODE_MANUAL_SHIFTED;
+ }
+
+ /**
+ * Returns true if more than one character is upper case, otherwise returns false.
+ */
+ public boolean isMostlyCaps() {
+ return mCapsCount > 1;
+ }
+
+ /**
+ * Returns true if we have digits in the composing word.
+ */
+ public boolean hasDigits() {
+ return mDigitsCount > 0;
+ }
+
+ /**
+ * Saves the caps mode at the start of composing.
+ *
+ * WordComposer needs to know about the caps mode for several reasons. The first is, we need
+ * to know after the fact what the reason was, to register the correct form into the user
+ * history dictionary: if the word was automatically capitalized, we should insert it in
+ * all-lower case but if it's a manual pressing of shift, then it should be inserted as is.
+ * Also, batch input needs to know about the current caps mode to display correctly
+ * capitalized suggestions.
+ * @param mode the mode at the time of start
+ */
+ public void setCapitalizedModeAtStartComposingTime(final int mode) {
+ mCapitalizedMode = mode;
+ }
+
+ /**
+ * Before fetching suggestions, we don't necessarily know about the capitalized mode yet.
+ *
+ * If we don't have a composing word yet, we take a note of this mode so that we can then
+ * supply this information to the suggestion process. If we have a composing word, then
+ * the previous mode has priority over this.
+ * @param mode the mode just before fetching suggestions
+ */
+ public void adviseCapitalizedModeBeforeFetchingSuggestions(final int mode) {
+ if (!isComposingWord()) {
+ mCapitalizedMode = mode;
+ }
+ }
+
+ /**
+ * Returns whether the word was automatically capitalized.
+ * @return whether the word was automatically capitalized
+ */
+ public boolean wasAutoCapitalized() {
+ return mCapitalizedMode == CAPS_MODE_AUTO_SHIFT_LOCKED
+ || mCapitalizedMode == CAPS_MODE_AUTO_SHIFTED;
+ }
+
+ /**
+ * Sets the auto-correction for this word.
+ */
+ public void setAutoCorrection(final SuggestedWordInfo autoCorrection) {
+ mAutoCorrection = autoCorrection;
+ }
+
+ /**
+ * @return the auto-correction for this word, or null if none.
+ */
+ public SuggestedWordInfo getAutoCorrectionOrNull() {
+ return mAutoCorrection;
+ }
+
+ /**
+ * @return whether we started composing this word by resuming suggestion on an existing string
+ */
+ public boolean isResumed() {
+ return mIsResumed;
+ }
+
+ // `type' should be one of the LastComposedWord.COMMIT_TYPE_* constants above.
+ // committedWord should contain suggestion spans if applicable.
+ public LastComposedWord commitWord(final int type, final CharSequence committedWord,
+ final String separatorString, final NgramContext ngramContext) {
+ // Note: currently, we come here whenever we commit a word. If it's a MANUAL_PICK
+ // or a DECIDED_WORD we may cancel the commit later; otherwise, we should deactivate
+ // the last composed word to ensure this does not happen.
+ final LastComposedWord lastComposedWord = new LastComposedWord(mEvents,
+ mInputPointers, mTypedWordCache.toString(), committedWord, separatorString,
+ ngramContext, mCapitalizedMode);
+ mInputPointers.reset();
+ if (type != LastComposedWord.COMMIT_TYPE_DECIDED_WORD
+ && type != LastComposedWord.COMMIT_TYPE_MANUAL_PICK) {
+ lastComposedWord.deactivate();
+ }
+ mCapsCount = 0;
+ mDigitsCount = 0;
+ mIsBatchMode = false;
+ mCombinerChain.reset();
+ mEvents.clear();
+ mCodePointSize = 0;
+ mIsOnlyFirstCharCapitalized = false;
+ mCapitalizedMode = CAPS_MODE_OFF;
+ refreshTypedWordCache();
+ mAutoCorrection = null;
+ mCursorPositionWithinWord = 0;
+ mIsResumed = false;
+ mRejectedBatchModeSuggestion = null;
+ return lastComposedWord;
+ }
+
+ public void resumeSuggestionOnLastComposedWord(final LastComposedWord lastComposedWord) {
+ mEvents.clear();
+ Collections.copy(mEvents, lastComposedWord.mEvents);
+ mInputPointers.set(lastComposedWord.mInputPointers);
+ mCombinerChain.reset();
+ refreshTypedWordCache();
+ mCapitalizedMode = lastComposedWord.mCapitalizedMode;
+ mAutoCorrection = null; // This will be filled by the next call to updateSuggestion.
+ mCursorPositionWithinWord = mCodePointSize;
+ mRejectedBatchModeSuggestion = null;
+ mIsResumed = true;
+ }
+
+ public boolean isBatchMode() {
+ return mIsBatchMode;
+ }
+
+ public void setRejectedBatchModeSuggestion(final String rejectedSuggestion) {
+ mRejectedBatchModeSuggestion = rejectedSuggestion;
+ }
+
+ public String getRejectedBatchModeSuggestion() {
+ return mRejectedBatchModeSuggestion;
+ }
+
+ @UsedForTesting
+ void addInputPointerForTest(int index, int keyX, int keyY) {
+ mInputPointers.addPointerAt(index, keyX, keyY, 0, 0);
+ }
+
+ @UsedForTesting
+ void setTypedWordCacheForTests(String typedWordCacheForTests) {
+ mTypedWordCache = typedWordCacheForTests;
+ }
+}