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. -->
<string name="spoken_description_to_alpha">Letters</string>
<!-- 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. -->
<string name="spoken_description_settings">Settings</string>
<!-- Spoken description for the "Tab" keyboard key. -->
@ -77,6 +77,12 @@
<!-- Spoken description for the "Previous" action keyboard key. -->
<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. -->
<string name="spoken_description_shiftmode_on">Shift enabled</string>
<!-- Spoken feedback after turning "Caps lock" mode on. -->
@ -91,6 +97,8 @@
<string name="spoken_description_mode_phone">Phone mode</string>
<!-- Spoken feedback after changing to the shifted phone dialer (symbols) keyboard. -->
<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. -->
<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="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_pinned_key">Pinned Action(s)</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_SHORTCUT, R.string.spoken_description_mic);
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_LANGUAGE_SWITCH,
R.string.spoken_description_language_switch);

View File

@ -181,7 +181,16 @@ public class KeyboardAccessibilityDelegate<KV extends KeyboardView>
node.setFocusable(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_LONG_CLICK);
node.setClickable(true);

View File

@ -18,10 +18,8 @@ package org.futo.inputmethod.accessibility;
import android.content.Context;
import android.graphics.Rect;
import android.os.SystemClock;
import android.util.Log;
import android.util.SparseIntArray;
import android.view.MotionEvent;
import org.futo.inputmethod.keyboard.Key;
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.PointerTracker;
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
@ -191,6 +188,9 @@ public final class MainKeyboardAccessibilityDelegate
case KeyboardId.ELEMENT_PHONE_SYMBOLS:
resId = R.string.spoken_description_mode_phone_shift;
break;
case KeyboardId.ELEMENT_NUMBER:
resId = R.string.spoken_description_mode_digits;
break;
default:
return;
}

View File

@ -23,7 +23,6 @@ import android.text.TextUtils;
import android.view.inputmethod.EditorInfo;
import org.futo.inputmethod.compat.EditorInfoCompatUtils;
import org.futo.inputmethod.latin.RichInputMethodSubtype;
import org.futo.inputmethod.latin.settings.LongPressKeySettings;
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);
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(
keyboardWidth,
keyboardHeight,
padding,
computedSize,
layoutSetName,
subtype.getLocale(),
editorInfo == null ? new EditorInfo() : editorInfo,
settingsValues.mIsNumberRowEnabled,
sizingCalculator.calculateGap(),
splitLayoutWidth != 0,
splitLayoutWidth,
settingsValues.mShowsActionKey ? settingsValues.mActionKeyId : null,
LongPressKeySettings.load(mThemeContext)
);
@ -353,9 +329,7 @@ public final class KeyboardSwitcher implements SwitchActions {
}
public boolean isShowingMoreKeysPanel() {
if (isShowingEmojiPalettes()) {
return false;
}
if(mKeyboardView == null) return false;
return mKeyboardView.isShowingMoreKeysPanel();
}

View File

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

View File

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

View File

@ -12,6 +12,7 @@ import android.os.Bundle
import android.util.Log
import android.view.KeyEvent
import android.view.View
import android.view.ViewGroup
import android.view.inputmethod.CompletionInfo
import android.view.inputmethod.EditorInfo
import android.view.inputmethod.InlineSuggestionsRequest
@ -21,10 +22,14 @@ import android.view.inputmethod.InputMethodSubtype
import androidx.annotation.RequiresApi
import androidx.compose.foundation.layout.size
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.key
import androidx.compose.runtime.mutableStateOf
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clipToBounds
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.viewinterop.AndroidView
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.xlm.LanguageModelFacilitator
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.KeyboardSizeStateProvider
import org.futo.inputmethod.v2keyboard.KeyboardSizingCalculator
import org.futo.inputmethod.v2keyboard.getHeight
private class UnlockedBroadcastReceiver(val onDeviceUnlocked: () -> Unit) : BroadcastReceiver() {
override fun onReceive(context: Context?, intent: Intent?) {
println("Unlocked Broadcast Receiver: ${intent?.action}")
if (intent?.action == Intent.ACTION_USER_UNLOCKED) {
onDeviceUnlocked()
}
}
}
class LatinIME : InputMethodService(), LifecycleOwner, ViewModelStoreOwner, SavedStateRegistryOwner,
LatinIMELegacy.SuggestionStripController, DynamicThemeProviderOwner, FoldStateProvider,
KeyboardSizeStateProvider {
open class InputMethodServiceCompose : InputMethodService(), LifecycleOwner, ViewModelStoreOwner, SavedStateRegistryOwner {
private lateinit var mLifecycleRegistry: LifecycleRegistry
private lateinit var mViewModelStore: ViewModelStore
private lateinit var mSavedStateRegistryController: SavedStateRegistryController
fun setOwners() {
val decorView = window.window?.decorView
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(
this as InputMethodService,
this as LatinIMELegacy.SuggestionStripController
)
val inputLogic get() = latinIMELegacy.mInputLogic
lateinit var languageModelFacilitator: LanguageModelFacilitator
@ -122,6 +174,8 @@ class LatinIME : InputMethodService(), LifecycleOwner, ViewModelStoreOwner, Save
val uixManager = UixManager(this)
lateinit var suggestionBlacklist: SuggestionBlacklist
val sizingCalculator = KeyboardSizingCalculator(this, uixManager)
private var activeThemeOption: ThemeOption? = null
private var activeColorScheme = VoiceInputTheme.obtainColors(this)
private var pendingRecreateKeyboard: Boolean = false
@ -130,6 +184,13 @@ class LatinIME : InputMethodService(), LifecycleOwner, ViewModelStoreOwner, Save
val colorScheme get() = activeColorScheme
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 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) {
size.value = calculateSize()
settingsRefreshRequired = settingsRefreshRequired || refreshSettings
if(!uixManager.isMainKeyboardHidden) {
@ -256,16 +336,6 @@ class LatinIME : InputMethodService(), LifecycleOwner, ViewModelStoreOwner, Save
val filter = IntentFilter(Intent.ACTION_USER_UNLOCKED)
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)
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()
Settings.getInstance().settingsChangedListeners.add { oldSettings, newSettings ->
@ -364,7 +449,6 @@ class LatinIME : InputMethodService(), LifecycleOwner, ViewModelStoreOwner, Save
unregisterReceiver(unlockReceiver)
stopJobs()
mLifecycleRegistry.currentState = Lifecycle.State.DESTROYED
viewModelStore.clear()
languageModelFacilitator.saveHistoryLog()
@ -381,6 +465,7 @@ class LatinIME : InputMethodService(), LifecycleOwner, ViewModelStoreOwner, Save
override fun onConfigurationChanged(newConfig: Configuration) {
Log.w("LatinIME", "Configuration changed")
size.value = calculateSize()
latinIMELegacy.onConfigurationChanged(newConfig)
super.onConfigurationChanged(newConfig)
}
@ -390,22 +475,22 @@ class LatinIME : InputMethodService(), LifecycleOwner, ViewModelStoreOwner, Save
}
private var legacyInputView: View? = null
private var touchableHeight: Int = 0
override fun onCreateInputView(): View {
Log.w("LatinIME", "Create input view")
legacyInputView = latinIMELegacy.onCreateInputView()
val composeView = super.onCreateInputView()
val composeView = uixManager.createComposeView()
legacyInputView = latinIMELegacy.onCreateInputView()
latinIMELegacy.setComposeInputView(composeView)
uixManager.setContent()
return composeView
}
private var inputViewHeight: Int = -1
// Both called by UixManager
fun updateTouchableHeight(to: Int) { touchableHeight = to }
fun getInputViewHeight(): Int = inputViewHeight
fun getViewHeight(): Int = composeView?.height ?: resources.displayMetrics.heightPixels
fun getViewWidth(): Int = composeView?.width ?: resources.displayMetrics.widthPixels
private var isInputModal = false
fun setInputModal(to: Boolean) {
@ -426,11 +511,11 @@ class LatinIME : InputMethodService(), LifecycleOwner, ViewModelStoreOwner, Save
}
}
key(legacyInputView) {
AndroidView(factory = {
legacyInputView!!
legacyInputView!!.also {
if(it.parent != null) (it.parent as ViewGroup).removeView(it)
}
}, modifier = modifier, onRelease = {
val view = it as InputView
view.deallocateMemory()
@ -445,7 +530,7 @@ class LatinIME : InputMethodService(), LifecycleOwner, ViewModelStoreOwner, Save
legacyInputView = newView
uixManager.setContent()
uixManager.getComposeView()?.let {
composeView?.let {
latinIMELegacy.setComposeInputView(it)
}
@ -455,7 +540,7 @@ class LatinIME : InputMethodService(), LifecycleOwner, ViewModelStoreOwner, Save
override fun setInputView(view: View?) {
super.setInputView(view)
uixManager.getComposeView()?.let {
composeView?.let {
latinIMELegacy.setComposeInputView(it)
}
@ -473,8 +558,6 @@ class LatinIME : InputMethodService(), LifecycleOwner, ViewModelStoreOwner, Save
}
override fun onStartInputView(info: EditorInfo?, restarting: Boolean) {
mLifecycleRegistry.currentState = Lifecycle.State.STARTED
lastEditorInfo = info
super.onStartInputView(info, restarting)
@ -561,36 +644,55 @@ class LatinIME : InputMethodService(), LifecycleOwner, ViewModelStoreOwner, Save
}
override fun onComputeInsets(outInsets: Insets?) {
val composeView = uixManager.getComposeView()
// This method may be called before {@link #setInputView(View)}.
if (legacyInputView == null || composeView == null) {
return
}
val inputHeight: Int = composeView.height
if (latinIMELegacy.isImeSuppressedByHardwareKeyboard && !legacyInputView!!.isShown) {
// 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
val viewHeight = composeView!!.height
val size = size.value ?: return
latinIMELegacy.setInsets(outInsets!!.apply {
touchableInsets = Insets.TOUCHABLE_INSETS_REGION;
touchableRegion.set(touchLeft, touchTop, touchRight, touchBottom);
contentTopInsets = visibleTopY
visibleTopInsets = visibleTopY
when(size) {
is FloatingKeyboardSize -> {
val height = uixManager.touchableHeight
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()
}
override val lifecycle: Lifecycle
get() = mLifecycleRegistry
override val savedStateRegistry: SavedStateRegistry
get() = mSavedStateRegistryController.savedStateRegistry
override val viewModelStore: ViewModelStore
get() = mViewModelStore
private fun onDeviceUnlocked() {
Log.i("LatinIME", "DEVICE has UNLOCKED!!! Reloading settings...")
// 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).
@Override
public void setNeutralSuggestionStrip() {
final SettingsValues currentSettings = mSettings.getCurrent();
final SuggestedWords neutralSuggestions = currentSettings.mBigramPredictionEnabled
? SuggestedWords.getEmptyInstance()
: currentSettings.mSpacingAndPunctuations.mSuggestPuncList;
final SuggestedWords neutralSuggestions = SuggestedWords.getEmptyInstance();
setSuggestedWords(neutralSuggestions);
}

View File

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

View File

@ -164,8 +164,8 @@ public class SettingsValues {
final String autoCorrectionThresholdRawValue = mAutoCorrectEnabled
? res.getString(R.string.auto_correction_threshold_mode_index_modest)
: res.getString(R.string.auto_correction_threshold_mode_index_off);
mBigramPredictionEnabled = readBigramPredictionEnabled(prefs, res);
mTransformerPredictionEnabled = readTransformerPredictionEnabled(prefs, res);
mBigramPredictionEnabled = readBigramPredictionEnabled(prefs, res) || mTransformerPredictionEnabled;
mDoubleSpacePeriodTimeout = res.getInteger(R.integer.config_double_space_period_timeout);
mHasHardwareKeyboard = Settings.readHasHardwareKeyboard(res.getConfiguration());
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.lifecycle.LifecycleCoroutineScope
import org.futo.inputmethod.latin.LatinIME
import org.futo.inputmethod.latin.SuggestionBlacklist
import org.futo.inputmethod.latin.uix.theme.ThemeOption
import org.futo.inputmethod.v2keyboard.KeyboardSizingCalculator
import java.util.Locale
interface ActionInputTransaction {
@ -66,8 +68,11 @@ interface KeyboardManagerForAction {
fun activateAction(action: Action)
fun showActionEditor()
fun getSuggestionBlacklist(): SuggestionBlacklist
fun getLatinIMEForDebug(): LatinIME
fun isDeviceLocked(): Boolean
fun getSizingCalculator(): KeyboardSizingCalculator
}
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.drawText
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.rememberTextMeasurer
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.KIND_EMOJI_SUGGESTION
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.suggestions.SuggestionStripViewListener
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.Typography
import org.futo.inputmethod.latin.uix.theme.UixThemeWrapper
import java.lang.Integer.min
import kotlin.math.ceil
import kotlin.math.roundToInt
@ -220,17 +221,9 @@ fun AutoFitText(
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun RowScope.SuggestionItem(words: SuggestedWords, idx: Int, isPrimary: Boolean, onClick: () -> Unit, onLongClick: () -> Unit) {
val word = try {
words.getWord(idx)
} catch(e: IndexOutOfBoundsException) {
null
}
val wordInfo = try {
words.getInfo(idx)
} catch(e: IndexOutOfBoundsException) {
null
}
val wordInfo = words.getInfoOrNull(idx)
val isVerbatim = wordInfo?.kind == KIND_TYPED
val word = wordInfo?.mWord
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) {
if (word != null) {
AutoFitText(word, style = textStyle, modifier = textModifier
.align(Center)
.padding(2.dp))
val modifier = textModifier.align(Center).padding(2.dp)
if(isVerbatim) {
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
val ORDER_OF_SUGGESTIONS = listOf(1, 0, 2)
/** Other words, sorted by likelihood */
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
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) {
Spacer(modifier = Modifier.weight(1.0f))
return
}
if(maxSuggestions == 1 || words.mInputStyle == SuggestedWords.INPUT_STYLE_UPDATE_BATCH) {
SuggestionItem(
words,
0,
isPrimary = true,
onClick = { onClick(0) },
onLongClick = { onLongClick(0) }
)
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
val suggestionItem = @Composable { suggestion: SuggestedWordInfo? ->
if(suggestion != null) {
val idx = words.indexOf(suggestion)
SuggestionItem(
words,
idx,
isPrimary = idx == SuggestedWords.INDEX_OF_AUTO_CORRECTION,
onClick = { onClick(idx) },
onLongClick = { onLongClick(idx) }
)
} else {
Spacer(Modifier.weight(1.0f))
}
} catch(_: IndexOutOfBoundsException) {
}
// Check for "clueless" suggestions, and display typed word in center if so
try {
if(offset == 1) {
val info = words.getInfo(1)
if(info.mOriginatesFromTransformerLM && info.mScore < -50) {
offset = 0;
println(layout)
when {
layout.isGestureBatch ||
layout.presentableSuggestions.size <= 1 -> suggestionItem(layout.presentableSuggestions.firstOrNull())
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) {
}
val suggestionOrder = mutableListOf(
ORDER_OF_SUGGESTIONS[0] + offset,
ORDER_OF_SUGGESTIONS[1] + offset,
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
else -> {
var supplementalSuggestionIndex = 1
if(layout.emojiMatches.isEmpty()) {
suggestionItem(layout.sortedMatches.getOrNull(supplementalSuggestionIndex++))
} else {
suggestionItem(layout.emojiMatches[0])
}
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.Modifier
import androidx.compose.ui.draw.scale
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity
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.KeyboardLayoutSetV2Params
import org.futo.inputmethod.v2keyboard.LayoutManager
import org.futo.inputmethod.v2keyboard.RegularKeyboardSize
import java.util.Locale
import kotlin.math.roundToInt
@ -76,23 +76,8 @@ fun KeyboardLayoutPreview(id: String, width: Dp = 172.dp, locale: Locale? = null
}
}
val configuration = LocalConfiguration.current
val isLandscape = false//configuration.orientation == Configuration.ORIENTATION_LANDSCAPE
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 widthPx: Int = (320.0 * context.resources.displayMetrics.density).roundToInt()
val heightPx: Int = (200.0 * context.resources.displayMetrics.density).roundToInt()
val keyboard = remember { mutableStateOf<Keyboard?>(null) }
@ -107,16 +92,12 @@ fun KeyboardLayoutPreview(id: String, width: Dp = 172.dp, locale: Locale? = null
val layoutSet = KeyboardLayoutSetV2(
context,
KeyboardLayoutSetV2Params(
width = widthPx,
height = heightPx,
padding = Rect(),
computedSize = RegularKeyboardSize(width = widthPx, height = heightPx, padding = Rect()),
gap = 4.0f,
keyboardLayoutSet = id,
locale = loc ?: Locale.ENGLISH,
editorInfo = editorInfo,
numberRow = numberRow,
useSplitLayout = isLandscape,
splitLayoutWidth = widthPx * 2 / 3,
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.Context
import android.content.Intent
import android.graphics.Rect
import android.net.Uri
import android.os.Build
import android.os.VibrationEffect
@ -23,22 +24,33 @@ import androidx.compose.animation.fadeOut
import androidx.compose.animation.slideInVertically
import androidx.compose.animation.slideOutVertically
import androidx.compose.foundation.background
import androidx.compose.foundation.gestures.detectDragGestures
import androidx.compose.foundation.gestures.detectTapGestures
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxScope
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
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.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
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.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.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.contentColorFor
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.LaunchedEffect
@ -46,17 +58,24 @@ import androidx.compose.runtime.MutableState
import androidx.compose.runtime.compositionLocalOf
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.staticCompositionLocalOf
import androidx.compose.ui.Alignment
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.RectangleShape
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.layout.onSizeChanged
import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalLayoutDirection
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.dp
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.SuggestedWords
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.inputlogic.InputLogic
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.openManualUpdateCheck
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
val LocalManager = staticCompositionLocalOf<KeyboardManagerForAction> {
@ -279,7 +308,7 @@ class UixActionKeyboardManager(val uixManager: UixManager, val latinIME: LatinIM
override fun announce(s: String) {
AccessibilityUtils.init(getContext())
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
}
override fun getSizingCalculator(): KeyboardSizingCalculator =
latinIME.sizingCalculator
override fun getLatinIMEForDebug(): LatinIME = latinIME
override fun getSuggestionBlacklist(): SuggestionBlacklist = latinIME.suggestionBlacklist
}
data class ActiveDialogRequest(
@ -305,11 +339,12 @@ data class ActiveDialogRequest(
)
class UixManager(private val latinIME: LatinIME) {
internal val composeView: ComposeView?
get() = latinIME.composeView
private var shouldShowSuggestionStrip: Boolean = true
private var suggestedWords: SuggestedWords? = null
private var composeView: ComposeView? = null
private var currWindowAction: Action? = null
private var persistentStates: HashMap<Action, PersistentActionState?> = hashMapOf()
@ -327,6 +362,9 @@ class UixManager(private val latinIME: LatinIME) {
latinIME.deferSetSetting(ActionBarExpanded, isActionsExpanded.value)
}
val actionsExpanded: Boolean
get() = isActionsExpanded.value
private var isShowingActionEditor = mutableStateOf(false)
fun showActionEditor() {
isShowingActionEditor.value = true
@ -337,6 +375,12 @@ class UixManager(private val latinIME: LatinIME) {
var isInputOverridden = mutableStateOf(false)
var currWindowActionWindow: ActionWindow? = null
val isActionWindowDocked: Boolean
get() = currWindowActionWindow != null
private var measuredTouchableHeight = 0
val touchableHeight: Int
get() = measuredTouchableHeight
val isMainKeyboardHidden get() = mainKeyboardHidden
@ -626,46 +670,249 @@ class UixManager(private val latinIME: LatinIME) {
)
}
fun setContent() {
composeView?.setContent {
UixThemeWrapper(latinIME.colorScheme) {
DataStoreCacheProvider {
CompositionLocalProvider(LocalManager provides keyboardManagerForAction) {
CompositionLocalProvider(LocalThemeProvider provides latinIME.getDrawableProvider()) {
CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Ltr) {
CompositionLocalProvider(LocalFoldingState provides foldingOptions.value) {
InputDarkener(isInputOverridden.value || isShowingActionEditor.value) {
closeActionWindow()
isShowingActionEditor.value = false
}
@Composable
private fun OffsetPositioner(offset: Offset, content: @Composable () -> Unit) {
Column(modifier = Modifier.fillMaxHeight().absoluteOffset { IntOffset(offset.x.toInt(), 0) }) {
Spacer(Modifier.weight(1.0f))
content()
Spacer(Modifier.height(with(LocalDensity.current) { offset.y.toDp() }))
}
}
Column {
Spacer(modifier = Modifier.weight(1.0f))
Surface(modifier = Modifier.onSizeChanged {
latinIME.updateTouchableHeight(it.height)
}, color = latinIME.keyboardColor) {
Box {
Column {
when {
currWindowActionWindow != null -> ActionViewWithHeader(
currWindowActionWindow!!
)
@Composable
private fun KeyboardSurface(
requiredWidthPx: Int,
backgroundColor: Color,
modifier: Modifier = Modifier,
shape: Shape = RectangleShape,
padding: Rect = Rect(),
content: @Composable BoxScope.() -> Unit
) = with(LocalDensity.current) {
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() {
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() {
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,
"select_all" to SelectAllAction,
"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 }

View File

@ -240,7 +240,7 @@ public class LanguageModelFacilitator(
try {
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()
return
}

View File

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

View File

@ -2,12 +2,27 @@ package org.futo.inputmethod.v2keyboard
import android.content.Context
import android.graphics.Rect
import androidx.datastore.preferences.core.booleanPreferencesKey
import androidx.datastore.preferences.core.floatPreferencesKey
import androidx.datastore.preferences.core.stringPreferencesKey
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.LatinIME
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.setSettingBlocking
import org.futo.inputmethod.latin.utils.ResourceUtils
import kotlin.math.roundToInt
@ -17,13 +32,140 @@ interface KeyboardSizeStateProvider {
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()
//class FloatingKeyboardSize(val x: Int, val y: Int, val width: Int, val height: Int): ComputedKeyboardSize()
enum class OneHandedDirection {
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 {
Portrait,
@ -31,117 +173,230 @@ enum class KeyboardSizeSettingKind {
FoldableInnerDisplay
}
val SplitKeyboardSettings = mapOf(
KeyboardSizeSettingKind.Portrait to SettingsKey(
booleanPreferencesKey("split_keyboard_portrait"), false),
KeyboardSizeSettingKind.Landscape to SettingsKey(
booleanPreferencesKey("split_keyboard_landscape"), true),
KeyboardSizeSettingKind.FoldableInnerDisplay to SettingsKey(
booleanPreferencesKey("split_keyboard_fold"), true),
val DefaultKeyboardSettings = mapOf(
KeyboardSizeSettingKind.Portrait to SavedKeyboardSizingSettings(
currentMode = KeyboardMode.Regular,
heightMultiplier = 1.0f,
paddingDp = Rect(2, 4, 2, 10),
splitWidthFraction = 4.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.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(
floatPreferencesKey("keyboardHeightMultiplier"), 1.0f),
stringPreferencesKey("keyboard_settings_portrait"), ""),
KeyboardSizeSettingKind.Landscape to SettingsKey(
floatPreferencesKey("keyboard_height_landscape"), 0.9f),
stringPreferencesKey("keyboard_settings_landscape"), ""),
KeyboardSizeSettingKind.FoldableInnerDisplay to SettingsKey(
floatPreferencesKey("keyboard_height_fold"), 0.67f),
stringPreferencesKey("keyboard_settings_fold"), ""),
)
val KeyboardOffsetSettings = mapOf(
KeyboardSizeSettingKind.Portrait to SettingsKey(
floatPreferencesKey("keyboard_offset_portrait"), 8.0f),
KeyboardSizeSettingKind.Landscape to SettingsKey(
floatPreferencesKey("keyboard_offset_landscape"), 0.0f),
KeyboardSizeSettingKind.FoldableInnerDisplay to SettingsKey(
floatPreferencesKey("keyboard_offset_fold"), 8.0f),
)
class KeyboardSizingCalculator(val context: Context, val uixManager: UixManager) {
val sizeStateProvider = context as KeyboardSizeStateProvider
val foldStateProvider = context as FoldStateProvider
val KeyboardSideInsetSettings = mapOf(
KeyboardSizeSettingKind.Portrait to SettingsKey(
floatPreferencesKey("keyboard_inset_portrait"), 2.0f),
KeyboardSizeSettingKind.Landscape to SettingsKey(
floatPreferencesKey("keyboard_inset_landscape"), 8.0f),
KeyboardSizeSettingKind.FoldableInnerDisplay to SettingsKey(
floatPreferencesKey("keyboard_inset_fold"), 44.0f),
)
private fun dp(v: Number): Int =
(v.toFloat() * context.resources.displayMetrics.density).toInt()
private fun dp(v: Rect): Rect =
Rect(dp(v.left), dp(v.top), dp(v.right), dp(v.bottom))
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) {
private val sizeStateProvider = context as KeyboardSizeStateProvider
private val foldStateProvider = context as FoldStateProvider
val originX = rectPx.left
val originY = rectPx.top
private fun isSplitKeyboard(mode: KeyboardSizeSettingKind): Boolean =
context.getSettingBlocking(SplitKeyboardSettings[mode]!!)
if(originX < 0){
rectPx.left -= originX
rectPx.right -= originX
}
private fun heightMultiplier(mode: KeyboardSizeSettingKind): Float =
context.getSettingBlocking(KeyboardHeightSettings[mode]!!)
if(originY < 0) {
rectPx.top -= originY
rectPx.bottom -= originY
}
private fun bottomOffsetPx(mode: KeyboardSizeSettingKind): Int =
(context.getSettingBlocking(KeyboardOffsetSettings[mode]!!) * context.resources.displayMetrics.density).toInt()
if(rectPx.right > context.resources.displayMetrics.widthPixels) {
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 =
(context.getSettingBlocking(KeyboardSideInsetSettings[mode]!!) * context.resources.displayMetrics.density).toInt()
return rectPx
}
private fun topPaddingPx(mode: KeyboardSizeSettingKind): Int =
(when(mode) {
KeyboardSizeSettingKind.Portrait -> 4.0f
KeyboardSizeSettingKind.Landscape -> 0.0f
KeyboardSizeSettingKind.FoldableInnerDisplay -> 8.0f
} * context.resources.displayMetrics.density).toInt()
fun getSavedSettings(): SavedKeyboardSizingSettings =
SavedKeyboardSizingSettings.fromJsonString(context.getSettingBlocking(
KeyboardSettings[sizeStateProvider.currentSizeState]!!
)) ?: DefaultKeyboardSettings[sizeStateProvider.currentSizeState]!!
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 {
val savedSettings = getSavedSettings()
val layout = LayoutManager.getLayout(context, layoutName)
val effectiveRowCount = layout.effectiveRows.size
val configuration = context.resources.configuration
val displayMetrics = context.resources.displayMetrics
val mode = sizeStateProvider.currentSizeState
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 singularRowHeight = (ResourceUtils.getDefaultKeyboardHeight(context.resources) / 4.0) *
savedSettings.heightMultiplier
val numRows = 4.0 +
((effectiveRowCount - 5) / 2.0).coerceAtLeast(0.0) +
if(isNumberRowActive) { 0.5 } else { 0.0 }
println("Num rows; $numRows, $effectiveRowCount ($layoutName) ($layout)")
val recommendedHeight = numRows * singularRowHeight
val foldState = foldStateProvider.foldState.feature
val window = (context as LatinIME).window.window
val width = ResourceUtils.getDefaultKeyboardWidth(window, context.resources)
return when {
// 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 ->
SplitKeyboardSize(
displayMetrics.heightPixels / 2 - (displayMetrics.density * 80.0f).toInt(),
Rect(
height = displayMetrics.heightPixels / 2 - (displayMetrics.density * 80.0f).toInt(),
width = width,
padding = Rect(
(displayMetrics.density * 44.0f).roundToInt(),
(displayMetrics.density * 20.0f).roundToInt(),
(displayMetrics.density * 50.0f).roundToInt(),
(displayMetrics.density * 44.0f).roundToInt(),
(displayMetrics.density * 12.0f).roundToInt(),
),
displayMetrics.widthPixels * 3 / 5
splitLayoutWidth = displayMetrics.widthPixels * 3 / 5
)
isSplit -> SplitKeyboardSize(
recommendedHeight.roundToInt(),
Rect(sideInset, topPadding, sideInset, bottomOffset),
displayMetrics.widthPixels * 3 / 5)
savedSettings.currentMode == KeyboardMode.Split ->
SplitKeyboardSize(
height = recommendedHeight.roundToInt(),
width = width,
padding = dp(savedSettings.paddingDp),
splitLayoutWidth = (displayMetrics.widthPixels * savedSettings.splitWidthFraction).toInt()
)
else -> RegularKeyboardSize(
recommendedHeight.roundToInt(),
Rect(sideInset, topPadding, sideInset, bottomOffset),
)
savedSettings.currentMode == KeyboardMode.OneHanded ->
OneHandedKeyboardSize(
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)
}
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(
val size: ComputedKeyboardSize,
val gap: Dp,
val useSplitLayout: Boolean,
val splitLayoutWidth: Int,
val padding: Rect,
val standardRowHeight: Double,
val element: KeyboardLayoutElement,
)
@ -131,8 +129,6 @@ data class LayoutEngine(
}
private fun computeRowHeight(): Double {
//val normalKeyboardHeight = ((rowHeight.value + verticalGap.value) * density) * 3
val normalKeyboardHeight = totalRowHeight
// 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.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) {
layoutParams.splitLayoutWidth
(layoutParams.size as SplitKeyboardSize).splitLayoutWidth
} else if(isOneHandedLayout) {
(layoutParams.size as OneHandedKeyboardSize).layoutWidth
} else {
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 regularKeyWidth = computeRegularKeyWidth()
@ -551,10 +557,10 @@ data class LayoutEngine(
}
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) {
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)
}
@ -570,7 +576,7 @@ data class LayoutEngine(
}
private fun addKeys(rows: List<LayoutRow>): Int {
var currentY = 0.0f + layoutParams.padding.top
var currentY = 0.0f + padding.top
rows.forEach { row ->
addRow(row, currentY.toInt())
currentY += row.height
@ -588,14 +594,14 @@ data class LayoutEngine(
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.mOccupiedWidth = params.mId.mWidth + layoutParams.padding.left + layoutParams.padding.right
params.mTopPadding = 0//layoutParams.padding.top
params.mBottomPadding = 0//layoutParams.padding.bottom
params.mLeftPadding = 0//layoutParams.padding.left
params.mRightPadding = 0//layoutParams.padding.right
params.mOccupiedWidth = params.mId.mWidth + padding.left + padding.right
params.mTopPadding = 0
params.mBottomPadding = 0
params.mLeftPadding = 0
params.mRightPadding = 0
params.mBaseWidth = params.mOccupiedWidth
params.mDefaultKeyWidth = regularKeyWidth.roundToInt()

View File

@ -33,7 +33,7 @@ object LayoutManager {
private fun getAllLayoutPaths(assetManager: AssetManager): List<String> {
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 ""
val letter = row.getOrNull(centeredCol)
if(letter == 'ñ') {
println("It's ñ")
}
return if(letter != null) {
"!text/qwertysyms_$letter"
} else {