Add ability to expand keyboard during action

This commit is contained in:
Aleksandras Kostarevas 2024-01-09 17:26:05 +02:00
parent fd905bcabd
commit 25e66ec447
10 changed files with 292 additions and 134 deletions

View File

@ -0,0 +1,13 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M6,9l6,6l6,-6"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="#ffffff"
android:strokeLineCap="round"/>
</vector>

View File

@ -0,0 +1,20 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M18,6L6,18"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="#ffffff"
android:strokeLineCap="round"/>
<path
android:pathData="M6,6L18,18"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="#ffffff"
android:strokeLineCap="round"/>
</vector>

View File

@ -15,6 +15,7 @@ import androidx.annotation.RequiresApi
import androidx.compose.foundation.layout.size
import androidx.compose.material3.ColorScheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.key
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clipToBounds
@ -169,7 +170,7 @@ class LatinIME : InputMethodService(), LifecycleOwner, ViewModelStoreOwner, Save
updateDrawableProvider(newTheme.obtainColors(this))
deferSetSetting(THEME_KEY, newTheme.key)
if(!uixManager.isActionWindowOpen) {
if(!uixManager.isMainKeyboardHidden) {
recreateKeyboard()
} else {
pendingRecreateKeyboard = true
@ -180,10 +181,10 @@ class LatinIME : InputMethodService(), LifecycleOwner, ViewModelStoreOwner, Save
// Called by UixManager when the intention is to subsequently call LegacyKeyboardView with hidden=false
// Maybe this can be changed to LaunchedEffect
fun onKeyboardShown() {
if(pendingRecreateKeyboard) {
pendingRecreateKeyboard = false
recreateKeyboard()
}
//if(pendingRecreateKeyboard) {
// pendingRecreateKeyboard = false
// recreateKeyboard()
//}
}
@ -252,6 +253,17 @@ class LatinIME : InputMethodService(), LifecycleOwner, ViewModelStoreOwner, Save
// shown, but resized to 0 if an action window is open
@Composable
internal fun LegacyKeyboardView(hidden: Boolean) {
LaunchedEffect(hidden) {
if(hidden) {
latinIMELegacy.mKeyboardSwitcher.saveKeyboardState()
} else {
if(pendingRecreateKeyboard) {
pendingRecreateKeyboard = false
recreateKeyboard()
}
}
}
val modifier = if(hidden) {
Modifier
.clipToBounds()

View File

@ -39,7 +39,7 @@ interface ActionWindow {
fun windowName(): String
@Composable
fun WindowContents()
fun WindowContents(keyboardShown: Boolean)
fun close()
}
@ -57,6 +57,8 @@ interface PersistentActionState {
data class Action(
@DrawableRes val icon: Int,
@StringRes val name: Int,
val canShowKeyboard: Boolean = false,
val windowImpl: ((KeyboardManagerForAction, PersistentActionState?) -> ActionWindow)?,
val simplePressImpl: ((KeyboardManagerForAction, PersistentActionState?) -> Unit)?,
val persistentState: ((KeyboardManagerForAction) -> PersistentActionState)? = null,

View File

@ -21,6 +21,7 @@ import androidx.compose.material3.IconButton
import androidx.compose.material3.IconButtonDefaults
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.dynamicDarkColorScheme
import androidx.compose.material3.dynamicLightColorScheme
@ -69,6 +70,7 @@ import org.futo.inputmethod.latin.uix.actions.ThemeAction
import org.futo.inputmethod.latin.uix.actions.UndoAction
import org.futo.inputmethod.latin.uix.actions.VoiceInputAction
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
@ -413,6 +415,107 @@ fun ActionBar(
}
}
@Composable
fun ActionWindowBar(
windowName: String,
canExpand: Boolean,
onBack: () -> Unit,
onExpand: () -> Unit
) {
Surface(
modifier = Modifier
.fillMaxWidth()
.height(40.dp), color = MaterialTheme.colorScheme.background
)
{
Row {
IconButton(onClick = onBack) {
Icon(
painter = painterResource(id = R.drawable.arrow_left_26),
contentDescription = "Back"
)
}
Text(
windowName,
style = Typography.titleMedium,
modifier = Modifier.align(CenterVertically)
)
Spacer(modifier = Modifier.weight(1.0f))
if(canExpand) {
IconButton(onClick = onExpand) {
Icon(
painter = painterResource(id = R.drawable.arrow_up),
contentDescription = "Show Keyboard"
)
}
}
}
}
}
@Composable
fun CollapsibleSuggestionsBar(
onClose: () -> Unit,
onCollapse: () -> Unit,
words: SuggestedWords?,
suggestionStripListener: SuggestionStripView.Listener,
inlineSuggestions: List<MutableState<View?>>,
) {
Surface(modifier = Modifier
.fillMaxWidth()
.height(40.dp), color = MaterialTheme.colorScheme.background)
{
Row {
val color = MaterialTheme.colorScheme.primary
IconButton(
onClick = onClose,
modifier = Modifier
.width(42.dp)
.fillMaxHeight()
.drawBehind {
drawCircle(color = color, radius = size.width / 3.0f + 1.0f)
},
colors = IconButtonDefaults.iconButtonColors(contentColor = MaterialTheme.colorScheme.onPrimary)
) {
Icon(
painter = painterResource(id = R.drawable.close),
contentDescription = "Close"
)
}
if(inlineSuggestions.isNotEmpty() && Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
InlineSuggestions(inlineSuggestions)
} else if(words != null) {
SuggestionItems(words) {
suggestionStripListener.pickSuggestionManually(
words.getInfo(it)
)
}
} else {
Spacer(modifier = Modifier.weight(1.0f))
}
IconButton(
onClick = onCollapse,
modifier = Modifier
.width(42.dp)
.fillMaxHeight(),
colors = IconButtonDefaults.iconButtonColors(contentColor = MaterialTheme.colorScheme.onBackground)
) {
Icon(
painter = painterResource(id = R.drawable.arrow_down),
contentDescription = "Collapse"
)
}
}
}
}
@ -500,6 +603,18 @@ fun PreviewExpandedActionBar(colorScheme: ColorScheme = DarkColorScheme) {
}
}
@Composable
@Preview
fun PreviewCollapsibleBar(colorScheme: ColorScheme = DarkColorScheme) {
CollapsibleSuggestionsBar(
onCollapse = { },
onClose = { },
words = exampleSuggestedWords,
suggestionStripListener = ExampleListener(),
inlineSuggestions = listOf()
)
}
@Composable
@Preview

View File

@ -7,36 +7,26 @@ import android.view.inputmethod.InlineSuggestionsResponse
import androidx.annotation.RequiresApi
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.onSizeChanged
import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.ViewCompositionStrategy
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp
import androidx.lifecycle.LifecycleCoroutineScope
import androidx.lifecycle.lifecycleScope
import kotlinx.coroutines.launch
import org.futo.inputmethod.latin.LatinIME
import org.futo.inputmethod.latin.R
import org.futo.inputmethod.latin.SuggestedWords
import org.futo.inputmethod.latin.common.Constants
import org.futo.inputmethod.latin.inputlogic.InputLogic
import org.futo.inputmethod.latin.suggestions.SuggestionStripView
import org.futo.inputmethod.latin.uix.theme.ThemeOption
import org.futo.inputmethod.latin.uix.theme.Typography
import org.futo.inputmethod.latin.uix.theme.UixThemeWrapper
private class LatinIMEActionInputTransaction(
@ -141,8 +131,11 @@ class UixManager(private val latinIME: LatinIME) {
private var inlineSuggestions: List<MutableState<View?>> = listOf()
private val keyboardManagerForAction = UixActionKeyboardManager(this, latinIME)
private var mainKeyboardHidden = false
var currWindowActionWindow: ActionWindow? = null
val isActionWindowOpen get() = currWindowActionWindow != null
val isMainKeyboardHidden get() = mainKeyboardHidden
private fun onActionActivated(action: Action) {
latinIME.inputLogic.finishInput()
@ -178,7 +171,7 @@ class UixManager(private val latinIME: LatinIME) {
private fun enterActionWindowView(action: Action) {
assert(action.windowImpl != null)
//latinIMELegacy.mKeyboardSwitcher.saveKeyboardState()
mainKeyboardHidden = true
currWindowAction = action
@ -199,43 +192,62 @@ class UixManager(private val latinIME: LatinIME) {
currWindowAction = null
currWindowActionWindow = null
mainKeyboardHidden = false
latinIME.onKeyboardShown()
setContent()
}
private fun toggleExpandAction() {
mainKeyboardHidden = !mainKeyboardHidden
if(!mainKeyboardHidden) {
latinIME.onKeyboardShown()
}
setContent()
}
@Composable
private fun ActionViewWithHeader(windowImpl: ActionWindow) {
val heightDiv = if(mainKeyboardHidden) {
1
} else {
1.5
}
Column {
Surface(
modifier = Modifier
.fillMaxWidth()
.height(40.dp), color = MaterialTheme.colorScheme.background
)
{
Row {
IconButton(onClick = {
returnBackToMainKeyboardViewFromAction()
}) {
Icon(
painter = painterResource(id = R.drawable.arrow_left_26),
contentDescription = "Back"
)
}
Text(
windowImpl.windowName(),
style = Typography.titleMedium,
modifier = Modifier.align(Alignment.CenterVertically)
)
}
if(mainKeyboardHidden) {
ActionWindowBar(
onBack = { returnBackToMainKeyboardViewFromAction() },
canExpand = currWindowAction!!.canShowKeyboard,
onExpand = { toggleExpandAction() },
windowName = windowImpl.windowName()
)
}
Box(modifier = Modifier
.fillMaxWidth()
.height(with(LocalDensity.current) { latinIME.getInputViewHeight().toDp() })
.height(with(LocalDensity.current) {
(latinIME.getInputViewHeight().toFloat() / heightDiv.toFloat()).toDp()
})
) {
windowImpl.WindowContents()
windowImpl.WindowContents(keyboardShown = !isMainKeyboardHidden)
}
if(!mainKeyboardHidden) {
val suggestedWordsOrNull = if (shouldShowSuggestionStrip) {
suggestedWords
} else {
null
}
CollapsibleSuggestionsBar(
onCollapse = { toggleExpandAction() },
onClose = { returnBackToMainKeyboardViewFromAction() },
words = suggestedWordsOrNull,
suggestionStripListener = latinIME.latinIMELegacy as SuggestionStripView.Listener,
inlineSuggestions = inlineSuggestions
)
}
}
}
@ -250,14 +262,14 @@ class UixManager(private val latinIME: LatinIME) {
}) {
Column {
when {
isActionWindowOpen -> ActionViewWithHeader(
currWindowActionWindow != null -> ActionViewWithHeader(
currWindowActionWindow!!
)
else -> MainKeyboardViewWithActionBar()
}
latinIME.LegacyKeyboardView(hidden = isActionWindowOpen)
latinIME.LegacyKeyboardView(hidden = isMainKeyboardHidden)
}
}
}

View File

@ -162,7 +162,7 @@ data class BitmapRecycler(
}
@Composable
fun EmojiGrid(onClick: (EmojiItem) -> Unit, onExit: () -> Unit, onBackspace: () -> Unit, onSpace: () -> Unit, bitmaps: BitmapRecycler, emojis: List<EmojiItem>) {
fun EmojiGrid(onClick: (EmojiItem) -> Unit, onExit: () -> Unit, onBackspace: () -> Unit, onSpace: () -> Unit, bitmaps: BitmapRecycler, emojis: List<EmojiItem>, keyboardShown: Boolean) {
val context = LocalContext.current
val spToDp = context.resources.displayMetrics.scaledDensity / context.resources.displayMetrics.density
@ -182,41 +182,48 @@ fun EmojiGrid(onClick: (EmojiItem) -> Unit, onExit: () -> Unit, onBackspace: ()
}
}
}
Surface(color = MaterialTheme.colorScheme.background, modifier = Modifier
.fillMaxWidth()
.height(48.dp)) {
Row(modifier = Modifier.padding(2.dp, 8.dp, 2.dp, 0.dp)) {
IconButton(onClick = { onExit() }) {
Text("ABC", fontSize = 14.sp)
}
Button(onClick = { onSpace() }, modifier = Modifier
.weight(1.0f)
.padding(8.dp, 2.dp), colors = ButtonDefaults.buttonColors(
containerColor = MaterialTheme.colorScheme.outline.copy(alpha = 0.33f),
contentColor = MaterialTheme.colorScheme.onBackground,
disabledContainerColor = MaterialTheme.colorScheme.outline,
disabledContentColor = MaterialTheme.colorScheme.onBackground,
), shape = RoundedCornerShape(32.dp)) {
Text("")
}
if(!keyboardShown) {
Surface(
color = MaterialTheme.colorScheme.background, modifier = Modifier
.fillMaxWidth()
.height(48.dp)
) {
Row(modifier = Modifier.padding(2.dp, 8.dp, 2.dp, 0.dp)) {
IconButton(onClick = { onExit() }) {
Text("ABC", fontSize = 14.sp)
}
IconButton(onClick = { onBackspace() }) {
val icon = painterResource(id = R.drawable.delete)
val iconColor = MaterialTheme.colorScheme.onBackground
Button(
onClick = { onSpace() }, modifier = Modifier
.weight(1.0f)
.padding(8.dp, 2.dp), colors = ButtonDefaults.buttonColors(
containerColor = MaterialTheme.colorScheme.outline.copy(alpha = 0.33f),
contentColor = MaterialTheme.colorScheme.onBackground,
disabledContainerColor = MaterialTheme.colorScheme.outline,
disabledContentColor = MaterialTheme.colorScheme.onBackground,
), shape = RoundedCornerShape(32.dp)
) {
Text("")
}
Canvas(modifier = Modifier.fillMaxSize()) {
translate(
left = this.size.width / 2.0f - icon.intrinsicSize.width / 2.0f,
top = this.size.height / 2.0f - icon.intrinsicSize.height / 2.0f
) {
with(icon) {
draw(
icon.intrinsicSize,
colorFilter = androidx.compose.ui.graphics.ColorFilter.tint(
iconColor
IconButton(onClick = { onBackspace() }) {
val icon = painterResource(id = R.drawable.delete)
val iconColor = MaterialTheme.colorScheme.onBackground
Canvas(modifier = Modifier.fillMaxSize()) {
translate(
left = this.size.width / 2.0f - icon.intrinsicSize.width / 2.0f,
top = this.size.height / 2.0f - icon.intrinsicSize.height / 2.0f
) {
with(icon) {
draw(
icon.intrinsicSize,
colorFilter = androidx.compose.ui.graphics.ColorFilter.tint(
iconColor
)
)
)
}
}
}
}
@ -268,6 +275,7 @@ class PersistentEmojiState: PersistentActionState {
val EmojiAction = Action(
icon = R.drawable.smile,
name = R.string.emoji_action_title,
canShowKeyboard = true,
simplePressImpl = null,
persistentState = { manager ->
val state = PersistentEmojiState()
@ -287,7 +295,7 @@ val EmojiAction = Action(
}
@Composable
override fun WindowContents() {
override fun WindowContents(keyboardShown: Boolean) {
state.emojis.value?.let { emojis ->
EmojiGrid(onClick = {
manager.typeText(it.emoji)
@ -297,7 +305,7 @@ val EmojiAction = Action(
manager.sendCodePointEvent(Constants.CODE_SPACE)
}, onBackspace = {
manager.sendCodePointEvent(Constants.CODE_DELETE)
}, bitmaps = state.bitmaps, emojis = emojis)
}, bitmaps = state.bitmaps, emojis = emojis, keyboardShown = keyboardShown)
}
}

View File

@ -218,7 +218,7 @@ fun CtrlShiftMetaKeys(modifier: Modifier, ctrlState: MutableState<Boolean>, shif
}
@Composable
fun SideKeys(modifier: Modifier, onEvent: (Int, Int) -> Unit, onCodePoint: (Int) -> Unit) {
fun SideKeys(modifier: Modifier, onEvent: (Int, Int) -> Unit, onCodePoint: (Int) -> Unit, keyboardShown: Boolean) {
Column(modifier = modifier) {
ActionKey(
modifier = Modifier
@ -248,18 +248,20 @@ fun SideKeys(modifier: Modifier, onEvent: (Int, Int) -> Unit, onCodePoint: (Int)
)
}
ActionKey(
modifier = Modifier
.weight(1.0f)
.fillMaxWidth(),
repeatable = true,
color = MaterialTheme.colorScheme.primaryContainer,
onTrigger = { onCodePoint(Constants.CODE_DELETE) }
) {
IconWithColor(
iconId = R.drawable.delete,
iconColor = MaterialTheme.colorScheme.onPrimaryContainer
)
if(!keyboardShown) {
ActionKey(
modifier = Modifier
.weight(1.0f)
.fillMaxWidth(),
repeatable = true,
color = MaterialTheme.colorScheme.primaryContainer,
onTrigger = { onCodePoint(Constants.CODE_DELETE) }
) {
IconWithColor(
iconId = R.drawable.delete,
iconColor = MaterialTheme.colorScheme.onPrimaryContainer
)
}
}
@ -298,7 +300,7 @@ fun SideKeys(modifier: Modifier, onEvent: (Int, Int) -> Unit, onCodePoint: (Int)
}
@Composable
fun TextEditScreen(onCodePoint: (Int) -> Unit, onEvent: (Int, Int) -> Unit) {
fun TextEditScreen(onCodePoint: (Int) -> Unit, onEvent: (Int, Int) -> Unit, keyboardShown: Boolean) {
val shiftState = remember { mutableStateOf(false) }
val ctrlState = remember { mutableStateOf(false) }
@ -331,7 +333,8 @@ fun TextEditScreen(onCodePoint: (Int) -> Unit, onEvent: (Int, Int) -> Unit) {
.fillMaxHeight()
.weight(1.0f),
onEvent = onEvent,
onCodePoint = onCodePoint
onCodePoint = onCodePoint,
keyboardShown = keyboardShown
)
}
}
@ -341,6 +344,7 @@ val TextEditAction = Action(
name = R.string.text_edit_action_title,
simplePressImpl = null,
persistentState = null,
canShowKeyboard = true,
windowImpl = { manager, persistentState ->
object : ActionWindow {
@Composable
@ -349,8 +353,8 @@ val TextEditAction = Action(
}
@Composable
override fun WindowContents() {
TextEditScreen(onCodePoint = { a -> manager.sendCodePointEvent(a)}, onEvent = { a, b -> manager.sendKeyEvent(a, b) })
override fun WindowContents(keyboardShown: Boolean) {
TextEditScreen(onCodePoint = { a -> manager.sendCodePointEvent(a)}, onEvent = { a, b -> manager.sendKeyEvent(a, b) }, keyboardShown = keyboardShown)
}
override fun close() {
@ -363,6 +367,13 @@ val TextEditAction = Action(
@Preview(showBackground = true)
fun TextEditScreenPreview() {
Surface(modifier = Modifier.height(256.dp)) {
TextEditScreen(onCodePoint = { }, onEvent = { _, _ -> })
TextEditScreen(onCodePoint = { }, onEvent = { _, _ -> }, keyboardShown = false)
}
}
@Composable
@Preview(showBackground = true)
fun TextEditScreenPreviewWithKb() {
Surface(modifier = Modifier.height(256.dp)) {
TextEditScreen(onCodePoint = { }, onEvent = { _, _ -> }, keyboardShown = true)
}
}

View File

@ -1,27 +1,17 @@
package org.futo.inputmethod.latin.uix.actions
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.material3.Button
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import org.futo.inputmethod.latin.R
import org.futo.inputmethod.latin.uix.Action
import org.futo.inputmethod.latin.uix.ActionWindow
import org.futo.inputmethod.latin.uix.KeyboardManagerForAction
import org.futo.inputmethod.latin.uix.theme.ThemeOptionKeys
import org.futo.inputmethod.latin.uix.theme.ThemeOptions
import org.futo.inputmethod.latin.uix.theme.selector.ThemePicker
val ThemeAction = Action(
icon = R.drawable.eye,
name = R.string.theme_switcher_action_title,
simplePressImpl = null,
canShowKeyboard = true,
windowImpl = { manager, _ ->
object : ActionWindow {
@Composable
@ -30,33 +20,8 @@ val ThemeAction = Action(
}
@Composable
override fun WindowContents() {
val context = LocalContext.current
override fun WindowContents(keyboardShown: Boolean) {
ThemePicker { manager.updateTheme(it) }
/*
LazyColumn(
modifier = Modifier
.padding(8.dp, 0.dp)
.fillMaxWidth()
)
{
items(ThemeOptionKeys.count()) {
val key = ThemeOptionKeys[it]
val themeOption = ThemeOptions[key]
if (themeOption != null && themeOption.available(context)) {
Button(onClick = {
manager.updateTheme(
themeOption
)
}) {
Text(stringResource(themeOption.name))
}
}
}
}
*/
}
override fun close() {

View File

@ -180,7 +180,7 @@ private class VoiceInputActionWindow(
}
@Composable
override fun WindowContents() {
override fun WindowContents(keyboardShown: Boolean) {
Box(modifier = Modifier
.fillMaxSize()
.clickable(enabled = true,