diff --git a/java/res/values/strings-uix.xml b/java/res/values/strings-uix.xml index 6627fea9f4..bda2db7a6d 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 @@ -470,6 +477,8 @@ Configure additional layouts in the languages screen Show action/suggestions bar Show the bar containing suggestions. Recommended to keep enabled + Compact suggestion bar + Reduce the height of the suggestion bar and remove buttons from it Inline autofill Display password manager autofill and auto-reply suggestions (provided by app) in suggestion bar Quick period key 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/ActionBar.kt b/java/src/org/futo/inputmethod/latin/uix/ActionBar.kt index 0d981c1e17..d755eed96d 100644 --- a/java/src/org/futo/inputmethod/latin/uix/ActionBar.kt +++ b/java/src/org/futo/inputmethod/latin/uix/ActionBar.kt @@ -49,6 +49,7 @@ import androidx.compose.material3.Text import androidx.compose.material3.TextButton import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider +import androidx.compose.runtime.staticCompositionLocalOf import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.MutableFloatState @@ -161,6 +162,17 @@ import kotlin.math.roundToInt */ val ActionBarHeight = 40.dp +val CompactActionBarHeight = 26.dp + +val SuggestionBarCompactSetting = SettingsKey( + booleanPreferencesKey("suggestion_bar_compact"), + false +) + +val LocalSuggestionBarCompact = staticCompositionLocalOf { false } + +val currentActionBarHeight: Dp + @Composable get() = if (LocalSuggestionBarCompact.current) CompactActionBarHeight else ActionBarHeight val ActionBarScrollIndexSetting = SettingsKey( intPreferencesKey("action_bar_scroll_index"), @@ -196,7 +208,6 @@ val suggestionStylePrimary = TextStyle( fontSize = 18.sp, lineHeight = 26.sp, letterSpacing = 0.5.sp, - //textAlign = TextAlign.Center ) val suggestionStyleAlternative = TextStyle( @@ -205,7 +216,22 @@ val suggestionStyleAlternative = TextStyle( fontSize = 18.sp, lineHeight = 26.sp, letterSpacing = 0.5.sp, - //textAlign = TextAlign.Center +) + +val suggestionStylePrimaryCompact = TextStyle( + fontFamily = FontFamily.SansSerif, + fontWeight = FontWeight.Medium, + fontSize = 14.sp, + lineHeight = 18.sp, + letterSpacing = 0.5.sp, +) + +val suggestionStyleAlternativeCompact = TextStyle( + fontFamily = FontFamily.SansSerif, + fontWeight = FontWeight.Normal, + fontSize = 14.sp, + lineHeight = 18.sp, + letterSpacing = 0.5.sp, ) val suggestionStyleCandidateDescription = TextStyle( @@ -315,9 +341,12 @@ fun RowScope.SuggestionItem(words: SuggestedWords, idx: Int, isPrimary: Boolean, else -> Modifier } - val textStyle = when(isAutocorrect) { - true -> suggestionStylePrimary - false -> suggestionStyleAlternative + val compact = LocalSuggestionBarCompact.current + val textStyle = when { + isAutocorrect && compact -> suggestionStylePrimaryCompact + isAutocorrect -> suggestionStylePrimary + compact -> suggestionStyleAlternativeCompact + else -> suggestionStyleAlternative }.copy(color = color).withCustomFont() Box( @@ -787,13 +816,15 @@ fun ActionBar( val view = LocalView.current val context = LocalContext.current + val compact = LocalSuggestionBarCompact.current + val barHeight = if (compact) CompactActionBarHeight else ActionBarHeight val oldActionBar = useDataStore(OldStyleActionsBar) - val useDoubleHeight = isActionsExpanded && oldActionBar.value == false + val useDoubleHeight = isActionsExpanded && oldActionBar.value == false && !compact Column(Modifier .height( - ActionBarHeight * (if (useDoubleHeight) 2 else 1).let { + barHeight * (if (useDoubleHeight) 2 else 1).let { if(needToUseExpandableSuggestionUi) { it - 1 } else { @@ -805,7 +836,7 @@ fun ActionBar( testTag = "ActionBar" testTagsAsResourceId = true }) { - if(isActionsExpanded && !oldActionBar.value) { + if(isActionsExpanded && !oldActionBar.value && !compact) { ActionSep() Surface( @@ -827,30 +858,33 @@ fun ActionBar( .weight(1.0f), color = actionBarColor() ) { Row(Modifier.safeKeyboardPadding()) { - ExpandActionsButton(isActionsExpanded) { - toggleActionsExpanded() + if (!compact) { + ExpandActionsButton(isActionsExpanded) { + toggleActionsExpanded() - keyboardManagerForAction?.performHapticAndAudioFeedback( - Constants.CODE_TAB, - view - ) + keyboardManagerForAction?.performHapticAndAudioFeedback( + Constants.CODE_TAB, + view + ) + } } - if(oldActionBar.value && isActionsExpanded) { + if(oldActionBar.value && isActionsExpanded && !compact) { Box(modifier = Modifier .weight(1.0f) .fillMaxHeight()) { ActionItems(onActionActivated, onActionAltActivated) } } else { - if (importantNotice != null) { + if (importantNotice != null && !compact) { ImportantNoticeView(importantNotice) } else { if(Build.VERSION.SDK_INT >= Build.VERSION_CODES.R && inlineSuggestions.isNotEmpty() + && !compact ) { InlineSuggestions(inlineSuggestions) - } else if(quickClipState != null) { + } else if(quickClipState != null && !compact) { QuickClipView(quickClipState, onQuickClipDismiss) } else if (words != null) { SuggestionItems( @@ -873,7 +907,7 @@ fun ActionBar( Spacer(modifier = Modifier.weight(1.0f)) } - if(inlineSuggestions.isEmpty()) { + if(inlineSuggestions.isEmpty() && !compact) { PinnedActionItems(onActionActivated, onActionAltActivated) } } @@ -892,7 +926,7 @@ fun ActionWindowBar( onBack: () -> Unit, onExpand: () -> Unit ) { - Column(Modifier.height(ActionBarHeight)) { + Column(Modifier.height(currentActionBarHeight)) { ActionSep() Surface( modifier = Modifier @@ -955,7 +989,7 @@ fun CollapsibleSuggestionsBar( words: SuggestedWords?, suggestionStripListener: SuggestionStripViewListener, ) { - Column(Modifier.height(ActionBarHeight)) { + Column(Modifier.height(currentActionBarHeight)) { ActionSep() Surface( modifier = Modifier @@ -1215,7 +1249,7 @@ private fun RowScope.InlineCandidates( } } itemsIndexed(wordList) { i, it -> - CandidateItem(Modifier.height(ActionBarHeight), it, + CandidateItem(Modifier.height(currentActionBarHeight), it, listener = suggestionStripListener, last = i == wordList.size-1, width = with(LocalDensity.current) { @@ -1335,7 +1369,7 @@ fun BoxScope.ActionBarWithExpandableCandidates( if(canShowSuggest) { Surface( - Modifier.fillMaxWidth().padding(0.dp, ActionBarHeight, 0.dp, 0.dp) + Modifier.fillMaxWidth().padding(0.dp, currentActionBarHeight, 0.dp, 0.dp) .heightIn(max = with(density) { keyboardOffset?.intValue?.toDp() ?: 0.dp }) .safeKeyboardPadding() ) { @@ -1343,7 +1377,7 @@ fun BoxScope.ActionBarWithExpandableCandidates( listToRender ?: emptyList(), itemMeasurer = { measureWord(density, widths, it) } ) { allocatedWidth, item, isLast -> - CandidateItem(Modifier.height(ActionBarHeight), item, listener = suggestionStripListener, width=with(density) { allocatedWidth.toDp() }, last=isLast) + CandidateItem(Modifier.height(currentActionBarHeight), item, listener = suggestionStripListener, width=with(density) { allocatedWidth.toDp() }, last=isLast) } } } @@ -1353,7 +1387,7 @@ fun BoxScope.ActionBarWithExpandableCandidates( testTag = "ActionBar" testTagsAsResourceId = true } - .height(ActionBarHeight) + .height(currentActionBarHeight) .align(Alignment.TopCenter) ) { ActionSep() diff --git a/java/src/org/futo/inputmethod/latin/uix/QuickClip.kt b/java/src/org/futo/inputmethod/latin/uix/QuickClip.kt index aeb88e8e23..625850e544 100644 --- a/java/src/org/futo/inputmethod/latin/uix/QuickClip.kt +++ b/java/src/org/futo/inputmethod/latin/uix/QuickClip.kt @@ -322,7 +322,7 @@ object QuickClip { @Preview @Composable private fun PreviewQuickClips() { - Row(Modifier.height(ActionBarHeight)) { + Row(Modifier.height(currentActionBarHeight)) { QuickClipView(QuickClipState( texts = listOf(QuickClipItem( kind = QuickClipKind.FullString, diff --git a/java/src/org/futo/inputmethod/latin/uix/UixManager.kt b/java/src/org/futo/inputmethod/latin/uix/UixManager.kt index 831f0ff540..976d59caa6 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 @@ -227,12 +228,13 @@ fun BoxScope.KeyboardBackground( shader != null -> KeyboardSurfaceShaderBackground(shader, modifier = Modifier.matchParentSize()) image != null && rect != null -> { val navbarHeight = navBarHeight() + val actionBarHeightDp = currentActionBarHeight Canvas(Modifier.matchParentSize()) { drawRect(colorScheme.keyboardSurface) val fixedWidth = computedSize?.width?.toFloat() ?: size.width val fixedHeight = when { - (computedSize != null) -> computedSize.height + ActionBarHeight.toPx() + navbarHeight.toPx() + (computedSize != null) -> computedSize.height + actionBarHeightDp.toPx() + navbarHeight.toPx() else -> size.height } @@ -777,7 +779,7 @@ class UixManager(private val latinIME: LatinIME) { currWindowActionWindow.value?.fixedWindowHeight ?: ((latinIME .getInputViewHeight() .toFloat() / heightDiv.toFloat()).toDp() + - if (actionsExpanded) ActionBarHeight else 0.dp) + if (actionsExpanded) currentActionBarHeight else 0.dp) }) .safeKeyboardPadding() ) { @@ -1099,7 +1101,7 @@ class UixManager(private val latinIME: LatinIME) { Column(modifier = Modifier .matchParentSize() .absolutePadding( - top = if (isActionsExpanded.value) ActionBarHeight else 0.dp + top = if (isActionsExpanded.value) currentActionBarHeight else 0.dp ), horizontalAlignment = when(size.direction) { // Aligned opposite of the keyboard OneHandedDirection.Left -> Alignment.End @@ -1185,11 +1187,13 @@ class UixManager(private val latinIME: LatinIME) { private fun ProvidersAndWrapper(content: @Composable () -> Unit) { UixThemeWrapper(latinIME.colorScheme) { DataStoreCacheProvider { + val compactSuggestionBar = useDataStoreValue(SuggestionBarCompactSetting) CompositionLocalProvider( LocalManager provides keyboardManagerForAction, LocalThemeProvider provides latinIME.getDrawableProvider(), LocalLayoutDirection provides LayoutDirection.Ltr, - LocalFoldingState provides foldingOptions.value + LocalFoldingState provides foldingOptions.value, + LocalSuggestionBarCompact provides compactSuggestionBar ) { Box(Modifier .fillMaxSize() @@ -1428,6 +1432,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/actions/EmojiAction.kt b/java/src/org/futo/inputmethod/latin/uix/actions/EmojiAction.kt index 3526a44ed9..499485a1a9 100644 --- a/java/src/org/futo/inputmethod/latin/uix/actions/EmojiAction.kt +++ b/java/src/org/futo/inputmethod/latin/uix/actions/EmojiAction.kt @@ -100,6 +100,7 @@ import org.futo.inputmethod.latin.uix.EmojiTracker.getRecentEmojis import org.futo.inputmethod.latin.uix.EmojiTracker.resetRecentEmojis import org.futo.inputmethod.latin.uix.EmojiTracker.useEmoji import org.futo.inputmethod.latin.uix.LocalKeyboardScheme +import org.futo.inputmethod.latin.uix.LocalSuggestionBarCompact import org.futo.inputmethod.latin.uix.PersistentActionState import org.futo.inputmethod.latin.uix.actions.emoji.EmojiItem import org.futo.inputmethod.latin.uix.actions.emoji.EmojiView @@ -1011,6 +1012,11 @@ val EmojiAction = Action( @Composable override fun WindowTitleBar(rowScope: RowScope) { val context = LocalContext.current + val compact = LocalSuggestionBarCompact.current + if(compact) { + super.WindowTitleBar(rowScope) + return + } if(searching.value) { with(rowScope) { Surface( diff --git a/java/src/org/futo/inputmethod/latin/uix/actions/KeyboardSizingActions.kt b/java/src/org/futo/inputmethod/latin/uix/actions/KeyboardSizingActions.kt index 5375ea9d95..8670b8d7fb 100644 --- a/java/src/org/futo/inputmethod/latin/uix/actions/KeyboardSizingActions.kt +++ b/java/src/org/futo/inputmethod/latin/uix/actions/KeyboardSizingActions.kt @@ -26,6 +26,7 @@ import androidx.compose.ui.unit.dp import org.futo.inputmethod.latin.R import org.futo.inputmethod.latin.uix.Action import org.futo.inputmethod.latin.uix.ActionBarHeight +import org.futo.inputmethod.latin.uix.currentActionBarHeight import org.futo.inputmethod.latin.uix.ActionWindow import org.futo.inputmethod.latin.uix.CloseResult import org.futo.inputmethod.latin.uix.TutorialMode @@ -92,7 +93,7 @@ val KeyboardModeAction = Action( override fun WindowContents(keyboardShown: Boolean) { val currMode = sizeCalculator.getSavedSettings().currentMode Column { - Row(Modifier.height(ActionBarHeight)) { + Row(Modifier.height(currentActionBarHeight)) { // Hide the back button in the resize tutorial if(manager.getTutorialMode() != TutorialMode.ResizerTutorial) { IconButton(onClick = { 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..044b5d7643 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 @@ -96,6 +96,7 @@ import org.futo.inputmethod.latin.settings.toLongPressKeyLayoutItems import org.futo.inputmethod.latin.uix.AndroidTextInput import org.futo.inputmethod.latin.uix.BasicThemeProvider import org.futo.inputmethod.latin.uix.KeyHintsSetting +import org.futo.inputmethod.latin.uix.SuggestionBarCompactSetting import org.futo.inputmethod.latin.uix.LocalKeyboardScheme import org.futo.inputmethod.latin.uix.SHOW_EMOJI_SUGGESTIONS import org.futo.inputmethod.latin.uix.SettingsKey @@ -107,6 +108,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 @@ -783,6 +785,13 @@ val KeyboardSettingsMenu = UserSettingsMenu( Icon(painterResource(id = R.drawable.more_horizontal), contentDescription = null) } ), + userSettingToggleDataStore( + title = R.string.keyboard_settings_compact_suggestion_bar, + subtitle = R.string.keyboard_settings_compact_suggestion_bar_subtitle, + setting = SuggestionBarCompactSetting, + ).copy(visibilityCheck = { + useDataStore(ActionBarDisplayedSetting).value + }), userSettingToggleDataStore( title = R.string.keyboard_settings_inline_autofill, subtitle = R.string.keyboard_settings_inline_autofill_subtitle, @@ -807,16 +816,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, diff --git a/java/src/org/futo/inputmethod/v2keyboard/Keyboard.kt b/java/src/org/futo/inputmethod/v2keyboard/Keyboard.kt index 2624629bf5..e5c05dd719 100644 --- a/java/src/org/futo/inputmethod/v2keyboard/Keyboard.kt +++ b/java/src/org/futo/inputmethod/v2keyboard/Keyboard.kt @@ -293,7 +293,16 @@ data class Keyboard( val autoShift: Boolean = true, val subKeyboards: Map = emptyMap(), - val imeHint: String? = null + val imeHint: String? = null, + + /** + * (optional) Whether keyboard rows use staggered (center-gapped) alignment. + * When true (default), rows with fewer keys than the widest row are centered with + * gaps on either side, matching the traditional staggered keyboard layout. + * When false, rows are left-aligned with no centering gaps, producing an + * ortholinear/grid-style layout. + */ + val staggered: Boolean = true //val element: KeyboardElement = KeyboardElement.Alphabet, diff --git a/java/src/org/futo/inputmethod/v2keyboard/KeyboardSizingCalculator.kt b/java/src/org/futo/inputmethod/v2keyboard/KeyboardSizingCalculator.kt index 6aa363ff41..1c39f9683d 100644 --- a/java/src/org/futo/inputmethod/v2keyboard/KeyboardSizingCalculator.kt +++ b/java/src/org/futo/inputmethod/v2keyboard/KeyboardSizingCalculator.kt @@ -26,6 +26,7 @@ import org.futo.inputmethod.latin.FoldStateProvider import org.futo.inputmethod.latin.LatinIME import org.futo.inputmethod.latin.settings.SettingsValues import org.futo.inputmethod.latin.uix.OldStyleActionsBar +import org.futo.inputmethod.latin.uix.SuggestionBarCompactSetting import org.futo.inputmethod.latin.uix.SettingsKey import org.futo.inputmethod.latin.uix.UixManager import org.futo.inputmethod.latin.uix.getSetting @@ -553,7 +554,8 @@ class KeyboardSizingCalculator(val context: Context, val uixManager: UixManager) } fun calculateSuggestionBarHeightDp(): Float { - return 40.0f + val compact = context.getSetting(SuggestionBarCompactSetting) == true + return if (compact) 26.0f else 40.0f } fun calculateTotalActionBarHeightPx(): Int = diff --git a/java/src/org/futo/inputmethod/v2keyboard/LayoutEngine.kt b/java/src/org/futo/inputmethod/v2keyboard/LayoutEngine.kt index f4519e35f3..19f2731bde 100644 --- a/java/src/org/futo/inputmethod/v2keyboard/LayoutEngine.kt +++ b/java/src/org/futo/inputmethod/v2keyboard/LayoutEngine.kt @@ -360,7 +360,15 @@ data class LayoutEngine( val totalRowWidth = computedRow.sumOf { it.widthPx.toDouble() }.toFloat() val rowLayoutWidth = if(splittable) { layoutWidth } else { unsplitLayoutWidth } - val entries = mergeDuplicates(computedRow.addGap(rowLayoutWidth - totalRowWidth)) + val entries = if(keyboard.staggered) { + mergeDuplicates(computedRow.addGap(rowLayoutWidth - totalRowWidth)) + } else { + // Stretch keys to fill the entire row width (ortholinear/grid layout) + val extraPerKey = (rowLayoutWidth - totalRowWidth) / computedRow.size + computedRow.map { key -> + LayoutEntry.Key(key.data, key.widthPx + extraPerKey) + } + } return LayoutRow( entries = entries,