Compare commits

...

14 Commits

Author SHA1 Message Date
Aleksandras Kostarevas
ccbd59ee80 Update suggestions bar to show verbatim word only when relevant and make it clear it's the verbatim word 2024-09-28 01:06:07 +03:00
Aleksandras Kostarevas
2f8d847186 Add basic resizer for floating mode 2024-09-27 22:38:46 +03:00
Aleksandras Kostarevas
112b7a291a Separate some compose UI components to make things more understandable 2024-09-27 20:20:18 +03:00
Aleksandras Kostarevas
2e2bba2ac2 Rename floatingBottomCenterOriginDp to floatingBottomOriginDp to better represent actual implementation 2024-09-27 18:53:02 +03:00
Aleksandras Kostarevas
28f3c07d5f Fix landscape floating keyboard going offscreen on the right 2024-09-27 18:50:47 +03:00
Aleksandras Kostarevas
1c9c94b83d Reduce max typed word length 2024-09-27 18:50:35 +03:00
Aleksandras Kostarevas
da95c69668 Remove InputView parent to mitigate crash when changing keyboard size 2024-09-27 18:44:36 +03:00
Aleksandras Kostarevas
a668a3c801 Disable some needless logging 2024-09-27 18:43:28 +03:00
Aleksandras Kostarevas
c5cb5efabd Do modal input when showing more keys panel (fixes Talkback bug) 2024-09-27 18:33:35 +03:00
Aleksandras Kostarevas
7fdf54ec61 Disable neutral punctuation suggestions 2024-09-27 18:21:39 +03:00
Aleksandras Kostarevas
2ff68bf7cd Fix bug causing cursor being moved upon selecting a word 2024-09-27 18:15:12 +03:00
Aleksandras Kostarevas
3d8233be92 Update keyboard sizing and add floating mode 2024-09-27 00:23:03 +03:00
Aleksandras Kostarevas
32b27b84c0 Remove stray debug comment 2024-09-23 22:27:43 +03:00
Aleksandras Kostarevas
4566a37e16 Add spoken description for numpad layout 2024-09-23 20:38:13 +03:00
26 changed files with 1249 additions and 394 deletions

View File

@ -53,7 +53,7 @@
<!-- Spoken description for the "To Alpha" keyboard key. --> <!-- Spoken description for the "To Alpha" keyboard key. -->
<string name="spoken_description_to_alpha">Letters</string> <string name="spoken_description_to_alpha">Letters</string>
<!-- Spoken description for the "To Numbers" keyboard key. --> <!-- Spoken description for the "To Numbers" keyboard key. -->
<string name="spoken_description_to_numeric">Numbers</string> <string name="spoken_description_to_numeric">Digits</string>
<!-- Spoken description for the "Settings" keyboard key. --> <!-- Spoken description for the "Settings" keyboard key. -->
<string name="spoken_description_settings">Settings</string> <string name="spoken_description_settings">Settings</string>
<!-- Spoken description for the "Tab" keyboard key. --> <!-- Spoken description for the "Tab" keyboard key. -->
@ -77,6 +77,12 @@
<!-- Spoken description for the "Previous" action keyboard key. --> <!-- Spoken description for the "Previous" action keyboard key. -->
<string name="spoken_description_action_previous">Previous</string> <string name="spoken_description_action_previous">Previous</string>
<!-- Spoken description for the "To Numbers" keyboard key. -->
<string name="spoken_description_to_alt_0">Page 1</string>
<string name="spoken_description_to_alt_1">Page 2</string>
<string name="spoken_description_to_alt_2">Page 3</string>
<!-- Spoken feedback after turning "Shift" mode on. --> <!-- Spoken feedback after turning "Shift" mode on. -->
<string name="spoken_description_shiftmode_on">Shift enabled</string> <string name="spoken_description_shiftmode_on">Shift enabled</string>
<!-- Spoken feedback after turning "Caps lock" mode on. --> <!-- Spoken feedback after turning "Caps lock" mode on. -->
@ -91,6 +97,8 @@
<string name="spoken_description_mode_phone">Phone mode</string> <string name="spoken_description_mode_phone">Phone mode</string>
<!-- Spoken feedback after changing to the shifted phone dialer (symbols) keyboard. --> <!-- Spoken feedback after changing to the shifted phone dialer (symbols) keyboard. -->
<string name="spoken_description_mode_phone_shift">Phone symbols mode</string> <string name="spoken_description_mode_phone_shift">Phone symbols mode</string>
<!-- Spoken feedback after changing to the digits keyboard. -->
<string name="spoken_description_mode_digits">Digits mode</string>
<!-- Spoken feedback when the keyboard is hidden. --> <!-- Spoken feedback when the keyboard is hidden. -->
<string name="announce_keyboard_hidden">Keyboard hidden</string> <string name="announce_keyboard_hidden">Keyboard hidden</string>

View File

@ -16,6 +16,11 @@
<string name="more_actions_action_title">All Actions</string> <string name="more_actions_action_title">All Actions</string>
<string name="bug_viewer_action_title">Bug Viewer</string> <string name="bug_viewer_action_title">Bug Viewer</string>
<string name="left_handed_keyboard_action_title">Left-Handed Keyboard</string>
<string name="right_handed_keyboard_action_title">Right-Handed Keyboard</string>
<string name="split_keyboard_action_title">Split Keyboard</string>
<string name="floating_keyboard_action_title">Floating Keyboard</string>
<string name="action_kind_action_key">Action Key</string> <string name="action_kind_action_key">Action Key</string>
<string name="action_kind_pinned_key">Pinned Action(s)</string> <string name="action_kind_pinned_key">Pinned Action(s)</string>
<string name="action_kind_favorites">Favorite Actions</string> <string name="action_kind_favorites">Favorite Actions</string>

View File

@ -62,6 +62,10 @@ final class KeyCodeDescriptionMapper {
mKeyCodeMap.put(Constants.CODE_SHIFT, R.string.spoken_description_shift); mKeyCodeMap.put(Constants.CODE_SHIFT, R.string.spoken_description_shift);
mKeyCodeMap.put(Constants.CODE_SHORTCUT, R.string.spoken_description_mic); mKeyCodeMap.put(Constants.CODE_SHORTCUT, R.string.spoken_description_mic);
mKeyCodeMap.put(Constants.CODE_SWITCH_ALPHA_SYMBOL, R.string.spoken_description_to_symbol); mKeyCodeMap.put(Constants.CODE_SWITCH_ALPHA_SYMBOL, R.string.spoken_description_to_symbol);
mKeyCodeMap.put(Constants.CODE_TO_NUMBER_LAYOUT, R.string.spoken_description_to_numeric);
mKeyCodeMap.put(Constants.CODE_TO_ALT_0_LAYOUT, R.string.spoken_description_to_alt_0);
mKeyCodeMap.put(Constants.CODE_TO_ALT_1_LAYOUT, R.string.spoken_description_to_alt_1);
mKeyCodeMap.put(Constants.CODE_TO_ALT_2_LAYOUT, R.string.spoken_description_to_alt_2);
mKeyCodeMap.put(Constants.CODE_TAB, R.string.spoken_description_tab); mKeyCodeMap.put(Constants.CODE_TAB, R.string.spoken_description_tab);
mKeyCodeMap.put(Constants.CODE_LANGUAGE_SWITCH, mKeyCodeMap.put(Constants.CODE_LANGUAGE_SWITCH,
R.string.spoken_description_language_switch); R.string.spoken_description_language_switch);

View File

@ -181,7 +181,16 @@ public class KeyboardAccessibilityDelegate<KV extends KeyboardView>
node.setFocusable(true); node.setFocusable(true);
node.setScreenReaderFocusable(true); node.setScreenReaderFocusable(true);
if(k.isActionKey() || k.getCode() == Constants.CODE_SWITCH_ALPHA_SYMBOL || k.getCode() == Constants.CODE_EMOJI || k.getCode() == Constants.CODE_SYMBOL_SHIFT || (k.getCode() >= Constants.CODE_ACTION_0 && k.getCode() <= Constants.CODE_ACTION_MAX)) { if(k.isActionKey() ||
k.getCode() == Constants.CODE_SWITCH_ALPHA_SYMBOL ||
k.getCode() == Constants.CODE_EMOJI ||
k.getCode() == Constants.CODE_SYMBOL_SHIFT ||
k.getCode() == Constants.CODE_TO_ALT_0_LAYOUT ||
k.getCode() == Constants.CODE_TO_ALT_1_LAYOUT ||
k.getCode() == Constants.CODE_TO_ALT_2_LAYOUT ||
k.getCode() == Constants.CODE_TO_NUMBER_LAYOUT ||
(k.getCode() >= Constants.CODE_ACTION_0 && k.getCode() <= Constants.CODE_ACTION_MAX)
) {
node.addAction(AccessibilityNodeInfoCompat.ACTION_CLICK); node.addAction(AccessibilityNodeInfoCompat.ACTION_CLICK);
node.addAction(AccessibilityNodeInfoCompat.ACTION_LONG_CLICK); node.addAction(AccessibilityNodeInfoCompat.ACTION_LONG_CLICK);
node.setClickable(true); node.setClickable(true);

View File

@ -18,10 +18,8 @@ package org.futo.inputmethod.accessibility;
import android.content.Context; import android.content.Context;
import android.graphics.Rect; import android.graphics.Rect;
import android.os.SystemClock;
import android.util.Log; import android.util.Log;
import android.util.SparseIntArray; import android.util.SparseIntArray;
import android.view.MotionEvent;
import org.futo.inputmethod.keyboard.Key; import org.futo.inputmethod.keyboard.Key;
import org.futo.inputmethod.keyboard.KeyDetector; import org.futo.inputmethod.keyboard.KeyDetector;
@ -30,7 +28,6 @@ import org.futo.inputmethod.keyboard.KeyboardId;
import org.futo.inputmethod.keyboard.MainKeyboardView; import org.futo.inputmethod.keyboard.MainKeyboardView;
import org.futo.inputmethod.keyboard.PointerTracker; import org.futo.inputmethod.keyboard.PointerTracker;
import org.futo.inputmethod.latin.R; import org.futo.inputmethod.latin.R;
import org.futo.inputmethod.latin.utils.SubtypeLocaleUtils;
/** /**
* This class represents a delegate that can be registered in {@link MainKeyboardView} to enhance * This class represents a delegate that can be registered in {@link MainKeyboardView} to enhance
@ -191,6 +188,9 @@ public final class MainKeyboardAccessibilityDelegate
case KeyboardId.ELEMENT_PHONE_SYMBOLS: case KeyboardId.ELEMENT_PHONE_SYMBOLS:
resId = R.string.spoken_description_mode_phone_shift; resId = R.string.spoken_description_mode_phone_shift;
break; break;
case KeyboardId.ELEMENT_NUMBER:
resId = R.string.spoken_description_mode_digits;
break;
default: default:
return; return;
} }

View File

@ -23,7 +23,6 @@ import android.text.TextUtils;
import android.view.inputmethod.EditorInfo; import android.view.inputmethod.EditorInfo;
import org.futo.inputmethod.compat.EditorInfoCompatUtils; import org.futo.inputmethod.compat.EditorInfoCompatUtils;
import org.futo.inputmethod.latin.RichInputMethodSubtype;
import org.futo.inputmethod.latin.settings.LongPressKeySettings; import org.futo.inputmethod.latin.settings.LongPressKeySettings;
import org.futo.inputmethod.latin.utils.InputTypeUtils; import org.futo.inputmethod.latin.utils.InputTypeUtils;

View File

@ -140,40 +140,16 @@ public final class KeyboardSwitcher implements SwitchActions {
} }
final KeyboardSizingCalculator sizingCalculator = new KeyboardSizingCalculator(mLatinIMELegacy.getInputMethodService()); final KeyboardSizingCalculator sizingCalculator = ((LatinIME)mLatinIMELegacy.getInputMethodService()).getSizingCalculator();
final ComputedKeyboardSize computedSize = sizingCalculator.calculate(layoutSetName, settingsValues.mIsNumberRowEnabled); final ComputedKeyboardSize computedSize = sizingCalculator.calculate(layoutSetName, settingsValues.mIsNumberRowEnabled);
int keyboardWidth = 0;
int keyboardHeight = 0;
int splitLayoutWidth = 0;
Rect padding = new Rect();
Window window = mLatinIMELegacy.getInputMethodService().getWindow().getWindow();
if(computedSize instanceof SplitKeyboardSize) {
keyboardWidth = ResourceUtils.getDefaultKeyboardWidth(window, res);
keyboardHeight = ((SplitKeyboardSize) computedSize).getHeight();
splitLayoutWidth = ((SplitKeyboardSize) computedSize).getSplitLayoutWidth();
padding = ((SplitKeyboardSize) computedSize).getPadding();
}else if(computedSize instanceof RegularKeyboardSize) {
keyboardWidth = ResourceUtils.getDefaultKeyboardWidth(window, res);
keyboardHeight = ((RegularKeyboardSize) computedSize).getHeight();
padding = ((RegularKeyboardSize) computedSize).getPadding();
}
final KeyboardLayoutSetV2Params params = new KeyboardLayoutSetV2Params( final KeyboardLayoutSetV2Params params = new KeyboardLayoutSetV2Params(
keyboardWidth, computedSize,
keyboardHeight,
padding,
layoutSetName, layoutSetName,
subtype.getLocale(), subtype.getLocale(),
editorInfo == null ? new EditorInfo() : editorInfo, editorInfo == null ? new EditorInfo() : editorInfo,
settingsValues.mIsNumberRowEnabled, settingsValues.mIsNumberRowEnabled,
sizingCalculator.calculateGap(), sizingCalculator.calculateGap(),
splitLayoutWidth != 0,
splitLayoutWidth,
settingsValues.mShowsActionKey ? settingsValues.mActionKeyId : null, settingsValues.mShowsActionKey ? settingsValues.mActionKeyId : null,
LongPressKeySettings.load(mThemeContext) LongPressKeySettings.load(mThemeContext)
); );
@ -353,9 +329,7 @@ public final class KeyboardSwitcher implements SwitchActions {
} }
public boolean isShowingMoreKeysPanel() { public boolean isShowingMoreKeysPanel() {
if (isShowingEmojiPalettes()) { if(mKeyboardView == null) return false;
return false;
}
return mKeyboardView.isShowingMoreKeysPanel(); return mKeyboardView.isShowingMoreKeysPanel();
} }

View File

@ -132,16 +132,8 @@ public final class KeyPreviewChoreographer {
final int keyPreviewPosition; final int keyPreviewPosition;
int previewX = key.getDrawX() - (previewWidth - keyDrawWidth) / 2 int previewX = key.getDrawX() - (previewWidth - keyDrawWidth) / 2
+ CoordinateUtils.x(originCoords); + CoordinateUtils.x(originCoords);
if (previewX < 0) { keyPreviewPosition = KeyPreviewView.POSITION_MIDDLE;
previewX = 0; final boolean hasMoreKeys = !key.getMoreKeys().isEmpty();
keyPreviewPosition = KeyPreviewView.POSITION_LEFT;
} else if (previewX > keyboardViewWidth - previewWidth) {
previewX = keyboardViewWidth - previewWidth;
keyPreviewPosition = KeyPreviewView.POSITION_RIGHT;
} else {
keyPreviewPosition = KeyPreviewView.POSITION_MIDDLE;
}
final boolean hasMoreKeys = (key.getMoreKeys() != null);
keyPreviewView.setPreviewBackground(hasMoreKeys, keyPreviewPosition); keyPreviewView.setPreviewBackground(hasMoreKeys, keyPreviewPosition);
// The key preview is placed vertically above the top edge of the parent key with an // The key preview is placed vertically above the top edge of the parent key with an
// arbitrary offset. // arbitrary offset.

View File

@ -126,8 +126,8 @@ internal data class SavedKeyboardState(
class KeyboardState(private val switchActions: SwitchActions) { class KeyboardState(private val switchActions: SwitchActions) {
companion object { companion object {
private const val TAG = "KeyboardState" private const val TAG = "KeyboardState"
private const val DEBUG_EVENT = true private const val DEBUG_EVENT = false
private const val DEBUG_INTERNAL_ACTION = true private const val DEBUG_INTERNAL_ACTION = false
} }
private val shiftKeyState = ShiftKeyState("Shift") private val shiftKeyState = ShiftKeyState("Shift")

View File

@ -12,6 +12,7 @@ import android.os.Bundle
import android.util.Log import android.util.Log
import android.view.KeyEvent import android.view.KeyEvent
import android.view.View import android.view.View
import android.view.ViewGroup
import android.view.inputmethod.CompletionInfo import android.view.inputmethod.CompletionInfo
import android.view.inputmethod.EditorInfo import android.view.inputmethod.EditorInfo
import android.view.inputmethod.InlineSuggestionsRequest import android.view.inputmethod.InlineSuggestionsRequest
@ -21,10 +22,14 @@ import android.view.inputmethod.InputMethodSubtype
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
import androidx.compose.foundation.layout.size import androidx.compose.foundation.layout.size
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.key import androidx.compose.runtime.key
import androidx.compose.runtime.mutableStateOf
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clipToBounds import androidx.compose.ui.draw.clipToBounds
import androidx.compose.ui.layout.onSizeChanged import androidx.compose.ui.layout.onSizeChanged
import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.platform.ViewCompositionStrategy
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.viewinterop.AndroidView import androidx.compose.ui.viewinterop.AndroidView
import androidx.lifecycle.Lifecycle import androidx.lifecycle.Lifecycle
@ -75,28 +80,27 @@ import org.futo.inputmethod.latin.uix.theme.applyWindowColors
import org.futo.inputmethod.latin.uix.theme.presets.VoiceInputTheme import org.futo.inputmethod.latin.uix.theme.presets.VoiceInputTheme
import org.futo.inputmethod.latin.xlm.LanguageModelFacilitator import org.futo.inputmethod.latin.xlm.LanguageModelFacilitator
import org.futo.inputmethod.updates.scheduleUpdateCheckingJob import org.futo.inputmethod.updates.scheduleUpdateCheckingJob
import org.futo.inputmethod.v2keyboard.ComputedKeyboardSize
import org.futo.inputmethod.v2keyboard.FloatingKeyboardSize
import org.futo.inputmethod.v2keyboard.KeyboardSettings
import org.futo.inputmethod.v2keyboard.KeyboardSizeSettingKind import org.futo.inputmethod.v2keyboard.KeyboardSizeSettingKind
import org.futo.inputmethod.v2keyboard.KeyboardSizeStateProvider import org.futo.inputmethod.v2keyboard.KeyboardSizeStateProvider
import org.futo.inputmethod.v2keyboard.KeyboardSizingCalculator
import org.futo.inputmethod.v2keyboard.getHeight
private class UnlockedBroadcastReceiver(val onDeviceUnlocked: () -> Unit) : BroadcastReceiver() { private class UnlockedBroadcastReceiver(val onDeviceUnlocked: () -> Unit) : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) { override fun onReceive(context: Context?, intent: Intent?) {
println("Unlocked Broadcast Receiver: ${intent?.action}")
if (intent?.action == Intent.ACTION_USER_UNLOCKED) { if (intent?.action == Intent.ACTION_USER_UNLOCKED) {
onDeviceUnlocked() onDeviceUnlocked()
} }
} }
} }
class LatinIME : InputMethodService(), LifecycleOwner, ViewModelStoreOwner, SavedStateRegistryOwner, open class InputMethodServiceCompose : InputMethodService(), LifecycleOwner, ViewModelStoreOwner, SavedStateRegistryOwner {
LatinIMELegacy.SuggestionStripController, DynamicThemeProviderOwner, FoldStateProvider,
KeyboardSizeStateProvider {
private lateinit var mLifecycleRegistry: LifecycleRegistry private lateinit var mLifecycleRegistry: LifecycleRegistry
private lateinit var mViewModelStore: ViewModelStore private lateinit var mViewModelStore: ViewModelStore
private lateinit var mSavedStateRegistryController: SavedStateRegistryController private lateinit var mSavedStateRegistryController: SavedStateRegistryController
fun setOwners() { fun setOwners() {
val decorView = window.window?.decorView val decorView = window.window?.decorView
if (decorView?.findViewTreeLifecycleOwner() == null) { if (decorView?.findViewTreeLifecycleOwner() == null) {
@ -110,11 +114,59 @@ class LatinIME : InputMethodService(), LifecycleOwner, ViewModelStoreOwner, Save
} }
} }
override fun onCreate() {
super.onCreate()
mLifecycleRegistry = LifecycleRegistry(this)
mLifecycleRegistry.currentState = Lifecycle.State.INITIALIZED
mViewModelStore = ViewModelStore()
mSavedStateRegistryController = SavedStateRegistryController.create(this)
mSavedStateRegistryController.performRestore(null)
mLifecycleRegistry.currentState = Lifecycle.State.CREATED
}
override fun onStartInputView(editorInfo: EditorInfo?, restarting: Boolean) {
super.onStartInputView(editorInfo, restarting)
mLifecycleRegistry.currentState = Lifecycle.State.STARTED
}
override fun onDestroy() {
super.onDestroy()
mLifecycleRegistry.currentState = Lifecycle.State.DESTROYED
}
override val lifecycle: Lifecycle
get() = mLifecycleRegistry
override val savedStateRegistry: SavedStateRegistry
get() = mSavedStateRegistryController.savedStateRegistry
override val viewModelStore: ViewModelStore
get() = mViewModelStore
internal var composeView: ComposeView? = null
override fun onCreateInputView(): View =
ComposeView(this).apply {
setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
setParentCompositionContext(null)
setOwners()
composeView = this
}
}
class LatinIME : InputMethodServiceCompose(), LatinIMELegacy.SuggestionStripController,
DynamicThemeProviderOwner, FoldStateProvider, KeyboardSizeStateProvider {
val latinIMELegacy = LatinIMELegacy( val latinIMELegacy = LatinIMELegacy(
this as InputMethodService, this as InputMethodService,
this as LatinIMELegacy.SuggestionStripController this as LatinIMELegacy.SuggestionStripController
) )
val inputLogic get() = latinIMELegacy.mInputLogic val inputLogic get() = latinIMELegacy.mInputLogic
lateinit var languageModelFacilitator: LanguageModelFacilitator lateinit var languageModelFacilitator: LanguageModelFacilitator
@ -122,6 +174,8 @@ class LatinIME : InputMethodService(), LifecycleOwner, ViewModelStoreOwner, Save
val uixManager = UixManager(this) val uixManager = UixManager(this)
lateinit var suggestionBlacklist: SuggestionBlacklist lateinit var suggestionBlacklist: SuggestionBlacklist
val sizingCalculator = KeyboardSizingCalculator(this, uixManager)
private var activeThemeOption: ThemeOption? = null private var activeThemeOption: ThemeOption? = null
private var activeColorScheme = VoiceInputTheme.obtainColors(this) private var activeColorScheme = VoiceInputTheme.obtainColors(this)
private var pendingRecreateKeyboard: Boolean = false private var pendingRecreateKeyboard: Boolean = false
@ -130,6 +184,13 @@ class LatinIME : InputMethodService(), LifecycleOwner, ViewModelStoreOwner, Save
val colorScheme get() = activeColorScheme val colorScheme get() = activeColorScheme
val keyboardColor get() = drawableProvider?.primaryKeyboardColor?.let { androidx.compose.ui.graphics.Color(it) } ?: colorScheme.surface val keyboardColor get() = drawableProvider?.primaryKeyboardColor?.let { androidx.compose.ui.graphics.Color(it) } ?: colorScheme.surface
val size: MutableState<ComputedKeyboardSize?> = mutableStateOf(null)
private fun calculateSize(): ComputedKeyboardSize
= sizingCalculator.calculate(
latinIMELegacy.mKeyboardSwitcher.keyboard?.mId?.mKeyboardLayoutSetName ?: "qwerty",
latinIMELegacy.mKeyboardSwitcher.keyboard?.mId?.mNumberRow ?: false
)
private var drawableProvider: DynamicThemeProvider? = null private var drawableProvider: DynamicThemeProvider? = null
private var lastEditorInfo: EditorInfo? = null private var lastEditorInfo: EditorInfo? = null
@ -204,7 +265,26 @@ class LatinIME : InputMethodService(), LifecycleOwner, ViewModelStoreOwner, Save
} }
} }
private fun onSizeUpdated() {
val newSize = calculateSize()
val shouldInvalidateKeyboard = size.value?.let { oldSize ->
when {
oldSize is FloatingKeyboardSize && newSize is FloatingKeyboardSize -> {
oldSize.width != newSize.width || oldSize.height != newSize.height
}
else -> true
}
} ?: true
size.value = newSize
if(shouldInvalidateKeyboard) {
invalidateKeyboard(true)
}
}
fun invalidateKeyboard(refreshSettings: Boolean = false) { fun invalidateKeyboard(refreshSettings: Boolean = false) {
size.value = calculateSize()
settingsRefreshRequired = settingsRefreshRequired || refreshSettings settingsRefreshRequired = settingsRefreshRequired || refreshSettings
if(!uixManager.isMainKeyboardHidden) { if(!uixManager.isMainKeyboardHidden) {
@ -256,16 +336,6 @@ class LatinIME : InputMethodService(), LifecycleOwner, ViewModelStoreOwner, Save
val filter = IntentFilter(Intent.ACTION_USER_UNLOCKED) val filter = IntentFilter(Intent.ACTION_USER_UNLOCKED)
registerReceiver(unlockReceiver, filter) registerReceiver(unlockReceiver, filter)
mLifecycleRegistry = LifecycleRegistry(this)
mLifecycleRegistry.currentState = Lifecycle.State.INITIALIZED
mViewModelStore = ViewModelStore()
mSavedStateRegistryController = SavedStateRegistryController.create(this)
mSavedStateRegistryController.performRestore(null)
mLifecycleRegistry.currentState = Lifecycle.State.CREATED
suggestionBlacklist = SuggestionBlacklist(latinIMELegacy.mSettings, this, lifecycleScope) suggestionBlacklist = SuggestionBlacklist(latinIMELegacy.mSettings, this, lifecycleScope)
Subtypes.addDefaultSubtypesIfNecessary(this) Subtypes.addDefaultSubtypesIfNecessary(this)
@ -348,6 +418,21 @@ class LatinIME : InputMethodService(), LifecycleOwner, ViewModelStoreOwner, Save
} }
} }
// Listen to size changes
launchJob {
val prev: MutableMap<KeyboardSizeSettingKind, String?> =
KeyboardSizeSettingKind.entries.associateWith { null }.toMutableMap()
dataStore.data.collect { data ->
prev.keys.toList().forEach {
if(data[KeyboardSettings[it]!!.key] != prev[it]) {
prev[it] = data[KeyboardSettings[it]!!.key]
onSizeUpdated()
}
}
}
}
uixManager.onCreate() uixManager.onCreate()
Settings.getInstance().settingsChangedListeners.add { oldSettings, newSettings -> Settings.getInstance().settingsChangedListeners.add { oldSettings, newSettings ->
@ -364,7 +449,6 @@ class LatinIME : InputMethodService(), LifecycleOwner, ViewModelStoreOwner, Save
unregisterReceiver(unlockReceiver) unregisterReceiver(unlockReceiver)
stopJobs() stopJobs()
mLifecycleRegistry.currentState = Lifecycle.State.DESTROYED
viewModelStore.clear() viewModelStore.clear()
languageModelFacilitator.saveHistoryLog() languageModelFacilitator.saveHistoryLog()
@ -381,6 +465,7 @@ class LatinIME : InputMethodService(), LifecycleOwner, ViewModelStoreOwner, Save
override fun onConfigurationChanged(newConfig: Configuration) { override fun onConfigurationChanged(newConfig: Configuration) {
Log.w("LatinIME", "Configuration changed") Log.w("LatinIME", "Configuration changed")
size.value = calculateSize()
latinIMELegacy.onConfigurationChanged(newConfig) latinIMELegacy.onConfigurationChanged(newConfig)
super.onConfigurationChanged(newConfig) super.onConfigurationChanged(newConfig)
} }
@ -390,22 +475,22 @@ class LatinIME : InputMethodService(), LifecycleOwner, ViewModelStoreOwner, Save
} }
private var legacyInputView: View? = null private var legacyInputView: View? = null
private var touchableHeight: Int = 0
override fun onCreateInputView(): View { override fun onCreateInputView(): View {
Log.w("LatinIME", "Create input view") val composeView = super.onCreateInputView()
legacyInputView = latinIMELegacy.onCreateInputView()
val composeView = uixManager.createComposeView() legacyInputView = latinIMELegacy.onCreateInputView()
latinIMELegacy.setComposeInputView(composeView) latinIMELegacy.setComposeInputView(composeView)
uixManager.setContent()
return composeView return composeView
} }
private var inputViewHeight: Int = -1 private var inputViewHeight: Int = -1
// Both called by UixManager
fun updateTouchableHeight(to: Int) { touchableHeight = to }
fun getInputViewHeight(): Int = inputViewHeight fun getInputViewHeight(): Int = inputViewHeight
fun getViewHeight(): Int = composeView?.height ?: resources.displayMetrics.heightPixels
fun getViewWidth(): Int = composeView?.width ?: resources.displayMetrics.widthPixels
private var isInputModal = false private var isInputModal = false
fun setInputModal(to: Boolean) { fun setInputModal(to: Boolean) {
@ -426,11 +511,11 @@ class LatinIME : InputMethodService(), LifecycleOwner, ViewModelStoreOwner, Save
} }
} }
key(legacyInputView) { key(legacyInputView) {
AndroidView(factory = { AndroidView(factory = {
legacyInputView!! legacyInputView!!.also {
if(it.parent != null) (it.parent as ViewGroup).removeView(it)
}
}, modifier = modifier, onRelease = { }, modifier = modifier, onRelease = {
val view = it as InputView val view = it as InputView
view.deallocateMemory() view.deallocateMemory()
@ -445,7 +530,7 @@ class LatinIME : InputMethodService(), LifecycleOwner, ViewModelStoreOwner, Save
legacyInputView = newView legacyInputView = newView
uixManager.setContent() uixManager.setContent()
uixManager.getComposeView()?.let { composeView?.let {
latinIMELegacy.setComposeInputView(it) latinIMELegacy.setComposeInputView(it)
} }
@ -455,7 +540,7 @@ class LatinIME : InputMethodService(), LifecycleOwner, ViewModelStoreOwner, Save
override fun setInputView(view: View?) { override fun setInputView(view: View?) {
super.setInputView(view) super.setInputView(view)
uixManager.getComposeView()?.let { composeView?.let {
latinIMELegacy.setComposeInputView(it) latinIMELegacy.setComposeInputView(it)
} }
@ -473,8 +558,6 @@ class LatinIME : InputMethodService(), LifecycleOwner, ViewModelStoreOwner, Save
} }
override fun onStartInputView(info: EditorInfo?, restarting: Boolean) { override fun onStartInputView(info: EditorInfo?, restarting: Boolean) {
mLifecycleRegistry.currentState = Lifecycle.State.STARTED
lastEditorInfo = info lastEditorInfo = info
super.onStartInputView(info, restarting) super.onStartInputView(info, restarting)
@ -561,36 +644,55 @@ class LatinIME : InputMethodService(), LifecycleOwner, ViewModelStoreOwner, Save
} }
override fun onComputeInsets(outInsets: Insets?) { override fun onComputeInsets(outInsets: Insets?) {
val composeView = uixManager.getComposeView()
// This method may be called before {@link #setInputView(View)}. // This method may be called before {@link #setInputView(View)}.
if (legacyInputView == null || composeView == null) { if (legacyInputView == null || composeView == null) {
return return
} }
val inputHeight: Int = composeView.height val viewHeight = composeView!!.height
if (latinIMELegacy.isImeSuppressedByHardwareKeyboard && !legacyInputView!!.isShown) { val size = size.value ?: return
// If there is a hardware keyboard and a visible software keyboard view has been hidden,
// no visual element will be shown on the screen.
latinIMELegacy.setInsets(outInsets!!.apply {
contentTopInsets = inputHeight
visibleTopInsets = inputHeight
})
return
}
val visibleTopY = inputHeight - touchableHeight
val touchLeft = 0
val touchTop = if(isInputModal) { 0 } else { visibleTopY }
val touchRight = composeView.width
val touchBottom = inputHeight
latinIMELegacy.setInsets(outInsets!!.apply { latinIMELegacy.setInsets(outInsets!!.apply {
touchableInsets = Insets.TOUCHABLE_INSETS_REGION; when(size) {
touchableRegion.set(touchLeft, touchTop, touchRight, touchBottom); is FloatingKeyboardSize -> {
contentTopInsets = visibleTopY val height = uixManager.touchableHeight
visibleTopInsets = visibleTopY
val left = size.bottomOrigin.first
val bottomYFromBottom = size.bottomOrigin.second
var bottom = viewHeight - bottomYFromBottom
var top = bottom - height
val right = left + size.width
if(top < 0) {
bottom -= top
top -= top
}
touchableInsets = Insets.TOUCHABLE_INSETS_REGION
touchableRegion.set(left, top, right, bottom)
contentTopInsets = viewHeight
visibleTopInsets = viewHeight
}
else -> {
touchableInsets = Insets.TOUCHABLE_INSETS_CONTENT
val touchableHeight = uixManager.touchableHeight
val topInset = if(touchableHeight < 1 || touchableHeight >= viewHeight - 1) {
val actionBarHeight = sizingCalculator.calculateTotalActionBarHeightPx()
viewHeight - size.getHeight() - actionBarHeight
} else {
viewHeight - touchableHeight
}
contentTopInsets = topInset
visibleTopInsets = topInset
}
}
if(isInputModal || latinIMELegacy.mKeyboardSwitcher?.isShowingMoreKeysPanel == true) {
touchableInsets = Insets.TOUCHABLE_INSETS_REGION
touchableRegion.set(0, 0, composeView!!.width, composeView!!.height)
}
}) })
} }
@ -737,14 +839,6 @@ class LatinIME : InputMethodService(), LifecycleOwner, ViewModelStoreOwner, Save
return super.getCurrentInputConnection() return super.getCurrentInputConnection()
} }
override val lifecycle: Lifecycle
get() = mLifecycleRegistry
override val savedStateRegistry: SavedStateRegistry
get() = mSavedStateRegistryController.savedStateRegistry
override val viewModelStore: ViewModelStore
get() = mViewModelStore
private fun onDeviceUnlocked() { private fun onDeviceUnlocked() {
Log.i("LatinIME", "DEVICE has UNLOCKED!!! Reloading settings...") Log.i("LatinIME", "DEVICE has UNLOCKED!!! Reloading settings...")
// Every place that called getDefaultSharedPreferences now needs to be refreshed or call it again // Every place that called getDefaultSharedPreferences now needs to be refreshed or call it again

View File

@ -1617,10 +1617,7 @@ public class LatinIMELegacy implements KeyboardActionListener,
// punctuation suggestions (if it's disabled). // punctuation suggestions (if it's disabled).
@Override @Override
public void setNeutralSuggestionStrip() { public void setNeutralSuggestionStrip() {
final SettingsValues currentSettings = mSettings.getCurrent(); final SuggestedWords neutralSuggestions = SuggestedWords.getEmptyInstance();
final SuggestedWords neutralSuggestions = currentSettings.mBigramPredictionEnabled
? SuggestedWords.getEmptyInstance()
: currentSettings.mSpacingAndPunctuations.mSuggestPuncList;
setSuggestedWords(neutralSuggestions); setSuggestedWords(neutralSuggestions);
} }

View File

@ -439,6 +439,7 @@ public final class InputLogic {
// The cursor has been moved : we now accept to perform recapitalization // The cursor has been moved : we now accept to perform recapitalization
mRecapitalizeStatus.enable(); mRecapitalizeStatus.enable();
// We moved the cursor. If we are touching a word, we need to resume suggestion. // We moved the cursor. If we are touching a word, we need to resume suggestion.
mIsAutoCorrectionIndicatorOn = false;
mLatinIMELegacy.mHandler.postResumeSuggestions(true /* shouldDelay */); mLatinIMELegacy.mHandler.postResumeSuggestions(true /* shouldDelay */);
// Stop the last recapitalization, if started. // Stop the last recapitalization, if started.
mRecapitalizeStatus.stop(); mRecapitalizeStatus.stop();

View File

@ -164,8 +164,8 @@ public class SettingsValues {
final String autoCorrectionThresholdRawValue = mAutoCorrectEnabled final String autoCorrectionThresholdRawValue = mAutoCorrectEnabled
? res.getString(R.string.auto_correction_threshold_mode_index_modest) ? res.getString(R.string.auto_correction_threshold_mode_index_modest)
: res.getString(R.string.auto_correction_threshold_mode_index_off); : res.getString(R.string.auto_correction_threshold_mode_index_off);
mBigramPredictionEnabled = readBigramPredictionEnabled(prefs, res);
mTransformerPredictionEnabled = readTransformerPredictionEnabled(prefs, res); mTransformerPredictionEnabled = readTransformerPredictionEnabled(prefs, res);
mBigramPredictionEnabled = readBigramPredictionEnabled(prefs, res) || mTransformerPredictionEnabled;
mDoubleSpacePeriodTimeout = res.getInteger(R.integer.config_double_space_period_timeout); mDoubleSpacePeriodTimeout = res.getInteger(R.integer.config_double_space_period_timeout);
mHasHardwareKeyboard = Settings.readHasHardwareKeyboard(res.getConfiguration()); mHasHardwareKeyboard = Settings.readHasHardwareKeyboard(res.getConfiguration());
mEnableMetricsLogging = prefs.getBoolean(Settings.PREF_ENABLE_METRICS_LOGGING, true); mEnableMetricsLogging = prefs.getBoolean(Settings.PREF_ENABLE_METRICS_LOGGING, true);

View File

@ -15,7 +15,9 @@ import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.lifecycle.LifecycleCoroutineScope import androidx.lifecycle.LifecycleCoroutineScope
import org.futo.inputmethod.latin.LatinIME import org.futo.inputmethod.latin.LatinIME
import org.futo.inputmethod.latin.SuggestionBlacklist
import org.futo.inputmethod.latin.uix.theme.ThemeOption import org.futo.inputmethod.latin.uix.theme.ThemeOption
import org.futo.inputmethod.v2keyboard.KeyboardSizingCalculator
import java.util.Locale import java.util.Locale
interface ActionInputTransaction { interface ActionInputTransaction {
@ -66,8 +68,11 @@ interface KeyboardManagerForAction {
fun activateAction(action: Action) fun activateAction(action: Action)
fun showActionEditor() fun showActionEditor()
fun getSuggestionBlacklist(): SuggestionBlacklist
fun getLatinIMEForDebug(): LatinIME fun getLatinIMEForDebug(): LatinIME
fun isDeviceLocked(): Boolean fun isDeviceLocked(): Boolean
fun getSizingCalculator(): KeyboardSizingCalculator
} }
interface ActionWindow { interface ActionWindow {

View File

@ -72,6 +72,7 @@ import androidx.compose.ui.text.ExperimentalTextApi
import androidx.compose.ui.text.TextStyle import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.drawText import androidx.compose.ui.text.drawText
import androidx.compose.ui.text.font.FontFamily import androidx.compose.ui.text.font.FontFamily
import androidx.compose.ui.text.font.FontStyle
import androidx.compose.ui.text.font.FontWeight import androidx.compose.ui.text.font.FontWeight
import androidx.compose.ui.text.rememberTextMeasurer import androidx.compose.ui.text.rememberTextMeasurer
import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.text.style.TextOverflow
@ -87,6 +88,7 @@ import org.futo.inputmethod.latin.SuggestedWords
import org.futo.inputmethod.latin.SuggestedWords.SuggestedWordInfo import org.futo.inputmethod.latin.SuggestedWords.SuggestedWordInfo
import org.futo.inputmethod.latin.SuggestedWords.SuggestedWordInfo.KIND_EMOJI_SUGGESTION import org.futo.inputmethod.latin.SuggestedWords.SuggestedWordInfo.KIND_EMOJI_SUGGESTION
import org.futo.inputmethod.latin.SuggestedWords.SuggestedWordInfo.KIND_TYPED import org.futo.inputmethod.latin.SuggestedWords.SuggestedWordInfo.KIND_TYPED
import org.futo.inputmethod.latin.SuggestionBlacklist
import org.futo.inputmethod.latin.common.Constants import org.futo.inputmethod.latin.common.Constants
import org.futo.inputmethod.latin.suggestions.SuggestionStripViewListener import org.futo.inputmethod.latin.suggestions.SuggestionStripViewListener
import org.futo.inputmethod.latin.uix.actions.FavoriteActions import org.futo.inputmethod.latin.uix.actions.FavoriteActions
@ -98,7 +100,6 @@ import org.futo.inputmethod.latin.uix.settings.useDataStoreValue
import org.futo.inputmethod.latin.uix.theme.DarkColorScheme import org.futo.inputmethod.latin.uix.theme.DarkColorScheme
import org.futo.inputmethod.latin.uix.theme.Typography import org.futo.inputmethod.latin.uix.theme.Typography
import org.futo.inputmethod.latin.uix.theme.UixThemeWrapper import org.futo.inputmethod.latin.uix.theme.UixThemeWrapper
import java.lang.Integer.min
import kotlin.math.ceil import kotlin.math.ceil
import kotlin.math.roundToInt import kotlin.math.roundToInt
@ -220,17 +221,9 @@ fun AutoFitText(
@OptIn(ExperimentalFoundationApi::class) @OptIn(ExperimentalFoundationApi::class)
@Composable @Composable
fun RowScope.SuggestionItem(words: SuggestedWords, idx: Int, isPrimary: Boolean, onClick: () -> Unit, onLongClick: () -> Unit) { fun RowScope.SuggestionItem(words: SuggestedWords, idx: Int, isPrimary: Boolean, onClick: () -> Unit, onLongClick: () -> Unit) {
val word = try { val wordInfo = words.getInfoOrNull(idx)
words.getWord(idx) val isVerbatim = wordInfo?.kind == KIND_TYPED
} catch(e: IndexOutOfBoundsException) { val word = wordInfo?.mWord
null
}
val wordInfo = try {
words.getInfo(idx)
} catch(e: IndexOutOfBoundsException) {
null
}
val actualIsPrimary = isPrimary && (words.mWillAutoCorrect || ((wordInfo?.isExactMatch) == true)) val actualIsPrimary = isPrimary && (words.mWillAutoCorrect || ((wordInfo?.isExactMatch) == true))
@ -277,9 +270,12 @@ fun RowScope.SuggestionItem(words: SuggestedWords, idx: Int, isPrimary: Boolean,
) { ) {
CompositionLocalProvider(LocalContentColor provides MaterialTheme.colorScheme.onBackground) { CompositionLocalProvider(LocalContentColor provides MaterialTheme.colorScheme.onBackground) {
if (word != null) { if (word != null) {
AutoFitText(word, style = textStyle, modifier = textModifier val modifier = textModifier.align(Center).padding(2.dp)
.align(Center) if(isVerbatim) {
.padding(2.dp)) AutoFitText('"' + word + '"', style = textStyle.copy(fontStyle = FontStyle.Italic), modifier = modifier)
} else {
AutoFitText(word, style = textStyle, modifier = modifier)
}
} }
} }
} }
@ -296,87 +292,137 @@ fun RowScope.SuggestionItem(words: SuggestedWords, idx: Int, isPrimary: Boolean,
} }
data class SuggestionLayout(
/** Set to the word to be autocorrected to */
val autocorrectMatch: SuggestedWordInfo?,
// Show the most probable in the middle, then left, then right /** Other words, sorted by likelihood */
val ORDER_OF_SUGGESTIONS = listOf(1, 0, 2) val sortedMatches: List<SuggestedWordInfo>,
/** Emoji suggestions if they are to be shown */
val emojiMatches: List<SuggestedWordInfo>,
/** The exact word the user typed */
val verbatimWord: SuggestedWordInfo?,
/** Set to true if the best match is so unlikely that we should show verbatim instead */
val areSuggestionsClueless: Boolean,
/** Set to true if this is a gesture update, and we should only show one suggestion */
val isGestureBatch: Boolean,
val presentableSuggestions: List<SuggestedWordInfo>
)
fun SuggestedWords.getInfoOrNull(idx: Int): SuggestedWordInfo? = try {
getInfo(idx)
} catch(e: IndexOutOfBoundsException) {
null
}
fun makeSuggestionLayout(words: SuggestedWords, blacklist: SuggestionBlacklist): SuggestionLayout {
val typedWord = words.getInfoOrNull(SuggestedWords.INDEX_OF_TYPED_WORD)?.let {
if(it.kind == KIND_TYPED) { it } else { null }
}?.let {
if(blacklist.isSuggestedWordOk(it)) {
it
} else {
null
}
}
val autocorrectMatch = words.getInfoOrNull(SuggestedWords.INDEX_OF_AUTO_CORRECTION)?.let {
if(words.mWillAutoCorrect) { it } else { null }
}
// We actually have to avoid sorting these because they are provided sorted in an important order
val emojiMatches = words.mSuggestedWordInfoList.filter {
it.kind == KIND_EMOJI_SUGGESTION
}
val sortedMatches = words.mSuggestedWordInfoList.filter {
it != typedWord && it.kind != KIND_TYPED && it != autocorrectMatch && !emojiMatches.contains(it)
}
val areSuggestionsClueless = (autocorrectMatch ?: sortedMatches.getOrNull(0))?.let {
it.mOriginatesFromTransformerLM && it.mScore < -50
} ?: false
val isGestureBatch = words.mInputStyle == SuggestedWords.INPUT_STYLE_UPDATE_BATCH
val presentableSuggestions = (
listOf(
typedWord,
autocorrectMatch,
) + sortedMatches
).filterNotNull()
return SuggestionLayout(
autocorrectMatch = autocorrectMatch,
sortedMatches = sortedMatches,
emojiMatches = emojiMatches,
verbatimWord = typedWord,
areSuggestionsClueless = areSuggestionsClueless,
isGestureBatch = isGestureBatch,
presentableSuggestions = presentableSuggestions
)
}
@Composable @Composable
fun RowScope.SuggestionItems(words: SuggestedWords, onClick: (i: Int) -> Unit, onLongClick: (i: Int) -> Unit) { fun RowScope.SuggestionItems(words: SuggestedWords, onClick: (i: Int) -> Unit, onLongClick: (i: Int) -> Unit) {
val maxSuggestions = min(ORDER_OF_SUGGESTIONS.size, words.size()) val layout = makeSuggestionLayout(words, LocalManager.current.getSuggestionBlacklist())
if(maxSuggestions == 0) { val suggestionItem = @Composable { suggestion: SuggestedWordInfo? ->
Spacer(modifier = Modifier.weight(1.0f)) if(suggestion != null) {
return val idx = words.indexOf(suggestion)
} SuggestionItem(
words,
if(maxSuggestions == 1 || words.mInputStyle == SuggestedWords.INPUT_STYLE_UPDATE_BATCH) { idx,
SuggestionItem( isPrimary = idx == SuggestedWords.INDEX_OF_AUTO_CORRECTION,
words, onClick = { onClick(idx) },
0, onLongClick = { onLongClick(idx) }
isPrimary = true, )
onClick = { onClick(0) }, } else {
onLongClick = { onLongClick(0) } Spacer(Modifier.weight(1.0f))
)
return
} else if(words.mInputStyle == SuggestedWords.INPUT_STYLE_TAIL_BATCH && maxSuggestions > 1) {
//words.mSuggestedWordInfoList.removeAt(0);
}
var offset = 0
try {
val info = words.getInfo(0)
if (info.kind == KIND_TYPED && !info.isExactMatch && !info.isExactMatchWithIntentionalOmission) {
offset = 1
} }
} catch(_: IndexOutOfBoundsException) {
} }
// Check for "clueless" suggestions, and display typed word in center if so println(layout)
try { when {
if(offset == 1) { layout.isGestureBatch ||
val info = words.getInfo(1) layout.presentableSuggestions.size <= 1 -> suggestionItem(layout.presentableSuggestions.firstOrNull())
if(info.mOriginatesFromTransformerLM && info.mScore < -50) {
offset = 0; layout.autocorrectMatch != null -> {
var supplementalSuggestionIndex = 0
if(layout.emojiMatches.isEmpty()) {
suggestionItem(layout.sortedMatches.getOrNull(supplementalSuggestionIndex++))
} else {
suggestionItem(layout.emojiMatches[0])
}
SuggestionSeparator()
suggestionItem(layout.autocorrectMatch)
SuggestionSeparator()
if(layout.verbatimWord != null && layout.verbatimWord.mWord != layout.autocorrectMatch.mWord) {
suggestionItem(layout.verbatimWord)
} else {
suggestionItem(layout.sortedMatches.getOrNull(supplementalSuggestionIndex))
} }
} }
} catch(_: IndexOutOfBoundsException) {
} else -> {
var supplementalSuggestionIndex = 1
if(layout.emojiMatches.isEmpty()) {
val suggestionOrder = mutableListOf( suggestionItem(layout.sortedMatches.getOrNull(supplementalSuggestionIndex++))
ORDER_OF_SUGGESTIONS[0] + offset, } else {
ORDER_OF_SUGGESTIONS[1] + offset, suggestionItem(layout.emojiMatches[0])
if(offset == 1) { 0 - offset } else { ORDER_OF_SUGGESTIONS[2] } + offset,
)
// Find emoji
try {
for(i in 0 until words.size()) {
val info = words.getInfo(i)
if(info.mKindAndFlags == KIND_EMOJI_SUGGESTION && i > 2) {
suggestionOrder[0] = i
} }
SuggestionSeparator()
suggestionItem(layout.sortedMatches.getOrNull(0))
SuggestionSeparator()
suggestionItem(layout.sortedMatches.getOrNull(supplementalSuggestionIndex))
} }
} catch(_: IndexOutOfBoundsException) {
}
for (i in 0 until maxSuggestions) {
SuggestionItem(
words,
suggestionOrder[i],
isPrimary = i == (maxSuggestions / 2),
onClick = { onClick(suggestionOrder[i]) },
onLongClick = { onLongClick(suggestionOrder[i]) }
)
if (i < maxSuggestions - 1) SuggestionSeparator()
} }
} }

View File

@ -13,7 +13,6 @@ import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.scale import androidx.compose.ui.draw.scale
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.Dp
@ -30,6 +29,7 @@ import org.futo.inputmethod.latin.R
import org.futo.inputmethod.v2keyboard.KeyboardLayoutSetV2 import org.futo.inputmethod.v2keyboard.KeyboardLayoutSetV2
import org.futo.inputmethod.v2keyboard.KeyboardLayoutSetV2Params import org.futo.inputmethod.v2keyboard.KeyboardLayoutSetV2Params
import org.futo.inputmethod.v2keyboard.LayoutManager import org.futo.inputmethod.v2keyboard.LayoutManager
import org.futo.inputmethod.v2keyboard.RegularKeyboardSize
import java.util.Locale import java.util.Locale
import kotlin.math.roundToInt import kotlin.math.roundToInt
@ -76,23 +76,8 @@ fun KeyboardLayoutPreview(id: String, width: Dp = 172.dp, locale: Locale? = null
} }
} }
val configuration = LocalConfiguration.current val widthPx: Int = (320.0 * context.resources.displayMetrics.density).roundToInt()
val isLandscape = false//configuration.orientation == Configuration.ORIENTATION_LANDSCAPE val heightPx: Int = (200.0 * context.resources.displayMetrics.density).roundToInt()
val widthPx: Int
val heightPx: Int
when {
isLandscape -> {
widthPx = (500.0 * context.resources.displayMetrics.density).roundToInt()
heightPx = (180.0 * context.resources.displayMetrics.density).roundToInt()
}
else -> {
widthPx = (320.0 * context.resources.displayMetrics.density).roundToInt()
heightPx = (200.0 * context.resources.displayMetrics.density).roundToInt()
}
}
val keyboard = remember { mutableStateOf<Keyboard?>(null) } val keyboard = remember { mutableStateOf<Keyboard?>(null) }
@ -107,16 +92,12 @@ fun KeyboardLayoutPreview(id: String, width: Dp = 172.dp, locale: Locale? = null
val layoutSet = KeyboardLayoutSetV2( val layoutSet = KeyboardLayoutSetV2(
context, context,
KeyboardLayoutSetV2Params( KeyboardLayoutSetV2Params(
width = widthPx, computedSize = RegularKeyboardSize(width = widthPx, height = heightPx, padding = Rect()),
height = heightPx,
padding = Rect(),
gap = 4.0f, gap = 4.0f,
keyboardLayoutSet = id, keyboardLayoutSet = id,
locale = loc ?: Locale.ENGLISH, locale = loc ?: Locale.ENGLISH,
editorInfo = editorInfo, editorInfo = editorInfo,
numberRow = numberRow, numberRow = numberRow,
useSplitLayout = isLandscape,
splitLayoutWidth = widthPx * 2 / 3,
bottomActionKey = null bottomActionKey = null
) )
) )

View File

@ -0,0 +1,130 @@
package org.futo.inputmethod.latin.uix
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.gestures.detectDragGestures
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxScope
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.RectangleShape
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.toSize
enum class CurrentDraggingTarget {
TopLeft,
TopRight,
BottomLeft,
BottomRight,
Center
}
private fun CurrentDraggingTarget.computeOffset(size: Size): Offset = when(this) {
CurrentDraggingTarget.TopLeft -> Offset(0.0f, 0.0f)
CurrentDraggingTarget.TopRight -> Offset(size.width, 0.0f)
CurrentDraggingTarget.BottomLeft -> Offset(0.0f, size.height)
CurrentDraggingTarget.BottomRight -> Offset(size.width, size.height)
CurrentDraggingTarget.Center -> Offset(size.width * 0.5f, size.height * 0.5f)
}
private fun CurrentDraggingTarget.computeOffset(size: IntSize): Offset =
computeOffset(size.toSize())
private fun CurrentDraggingTarget.dragDelta(offset: Offset): DragDelta = when(this) {
CurrentDraggingTarget.TopLeft -> DragDelta(left = offset.x, top = offset.y)
CurrentDraggingTarget.TopRight -> DragDelta(right = offset.x, top = offset.y)
CurrentDraggingTarget.BottomLeft -> DragDelta(left = offset.x, bottom = offset.y)
CurrentDraggingTarget.BottomRight -> DragDelta(right = offset.x, bottom = offset.y)
CurrentDraggingTarget.Center -> DragDelta(
left = offset.x,
right = offset.x,
top = offset.y,
bottom = offset.y
)
}
data class DragDelta(
val left: Float = 0.0f,
val top: Float = 0.0f,
val right: Float = 0.0f,
val bottom: Float = 0.0f
)
@Composable
fun BoxScope.ResizerRect(onDragged: (DragDelta) -> Boolean, showResetApply: Boolean, onApply: () -> Unit, onReset: () -> Unit) {
val shape = RectangleShape
val draggingState = remember { mutableStateOf<CurrentDraggingTarget?>(null) }
val wasAccepted = remember { mutableStateOf(true) }
Box(Modifier
.matchParentSize()
.background(MaterialTheme.colorScheme.background.copy(alpha = 0.5f), shape)
.border(3.dp, MaterialTheme.colorScheme.primary, shape)
.pointerInput(Unit) {
detectDragGestures(
onDragStart = { offset ->
draggingState.value = CurrentDraggingTarget.entries.minBy {
offset.minus(it.computeOffset(size)).getDistanceSquared()
}
},
onDrag = { _, amount ->
draggingState.value?.let {
wasAccepted.value = onDragged(it.dragDelta(amount))
}
},
onDragEnd = {
draggingState.value = null
wasAccepted.value = true
}
)
}
) {
val primaryColor = MaterialTheme.colorScheme.primary
val primaryInverseColor = MaterialTheme.colorScheme.inversePrimary
val errorColor = MaterialTheme.colorScheme.error
val radius = with(LocalDensity.current) { 24.dp.toPx() }
Canvas(Modifier.matchParentSize(), onDraw = {
CurrentDraggingTarget.entries.forEach {
drawCircle(
color = if (!wasAccepted.value) {
errorColor
} else if (draggingState.value == it) {
primaryInverseColor
} else {
primaryColor
},
radius = radius,
center = it.computeOffset(size)
)
}
})
if (showResetApply) {
Row(Modifier.align(Alignment.BottomCenter).padding(16.dp)) {
TextButton({ onReset() }) { Text("Reset") }
Spacer(Modifier.width(8.dp))
TextButton({ onApply() }) { Text("Apply") }
}
}
}
}

View File

@ -4,6 +4,7 @@ import android.app.Activity
import android.content.ClipDescription import android.content.ClipDescription
import android.content.Context import android.content.Context
import android.content.Intent import android.content.Intent
import android.graphics.Rect
import android.net.Uri import android.net.Uri
import android.os.Build import android.os.Build
import android.os.VibrationEffect import android.os.VibrationEffect
@ -23,22 +24,33 @@ import androidx.compose.animation.fadeOut
import androidx.compose.animation.slideInVertically import androidx.compose.animation.slideInVertically
import androidx.compose.animation.slideOutVertically import androidx.compose.animation.slideOutVertically
import androidx.compose.foundation.background import androidx.compose.foundation.background
import androidx.compose.foundation.gestures.detectDragGestures
import androidx.compose.foundation.gestures.detectTapGestures import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxScope import androidx.compose.foundation.layout.BoxScope
import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.absoluteOffset
import androidx.compose.foundation.layout.absolutePadding
import androidx.compose.foundation.layout.fillMaxHeight import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.requiredWidth
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.shape.RoundedCornerShape import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material.icons.Icons
import androidx.compose.material.icons.filled.Menu
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.LocalContentColor
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface import androidx.compose.material3.Surface
import androidx.compose.material3.Text import androidx.compose.material3.Text
import androidx.compose.material3.TextButton import androidx.compose.material3.TextButton
import androidx.compose.material3.contentColorFor
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.LaunchedEffect
@ -46,17 +58,24 @@ import androidx.compose.runtime.MutableState
import androidx.compose.runtime.compositionLocalOf import androidx.compose.runtime.compositionLocalOf
import androidx.compose.runtime.getValue import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.staticCompositionLocalOf import androidx.compose.runtime.staticCompositionLocalOf
import androidx.compose.ui.Alignment import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clipToBounds
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.RectangleShape
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.input.pointer.pointerInput import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.layout.onSizeChanged import androidx.compose.ui.layout.onSizeChanged
import androidx.compose.ui.platform.ComposeView import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.platform.LocalView import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.platform.ViewCompositionStrategy import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.LayoutDirection import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.lifecycle.LifecycleCoroutineScope import androidx.lifecycle.LifecycleCoroutineScope
@ -75,6 +94,7 @@ import org.futo.inputmethod.latin.LatinIME
import org.futo.inputmethod.latin.R import org.futo.inputmethod.latin.R
import org.futo.inputmethod.latin.SuggestedWords import org.futo.inputmethod.latin.SuggestedWords
import org.futo.inputmethod.latin.SuggestedWords.SuggestedWordInfo import org.futo.inputmethod.latin.SuggestedWords.SuggestedWordInfo
import org.futo.inputmethod.latin.SuggestionBlacklist
import org.futo.inputmethod.latin.common.Constants import org.futo.inputmethod.latin.common.Constants
import org.futo.inputmethod.latin.inputlogic.InputLogic import org.futo.inputmethod.latin.inputlogic.InputLogic
import org.futo.inputmethod.latin.suggestions.SuggestionStripViewListener import org.futo.inputmethod.latin.suggestions.SuggestionStripViewListener
@ -96,6 +116,15 @@ import org.futo.inputmethod.updates.deferManualUpdate
import org.futo.inputmethod.updates.isManualUpdateTimeExpired import org.futo.inputmethod.updates.isManualUpdateTimeExpired
import org.futo.inputmethod.updates.openManualUpdateCheck import org.futo.inputmethod.updates.openManualUpdateCheck
import org.futo.inputmethod.updates.retrieveSavedLastUpdateCheckResult import org.futo.inputmethod.updates.retrieveSavedLastUpdateCheckResult
import org.futo.inputmethod.v2keyboard.ComputedKeyboardSize
import org.futo.inputmethod.v2keyboard.FloatingKeyboardSize
import org.futo.inputmethod.v2keyboard.KeyboardSizingCalculator
import org.futo.inputmethod.v2keyboard.OneHandedDirection
import org.futo.inputmethod.v2keyboard.OneHandedKeyboardSize
import org.futo.inputmethod.v2keyboard.RegularKeyboardSize
import org.futo.inputmethod.v2keyboard.SplitKeyboardSize
import org.futo.inputmethod.v2keyboard.getPadding
import org.futo.inputmethod.v2keyboard.getWidth
import java.util.Locale import java.util.Locale
val LocalManager = staticCompositionLocalOf<KeyboardManagerForAction> { val LocalManager = staticCompositionLocalOf<KeyboardManagerForAction> {
@ -279,7 +308,7 @@ class UixActionKeyboardManager(val uixManager: UixManager, val latinIME: LatinIM
override fun announce(s: String) { override fun announce(s: String) {
AccessibilityUtils.init(getContext()) AccessibilityUtils.init(getContext())
if(AccessibilityUtils.getInstance().isAccessibilityEnabled) { if(AccessibilityUtils.getInstance().isAccessibilityEnabled) {
AccessibilityUtils.getInstance().announceForAccessibility(uixManager.getComposeView(), s) AccessibilityUtils.getInstance().announceForAccessibility(uixManager.composeView, s)
} }
} }
@ -295,7 +324,12 @@ class UixActionKeyboardManager(val uixManager: UixManager, val latinIME: LatinIM
return getContext().isDeviceLocked return getContext().isDeviceLocked
} }
override fun getSizingCalculator(): KeyboardSizingCalculator =
latinIME.sizingCalculator
override fun getLatinIMEForDebug(): LatinIME = latinIME override fun getLatinIMEForDebug(): LatinIME = latinIME
override fun getSuggestionBlacklist(): SuggestionBlacklist = latinIME.suggestionBlacklist
} }
data class ActiveDialogRequest( data class ActiveDialogRequest(
@ -305,11 +339,12 @@ data class ActiveDialogRequest(
) )
class UixManager(private val latinIME: LatinIME) { class UixManager(private val latinIME: LatinIME) {
internal val composeView: ComposeView?
get() = latinIME.composeView
private var shouldShowSuggestionStrip: Boolean = true private var shouldShowSuggestionStrip: Boolean = true
private var suggestedWords: SuggestedWords? = null private var suggestedWords: SuggestedWords? = null
private var composeView: ComposeView? = null
private var currWindowAction: Action? = null private var currWindowAction: Action? = null
private var persistentStates: HashMap<Action, PersistentActionState?> = hashMapOf() private var persistentStates: HashMap<Action, PersistentActionState?> = hashMapOf()
@ -327,6 +362,9 @@ class UixManager(private val latinIME: LatinIME) {
latinIME.deferSetSetting(ActionBarExpanded, isActionsExpanded.value) latinIME.deferSetSetting(ActionBarExpanded, isActionsExpanded.value)
} }
val actionsExpanded: Boolean
get() = isActionsExpanded.value
private var isShowingActionEditor = mutableStateOf(false) private var isShowingActionEditor = mutableStateOf(false)
fun showActionEditor() { fun showActionEditor() {
isShowingActionEditor.value = true isShowingActionEditor.value = true
@ -337,6 +375,12 @@ class UixManager(private val latinIME: LatinIME) {
var isInputOverridden = mutableStateOf(false) var isInputOverridden = mutableStateOf(false)
var currWindowActionWindow: ActionWindow? = null var currWindowActionWindow: ActionWindow? = null
val isActionWindowDocked: Boolean
get() = currWindowActionWindow != null
private var measuredTouchableHeight = 0
val touchableHeight: Int
get() = measuredTouchableHeight
val isMainKeyboardHidden get() = mainKeyboardHidden val isMainKeyboardHidden get() = mainKeyboardHidden
@ -626,46 +670,249 @@ class UixManager(private val latinIME: LatinIME) {
) )
} }
fun setContent() { @Composable
composeView?.setContent { private fun OffsetPositioner(offset: Offset, content: @Composable () -> Unit) {
UixThemeWrapper(latinIME.colorScheme) { Column(modifier = Modifier.fillMaxHeight().absoluteOffset { IntOffset(offset.x.toInt(), 0) }) {
DataStoreCacheProvider { Spacer(Modifier.weight(1.0f))
CompositionLocalProvider(LocalManager provides keyboardManagerForAction) { content()
CompositionLocalProvider(LocalThemeProvider provides latinIME.getDrawableProvider()) { Spacer(Modifier.height(with(LocalDensity.current) { offset.y.toDp() }))
CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Ltr) { }
CompositionLocalProvider(LocalFoldingState provides foldingOptions.value) { }
InputDarkener(isInputOverridden.value || isShowingActionEditor.value) {
closeActionWindow()
isShowingActionEditor.value = false
}
Column { @Composable
Spacer(modifier = Modifier.weight(1.0f)) private fun KeyboardSurface(
Surface(modifier = Modifier.onSizeChanged { requiredWidthPx: Int,
latinIME.updateTouchableHeight(it.height) backgroundColor: Color,
}, color = latinIME.keyboardColor) { modifier: Modifier = Modifier,
Box { shape: Shape = RectangleShape,
Column { padding: Rect = Rect(),
when { content: @Composable BoxScope.() -> Unit
currWindowActionWindow != null -> ActionViewWithHeader( ) = with(LocalDensity.current) {
currWindowActionWindow!! Box(modifier
) .onSizeChanged { measuredTouchableHeight = it.height}
.background(backgroundColor, shape)
.requiredWidth(requiredWidthPx.toDp())
.absolutePadding(
left = padding.left.toDp(),
top = padding.top.toDp(),
right = padding.right.toDp(),
bottom = padding.bottom.toDp(),
)
.clipToBounds()
) {
CompositionLocalProvider(LocalContentColor provides contentColorFor(backgroundColor)) {
content()
}
}
}
else -> MainKeyboardViewWithActionBar() @Composable
} private fun FloatingKeyboardContents(
pointerInputKey: Any?,
onDragged: (Offset) -> Unit,
onDragEnd: () -> Unit,
onResizerOpen: () -> Unit,
content: @Composable BoxScope.(actionBarGap: Dp) -> Unit
) {
// Content
Box(Modifier.fillMaxWidth()) {
content(4.dp)
}
latinIME.LegacyKeyboardView(hidden = isMainKeyboardHidden) // Bottom drag bar
} Spacer(modifier = Modifier.height(24.dp))
Box(modifier = Modifier
.fillMaxWidth()
.height(20.dp)
.pointerInput(pointerInputKey) {
detectDragGestures(
onDrag = { _, dragAmount -> onDragged(dragAmount)},
onDragEnd = { onDragEnd() })
}) {
ForgetWordDialog() IconButton(onClick = {
} onResizerOpen()
}, Modifier.align(Alignment.CenterEnd)) {
Icon(Icons.Default.Menu, contentDescription = "resize")
}
} Box(
modifier = Modifier.fillMaxWidth(0.6f).height(4.dp)
.align(Alignment.TopCenter).background(
MaterialTheme.colorScheme.onBackground.copy(alpha = 0.6f),
RoundedCornerShape(100)
)
)
}
}
} @Composable
private fun FloatingKeyboardWindow(
size: FloatingKeyboardSize,
content: @Composable BoxScope.(actionBarGap: Dp) -> Unit
) = with(LocalDensity.current) {
val offset = remember(size) { mutableStateOf(Offset(size.bottomOrigin.first.toFloat(), size.bottomOrigin.second.toFloat())) }
ActionEditorHost() val resizing = remember { mutableStateOf(false) }
}
val onDragDelta: (DragDelta) -> Boolean = remember { { delta ->
// Matching the necessary coordinate space
var deltaX = delta.left
var deltaY = -delta.bottom
var deltaWidth = delta.right - delta.left
var deltaHeight = delta.bottom - delta.top
var result = true
// TODO: Limit the values so that we do not go off-screen
// If we have reached a minimum limit, return false
// Basic limiting for minimum size
val currSettings = latinIME.sizingCalculator.getSavedSettings()
val currSize = Size(
currSettings.floatingWidthDp.dp.toPx(),
currSettings.floatingHeightDp.dp.toPx()
)
if(currSize.width + deltaWidth < 200.dp.toPx()) {
deltaWidth = deltaWidth.coerceAtLeast(200.dp.toPx() - currSize.width)
deltaX = 0.0f
result = false
}
if(currSize.height + deltaHeight < 160.dp.toPx()) {
deltaHeight = deltaHeight.coerceAtLeast(160.dp.toPx() - currSize.height)
deltaY = 0.0f
result = false
}
latinIME.sizingCalculator.editSavedSettings { settings ->
settings.copy(
floatingBottomOriginDp = Pair(
settings.floatingBottomOriginDp.first + deltaX.toDp().value,
settings.floatingBottomOriginDp.second + deltaY.toDp().value
),
floatingWidthDp = settings.floatingWidthDp + deltaWidth.toDp().value,
floatingHeightDp = settings.floatingHeightDp + deltaHeight.toDp().value
)
}
result
} }
OffsetPositioner(offset.value) {
KeyboardSurface(
requiredWidthPx = size.width,
backgroundColor = latinIME.keyboardColor,
shape = RoundedCornerShape(16.dp),
padding = size.decorationPadding
) {
Column {
FloatingKeyboardContents(
pointerInputKey = size,
onDragged = { dragAmount ->
var newOffset = offset.value.copy(
x = offset.value.x + dragAmount.x,
y = offset.value.y - dragAmount.y
)
// Ensure we are not out of bounds
newOffset = newOffset.copy(
newOffset.x.coerceAtLeast(0.0f),
newOffset.y.coerceAtLeast(0.0f)
)
newOffset = newOffset.copy(
newOffset.x.coerceAtMost(
latinIME.getViewWidth().toFloat() - size.width
),
newOffset.y.coerceAtMost(
latinIME.getViewHeight().toFloat() - measuredTouchableHeight
)
)
offset.value = newOffset
},
onDragEnd = {
latinIME.sizingCalculator.editSavedSettings { settings ->
settings.copy(
floatingBottomOriginDp = Pair(
offset.value.x.toDp().value,
offset.value.y.toDp().value
)
)
}
},
onResizerOpen = {
resizing.value = true
},
content = content
)
}
if(resizing.value) {
ResizerRect(onDragDelta, showResetApply = true, onApply = {
resizing.value = false
}, onReset = { })
}
}
}
}
@Composable
private fun NonFloatingKeyboardWindow(
size: ComputedKeyboardSize,
content: @Composable BoxScope.(actionBarGap: Dp) -> Unit
) = with(LocalDensity.current) {
OffsetPositioner(Offset(0.0f, 0.0f)) {
KeyboardSurface(
requiredWidthPx = size.getWidth(),
backgroundColor = latinIME.keyboardColor,
padding = size.getPadding()
) {
val actionBarGap = 4.dp
when(size) {
is OneHandedKeyboardSize -> {
Box(modifier = Modifier.width(size.layoutWidth.toDp()).align(
when(size.direction) {
OneHandedDirection.Left -> Alignment.CenterStart
OneHandedDirection.Right -> Alignment.CenterEnd
}
)) {
content(actionBarGap)
}
}
else -> {
content(actionBarGap)
}
}
}
}
}
@Composable
fun KeyboardWindowSelector(content: @Composable BoxScope.(actionBarGap: Dp) -> Unit) {
val size = latinIME.size.value
when(size) {
is FloatingKeyboardSize -> FloatingKeyboardWindow(size, content)
is OneHandedKeyboardSize,
is RegularKeyboardSize,
is SplitKeyboardSize -> NonFloatingKeyboardWindow(size, content)
null -> return
}
}
@Composable
private fun ProvidersAndWrapper(content: @Composable () -> Unit) {
UixThemeWrapper(latinIME.colorScheme) {
DataStoreCacheProvider {
CompositionLocalProvider(LocalManager provides keyboardManagerForAction) {
CompositionLocalProvider(LocalThemeProvider provides latinIME.getDrawableProvider()) {
CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Ltr) {
CompositionLocalProvider(LocalFoldingState provides foldingOptions.value) {
content()
} }
} }
} }
@ -674,6 +921,37 @@ class UixManager(private val latinIME: LatinIME) {
} }
} }
fun setContent() {
composeView?.setContent {
ProvidersAndWrapper {
InputDarkener(isInputOverridden.value || isShowingActionEditor.value) {
closeActionWindow()
isShowingActionEditor.value = false
}
KeyboardWindowSelector { gap ->
Column {
when {
currWindowActionWindow != null -> ActionViewWithHeader(
currWindowActionWindow!!
)
else -> MainKeyboardViewWithActionBar()
}
Spacer(modifier = Modifier.height(gap))
latinIME.LegacyKeyboardView(hidden = isMainKeyboardHidden)
}
ForgetWordDialog()
}
ActionEditorHost()
}
}
}
suspend fun showUpdateNoticeIfNeeded() { suspend fun showUpdateNoticeIfNeeded() {
if(!BuildConfig.UPDATE_CHECKING) return if(!BuildConfig.UPDATE_CHECKING) return
@ -734,28 +1012,6 @@ class UixManager(private val latinIME: LatinIME) {
} }
} }
fun createComposeView(): View {
if(composeView != null) {
composeView = null
//throw IllegalStateException("Attempted to create compose view, when one is already created!")
}
composeView = ComposeView(latinIME).apply {
setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
setParentCompositionContext(null)
latinIME.setOwners()
}
setContent()
return composeView!!
}
fun getComposeView(): View? {
return composeView
}
fun onColorSchemeChanged() { fun onColorSchemeChanged() {
setContent() setContent()
} }

View File

@ -0,0 +1,86 @@
package org.futo.inputmethod.latin.uix.actions
import org.futo.inputmethod.latin.R
import org.futo.inputmethod.latin.uix.Action
import org.futo.inputmethod.v2keyboard.KeyboardMode
import org.futo.inputmethod.v2keyboard.OneHandedDirection
val LeftHandedKeyboardAction = Action(
icon = R.drawable.arrow_left,
name = R.string.left_handed_keyboard_action_title,
simplePressImpl = { manager, _ ->
manager.getSizingCalculator().editSavedSettings {
if(it.currentMode == KeyboardMode.OneHanded
&& it.oneHandedDirection == OneHandedDirection.Left) {
it.copy(
currentMode = KeyboardMode.Regular
)
} else {
it.copy(
oneHandedDirection = OneHandedDirection.Left,
currentMode = KeyboardMode.OneHanded
)
}
}
},
windowImpl = null,
)
val RightHandedKeyboardAction = Action(
icon = R.drawable.arrow_right,
name = R.string.right_handed_keyboard_action_title,
simplePressImpl = { manager, _ ->
manager.getSizingCalculator().editSavedSettings {
if(it.currentMode == KeyboardMode.OneHanded
&& it.oneHandedDirection == OneHandedDirection.Right) {
it.copy(
currentMode = KeyboardMode.Regular
)
} else {
it.copy(
oneHandedDirection = OneHandedDirection.Right,
currentMode = KeyboardMode.OneHanded
)
}
}
},
windowImpl = null,
)
val SplitKeyboardAction = Action(
icon = R.drawable.arrow_down,
name = R.string.split_keyboard_action_title,
simplePressImpl = { manager, _ ->
manager.getSizingCalculator().editSavedSettings {
if(it.currentMode == KeyboardMode.Split) {
it.copy(
currentMode = KeyboardMode.Regular
)
} else {
it.copy(
currentMode = KeyboardMode.Split
)
}
}
},
windowImpl = null,
)
val FloatingKeyboardAction = Action(
icon = R.drawable.arrow_up,
name = R.string.floating_keyboard_action_title,
simplePressImpl = { manager, _ ->
manager.getSizingCalculator().editSavedSettings {
if(it.currentMode == KeyboardMode.Floating) {
it.copy(
currentMode = KeyboardMode.Regular
)
} else {
it.copy(
currentMode = KeyboardMode.Floating
)
}
}
},
windowImpl = null,
)

View File

@ -31,7 +31,11 @@ val AllActionsMap = mapOf(
"copy" to CopyAction, "copy" to CopyAction,
"select_all" to SelectAllAction, "select_all" to SelectAllAction,
"more" to MoreActionsAction, "more" to MoreActionsAction,
"bugs" to BugViewerAction "bugs" to BugViewerAction,
"onehanded_left" to LeftHandedKeyboardAction,
"onehanded_right" to RightHandedKeyboardAction,
"split_keyboard" to SplitKeyboardAction,
"floating_keyboard" to FloatingKeyboardAction
) )
val ActionToId = AllActionsMap.entries.associate { it.value to it.key } val ActionToId = AllActionsMap.entries.associate { it.value to it.key }

View File

@ -240,7 +240,7 @@ public class LanguageModelFacilitator(
try { try {
inputLogic.mWordComposer.setAutoCorrection(null) inputLogic.mWordComposer.setAutoCorrection(null)
if(values.composedData.mTypedWord.length > BinaryDictionary.DICTIONARY_MAX_WORD_LENGTH) { if(values.composedData.mTypedWord.length > BinaryDictionary.DICTIONARY_MAX_WORD_LENGTH-1) {
inputLogic.mSuggestionStripViewAccessor.setNeutralSuggestionStrip() inputLogic.mSuggestionStripViewAccessor.setNeutralSuggestionStrip()
return return
} }

View File

@ -76,16 +76,12 @@ fun getPrimaryLayoutOverride(editorInfo: EditorInfo?): String? {
} }
data class KeyboardLayoutSetV2Params( data class KeyboardLayoutSetV2Params(
val width: Int, val computedSize: ComputedKeyboardSize,
val height: Int,
val padding: Rect,
val keyboardLayoutSet: String, val keyboardLayoutSet: String,
val locale: Locale, val locale: Locale,
val editorInfo: EditorInfo?, val editorInfo: EditorInfo?,
val numberRow: Boolean, val numberRow: Boolean,
val gap: Float = 4.0f, val gap: Float = 4.0f,
val useSplitLayout: Boolean,
val splitLayoutWidth: Int,
val bottomActionKey: Int?, val bottomActionKey: Int?,
val longPressKeySettings: LongPressKeySettings? = null val longPressKeySettings: LongPressKeySettings? = null
) )
@ -184,14 +180,15 @@ class KeyboardLayoutSetV2 internal constructor(
NumberRowMode.AlwaysDisabled -> false NumberRowMode.AlwaysDisabled -> false
} }
private val widthMinusPadding = params.width - params.padding.left - params.padding.right private val height = params.computedSize.getHeight()
private val heightMinusPadding = params.height - params.padding.top - params.padding.bottom
private val padding = params.computedSize.getPadding()
private val widthMinusPadding = params.computedSize.getTotalKeyboardWidth()
private val heightMinusPadding = height - padding.top - padding.bottom
private val singularRowHeight: Double private val singularRowHeight: Double
get() = heightMinusPadding?.let { it / 4.0 } ?: run { get() = heightMinusPadding / 4.0
(ResourceUtils.getDefaultKeyboardHeight(context.resources) / 4.0) *
keyboardHeightMultiplier
}
fun getKeyboard(element: KeyboardLayoutElement): Keyboard { fun getKeyboard(element: KeyboardLayoutElement): Keyboard {
@ -221,10 +218,8 @@ class KeyboardLayoutSetV2 internal constructor(
} }
val layoutParams = LayoutParams( val layoutParams = LayoutParams(
size = params.computedSize,
gap = params.gap.dp, gap = params.gap.dp,
useSplitLayout = params.useSplitLayout,
splitLayoutWidth = params.splitLayoutWidth,
padding = params.padding,
standardRowHeight = singularRowHeight, standardRowHeight = singularRowHeight,
element = element element = element
) )

View File

@ -2,12 +2,27 @@ package org.futo.inputmethod.v2keyboard
import android.content.Context import android.content.Context
import android.graphics.Rect import android.graphics.Rect
import androidx.datastore.preferences.core.booleanPreferencesKey import androidx.datastore.preferences.core.stringPreferencesKey
import androidx.datastore.preferences.core.floatPreferencesKey
import androidx.window.layout.FoldingFeature import androidx.window.layout.FoldingFeature
import kotlinx.serialization.KSerializer
import kotlinx.serialization.Serializable
import kotlinx.serialization.Serializer
import kotlinx.serialization.descriptors.SerialDescriptor
import kotlinx.serialization.descriptors.buildClassSerialDescriptor
import kotlinx.serialization.descriptors.element
import kotlinx.serialization.encodeToString
import kotlinx.serialization.encoding.CompositeDecoder
import kotlinx.serialization.encoding.Decoder
import kotlinx.serialization.encoding.Encoder
import kotlinx.serialization.encoding.decodeStructure
import kotlinx.serialization.encoding.encodeStructure
import kotlinx.serialization.json.Json
import org.futo.inputmethod.latin.FoldStateProvider import org.futo.inputmethod.latin.FoldStateProvider
import org.futo.inputmethod.latin.LatinIME
import org.futo.inputmethod.latin.uix.SettingsKey import org.futo.inputmethod.latin.uix.SettingsKey
import org.futo.inputmethod.latin.uix.UixManager
import org.futo.inputmethod.latin.uix.getSettingBlocking import org.futo.inputmethod.latin.uix.getSettingBlocking
import org.futo.inputmethod.latin.uix.setSettingBlocking
import org.futo.inputmethod.latin.utils.ResourceUtils import org.futo.inputmethod.latin.utils.ResourceUtils
import kotlin.math.roundToInt import kotlin.math.roundToInt
@ -17,13 +32,140 @@ interface KeyboardSizeStateProvider {
sealed class ComputedKeyboardSize() sealed class ComputedKeyboardSize()
class RegularKeyboardSize(val height: Int, val padding: Rect) : ComputedKeyboardSize() class RegularKeyboardSize(val height: Int, val width: Int, val padding: Rect) : ComputedKeyboardSize()
class SplitKeyboardSize(val height: Int, val padding: Rect, val splitLayoutWidth: Int) : ComputedKeyboardSize() class SplitKeyboardSize(val height: Int, val width: Int, val padding: Rect, val splitLayoutWidth: Int) : ComputedKeyboardSize()
//class OneHandedKeyboardSize(val height: Int, val offset: Int, val sideInset: Int, val isLeft: Boolean, val width: Int): ComputedKeyboardSize() enum class OneHandedDirection {
//class FloatingKeyboardSize(val x: Int, val y: Int, val width: Int, val height: Int): ComputedKeyboardSize() Left,
Right
}
class OneHandedKeyboardSize(val height: Int, val width: Int, val padding: Rect, val layoutWidth: Int, val direction: OneHandedDirection): ComputedKeyboardSize()
class FloatingKeyboardSize(
val bottomOrigin: Pair<Int, Int>,
val width: Int,
val height: Int,
val decorationPadding: Rect
): ComputedKeyboardSize()
fun ComputedKeyboardSize.getHeight(): Int = when(this) {
is FloatingKeyboardSize -> height
is OneHandedKeyboardSize -> height
is RegularKeyboardSize -> height
is SplitKeyboardSize -> height
}
fun ComputedKeyboardSize.getWidth(): Int = when(this) {
is FloatingKeyboardSize -> width
is OneHandedKeyboardSize -> width
is RegularKeyboardSize -> width
is SplitKeyboardSize -> width
}
fun ComputedKeyboardSize.getPadding(): Rect = when(this) {
is FloatingKeyboardSize -> decorationPadding
is OneHandedKeyboardSize -> padding
is RegularKeyboardSize -> padding
is SplitKeyboardSize -> padding
}
fun ComputedKeyboardSize.getTotalKeyboardWidth(): Int = when(this) {
is FloatingKeyboardSize -> width - decorationPadding.left - decorationPadding.right
is OneHandedKeyboardSize -> layoutWidth
is RegularKeyboardSize -> width - padding.left - padding.right
is SplitKeyboardSize -> width - padding.left - padding.right
}
enum class KeyboardMode {
Regular,
Split,
OneHanded,
Floating
}
@Serializer(forClass = Rect::class)
object RectSerializer : KSerializer<Rect> {
override val descriptor: SerialDescriptor = buildClassSerialDescriptor("Rect") {
element<Int>("left")
element<Int>("top")
element<Int>("right")
element<Int>("bottom")
}
override fun serialize(encoder: Encoder, value: Rect) {
encoder.encodeStructure(descriptor) {
encodeIntElement(descriptor, 0, value.left)
encodeIntElement(descriptor, 1, value.top)
encodeIntElement(descriptor, 2, value.right)
encodeIntElement(descriptor, 3, value.bottom)
}
}
override fun deserialize(decoder: Decoder): Rect {
return decoder.decodeStructure(descriptor) {
var left = 0
var top = 0
var right = 0
var bottom = 0
while (true) {
when (val index = decodeElementIndex(descriptor)) {
0 -> left = decodeIntElement(descriptor, 0)
1 -> top = decodeIntElement(descriptor, 1)
2 -> right = decodeIntElement(descriptor, 2)
3 -> bottom = decodeIntElement(descriptor, 3)
CompositeDecoder.DECODE_DONE -> break
else -> error("Unexpected index: $index")
}
}
Rect(left, top, right, bottom)
}
}
}
@Serializable
data class SavedKeyboardSizingSettings(
val currentMode: KeyboardMode,
val heightMultiplier: Float,
val paddingDp: @Serializable(RectSerializer::class) Rect,
// Split
val splitWidthFraction: Float,
// One handed, values with respect to left handed mode
// left = padding
// right = width + padding
// bottom = padding for bottom
val oneHandedRectDp: @Serializable(RectSerializer::class) Rect,
val oneHandedDirection: OneHandedDirection,
// Floating
// bottom left of the floating keyboard, relative to bottom left of screen, .second is Y up
val floatingBottomOriginDp: Pair<Float, Float>,
val floatingWidthDp: Float,
val floatingHeightDp: Float
) {
fun toJsonString(): String =
Json.encodeToString(this)
companion object {
@JvmStatic
fun fromJsonString(s: String): SavedKeyboardSizingSettings? =
try {
Json.decodeFromString(s)
} catch (e: Exception) {
//e.printStackTrace()
null
}
}
}
enum class KeyboardSizeSettingKind { enum class KeyboardSizeSettingKind {
Portrait, Portrait,
@ -31,117 +173,230 @@ enum class KeyboardSizeSettingKind {
FoldableInnerDisplay FoldableInnerDisplay
} }
val SplitKeyboardSettings = mapOf( val DefaultKeyboardSettings = mapOf(
KeyboardSizeSettingKind.Portrait to SettingsKey( KeyboardSizeSettingKind.Portrait to SavedKeyboardSizingSettings(
booleanPreferencesKey("split_keyboard_portrait"), false), currentMode = KeyboardMode.Regular,
KeyboardSizeSettingKind.Landscape to SettingsKey( heightMultiplier = 1.0f,
booleanPreferencesKey("split_keyboard_landscape"), true), paddingDp = Rect(2, 4, 2, 10),
KeyboardSizeSettingKind.FoldableInnerDisplay to SettingsKey( splitWidthFraction = 4.0f / 5.0f,
booleanPreferencesKey("split_keyboard_fold"), true), oneHandedDirection = OneHandedDirection.Right,
oneHandedRectDp = Rect(4, 4, 364, 30),
floatingBottomOriginDp = Pair(0.0f, 0.0f),
floatingHeightDp = 240.0f,
floatingWidthDp = 360.0f
),
KeyboardSizeSettingKind.Landscape to SavedKeyboardSizingSettings(
currentMode = KeyboardMode.Split,
heightMultiplier = 0.9f,
paddingDp = Rect(8, 2, 8, 2),
splitWidthFraction = 3.0f / 5.0f,
oneHandedDirection = OneHandedDirection.Right,
oneHandedRectDp = Rect(4, 4, 364, 30),
floatingBottomOriginDp = Pair(0.0f, 0.0f),
floatingHeightDp = 240.0f,
floatingWidthDp = 360.0f
),
KeyboardSizeSettingKind.FoldableInnerDisplay to SavedKeyboardSizingSettings(
currentMode = KeyboardMode.Split,
heightMultiplier = 0.67f,
paddingDp = Rect(44, 4, 44, 8),
splitWidthFraction = 3.0f / 5.0f,
oneHandedDirection = OneHandedDirection.Right,
oneHandedRectDp = Rect(4, 4, 364, 30),
floatingBottomOriginDp = Pair(0.0f, 0.0f),
floatingHeightDp = 240.0f,
floatingWidthDp = 360.0f
),
) )
val KeyboardHeightSettings = mapOf( val KeyboardSettings = mapOf(
KeyboardSizeSettingKind.Portrait to SettingsKey( KeyboardSizeSettingKind.Portrait to SettingsKey(
floatPreferencesKey("keyboardHeightMultiplier"), 1.0f), stringPreferencesKey("keyboard_settings_portrait"), ""),
KeyboardSizeSettingKind.Landscape to SettingsKey( KeyboardSizeSettingKind.Landscape to SettingsKey(
floatPreferencesKey("keyboard_height_landscape"), 0.9f), stringPreferencesKey("keyboard_settings_landscape"), ""),
KeyboardSizeSettingKind.FoldableInnerDisplay to SettingsKey( KeyboardSizeSettingKind.FoldableInnerDisplay to SettingsKey(
floatPreferencesKey("keyboard_height_fold"), 0.67f), stringPreferencesKey("keyboard_settings_fold"), ""),
) )
val KeyboardOffsetSettings = mapOf( class KeyboardSizingCalculator(val context: Context, val uixManager: UixManager) {
KeyboardSizeSettingKind.Portrait to SettingsKey( val sizeStateProvider = context as KeyboardSizeStateProvider
floatPreferencesKey("keyboard_offset_portrait"), 8.0f), val foldStateProvider = context as FoldStateProvider
KeyboardSizeSettingKind.Landscape to SettingsKey(
floatPreferencesKey("keyboard_offset_landscape"), 0.0f),
KeyboardSizeSettingKind.FoldableInnerDisplay to SettingsKey(
floatPreferencesKey("keyboard_offset_fold"), 8.0f),
)
val KeyboardSideInsetSettings = mapOf( private fun dp(v: Number): Int =
KeyboardSizeSettingKind.Portrait to SettingsKey( (v.toFloat() * context.resources.displayMetrics.density).toInt()
floatPreferencesKey("keyboard_inset_portrait"), 2.0f),
KeyboardSizeSettingKind.Landscape to SettingsKey( private fun dp(v: Rect): Rect =
floatPreferencesKey("keyboard_inset_landscape"), 8.0f), Rect(dp(v.left), dp(v.top), dp(v.right), dp(v.bottom))
KeyboardSizeSettingKind.FoldableInnerDisplay to SettingsKey(
floatPreferencesKey("keyboard_inset_fold"), 44.0f), private fun limitFloating(rectPx: Rect): Rect {
) val width = rectPx.width()
val height = rectPx.height()
val minWidth = dp(160)
val minHeight = dp(160)
if(width < minWidth) {
val delta = minWidth - width
rectPx.left -= delta / 2
rectPx.right += delta / 2
}
if(height < minHeight) {
val delta = minHeight - height
rectPx.top -= delta
rectPx.bottom += delta
}
val maxWidth = context.resources.displayMetrics.widthPixels * 2 / 3
val maxHeight = context.resources.displayMetrics.heightPixels * 2 / 3
if(width > maxWidth) {
val delta = width - maxWidth
rectPx.left += delta / 2
rectPx.right -= delta / 2
}
if(height > maxHeight) {
val delta = height - maxHeight
rectPx.top += delta / 2
rectPx.bottom -= delta / 2
}
class KeyboardSizingCalculator(val context: Context) { val originX = rectPx.left
private val sizeStateProvider = context as KeyboardSizeStateProvider val originY = rectPx.top
private val foldStateProvider = context as FoldStateProvider
private fun isSplitKeyboard(mode: KeyboardSizeSettingKind): Boolean = if(originX < 0){
context.getSettingBlocking(SplitKeyboardSettings[mode]!!) rectPx.left -= originX
rectPx.right -= originX
}
private fun heightMultiplier(mode: KeyboardSizeSettingKind): Float = if(originY < 0) {
context.getSettingBlocking(KeyboardHeightSettings[mode]!!) rectPx.top -= originY
rectPx.bottom -= originY
}
private fun bottomOffsetPx(mode: KeyboardSizeSettingKind): Int = if(rectPx.right > context.resources.displayMetrics.widthPixels) {
(context.getSettingBlocking(KeyboardOffsetSettings[mode]!!) * context.resources.displayMetrics.density).toInt() val delta = rectPx.right - context.resources.displayMetrics.widthPixels
rectPx.right -= delta
rectPx.left -= delta
}
if(rectPx.bottom < 0) {
val delta = rectPx.bottom
rectPx.top -= delta
rectPx.bottom -= delta
}
private fun sideInsetPx(mode: KeyboardSizeSettingKind): Int = return rectPx
(context.getSettingBlocking(KeyboardSideInsetSettings[mode]!!) * context.resources.displayMetrics.density).toInt() }
private fun topPaddingPx(mode: KeyboardSizeSettingKind): Int = fun getSavedSettings(): SavedKeyboardSizingSettings =
(when(mode) { SavedKeyboardSizingSettings.fromJsonString(context.getSettingBlocking(
KeyboardSizeSettingKind.Portrait -> 4.0f KeyboardSettings[sizeStateProvider.currentSizeState]!!
KeyboardSizeSettingKind.Landscape -> 0.0f )) ?: DefaultKeyboardSettings[sizeStateProvider.currentSizeState]!!
KeyboardSizeSettingKind.FoldableInnerDisplay -> 8.0f
} * context.resources.displayMetrics.density).toInt() fun editSavedSettings(transform: (SavedKeyboardSizingSettings) -> SavedKeyboardSizingSettings) {
val sizeState = sizeStateProvider.currentSizeState
val savedSettings = SavedKeyboardSizingSettings.fromJsonString(context.getSettingBlocking(
KeyboardSettings[sizeState]!!
)) ?: DefaultKeyboardSettings[sizeState]!!
val transformed = transform(savedSettings)
if(transformed != savedSettings) {
context.setSettingBlocking(KeyboardSettings[sizeState]!!.key, transformed.toJsonString())
}
}
fun calculate(layoutName: String, isNumberRowActive: Boolean): ComputedKeyboardSize { fun calculate(layoutName: String, isNumberRowActive: Boolean): ComputedKeyboardSize {
val savedSettings = getSavedSettings()
val layout = LayoutManager.getLayout(context, layoutName) val layout = LayoutManager.getLayout(context, layoutName)
val effectiveRowCount = layout.effectiveRows.size val effectiveRowCount = layout.effectiveRows.size
val configuration = context.resources.configuration
val displayMetrics = context.resources.displayMetrics val displayMetrics = context.resources.displayMetrics
val mode = sizeStateProvider.currentSizeState val singularRowHeight = (ResourceUtils.getDefaultKeyboardHeight(context.resources) / 4.0) *
savedSettings.heightMultiplier
val isSplit = isSplitKeyboard(mode)
val heightMultiplier = heightMultiplier(mode)
val bottomOffset = bottomOffsetPx(mode)
val sideInset = sideInsetPx(mode)
val topPadding = topPaddingPx(mode)
val singularRowHeight = (ResourceUtils.getDefaultKeyboardHeight(context.resources) / 4.0) * heightMultiplier
val numRows = 4.0 + val numRows = 4.0 +
((effectiveRowCount - 5) / 2.0).coerceAtLeast(0.0) + ((effectiveRowCount - 5) / 2.0).coerceAtLeast(0.0) +
if(isNumberRowActive) { 0.5 } else { 0.0 } if(isNumberRowActive) { 0.5 } else { 0.0 }
println("Num rows; $numRows, $effectiveRowCount ($layoutName) ($layout)")
val recommendedHeight = numRows * singularRowHeight val recommendedHeight = numRows * singularRowHeight
val foldState = foldStateProvider.foldState.feature val foldState = foldStateProvider.foldState.feature
val window = (context as LatinIME).window.window
val width = ResourceUtils.getDefaultKeyboardWidth(window, context.resources)
return when { return when {
// Special case: 50% screen height no matter the row count or settings // Special case: 50% screen height no matter the row count or settings
foldState != null && foldState.state == FoldingFeature.State.HALF_OPENED && foldState.orientation == FoldingFeature.Orientation.HORIZONTAL -> foldState != null && foldState.state == FoldingFeature.State.HALF_OPENED && foldState.orientation == FoldingFeature.Orientation.HORIZONTAL ->
SplitKeyboardSize( SplitKeyboardSize(
displayMetrics.heightPixels / 2 - (displayMetrics.density * 80.0f).toInt(), height = displayMetrics.heightPixels / 2 - (displayMetrics.density * 80.0f).toInt(),
Rect( width = width,
padding = Rect(
(displayMetrics.density * 44.0f).roundToInt(), (displayMetrics.density * 44.0f).roundToInt(),
(displayMetrics.density * 20.0f).roundToInt(), (displayMetrics.density * 50.0f).roundToInt(),
(displayMetrics.density * 44.0f).roundToInt(), (displayMetrics.density * 44.0f).roundToInt(),
(displayMetrics.density * 12.0f).roundToInt(), (displayMetrics.density * 12.0f).roundToInt(),
), ),
displayMetrics.widthPixels * 3 / 5 splitLayoutWidth = displayMetrics.widthPixels * 3 / 5
) )
isSplit -> SplitKeyboardSize( savedSettings.currentMode == KeyboardMode.Split ->
recommendedHeight.roundToInt(), SplitKeyboardSize(
Rect(sideInset, topPadding, sideInset, bottomOffset), height = recommendedHeight.roundToInt(),
displayMetrics.widthPixels * 3 / 5) width = width,
padding = dp(savedSettings.paddingDp),
splitLayoutWidth = (displayMetrics.widthPixels * savedSettings.splitWidthFraction).toInt()
)
else -> RegularKeyboardSize( savedSettings.currentMode == KeyboardMode.OneHanded ->
recommendedHeight.roundToInt(), OneHandedKeyboardSize(
Rect(sideInset, topPadding, sideInset, bottomOffset), height = recommendedHeight.roundToInt(),
) width = width,
padding = dp(savedSettings.oneHandedRectDp).let { rect ->
when(savedSettings.oneHandedDirection) {
OneHandedDirection.Left -> Rect(rect.left, rect.top, rect.left, rect.bottom)
OneHandedDirection.Right -> Rect(rect.left, rect.top, rect.left, rect.bottom)
}
},
layoutWidth = dp(savedSettings.oneHandedRectDp.width()).coerceAtMost(displayMetrics.widthPixels * 9 / 10),
direction = savedSettings.oneHandedDirection
)
savedSettings.currentMode == KeyboardMode.Floating -> {
val singularRowHeightFloat = dp(savedSettings.floatingHeightDp) / 4.0f
val recommendedHeightFloat = singularRowHeightFloat * numRows
FloatingKeyboardSize(
bottomOrigin = Pair(
dp(savedSettings.floatingBottomOriginDp.first),
dp(savedSettings.floatingBottomOriginDp.second)
),
width = dp(savedSettings.floatingWidthDp),
height = recommendedHeightFloat.toInt(),
decorationPadding = dp(
Rect(
8,
8,
8,
8
)
)
)
}
else ->
RegularKeyboardSize(
height = recommendedHeight.roundToInt(),
width = width,
padding = dp(savedSettings.paddingDp)
)
} }
} }
@ -155,4 +410,14 @@ class KeyboardSizingCalculator(val context: Context) {
return (minDp / 100.0f).coerceIn(3.0f, 6.0f) return (minDp / 100.0f).coerceIn(3.0f, 6.0f)
} }
fun calculateSuggestionBarHeightDp(): Float {
return 40.0f
}
fun calculateTotalActionBarHeightPx(): Int =
when {
uixManager.actionsExpanded -> dp(2 * calculateSuggestionBarHeightDp())
else -> dp(calculateSuggestionBarHeightDp())
}
} }

View File

@ -81,10 +81,8 @@ data class LayoutRow(
) )
data class LayoutParams( data class LayoutParams(
val size: ComputedKeyboardSize,
val gap: Dp, val gap: Dp,
val useSplitLayout: Boolean,
val splitLayoutWidth: Int,
val padding: Rect,
val standardRowHeight: Double, val standardRowHeight: Double,
val element: KeyboardLayoutElement, val element: KeyboardLayoutElement,
) )
@ -131,8 +129,6 @@ data class LayoutEngine(
} }
private fun computeRowHeight(): Double { private fun computeRowHeight(): Double {
//val normalKeyboardHeight = ((rowHeight.value + verticalGap.value) * density) * 3
val normalKeyboardHeight = totalRowHeight val normalKeyboardHeight = totalRowHeight
// divide by total row height // divide by total row height
@ -140,19 +136,29 @@ data class LayoutEngine(
BottomRowHeightMode.Fixed -> ((normalKeyboardHeight - layoutParams.standardRowHeight) / rows.filter { !it.isBottomRow }.sumOf { it.rowHeight }) BottomRowHeightMode.Fixed -> ((normalKeyboardHeight - layoutParams.standardRowHeight) / rows.filter { !it.isBottomRow }.sumOf { it.rowHeight })
BottomRowHeightMode.Flexible -> (normalKeyboardHeight) / rows.sumOf { it.rowHeight } BottomRowHeightMode.Flexible -> (normalKeyboardHeight) / rows.sumOf { it.rowHeight }
} }
//return ((normalKeyboardHeight - bottomRowHeightPx) / rows.filter { !it.isBottomRow }.sumOf { it.rowHeight })
//return (normalKeyboardHeight) / rows.sumOf { it.rowHeight }
} }
private val isSplitLayout = layoutParams.useSplitLayout private val isSplitLayout = layoutParams.size is SplitKeyboardSize
private val isOneHandedLayout = layoutParams.size is OneHandedKeyboardSize
private val layoutWidth = if(isSplitLayout) { private val layoutWidth = if(isSplitLayout) {
layoutParams.splitLayoutWidth (layoutParams.size as SplitKeyboardSize).splitLayoutWidth
} else if(isOneHandedLayout) {
(layoutParams.size as OneHandedKeyboardSize).layoutWidth
} else { } else {
params.mId.mWidth params.mId.mWidth
} }
private val unsplitLayoutWidth = params.mId.mWidth private val unsplitLayoutWidth = if(isSplitLayout) {
params.mId.mWidth
} else {
layoutWidth
}
// TODO: Remove
private val padding = Rect(0, 0, 0, 0)
private val xOffset = 0
private val minimumBottomFunctionalKeyWidth = (layoutWidth * keyboard.minimumBottomRowFunctionalKeyWidth) private val minimumBottomFunctionalKeyWidth = (layoutWidth * keyboard.minimumBottomRowFunctionalKeyWidth)
private val regularKeyWidth = computeRegularKeyWidth() private val regularKeyWidth = computeRegularKeyWidth()
@ -551,10 +557,10 @@ data class LayoutEngine(
} }
private fun addRowAlignLeft(row: List<LayoutEntry>, y: Int, height: Int) private fun addRowAlignLeft(row: List<LayoutEntry>, y: Int, height: Int)
= addRow(row, 0.0f + layoutParams.padding.left, y, height) = addRow(row, 0.0f + padding.left + xOffset, y, height)
private fun addRowAlignRight(row: List<LayoutEntry>, y: Int, height: Int) { private fun addRowAlignRight(row: List<LayoutEntry>, y: Int, height: Int) {
val startingOffset = params.mId.mWidth - row.sumOf { it.widthPx.toDouble() }.toFloat() + layoutParams.padding.left val startingOffset = params.mId.mWidth - row.sumOf { it.widthPx.toDouble() }.toFloat() + padding.left
addRow(row, startingOffset, y, height) addRow(row, startingOffset, y, height)
} }
@ -570,7 +576,7 @@ data class LayoutEngine(
} }
private fun addKeys(rows: List<LayoutRow>): Int { private fun addKeys(rows: List<LayoutRow>): Int {
var currentY = 0.0f + layoutParams.padding.top var currentY = 0.0f + padding.top
rows.forEach { row -> rows.forEach { row ->
addRow(row, currentY.toInt()) addRow(row, currentY.toInt())
currentY += row.height currentY += row.height
@ -588,14 +594,14 @@ data class LayoutEngine(
val rows = computeRows(this.rows) val rows = computeRows(this.rows)
val totalKeyboardHeight = addKeys(rows).let { totalRowHeight.roundToInt() } + layoutParams.padding.top + layoutParams.padding.bottom val totalKeyboardHeight = addKeys(rows).let { totalRowHeight.roundToInt() } + padding.top + padding.bottom
params.mOccupiedHeight = totalKeyboardHeight - verticalGapPx.roundToInt() params.mOccupiedHeight = totalKeyboardHeight - verticalGapPx.roundToInt()
params.mOccupiedWidth = params.mId.mWidth + layoutParams.padding.left + layoutParams.padding.right params.mOccupiedWidth = params.mId.mWidth + padding.left + padding.right
params.mTopPadding = 0//layoutParams.padding.top params.mTopPadding = 0
params.mBottomPadding = 0//layoutParams.padding.bottom params.mBottomPadding = 0
params.mLeftPadding = 0//layoutParams.padding.left params.mLeftPadding = 0
params.mRightPadding = 0//layoutParams.padding.right params.mRightPadding = 0
params.mBaseWidth = params.mOccupiedWidth params.mBaseWidth = params.mOccupiedWidth
params.mDefaultKeyWidth = regularKeyWidth.roundToInt() params.mDefaultKeyWidth = regularKeyWidth.roundToInt()

View File

@ -33,7 +33,7 @@ object LayoutManager {
private fun getAllLayoutPaths(assetManager: AssetManager): List<String> { private fun getAllLayoutPaths(assetManager: AssetManager): List<String> {
return listFilesRecursively(assetManager, "layouts").filter { return listFilesRecursively(assetManager, "layouts").filter {
(it.endsWith(".yml") || it.endsWith(".yaml")) && it != "mapping.yaml" (it.endsWith(".yml") || it.endsWith(".yaml")) && it != "layouts/mapping.yaml"
} }
} }

View File

@ -43,9 +43,7 @@ private fun symsForCoord(keyCoordinate: KeyCoordinate): String {
if(centeredCol < 0) return "" if(centeredCol < 0) return ""
val letter = row.getOrNull(centeredCol) val letter = row.getOrNull(centeredCol)
if(letter == 'ñ') {
println("It's ñ")
}
return if(letter != null) { return if(letter != null) {
"!text/qwertysyms_$letter" "!text/qwertysyms_$letter"
} else { } else {