diff --git a/java/res/values/strings-uix.xml b/java/res/values/strings-uix.xml index 6627fea9f4..17096aaeaa 100644 --- a/java/res/values/strings-uix.xml +++ b/java/res/values/strings-uix.xml @@ -440,6 +440,13 @@ Swipe Typing (alpha) Allow swiping from key to key to write words. + Swipe left to delete word + Swipe right for space + Swipe down to accept middle prediction + Swipe diagonal down to accept side predictions + Swipe diagonal down right to accept right-side prediction, diagonal down left to accept left-side prediction + Swipe up to undo + Swipe Gestures Emoji Suggestions Suggest emojis while you\'re typing Vibration diff --git a/java/src/org/futo/inputmethod/keyboard/KeyboardActionListener.java b/java/src/org/futo/inputmethod/keyboard/KeyboardActionListener.java index 948fba78d3..a6be04242e 100644 --- a/java/src/org/futo/inputmethod/keyboard/KeyboardActionListener.java +++ b/java/src/org/futo/inputmethod/keyboard/KeyboardActionListener.java @@ -106,8 +106,16 @@ public interface KeyboardActionListener { public void onUpWithDeletePointerActive(); public void onUpWithPointerActive(); public void onSwipeLanguage(int direction); + public void onSwipeAction(int direction); public void onMovingCursorLockEvent(boolean canMoveCursor); + public static final int SWIPE_ACTION_LEFT = -1; + public static final int SWIPE_ACTION_RIGHT = 1; + public static final int SWIPE_ACTION_UP = 2; + public static final int SWIPE_ACTION_DOWN = 3; + public static final int SWIPE_ACTION_DOWN_LEFT = 4; + public static final int SWIPE_ACTION_DOWN_RIGHT = 5; + public static final KeyboardActionListener EMPTY_LISTENER = new Adapter(); public static class Adapter implements KeyboardActionListener { @@ -144,6 +152,8 @@ public void onUpWithPointerActive() {} @Override public void onSwipeLanguage(int direction) {} @Override + public void onSwipeAction(int direction) {} + @Override public void onMovingCursorLockEvent(boolean canMoveCursor) {} } } diff --git a/java/src/org/futo/inputmethod/keyboard/PointerTracker.java b/java/src/org/futo/inputmethod/keyboard/PointerTracker.java index e09c182dc5..c775a8ffe8 100644 --- a/java/src/org/futo/inputmethod/keyboard/PointerTracker.java +++ b/java/src/org/futo/inputmethod/keyboard/PointerTracker.java @@ -150,6 +150,10 @@ public PointerTrackerParams(final TypedArray mainKeyboardViewAttr) { private boolean mStartedOnFastLongPress; private boolean mCursorMoved = false; private boolean mSpacebarLongPressed = false; + private boolean mSwipeActionTriggered = false; + + private static final int sSwipeActionStep = + (int)(18.0 * Resources.getSystem().getDisplayMetrics().density); // true if keyboard layout has been changed. private boolean mKeyboardLayoutHasBeenChanged; @@ -745,8 +749,12 @@ private void onDownEventInternal(final int x, final int y, final long eventTime) mStartTime = System.currentTimeMillis(); mStartedOnFastLongPress = key.isFastLongPress(); mSpacebarLongPressed = false; + mSwipeActionTriggered = false; - mIsSlidingCursor = key.getCode() == Constants.CODE_DELETE || key.getCode() == Constants.CODE_SPACE; + final boolean swipeActionsMode = !Settings.getInstance().getCurrent().mGestureInputEnabled; + mIsSlidingCursor = key.getCode() == Constants.CODE_DELETE + || key.getCode() == Constants.CODE_SPACE + || swipeActionsMode; mIsFlickingKey = !mIsSlidingCursor && key.getHasFlick(); mFlickDirection = key.flickDirection(0, 0); mCurrentKey = key; @@ -954,6 +962,72 @@ private void onMoveEventInternal(final int x, final int y, final long eventTime) final SettingsValues settingsValues = Settings.getInstance().getCurrent(); + // Swipe actions (when gesture typing is off) + // Skip spacebar — it has its own swipe gestures (cursor movement, language switch) + if (mIsSlidingCursor && oldKey != null && !mSwipeActionTriggered + && !settingsValues.mGestureInputEnabled + && oldKey.getCode() != Constants.CODE_SPACE + && oldKey.getCode() != Constants.CODE_DELETE) { + final int dx = x - mStartX; + final int dy = y - mStartY; + final long swipeDistSq = (long)dx * dx + (long)dy * dy; + final long thresholdSq = (long)sSwipeActionStep * sSwipeActionStep; + final int swipeIgnoreTime = settingsValues.mKeyLongpressTimeout + / MULTIPLIER_FOR_LONG_PRESS_TIMEOUT_IN_SLIDING_INPUT; + + if (swipeDistSq >= thresholdSq + && mStartTime + swipeIgnoreTime < System.currentTimeMillis()) { + final int absDx = Math.abs(dx); + final int absDy = Math.abs(dy); + int direction = 0; + boolean enabled = false; + + if (absDx > 2 * absDy) { + // Horizontal swipe + if (dx < 0) { + direction = KeyboardActionListener.SWIPE_ACTION_LEFT; + enabled = settingsValues.mSwipeLeftDelete; + } else { + direction = KeyboardActionListener.SWIPE_ACTION_RIGHT; + enabled = settingsValues.mSwipeRightSpace; + } + } else if (absDy > 2 * absDx) { + // Vertical swipe + if (dy < 0) { + direction = KeyboardActionListener.SWIPE_ACTION_UP; + enabled = settingsValues.mSwipeUpUndo; + } else { + direction = KeyboardActionListener.SWIPE_ACTION_DOWN; + enabled = settingsValues.mSwipeDownPrediction; + } + } else if (dy > 0) { + // Diagonal down + if (dx < 0) { + direction = KeyboardActionListener.SWIPE_ACTION_DOWN_LEFT; + enabled = settingsValues.mSwipeDownLRPrediction; + } else { + direction = KeyboardActionListener.SWIPE_ACTION_DOWN_RIGHT; + enabled = settingsValues.mSwipeDownLRPrediction; + } + } + + if (enabled && direction != 0) { + mSwipeActionTriggered = true; + sTimerProxy.cancelKeyTimersOf(this); + sListener.onSwipeAction(direction); + mLastX = x; + mLastY = y; + return; + } + } + } + + if (mSwipeActionTriggered) { + mLastX = x; + mLastY = y; + return; + } + if (mIsSlidingCursor && oldKey != null && oldKey.getCode() == Constants.CODE_SPACE) { int pointerStep = sPointerStep; if(settingsValues.mSpacebarMode == Settings.SPACEBAR_MODE_SWIPE_LANGUAGE && !mSpacebarLongPressed) { @@ -1100,6 +1174,10 @@ private void onUpEventInternal(final int x, final int y, final long eventTime) { sListener.onUpWithPointerActive(); } + if (mSwipeActionTriggered) { + return; + } + if(mIsFlickingKey && currentKey != null) { final Key flickedKey = currentKey.flick(x - mStartX, y - mStartY); detectAndSendKey(flickedKey, mKeyX, mKeyY, eventTime); diff --git a/java/src/org/futo/inputmethod/latin/LatinIMELegacy.java b/java/src/org/futo/inputmethod/latin/LatinIMELegacy.java index 49bd237883..89a32ab4e7 100644 --- a/java/src/org/futo/inputmethod/latin/LatinIMELegacy.java +++ b/java/src/org/futo/inputmethod/latin/LatinIMELegacy.java @@ -47,6 +47,7 @@ import android.view.Window; import android.view.WindowManager; import android.view.inputmethod.EditorInfo; +import android.view.inputmethod.InputConnection; import android.view.inputmethod.InputMethodSubtype; import androidx.annotation.NonNull; @@ -648,6 +649,41 @@ public void onSwipeLanguage(int direction) { Subtypes.INSTANCE.switchToNextLanguage(mInputMethodService, direction); } + @Override + public void onSwipeAction(int direction) { + if (direction == KeyboardActionListener.SWIPE_ACTION_LEFT) { + // Delete word before cursor + sendCtrlKeyEvent(KeyEvent.KEYCODE_DEL); + } else if (direction == KeyboardActionListener.SWIPE_ACTION_RIGHT) { + // Insert space + onCodeInput(Constants.CODE_SPACE, + Constants.NOT_A_COORDINATE, Constants.NOT_A_COORDINATE, false); + } else if (direction == KeyboardActionListener.SWIPE_ACTION_UP) { + // Undo + sendCtrlKeyEvent(KeyEvent.KEYCODE_Z); + } else if (direction == KeyboardActionListener.SWIPE_ACTION_DOWN) { + // Accept center prediction + getLatinIME().getUixManager().pickSuggestionAtVisualPosition(1); + } else if (direction == KeyboardActionListener.SWIPE_ACTION_DOWN_LEFT) { + // Accept left prediction + getLatinIME().getUixManager().pickSuggestionAtVisualPosition(0); + } else if (direction == KeyboardActionListener.SWIPE_ACTION_DOWN_RIGHT) { + // Accept right prediction + getLatinIME().getUixManager().pickSuggestionAtVisualPosition(2); + } + } + + private void sendCtrlKeyEvent(int keyCode) { + final InputConnection ic = mInputMethodService.getCurrentInputConnection(); + if (ic != null) { + final long now = android.os.SystemClock.uptimeMillis(); + ic.sendKeyEvent(new KeyEvent( + now, now, KeyEvent.ACTION_DOWN, keyCode, 0, KeyEvent.META_CTRL_ON)); + ic.sendKeyEvent(new KeyEvent( + now, now, KeyEvent.ACTION_UP, keyCode, 0, KeyEvent.META_CTRL_ON)); + } + } + @Override public void onMovingCursorLockEvent(boolean canMoveCursor) { if(canMoveCursor) { diff --git a/java/src/org/futo/inputmethod/latin/settings/Settings.java b/java/src/org/futo/inputmethod/latin/settings/Settings.java index 61ac200881..f3f60b2c01 100644 --- a/java/src/org/futo/inputmethod/latin/settings/Settings.java +++ b/java/src/org/futo/inputmethod/latin/settings/Settings.java @@ -143,6 +143,12 @@ public final class Settings implements SharedPreferences.OnSharedPreferenceChang public static final String PREF_USE_WESTERN_NUMERALS = "pref_use_western_numerals"; + public static final String PREF_SWIPE_LEFT_DELETE = "swipe_left_delete"; + public static final String PREF_SWIPE_RIGHT_SPACE = "swipe_right_space"; + public static final String PREF_SWIPE_DOWN_PREDICTION = "swipe_down_prediction"; + public static final String PREF_SWIPE_DOWN_LR_PREDICTION = "swipe_down_lr_prediction"; + public static final String PREF_SWIPE_UP_UNDO = "swipe_up_undo"; + public static final int DEFAULT_ALT_SPACES_MODE = SPACES_MODE_ALL; // Emoji diff --git a/java/src/org/futo/inputmethod/latin/settings/SettingsValues.java b/java/src/org/futo/inputmethod/latin/settings/SettingsValues.java index 8c25837adb..b8f859c7d1 100644 --- a/java/src/org/futo/inputmethod/latin/settings/SettingsValues.java +++ b/java/src/org/futo/inputmethod/latin/settings/SettingsValues.java @@ -107,6 +107,12 @@ public class SettingsValues { public final int mNumberRowMode; public final int mAltSpacesMode; + public final boolean mSwipeLeftDelete; + public final boolean mSwipeRightSpace; + public final boolean mSwipeDownPrediction; + public final boolean mSwipeDownLRPrediction; + public final boolean mSwipeUpUndo; + // From the input box @Nonnull public final InputAttributes mInputAttributes; @@ -202,6 +208,12 @@ public SettingsValues(final Context context, final SharedPreferences prefs, fina : Settings.NUMBER_ROW_MODE_DEFAULT; mAltSpacesMode = prefs.getInt(Settings.PREF_ALT_SPACES_MODE, Settings.DEFAULT_ALT_SPACES_MODE); + mSwipeLeftDelete = prefs.getBoolean(Settings.PREF_SWIPE_LEFT_DELETE, false); + mSwipeRightSpace = prefs.getBoolean(Settings.PREF_SWIPE_RIGHT_SPACE, false); + mSwipeDownPrediction = prefs.getBoolean(Settings.PREF_SWIPE_DOWN_PREDICTION, false); + mSwipeDownLRPrediction = prefs.getBoolean(Settings.PREF_SWIPE_DOWN_LR_PREDICTION, false); + mSwipeUpUndo = prefs.getBoolean(Settings.PREF_SWIPE_UP_UNDO, false); + mShouldShowLxxSuggestionUi = Settings.SHOULD_SHOW_LXX_SUGGESTION_UI && prefs.getBoolean(DebugSettings.PREF_SHOULD_SHOW_LXX_SUGGESTION_UI, true); // Compute other readable settings diff --git a/java/src/org/futo/inputmethod/latin/uix/UixManager.kt b/java/src/org/futo/inputmethod/latin/uix/UixManager.kt index 831f0ff540..9ed08a42b5 100644 --- a/java/src/org/futo/inputmethod/latin/uix/UixManager.kt +++ b/java/src/org/futo/inputmethod/latin/uix/UixManager.kt @@ -132,6 +132,7 @@ import org.futo.inputmethod.latin.uix.settings.DataStoreCacheProvider import org.futo.inputmethod.latin.uix.settings.pages.ActionBarDisplayedSetting import org.futo.inputmethod.latin.uix.settings.pages.InlineAutofillSetting import org.futo.inputmethod.latin.uix.settings.useDataStore +import org.futo.inputmethod.latin.uix.settings.useDataStoreValue import org.futo.inputmethod.latin.uix.theme.KeyboardSurfaceShaderBackground import org.futo.inputmethod.latin.uix.theme.Typography import org.futo.inputmethod.latin.uix.theme.UixThemeAuto @@ -1428,6 +1429,57 @@ class UixManager(private val latinIME: LatinIME) { } } + fun getSuggestionAtVisualPosition(position: Int): SuggestedWordInfo? { + val words = suggestedWords.value ?: return null + val layout = makeSuggestionLayout(words, null) + + if (layout.isGestureBatch || (layout.emojiMatches.isEmpty() && layout.presentableSuggestions.size <= 1)) { + return if (position == 1) layout.presentableSuggestions.firstOrNull() else null + } + + if (layout.autocorrectMatch != null) { + var supplementalIndex = 0 + val left = if (layout.emojiMatches.isEmpty()) { + layout.sortedMatches.getOrNull(supplementalIndex++) + } else { + layout.emojiMatches.getOrNull(0) + } + val center = layout.autocorrectMatch + val right = if (layout.verbatimWord != null && layout.verbatimWord.mWord != layout.autocorrectMatch.mWord) { + layout.verbatimWord + } else { + layout.sortedMatches.getOrNull(supplementalIndex) + } + return when (position) { + 0 -> left + 1 -> center + 2 -> right + else -> null + } + } + + // No autocorrect + var supplementalIndex = 1 + val left = if (layout.emojiMatches.isEmpty()) { + layout.sortedMatches.getOrNull(supplementalIndex++) + } else { + layout.emojiMatches.getOrNull(0) + } + val center = layout.sortedMatches.getOrNull(0) + val right = layout.sortedMatches.getOrNull(supplementalIndex) + return when (position) { + 0 -> left + 1 -> center + 2 -> right + else -> null + } + } + + fun pickSuggestionAtVisualPosition(position: Int) { + val suggestion = getSuggestionAtVisualPosition(position) ?: return + latinIME.latinIMELegacy.pickSuggestionManually(suggestion) + } + fun requestForgetWord(suggestedWordInfo: SuggestedWords.SuggestedWordInfo) { keyboardManagerForAction.requestDialog( latinIME.getString(R.string.keyboard_suggest_blacklist_body, suggestedWordInfo.mWord), diff --git a/java/src/org/futo/inputmethod/latin/uix/settings/pages/Typing.kt b/java/src/org/futo/inputmethod/latin/uix/settings/pages/Typing.kt index de8fea13c6..23daadc1e2 100644 --- a/java/src/org/futo/inputmethod/latin/uix/settings/pages/Typing.kt +++ b/java/src/org/futo/inputmethod/latin/uix/settings/pages/Typing.kt @@ -107,6 +107,7 @@ import org.futo.inputmethod.latin.uix.settings.DropDownPickerSettingItem import org.futo.inputmethod.latin.uix.settings.LocalSharedPrefsCache import org.futo.inputmethod.latin.uix.settings.NavigationItemStyle import org.futo.inputmethod.latin.uix.settings.PrimarySettingToggleDataStoreItem +import org.futo.inputmethod.latin.uix.settings.SettingToggleRaw import org.futo.inputmethod.latin.uix.settings.ScreenTitle import org.futo.inputmethod.latin.uix.settings.ScrollableList import org.futo.inputmethod.latin.uix.settings.SettingItem @@ -807,16 +808,81 @@ val TypingSettingsMenu = UserSettingsMenu( AutoSpacesSetting() } ), - userSettingToggleSharedPrefs( - title = R.string.typing_settings_swipe, + UserSetting( + name = R.string.typing_settings_swipe, subtitle = R.string.typing_settings_swipe_subtitle, - key = Settings.PREF_GESTURE_INPUT, - default = {true}, - icon = { - Icon(painterResource(id = R.drawable.swipe_icon), contentDescription = null, - tint = MaterialTheme.colorScheme.onBackground.copy(alpha = 0.75f)) - } - ), + ) { + val gestureInput = useSharedPrefsBool(Settings.PREF_GESTURE_INPUT, true) + val leftDelete = useSharedPrefsBool(Settings.PREF_SWIPE_LEFT_DELETE, false) + val rightSpace = useSharedPrefsBool(Settings.PREF_SWIPE_RIGHT_SPACE, false) + val downPrediction = useSharedPrefsBool(Settings.PREF_SWIPE_DOWN_PREDICTION, false) + val downLrPrediction = useSharedPrefsBool(Settings.PREF_SWIPE_DOWN_LR_PREDICTION, false) + val upUndo = useSharedPrefsBool(Settings.PREF_SWIPE_UP_UNDO, false) + + SettingToggleRaw( + title = stringResource(R.string.typing_settings_swipe), + subtitle = stringResource(R.string.typing_settings_swipe_subtitle), + enabled = gestureInput.value, + setValue = { newValue -> + gestureInput.setValue(newValue) + if (newValue) { + leftDelete.setValue(false) + rightSpace.setValue(false) + downPrediction.setValue(false) + downLrPrediction.setValue(false) + upUndo.setValue(false) + } + }, + icon = { + Icon(painterResource(id = R.drawable.swipe_icon), contentDescription = null, + tint = MaterialTheme.colorScheme.onBackground.copy(alpha = 0.75f)) + } + ) + }, + + UserSetting(name = R.string.swipe_gestures_title) { + val gestureInputOn = useSharedPrefsBool(Settings.PREF_GESTURE_INPUT, true) + val enabled = !gestureInputOn.value + + val leftDelete = useSharedPrefsBool(Settings.PREF_SWIPE_LEFT_DELETE, false) + val rightSpace = useSharedPrefsBool(Settings.PREF_SWIPE_RIGHT_SPACE, false) + val downPrediction = useSharedPrefsBool(Settings.PREF_SWIPE_DOWN_PREDICTION, false) + val downLrPrediction = useSharedPrefsBool(Settings.PREF_SWIPE_DOWN_LR_PREDICTION, false) + val upUndo = useSharedPrefsBool(Settings.PREF_SWIPE_UP_UNDO, false) + + SettingToggleRaw( + title = stringResource(R.string.swipe_gesture_left_delete), + enabled = leftDelete.value, + disabled = !enabled, + setValue = { leftDelete.setValue(it) } + ) + SettingToggleRaw( + title = stringResource(R.string.swipe_gesture_right_space), + enabled = rightSpace.value, + disabled = !enabled, + setValue = { rightSpace.setValue(it) } + ) + SettingToggleRaw( + title = stringResource(R.string.swipe_gesture_down_prediction), + enabled = downPrediction.value, + disabled = !enabled, + setValue = { downPrediction.setValue(it) } + ) + SettingToggleRaw( + title = stringResource(R.string.swipe_gesture_down_lr_prediction), + subtitle = stringResource(R.string.swipe_gesture_down_lr_prediction_subtitle), + enabled = downLrPrediction.value, + disabled = !enabled, + setValue = { downLrPrediction.setValue(it) } + ) + SettingToggleRaw( + title = stringResource(R.string.swipe_gesture_up_undo), + enabled = upUndo.value, + disabled = !enabled, + setValue = { upUndo.setValue(it) } + ) + }, + userSettingToggleDataStore( title = R.string.typing_settings_suggest_emojis, subtitle = R.string.typing_settings_suggest_emojis_subtitle,