Update keyboard sizing and add floating mode

This commit is contained in:
Aleksandras Kostarevas 2024-09-27 00:23:03 +03:00
parent 32b27b84c0
commit 3d8233be92
13 changed files with 804 additions and 275 deletions

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

@ -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)
); );

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

@ -21,10 +21,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 +79,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 +113,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 +173,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 +183,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 +264,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 +335,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 +417,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 +448,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 +464,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 +474,21 @@ 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
private var isInputModal = false private var isInputModal = false
fun setInputModal(to: Boolean) { fun setInputModal(to: Boolean) {
@ -426,8 +509,6 @@ class LatinIME : InputMethodService(), LifecycleOwner, ViewModelStoreOwner, Save
} }
} }
key(legacyInputView) { key(legacyInputView) {
AndroidView(factory = { AndroidView(factory = {
legacyInputView!! legacyInputView!!
@ -445,7 +526,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 +536,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 +554,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 +640,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) {
touchableInsets = Insets.TOUCHABLE_INSETS_REGION
touchableRegion.set(0, 0, composeView!!.width, composeView!!.height)
}
}) })
} }
@ -737,14 +835,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

@ -16,6 +16,7 @@ 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.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 {
@ -68,6 +69,8 @@ interface KeyboardManagerForAction {
fun getLatinIMEForDebug(): LatinIME fun getLatinIMEForDebug(): LatinIME
fun isDeviceLocked(): Boolean fun isDeviceLocked(): Boolean
fun getSizingCalculator(): KeyboardSizingCalculator
} }
interface ActionWindow { interface ActionWindow {

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

@ -23,22 +23,29 @@ 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.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 +53,21 @@ 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.geometry.Offset
import androidx.compose.ui.graphics.Color import androidx.compose.ui.graphics.Color
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.LocalConfiguration
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
@ -96,6 +107,14 @@ 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.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 +298,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,6 +314,9 @@ 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
} }
@ -305,11 +327,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 +350,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 +363,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,6 +658,129 @@ class UixManager(private val latinIME: LatinIME) {
) )
} }
@Composable
fun SizePositionerSurface(content: @Composable BoxScope.(actionBarGap: Dp) -> Unit) {
val size = latinIME.size.value
when(size) {
is FloatingKeyboardSize -> {
val offset = remember(size) { mutableStateOf(Offset(size.bottomOrigin.first.toFloat(), size.bottomOrigin.second.toFloat())) }
val configuration = LocalConfiguration.current
with(LocalDensity.current) {
Column(modifier = Modifier.fillMaxHeight().absoluteOffset { IntOffset(offset.value.x.toInt(), 0) }) {
Spacer(Modifier.weight(1.0f))
Column(Modifier
.background(latinIME.keyboardColor, RoundedCornerShape(8.dp))
.requiredWidth(size.width.toDp())
.onSizeChanged {
measuredTouchableHeight = it.height
}
.absolutePadding(
left = size.decorationPadding.left.toDp(),
top = 0.dp,
right = size.decorationPadding.right.toDp(),
bottom = 0.dp,
)
) {
Box(Modifier.fillMaxWidth()) {
CompositionLocalProvider(
LocalContentColor provides contentColorFor(
latinIME.keyboardColor
)
) {
content(4.dp)
}
}
Spacer(modifier = Modifier.height(24.dp))
Box(modifier = Modifier
.fillMaxWidth()
.height(20.dp)
.pointerInput(size) {
detectDragGestures(onDrag = { change, 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(configuration.screenWidthDp.dp.toPx() - size.width),
newOffset.y.coerceAtMost(latinIME.getViewHeight().toFloat() - measuredTouchableHeight)
)
offset.value = newOffset
}, onDragEnd = {
latinIME.sizingCalculator.editSavedSettings { settings ->
settings.copy(
floatingBottomCenterOriginDp = Pair(
offset.value.x.toDp().value,
offset.value.y.toDp().value
)
)
}
})
}) {
Box(modifier = Modifier.fillMaxWidth(0.6f).height(4.dp).align(Alignment.TopCenter).background(
MaterialTheme.colorScheme.onBackground.copy(alpha = 0.6f), RoundedCornerShape(100)
))
}
}
Spacer(Modifier.height(offset.value.y.toDp()))
}
}
}
is OneHandedKeyboardSize,
is RegularKeyboardSize,
is SplitKeyboardSize -> {
Column {
Spacer(modifier = Modifier.weight(1.0f))
Surface(modifier = Modifier.onSizeChanged {
measuredTouchableHeight = it.height
}, color = latinIME.keyboardColor) {
with(LocalDensity.current) {
val actionBarGap = (size.getPadding().top / 2).toDp()
Box(Modifier.absolutePadding(
left = size.getPadding().left.toDp(),
top = actionBarGap,
right = size.getPadding().right.toDp(),
bottom = size.getPadding().bottom.toDp(),
).width(
(size.getWidth() - size.getPadding().left - size.getPadding().right).toDp()
)) {
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)
}
}
is RegularKeyboardSize -> {
content(actionBarGap)
}
is SplitKeyboardSize -> {
content(actionBarGap)
}
else -> throw IllegalStateException()
}
}
}
}
}
}
null -> return
}
}
fun setContent() { fun setContent() {
composeView?.setContent { composeView?.setContent {
UixThemeWrapper(latinIME.colorScheme) { UixThemeWrapper(latinIME.colorScheme) {
@ -639,29 +794,22 @@ class UixManager(private val latinIME: LatinIME) {
isShowingActionEditor.value = false isShowingActionEditor.value = false
} }
Column { SizePositionerSurface { gap ->
Spacer(modifier = Modifier.weight(1.0f)) Column {
Surface(modifier = Modifier.onSizeChanged { when {
latinIME.updateTouchableHeight(it.height) currWindowActionWindow != null -> ActionViewWithHeader(
}, color = latinIME.keyboardColor) { currWindowActionWindow!!
Box { )
Column {
when {
currWindowActionWindow != null -> ActionViewWithHeader(
currWindowActionWindow!!
)
else -> MainKeyboardViewWithActionBar() else -> MainKeyboardViewWithActionBar()
}
latinIME.LegacyKeyboardView(hidden = isMainKeyboardHidden)
}
ForgetWordDialog()
} }
Spacer(modifier = Modifier.height(gap))
latinIME.LegacyKeyboardView(hidden = isMainKeyboardHidden)
} }
ForgetWordDialog()
} }
ActionEditorHost() ActionEditorHost()
@ -734,28 +882,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

@ -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,139 @@ 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
val floatingBottomCenterOriginDp: Pair<Float, Float>, // relative to bottom left of screen, .second is Y up
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 +172,231 @@ 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),
floatingBottomCenterOriginDp = 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),
floatingBottomCenterOriginDp = 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),
floatingBottomCenterOriginDp = 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
println("Display metrics ${displayMetrics.widthPixels / displayMetrics.density}")
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.floatingBottomCenterOriginDp.first),
dp(savedSettings.floatingBottomCenterOriginDp.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"
} }
} }