Implement new ActionBar and editor

This commit is contained in:
Aleksandras Kostarevas 2024-07-20 22:49:10 +03:00
parent e306c29ccd
commit 5aaad74d3c
15 changed files with 876 additions and 274 deletions

View File

@ -222,10 +222,11 @@ dependencies {
implementation 'androidx.datastore:datastore-preferences:1.0.0'
implementation 'androidx.autofill:autofill:1.1.0'
//stableImplementation 'ch.acra:acra-http:5.11.1' // TODO: Remove upon release
stableImplementation 'ch.acra:acra-mail:5.11.1'
stableImplementation 'ch.acra:acra-dialog:5.11.1'
implementation 'sh.calvin.reorderable:reorderable:2.2.0'
implementation 'com.squareup.okhttp3:okhttp:4.11.0'
implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json:1.5.1'
implementation 'org.jetbrains.kotlinx:kotlinx-serialization-json-jvm:1.5.1'

View File

@ -0,0 +1,27 @@
<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="M12,12m-1,0a1,1 0,1 1,2 0a1,1 0,1 1,-2 0"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="#ffffff"
android:strokeLineCap="round"/>
<path
android:pathData="M19,12m-1,0a1,1 0,1 1,2 0a1,1 0,1 1,-2 0"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="#ffffff"
android:strokeLineCap="round"/>
<path
android:pathData="M5,12m-1,0a1,1 0,1 1,2 0a1,1 0,1 1,-2 0"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="#ffffff"
android:strokeLineCap="round"/>
</vector>

View File

@ -13,6 +13,7 @@
<string name="redo_action_title">Redo</string>
<string name="text_edit_action_title">Text Editor</string>
<string name="mem_debug_action_title">Debug Info</string>
<string name="more_actions_action_title">More Actions</string>
<string name="amoled_dark_theme_name">AMOLED Dark Purple</string>
<string name="classic_material_dark_theme_name">AOSP Material Dark</string>

View File

@ -140,7 +140,6 @@ class LatinIME : InputMethodService(), LifecycleOwner, ViewModelStoreOwner, Save
private fun recreateKeyboard() {
latinIMELegacy.updateTheme()
latinIMELegacy.mKeyboardSwitcher.mState.onLoadKeyboard(latinIMELegacy.currentAutoCapsState, latinIMELegacy.currentRecapitalizeState);
Log.w("LatinIME", "Recreating keyboard")
}
private var isNavigationBarVisible = false
@ -387,6 +386,11 @@ class LatinIME : InputMethodService(), LifecycleOwner, ViewModelStoreOwner, Save
fun updateTouchableHeight(to: Int) { touchableHeight = to }
fun getInputViewHeight(): Int = inputViewHeight
private var isInputModal = false
fun setInputModal(to: Boolean) {
isInputModal = to
}
// The keyboard view really doesn't like being detached, so it's always
// shown, but resized to 0 if an action window is open
@Composable
@ -554,7 +558,7 @@ class LatinIME : InputMethodService(), LifecycleOwner, ViewModelStoreOwner, Save
return
}
val visibleTopY = inputHeight - touchableHeight
val visibleTopY = if(isInputModal) { 0 } else { inputHeight - touchableHeight }
val touchLeft = 0
val touchTop = visibleTopY
@ -730,8 +734,6 @@ class LatinIME : InputMethodService(), LifecycleOwner, ViewModelStoreOwner, Save
latinIMELegacy.loadSettings()
recreateKeyboard()
Log.i("LatinIME", "DEVICE has UNLOCKED!!! Finished reloading: ${Settings.getInstance().current.dump()}")
languageModelFacilitator.loadHistoryLog()
// TODO: Spell checker service

View File

@ -26,7 +26,7 @@ fun LongPressKey.name(context: Context): String {
fun LongPressKey.description(context: Context): String {
return when(this) {
LongPressKey.Numbers -> "e.g. [1] on [q]"
LongPressKey.LanguageKeys -> "e.g. [á] on [a] in Spanish"
LongPressKey.LanguageKeys -> "e.g. [ñ] on [n] in Spanish"
LongPressKey.Symbols -> "e.g. [@] on [a]"
LongPressKey.QuickActions -> "e.g. [Copy] on [c]"
LongPressKey.MiscLetters -> "e.g. [ß] on [s] in all Latin script languages"

View File

@ -103,7 +103,7 @@ public final class Settings implements SharedPreferences.OnSharedPreferenceChang
public static final String PREF_SHOW_ACTION_KEY =
"pref_show_emoji_key";
public static final String PREF_ACTION_KEY_ID = "pref_action_key_id";
public static final String PREF_ACTION_KEY_ID = "pref_action_key_strid";
public static final String PREF_ENABLE_NUMBER_ROW = "pref_enable_number_row";

View File

@ -28,7 +28,7 @@ import android.view.inputmethod.EditorInfo;
import org.futo.inputmethod.compat.AppWorkaroundsUtils;
import org.futo.inputmethod.latin.InputAttributes;
import org.futo.inputmethod.latin.R;
import org.futo.inputmethod.latin.RichInputMethodManager;
import org.futo.inputmethod.latin.uix.actions.ActionRegistry;
import org.futo.inputmethod.latin.utils.AsyncResultHolder;
import org.futo.inputmethod.latin.utils.ResourceUtils;
import org.futo.inputmethod.latin.utils.TargetPackageInfoGetterTask;
@ -147,8 +147,8 @@ public class SettingsValues {
mIncludesOtherImesInLanguageSwitchList = Settings.ENABLE_SHOW_LANGUAGE_SWITCH_KEY_SETTINGS
? prefs.getBoolean(Settings.PREF_INCLUDE_OTHER_IMES_IN_LANGUAGE_SWITCH_LIST, false)
: true /* forcibly */;
mShowsActionKey = prefs.getBoolean(Settings.PREF_SHOW_ACTION_KEY, true);
mActionKeyId = prefs.getInt(Settings.PREF_ACTION_KEY_ID, 0);
mActionKeyId = ActionRegistry.INSTANCE.actionStringIdToIdx(prefs.getString(Settings.PREF_ACTION_KEY_ID, ""));
mShowsActionKey = mActionKeyId != -1;
mIsNumberRowEnabled = prefs.getBoolean(Settings.PREF_ENABLE_NUMBER_ROW, false);
mUseContactsDict = prefs.getBoolean(Settings.PREF_KEY_USE_CONTACTS_DICT, true);
mUsePersonalizedDicts = prefs.getBoolean(Settings.PREF_KEY_USE_PERSONALIZED_DICTS, true);

View File

@ -62,6 +62,8 @@ interface KeyboardManagerForAction {
fun requestDialog(text: String, options: List<DialogRequestItem>, onCancel: () -> Unit)
fun openInputMethodPicker()
fun activateAction(action: Action)
fun showActionEditor()
}
interface ActionWindow {

View File

@ -11,8 +11,8 @@ import androidx.compose.foundation.Canvas
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.background
import androidx.compose.foundation.combinedClickable
import androidx.compose.foundation.gestures.detectDragGesturesAfterLongPress
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.Spacer
@ -21,9 +21,12 @@ 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.size
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyItemScope
import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.foundation.lazy.rememberLazyListState
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.ColorScheme
import androidx.compose.material3.Icon
@ -34,36 +37,34 @@ import androidx.compose.material3.LocalTextStyle
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.TextButton
import androidx.compose.material3.contentColorFor
import androidx.compose.material3.dynamicDarkColorScheme
import androidx.compose.material3.dynamicLightColorScheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Alignment.Companion.Center
import androidx.compose.ui.Alignment.Companion.CenterVertically
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.draw.clip
import androidx.compose.ui.draw.drawBehind
import androidx.compose.ui.draw.rotate
import androidx.compose.ui.geometry.CornerRadius
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.geometry.Size
import androidx.compose.ui.graphics.Brush
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.graphics.RectangleShape
import androidx.compose.ui.graphics.drawscope.Fill
import androidx.compose.ui.graphics.drawscope.Stroke
import androidx.compose.ui.graphics.drawscope.scale
import androidx.compose.ui.graphics.drawscope.translate
import androidx.compose.ui.graphics.graphicsLayer
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
import androidx.compose.ui.input.pointer.pointerInput
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalHapticFeedback
import androidx.compose.ui.platform.LocalInspectionMode
import androidx.compose.ui.platform.LocalLifecycleOwner
import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
@ -80,8 +81,8 @@ import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp
import androidx.compose.ui.unit.sp
import androidx.compose.ui.zIndex
import kotlinx.coroutines.runBlocking
import androidx.datastore.preferences.core.booleanPreferencesKey
import androidx.datastore.preferences.core.intPreferencesKey
import org.futo.inputmethod.latin.R
import org.futo.inputmethod.latin.SuggestedWords
import org.futo.inputmethod.latin.SuggestedWords.SuggestedWordInfo
@ -89,11 +90,10 @@ import org.futo.inputmethod.latin.SuggestedWords.SuggestedWordInfo.KIND_EMOJI_SU
import org.futo.inputmethod.latin.SuggestedWords.SuggestedWordInfo.KIND_TYPED
import org.futo.inputmethod.latin.common.Constants
import org.futo.inputmethod.latin.suggestions.SuggestionStripView
import org.futo.inputmethod.latin.uix.actions.ActionRegistry
import org.futo.inputmethod.latin.uix.actions.DefaultActions
import org.futo.inputmethod.latin.uix.actions.DefaultActionsString
import org.futo.inputmethod.latin.uix.actions.ExpandableActionItems
import org.futo.inputmethod.latin.uix.actions.VoiceInputAction
import org.futo.inputmethod.latin.uix.actions.FavoriteActions
import org.futo.inputmethod.latin.uix.actions.MoreActionsAction
import org.futo.inputmethod.latin.uix.actions.PinnedActions
import org.futo.inputmethod.latin.uix.actions.toActionList
import org.futo.inputmethod.latin.uix.settings.useDataStoreValueBlocking
import org.futo.inputmethod.latin.uix.theme.DarkColorScheme
import org.futo.inputmethod.latin.uix.theme.Typography
@ -361,138 +361,170 @@ fun RowScope.SuggestionItems(words: SuggestedWords, onClick: (i: Int) -> Unit, o
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun LazyItemScope.ActionItem(idx: Int, action: Action, onSelect: (Action) -> Unit) {
val dragging = remember { mutableStateOf(false) }
val offsetX = remember { mutableFloatStateOf(0.0f) }
val offsetY = remember { mutableFloatStateOf(0.0f) }
val haptic = LocalHapticFeedback.current
fun LazyItemScope.ActionItem(idx: Int, action: Action, onSelect: (Action) -> Unit, onLongSelect: (Action) -> Unit) {
val width = 56.dp
val widthPx = with(LocalDensity.current) {
width.toPx()
}
val context = LocalContext.current
val isWindowAction = action.windowImpl != null
val col = MaterialTheme.colorScheme.secondaryContainer
val modifier = Modifier
.pointerInput(Unit) {
detectDragGesturesAfterLongPress(onDragStart = {
dragging.value = true
haptic.performHapticFeedback(HapticFeedbackType.LongPress)
offsetY.floatValue = -widthPx / 8.0f
}, onDragCancel = {
dragging.value = false
offsetX.floatValue = 0.0f
offsetY.floatValue = 0.0f
}, onDragEnd = {
dragging.value = false
offsetX.floatValue = 0.0f
offsetY.floatValue = 0.0f
}, onDrag = { change, dragAmount ->
change.consume()
offsetX.floatValue += dragAmount.x
offsetY.floatValue += dragAmount.y
if (offsetX.floatValue >= widthPx) {
offsetX.floatValue -= widthPx
runBlocking {
context.setSetting(
ExpandableActionItems, ActionRegistry.moveElement(
context.getSetting(ExpandableActionItems, DefaultActionsString),
DefaultActions,
action,
1
)
)
}
} else if (offsetX.floatValue <= -widthPx) {
offsetX.floatValue += widthPx
runBlocking {
context.setSetting(
ExpandableActionItems, ActionRegistry.moveElement(
context.getSetting(ExpandableActionItems, DefaultActionsString),
DefaultActions,
action,
-1
)
)
}
}
})
}
.width(width)
.let {
if (!dragging.value) {
it.animateItemPlacement()
} else {
it
.zIndex(10.0f)
.graphicsLayer {
clip = false
translationX = offsetX.floatValue
translationY = offsetY.floatValue
}
}
}
.drawBehind {
val radius = size.height / 4.0f
drawRoundRect(
col,
topLeft = Offset(size.width * 0.1f, size.height * 0.05f),
size = Size(size.width * 0.8f, size.height * 0.9f),
cornerRadius = CornerRadius(radius, radius),
style = if (isWindowAction) {
Fill
} else {
Stroke(width = 4.0f)
}
)
}
.fillMaxHeight()
val contentCol = if(isWindowAction) {
MaterialTheme.colorScheme.onSecondaryContainer
val contentCol = MaterialTheme.colorScheme.onBackground
Box(modifier = modifier
.clip(CircleShape)
.combinedClickable(onLongClick = action.altPressImpl?.let { { onLongSelect(action) } },
onClick = { onSelect(action) }), contentAlignment = Center) {
Icon(
painter = painterResource(id = action.icon),
contentDescription = stringResource(action.name),
tint = contentCol,
modifier = Modifier.size(20.dp),
)
}
}
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun ActionItemSmall(action: Action, onSelect: (Action) -> Unit, onLongSelect: (Action) -> Unit) {
val bgCol = if(!LocalInspectionMode.current) {
Color(LocalThemeProvider.current.keyColor)
} else {
MaterialTheme.colorScheme.onBackground
MaterialTheme.colorScheme.surfaceVariant
}
IconButton(onClick = { onSelect(action) }, modifier = modifier,
colors = IconButtonDefaults.iconButtonColors(contentColor = contentCol)
) {
Icon(
painter = painterResource(id = action.icon),
contentDescription = stringResource(action.name)
)
val circleRadius = with(LocalDensity.current) {
16.dp.toPx()
}
}
@Composable
fun ActionItemSmall(action: Action, onSelect: (Action) -> Unit) {
IconButton(onClick = {
onSelect(action)
}, modifier = Modifier
Box(modifier = Modifier
.width(42.dp)
.fillMaxHeight()) {
.fillMaxHeight()
.drawBehind {
drawCircle(
color = bgCol,
radius = circleRadius,
style = Fill
)
}
.clip(CircleShape)
.combinedClickable(onLongClick = action.altPressImpl?.let { { onLongSelect(action) } }) {
onSelect(
action
)
},
contentAlignment = Alignment.Center) {
Icon(
painter = painterResource(id = action.icon),
contentDescription = stringResource(action.name)
contentDescription = stringResource(action.name),
tint = contentColorFor(backgroundColor = bgCol),
modifier = Modifier.size(16.dp)
)
}
}
val ActionBarScrollIndexSetting = SettingsKey(
intPreferencesKey("action_bar_scroll_index"),
0
)
val ActionBarScrollOffsetSetting = SettingsKey(
intPreferencesKey("action_bar_scroll_offset"),
0
)
@Composable
fun ActionItems(onSelect: (Action) -> Unit) {
val actions = useDataStoreValueBlocking(key = ExpandableActionItems, default = DefaultActionsString)
fun ActionItems(onSelect: (Action) -> Unit, onLongSelect: (Action) -> Unit) {
val context = LocalContext.current
val lifecycle = LocalLifecycleOwner.current
val actions = if(!LocalInspectionMode.current) {
useDataStoreValueBlocking(FavoriteActions)
} else {
FavoriteActions.default
}
val actionItems = ActionRegistry.stringToActions(actions, DefaultActions)
val scrollItemIndex = if(LocalInspectionMode.current) { 0 } else {
remember {
context.getSettingBlocking(ActionBarScrollIndexSetting)
}
}
LazyRow {
items(actionItems.size, key = { actionItems[it].name }) {
ActionItem(it, actionItems[it], onSelect)
val scrollItemOffset = if(LocalInspectionMode.current) { 0 } else {
remember {
context.getSettingBlocking(ActionBarScrollOffsetSetting)
}
}
val actionItems = remember(actions) {
actions.toActionList()
}
val lazyListState = rememberLazyListState(scrollItemIndex, scrollItemOffset)
DisposableEffect(Unit) {
onDispose {
lifecycle.deferSetSetting(ActionBarScrollIndexSetting, lazyListState.firstVisibleItemIndex)
lifecycle.deferSetSetting(ActionBarScrollOffsetSetting, lazyListState.firstVisibleItemScrollOffset)
}
}
val bgCol = if(!LocalInspectionMode.current) {
Color(LocalThemeProvider.current.keyColor)
} else {
MaterialTheme.colorScheme.surfaceVariant
}
val gradientColor = if(bgCol.alpha > 0.5) {
bgCol.copy(alpha = 0.9f)
} else {
MaterialTheme.colorScheme.surface.copy(alpha = 0.9f)
}
val drawLeftGradient = lazyListState.firstVisibleItemIndex > 0
val drawRightGradient = lazyListState.layoutInfo.visibleItemsInfo.isNotEmpty() && actionItems.isNotEmpty() && (lazyListState.layoutInfo.visibleItemsInfo.lastOrNull()?.key != actionItems.lastOrNull()?.name)
Box {
LazyRow(state = lazyListState) {
item {
ActionItemSmall(action = MoreActionsAction, onSelect = {
onSelect(MoreActionsAction)
}, onLongSelect = { })
}
items(actionItems.size, key = { actionItems[it].name }) {
ActionItem(it, actionItems[it], onSelect, onLongSelect)
}
}
if(drawLeftGradient) {
Canvas(modifier = Modifier
.fillMaxHeight()
.width(72.dp)
.align(Alignment.CenterStart)) {
drawRect(
Brush.linearGradient(
0.0f to gradientColor,
1.0f to Color.Transparent,
start = Offset.Zero,
end = Offset(Float.POSITIVE_INFINITY, 0.0f)
)
)
}
}
if(drawRightGradient) {
Canvas(modifier = Modifier
.fillMaxHeight()
.width(72.dp)
.align(Alignment.CenterEnd)) {
drawRect(
Brush.linearGradient(
0.0f to Color.Transparent,
1.0f to gradientColor,
start = Offset.Zero,
end = Offset(Float.POSITIVE_INFINITY, 0.0f)
)
)
}
}
}
}
@ -500,12 +532,16 @@ fun ActionItems(onSelect: (Action) -> Unit) {
@Composable
fun ExpandActionsButton(isActionsOpen: Boolean, onClick: () -> Unit) {
val moreActionsColor = MaterialTheme.colorScheme.primary
val actionsContent = if(isActionsOpen) {
MaterialTheme.colorScheme.onPrimary
val bgCol = if(!LocalInspectionMode.current) {
Color(LocalThemeProvider.current.keyColor)
} else {
MaterialTheme.colorScheme.onBackground
MaterialTheme.colorScheme.surfaceVariant
}
val actionsContent = MaterialTheme.colorScheme.onSurface
val circleRadius = with(LocalDensity.current) {
16.dp.toPx()
}
IconButton(
@ -514,7 +550,7 @@ fun ExpandActionsButton(isActionsOpen: Boolean, onClick: () -> Unit) {
.width(42.dp)
.rotate(
if (isActionsOpen) {
180.0f
-90.0f
} else {
0.0f
}
@ -522,13 +558,9 @@ fun ExpandActionsButton(isActionsOpen: Boolean, onClick: () -> Unit) {
.fillMaxHeight()
.drawBehind {
drawCircle(
color = moreActionsColor,
radius = size.width / 3.0f + 1.0f,
style = if (!isActionsOpen) {
Stroke(3.0f)
} else {
Fill
}
color = bgCol,
radius = circleRadius,
style = Fill
)
},
@ -536,7 +568,8 @@ fun ExpandActionsButton(isActionsOpen: Boolean, onClick: () -> Unit) {
) {
Icon(
painter = painterResource(id = R.drawable.chevron_right),
contentDescription = "Open Actions"
contentDescription = "Open Actions",
Modifier.size(20.dp)
)
}
}
@ -580,66 +613,96 @@ fun ImportantNoticeView(
}
}
val ActionBarExpanded = SettingsKey(
booleanPreferencesKey("actionExpanded"),
false
)
@Composable
fun RowScope.PinnedActionItems(onSelect: (Action) -> Unit, onLongSelect: (Action) -> Unit) {
val actions = if(!LocalInspectionMode.current) {
useDataStoreValueBlocking(PinnedActions)
} else {
PinnedActions.default
}
val actionItems = remember(actions) {
actions.toActionList()
}
actionItems.forEach {
ActionItemSmall(it, onSelect, onLongSelect)
}
}
@Composable
fun ActionBar(
words: SuggestedWords?,
suggestionStripListener: SuggestionStripView.Listener,
onActionActivated: (Action) -> Unit,
onActionAltActivated: (Action) -> Unit,
inlineSuggestions: List<MutableState<View?>>,
forceOpenActionsInitially: Boolean = false,
isActionsExpanded: Boolean,
toggleActionsExpanded: () -> Unit,
importantNotice: ImportantNotice? = null,
keyboardManagerForAction: KeyboardManagerForAction? = null,
actionsForcedOpenByUser: MutableState<Boolean> = mutableStateOf(false)
) {
val view = LocalView.current
val context = LocalContext.current
val isActionsOpen = remember { mutableStateOf(forceOpenActionsInitially) }
val activateActionWithHaptic: (Action) -> Unit = {
keyboardManagerForAction?.performHapticAndAudioFeedback(Constants.CODE_TAB, view)
onActionActivated(it)
val actionBarHeight = 40.dp
val sepCol = if(!LocalInspectionMode.current) {
Color(LocalThemeProvider.current.keyColor)
} else {
MaterialTheme.colorScheme.surfaceVariant
}
LaunchedEffect(words) {
if(words != null && !words.isEmpty && !actionsForcedOpenByUser.value) {
isActionsOpen.value = false
actionsForcedOpenByUser.value = false
}
}
Column {
if(isActionsExpanded) {
Box(modifier = Modifier
.fillMaxWidth()
.height(1.dp)
.background(sepCol)) {}
LaunchedEffect(inlineSuggestions) {
if(inlineSuggestions.isNotEmpty()) {
isActionsOpen.value = false
actionsForcedOpenByUser.value = false
}
}
Surface(modifier = Modifier
.fillMaxWidth()
.height(40.dp), color = MaterialTheme.colorScheme.background)
{
Row {
ExpandActionsButton(isActionsOpen.value) {
isActionsOpen.value = !isActionsOpen.value
actionsForcedOpenByUser.value = isActionsOpen.value
if(isActionsOpen.value && importantNotice != null) {
importantNotice.onDismiss(context)
}
keyboardManagerForAction?.performHapticAndAudioFeedback(Constants.CODE_TAB, view)
Surface(
modifier = Modifier
.fillMaxWidth()
.height(actionBarHeight), color = MaterialTheme.colorScheme.background
) {
ActionItems(onActionActivated, onActionAltActivated)
}
}
if(importantNotice != null && !isActionsOpen.value) {
ImportantNoticeView(importantNotice)
}else {
Box(modifier = Modifier
.fillMaxWidth()
.height(1.dp)
.background(sepCol)) {}
AnimatedVisibility(isActionsOpen.value) {
ActionItems(activateActionWithHaptic)
Surface(
modifier = Modifier
.fillMaxWidth()
.height(actionBarHeight), color = MaterialTheme.colorScheme.background
) {
Row {
ExpandActionsButton(isActionsExpanded) {
toggleActionsExpanded()
keyboardManagerForAction?.performHapticAndAudioFeedback(
Constants.CODE_TAB,
view
)
}
if(!isActionsOpen.value) {
if (importantNotice != null) {
ImportantNoticeView(importantNotice)
} else {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
AnimatedVisibility(inlineSuggestions.isNotEmpty(), enter = fadeIn(), exit = fadeOut()) {
AnimatedVisibility(
inlineSuggestions.isNotEmpty(),
enter = fadeIn(),
exit = fadeOut()
) {
InlineSuggestions(inlineSuggestions)
}
}
@ -664,7 +727,8 @@ fun ActionBar(
} else {
Spacer(modifier = Modifier.weight(1.0f))
}
ActionItemSmall(VoiceInputAction, activateActionWithHaptic)
PinnedActionItems(onActionActivated, onActionAltActivated)
}
}
}
@ -823,7 +887,10 @@ fun PreviewActionBarWithSuggestions(colorScheme: ColorScheme = DarkColorScheme)
words = exampleSuggestedWords,
suggestionStripListener = ExampleListener(),
onActionActivated = { },
inlineSuggestions = listOf()
inlineSuggestions = listOf(),
isActionsExpanded = false,
toggleActionsExpanded = { },
onActionAltActivated = { }
)
}
}
@ -837,6 +904,9 @@ fun PreviewActionBarWithNotice(colorScheme: ColorScheme = DarkColorScheme) {
suggestionStripListener = ExampleListener(),
onActionActivated = { },
inlineSuggestions = listOf(),
isActionsExpanded = true,
toggleActionsExpanded = { },
onActionAltActivated = { },
importantNotice = object : ImportantNotice {
@Composable
override fun getText(): String {
@ -864,7 +934,10 @@ fun PreviewActionBarWithEmptySuggestions(colorScheme: ColorScheme = DarkColorSch
words = exampleSuggestedWordsEmpty,
suggestionStripListener = ExampleListener(),
onActionActivated = { },
inlineSuggestions = listOf()
inlineSuggestions = listOf(),
isActionsExpanded = true,
toggleActionsExpanded = { },
onActionAltActivated = { }
)
}
}
@ -878,7 +951,9 @@ fun PreviewExpandedActionBar(colorScheme: ColorScheme = DarkColorScheme) {
suggestionStripListener = ExampleListener(),
onActionActivated = { },
inlineSuggestions = listOf(),
forceOpenActionsInitially = true
isActionsExpanded = true,
toggleActionsExpanded = { },
onActionAltActivated = { }
)
}
}

View File

@ -60,7 +60,7 @@ fun ActionTextEditor(text: MutableState<String>) {
48.dp.toPx()
}
val inputType = EditorInfo.TYPE_CLASS_TEXT or EditorInfo.TYPE_TEXT_FLAG_AUTO_COMPLETE or EditorInfo.TYPE_TEXT_FLAG_NO_SUGGESTIONS
val inputType = EditorInfo.TYPE_CLASS_TEXT
val color = LocalContentColor.current
@ -79,6 +79,8 @@ fun ActionTextEditor(text: MutableState<String>) {
ViewGroup.LayoutParams.WRAP_CONTENT
)
privateImeOptions = "org.futo.inputmethod.latin.NoSuggestions=1"
setHeight(height.toInt())
val editorInfo = EditorInfo().apply {

View File

@ -66,6 +66,7 @@ fun<T> Preferences.get(key: SettingsKey<T>): T {
class BasicThemeProvider(val context: Context, val overrideColorScheme: ColorScheme? = null) :
DynamicThemeProvider {
override val primaryKeyboardColor: Int
override val keyColor: Int
override val keyboardBackground: Drawable
override val keyBackground: Drawable
@ -225,6 +226,9 @@ class BasicThemeProvider(val context: Context, val overrideColorScheme: ColorSch
} else {
transparent
}
this.keyColor = keyColor
val functionalKeyColor = if(keyBorders) {
adjustColorBrightnessForContrast(primaryKeyboardColor, primaryKeyboardColor, ratio / 2.0f + 0.5f, adjustSaturation = true)
} else {

View File

@ -6,6 +6,7 @@ import androidx.annotation.ColorInt
interface DynamicThemeProvider {
val primaryKeyboardColor: Int
val keyColor: Int
val keyboardBackground: Drawable
val keyBackground: Drawable

View File

@ -17,14 +17,20 @@ import android.view.inputmethod.InputConnection
import android.view.inputmethod.InputContentInfo
import androidx.annotation.RequiresApi
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.animateColorAsState
import androidx.compose.animation.fadeIn
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.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.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
@ -35,7 +41,9 @@ import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.staticCompositionLocalOf
import androidx.compose.ui.Alignment
@ -46,6 +54,7 @@ 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.LayoutDirection
import androidx.compose.ui.unit.dp
@ -59,12 +68,12 @@ import org.futo.inputmethod.latin.BuildConfig
import org.futo.inputmethod.latin.LanguageSwitcherDialog
import org.futo.inputmethod.latin.LatinIME
import org.futo.inputmethod.latin.R
import org.futo.inputmethod.latin.RichInputMethodManager
import org.futo.inputmethod.latin.SuggestedWords
import org.futo.inputmethod.latin.SuggestedWords.SuggestedWordInfo
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.actions.ActionEditor
import org.futo.inputmethod.latin.uix.actions.ActionRegistry
import org.futo.inputmethod.latin.uix.actions.AllActions
import org.futo.inputmethod.latin.uix.actions.EmojiAction
@ -85,6 +94,10 @@ val LocalManager = staticCompositionLocalOf<KeyboardManagerForAction> {
error("No LocalManager provided")
}
val LocalThemeProvider = staticCompositionLocalOf<DynamicThemeProvider> {
error("No LocalThemeProvider provided")
}
private class LatinIMEActionInputTransaction(
private val inputLogic: InputLogic,
shouldApplySpace: Boolean,
@ -232,10 +245,12 @@ class UixActionKeyboardManager(val uixManager: UixManager, val latinIME: LatinIM
override fun overrideInputConnection(inputConnection: InputConnection, editorInfo: EditorInfo) {
latinIME.overrideInputConnection(inputConnection, editorInfo)
uixManager.toggleExpandAction(true)
uixManager.isInputOverridden.value = true
}
override fun unsetInputConnection() {
latinIME.overrideInputConnection(null, null)
uixManager.isInputOverridden.value = false
}
override fun requestDialog(text: String, options: List<DialogRequestItem>, onCancel: () -> Unit) {
@ -253,6 +268,14 @@ class UixActionKeyboardManager(val uixManager: UixManager, val latinIME: LatinIM
AccessibilityUtils.getInstance().announceForAccessibility(uixManager.getComposeView(), s)
}
}
override fun activateAction(action: Action) {
uixManager.onActionActivated(action)
}
override fun showActionEditor() {
uixManager.showActionEditor()
}
}
data class ActiveDialogRequest(
@ -278,13 +301,24 @@ class UixManager(private val latinIME: LatinIME) {
private var numSuggestionsSinceNotice = 0
private var currentNotice: MutableState<ImportantNotice?> = mutableStateOf(null)
private val actionsForcedOpenByUser = mutableStateOf(false)
private var isActionsExpanded = mutableStateOf(false)
private fun toggleActionsExpanded() {
isActionsExpanded.value = !isActionsExpanded.value
latinIME.deferSetSetting(ActionBarExpanded, isActionsExpanded.value)
}
private var isShowingActionEditor = mutableStateOf(false)
fun showActionEditor() {
isShowingActionEditor.value = true
}
var isInputOverridden = mutableStateOf(false)
var currWindowActionWindow: ActionWindow? = null
val isMainKeyboardHidden get() = mainKeyboardHidden
private fun onActionActivated(rawAction: Action) {
fun onActionActivated(rawAction: Action) {
latinIME.inputLogic.finishInput()
val action = runBlocking {
@ -300,8 +334,20 @@ class UixManager(private val latinIME: LatinIME) {
}
}
fun onActionAltActivated(rawAction: Action) {
latinIME.inputLogic.finishInput()
val action = runBlocking {
ActionRegistry.getActionOverride(latinIME, rawAction)
}
action.altPressImpl?.invoke(keyboardManagerForAction, persistentStates[action])
}
@Composable
private fun MainKeyboardViewWithActionBar() {
val view = LocalView.current
Column {
// Don't show suggested words when it's not meant to be shown
val suggestedWordsOrNull = if(shouldShowSuggestionStrip) {
@ -314,10 +360,20 @@ class UixManager(private val latinIME: LatinIME) {
suggestedWordsOrNull,
latinIME.latinIMELegacy as SuggestionStripView.Listener,
inlineSuggestions = inlineSuggestions,
onActionActivated = { onActionActivated(it) },
onActionActivated = {
keyboardManagerForAction.performHapticAndAudioFeedback(Constants.CODE_TAB, view)
onActionActivated(it)
},
onActionAltActivated = {
if(it.altPressImpl != null) {
keyboardManagerForAction.performHapticAndAudioFeedback(Constants.CODE_TAB, view)
}
onActionAltActivated(it)
},
importantNotice = currentNotice.value,
keyboardManagerForAction = keyboardManagerForAction,
actionsForcedOpenByUser = actionsForcedOpenByUser
isActionsExpanded = isActionsExpanded.value,
toggleActionsExpanded = { toggleActionsExpanded() },
)
}
}
@ -341,7 +397,6 @@ class UixManager(private val latinIME: LatinIME) {
setContent()
actionsForcedOpenByUser.value = false
keyboardManagerForAction.announce("${latinIME.resources.getString(action.name)} mode")
}
@ -363,7 +418,6 @@ class UixManager(private val latinIME: LatinIME) {
setContent()
actionsForcedOpenByUser.value = false
keyboardManagerForAction.announce("$name closed")
}
@ -446,7 +500,9 @@ class UixManager(private val latinIME: LatinIME) {
}
) { }
Box(modifier = Modifier.matchParentSize().padding(8.dp)) {
Box(modifier = Modifier
.matchParentSize()
.padding(8.dp)) {
Surface(
shape = RoundedCornerShape(16.dp),
color = MaterialTheme.colorScheme.primaryContainer,
@ -495,32 +551,82 @@ class UixManager(private val latinIME: LatinIME) {
languageSwitcherDialog?.show()
}
@Composable
fun ActionEditorHost() {
Box(modifier = Modifier.fillMaxSize(), contentAlignment = Alignment.BottomCenter) {
AnimatedVisibility(
visible = isShowingActionEditor.value,
enter = slideInVertically { it },
exit = slideOutVertically { it },
content = {
ActionEditor()
},
)
}
}
@Composable
fun InputDarkener(darken: Boolean, onClose: () -> Unit) {
val color by animateColorAsState(
if (darken) Color.Black.copy(alpha = 0.25f) else Color.Transparent
)
LaunchedEffect(darken) {
latinIME.setInputModal(darken)
}
Box(Modifier
.background(color)
.fillMaxWidth()
.fillMaxHeight()
.then(
if (darken) {
Modifier.pointerInput(Unit) {
detectTapGestures {
onClose()
}
}
} else {
Modifier
})
)
}
fun setContent() {
composeView?.setContent {
UixThemeWrapper(latinIME.colorScheme) {
CompositionLocalProvider(LocalManager provides keyboardManagerForAction) {
CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Ltr ) {
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!!
)
CompositionLocalProvider(LocalThemeProvider provides latinIME.getDrawableProvider()) {
CompositionLocalProvider(LocalLayoutDirection provides LayoutDirection.Ltr) {
InputDarkener(isInputOverridden.value || isShowingActionEditor.value) {
closeActionWindow()
isShowingActionEditor.value = false
}
else -> MainKeyboardViewWithActionBar()
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!!
)
else -> MainKeyboardViewWithActionBar()
}
latinIME.LegacyKeyboardView(hidden = isMainKeyboardHidden)
}
latinIME.LegacyKeyboardView(hidden = isMainKeyboardHidden)
ForgetWordDialog()
}
ForgetWordDialog()
}
}
ActionEditorHost()
}
}
}
@ -616,7 +722,6 @@ class UixManager(private val latinIME: LatinIME) {
fun onInputFinishing() {
closeActionWindow()
actionsForcedOpenByUser.value = false
languageSwitcherDialog?.dismiss()
}
@ -674,9 +779,7 @@ class UixManager(private val latinIME: LatinIME) {
val action = AllActions.getOrNull(id) ?: throw IllegalArgumentException("No such action with ID $id")
if(alt) {
if(action.altPressImpl != null) {
action.altPressImpl.invoke(keyboardManagerForAction, persistentStates[action])
}
onActionAltActivated(action)
} else {
if (currWindowAction != null && action.windowImpl != null) {
closeActionWindow()
@ -721,5 +824,7 @@ class UixManager(private val latinIME: LatinIME) {
persistentStates[action] = action.persistentState?.let { it(keyboardManagerForAction) }
}
}
isActionsExpanded.value = latinIME.getSettingBlocking(ActionBarExpanded)
}
}

View File

@ -0,0 +1,239 @@
package org.futo.inputmethod.latin.uix.actions
import android.view.HapticFeedbackConstants
import androidx.compose.foundation.ExperimentalFoundationApi
import androidx.compose.foundation.clickable
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.defaultMinSize
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.lazy.grid.GridCells
import androidx.compose.foundation.lazy.grid.GridItemSpan
import androidx.compose.foundation.lazy.grid.LazyVerticalGrid
import androidx.compose.foundation.lazy.grid.items
import androidx.compose.foundation.lazy.grid.rememberLazyGridState
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Icon
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.OutlinedButton
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.remember
import androidx.compose.runtime.toMutableStateList
import androidx.compose.ui.Alignment
import androidx.compose.ui.Alignment.Companion.Center
import androidx.compose.ui.Alignment.Companion.CenterHorizontally
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalInspectionMode
import androidx.compose.ui.platform.LocalView
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.text.style.TextAlign
import androidx.compose.ui.tooling.preview.Preview
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.LocalManager
import org.futo.inputmethod.latin.uix.getSettingBlocking
import org.futo.inputmethod.latin.uix.settings.Tip
import org.futo.inputmethod.latin.uix.settings.useDataStoreValueBlocking
import org.futo.voiceinput.shared.ui.theme.Typography
import sh.calvin.reorderable.ReorderableItem
import sh.calvin.reorderable.rememberReorderableLazyGridState
@Composable
fun ActionItem(action: Action, modifier: Modifier = Modifier) {
Surface(color = MaterialTheme.colorScheme.primaryContainer, modifier = modifier
.fillMaxWidth()
.height(92.dp)
, shape = RoundedCornerShape(8.dp)) {
Box(modifier = Modifier
.fillMaxSize()
.padding(8.dp)) {
Column(modifier = Modifier
.align(Center)
.padding(8.dp)) {
Spacer(modifier = Modifier.weight(1.0f))
Icon(
painterResource(id = action.icon), contentDescription = null, modifier = Modifier.align(
CenterHorizontally
))
Spacer(modifier = Modifier.weight(1.0f))
Text(stringResource(id = action.name), modifier = Modifier.align(
CenterHorizontally), style = Typography.labelSmall, textAlign = TextAlign.Center)
}
}
}
}
@Composable
@Preview(showBackground = true)
fun MoreActionsView() {
val manager = if(LocalInspectionMode.current) { null } else { LocalManager.current }
val context = LocalContext.current
val actionList = if(LocalInspectionMode.current) {
ActionsSettings.default
} else {
useDataStoreValueBlocking(ActionsSettings)
}
val actions = remember(actionList) {
actionList.toActionEditorItems().toActionMap()[ActionCategory.More] ?: listOf()
}
LazyVerticalGrid(
modifier = Modifier.fillMaxWidth(),
columns = GridCells.Adaptive(98.dp),
horizontalArrangement = Arrangement.spacedBy(16.dp),
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
items(actions, key = { it.name }) {
ActionItem(it, Modifier.clickable {
manager!!.activateAction(it)
})
}
}
}
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun ActionsEditor() {
val context = LocalContext.current
val view = LocalView.current
val initialList: List<ActionEditorItem> = remember {
context.getSettingBlocking(ActionsSettings).toActionEditorItems().ensureWellFormed()
}
val list = remember { initialList.toMutableStateList() }
val lazyListState = rememberLazyGridState()
val reorderableLazyListState = rememberReorderableLazyGridState(lazyListState) { from, to ->
val itemToAdd = list.removeAt(from.index)
list.add(to.index, itemToAdd)
view.performHapticFeedback(HapticFeedbackConstants.SEGMENT_FREQUENT_TICK)
}
DisposableEffect(Unit) {
onDispose {
val map = list.toActionMap()
context.updateSettingsWithNewActions(map)
}
}
val actionMap = list.toActionMap()
LazyVerticalGrid(
modifier = Modifier
.fillMaxSize()
.padding(8.dp, 0.dp),
state = lazyListState,
columns = GridCells.Adaptive(98.dp),
horizontalArrangement = Arrangement.spacedBy(16.dp),
verticalArrangement = Arrangement.spacedBy(16.dp)
) {
items(list, key = { it.toKey() }, span = {
when(it) {
is ActionEditorItem.Item -> GridItemSpan(1)
is ActionEditorItem.Separator -> GridItemSpan(maxLineSpan)
}
}) {
when(it) {
is ActionEditorItem.Item -> {
ReorderableItem(reorderableLazyListState, key = it.toKey()) { isDragging ->
ActionItem(it.action, Modifier.longPressDraggableHandle(
onDragStarted = {
view.performHapticFeedback(HapticFeedbackConstants.DRAG_START)
},
onDragStopped = {
view.performHapticFeedback(HapticFeedbackConstants.GESTURE_END)
},
))
}
}
is ActionEditorItem.Separator -> {
ReorderableItem(reorderableLazyListState, key = it.toKey(), enabled = it.category != ActionCategory.entries[0]) { _ ->
Column {
if (it.category == ActionCategory.entries[0]) {
Box(Modifier.defaultMinSize(minHeight = 72.dp), contentAlignment = Alignment.BottomStart) {
if (actionMap[ActionCategory.ActionKey]?.let { it.size > 1 } == true) {
Tip("Only one Action Key can be set, anything after the first is ignored")
} else if (actionMap[ActionCategory.PinnedKey]?.let { it.size > 4 } == true) {
Tip("You have more pinned actions than seems reasonable. Consider moving some to favorites")
}
}
}
Text(it.category.name)
}
}
}
}
}
}
}
@OptIn(ExperimentalFoundationApi::class)
@Composable
fun ActionEditor() {
Surface(
modifier = Modifier
.fillMaxWidth()
.fillMaxHeight(0.75f),
color = MaterialTheme.colorScheme.background,
shape = RoundedCornerShape(32.dp, 32.dp, 0.dp, 0.dp)
) {
ActionsEditor()
}
}
val MoreActionsAction = Action(
icon = R.drawable.more_horizontal,
name = R.string.more_actions_action_title,
simplePressImpl = null,
windowImpl = { manager, _ ->
object : ActionWindow {
@Composable
override fun windowName(): String = stringResource(id = R.string.more_actions_action_title)
@Composable
override fun WindowContents(keyboardShown: Boolean) {
MoreActionsView()
}
@Composable
override fun WindowTitleBar(rowScope: RowScope) {
super.WindowTitleBar(rowScope)
OutlinedButton(onClick = { manager.showActionEditor() }, modifier = Modifier.padding(8.dp, 0.dp)) {
Text("Edit Pinned", color = MaterialTheme.colorScheme.onBackground)
}
}
override fun close() {
}
}
},
)

View File

@ -1,14 +1,16 @@
package org.futo.inputmethod.latin.uix.actions
import android.content.Context
import androidx.core.content.edit
import androidx.datastore.preferences.core.stringPreferencesKey
import org.futo.inputmethod.keyboard.internal.KeyboardCodesSet
import org.futo.inputmethod.latin.settings.Settings
import org.futo.inputmethod.latin.uix.Action
import org.futo.inputmethod.latin.uix.PreferenceUtils
import org.futo.inputmethod.latin.uix.SettingsKey
import org.futo.inputmethod.latin.uix.USE_SYSTEM_VOICE_INPUT
import org.futo.inputmethod.latin.uix.getSetting
val ExpandableActionItems = stringPreferencesKey("expandableActions")
val PinnedActionItems = stringPreferencesKey("pinnedActions")
import org.futo.inputmethod.latin.uix.setSettingBlocking
// Note: indices must stay stable
val AllActionsMap = mapOf(
@ -26,7 +28,8 @@ val AllActionsMap = mapOf(
"mem_dbg" to MemoryDebugAction,
"cut" to CutAction,
"copy" to CopyAction,
"select_all" to SelectAllAction
"select_all" to SelectAllAction,
"more" to MoreActionsAction
)
val ActionToId = AllActionsMap.entries.associate { it.value to it.key }
@ -36,37 +39,6 @@ val AllActions = AllActionsMap.values.toList()
val ActionIdToInt = AllActionsMap.entries.associate { it.key to AllActions.indexOf(it.value) }
object ActionRegistry {
val EnterActions = "!fixedColumnOrder!4,!needsDividers!," +
listOf(SwitchLanguageAction, TextEditAction, ClipboardHistoryAction, EmojiAction, UndoAction, RedoAction).map {
ActionToId[it]
}.joinToString(separator = ",") {
"!icon/action_$it|!code/action_$it"
}
fun stringToActions(string: String, defaults: List<Action>): List<Action> {
return string.split(",").mapNotNull { idx ->
idx.toIntOrNull()?.let { AllActions.getOrNull(it) }
}.let { list ->
val notIncluded = defaults.filter { action ->
!list.contains(action)
}
list + notIncluded
}
}
fun actionsToString(actions: List<Action>): String {
return actions.map { AllActions.indexOf(it) }.joinToString(separator = ",")
}
fun moveElement(string: String, defaults: List<Action>, action: Action, direction: Int): String {
val actions = stringToActions(string, defaults)
val index = actions.indexOf(action)
val filtered = actions.filter { it != action }.toMutableList()
filtered.add((index + direction).coerceIn(0 .. filtered.size), action)
return actionsToString(filtered)
}
suspend fun getActionOverride(context: Context, action: Action): Action {
return if(action == VoiceInputAction || action == SystemVoiceInputAction) {
val useSystemVoiceInput = context.getSetting(USE_SYSTEM_VOICE_INPUT)
@ -93,23 +65,194 @@ object ActionRegistry {
fun actionIdToName(context: Context, id: Int): String {
return context.getString(AllActions[id].name)
}
fun actionStringIdToIdx(id: String): Int {
return AllActionsMap.keys.indexOf(id)
}
fun actionToStringId(action: Action): String {
return AllActionsMap.entries.find { it.value == action }?.key ?: ""
}
}
val DefaultActions = listOf(
EmojiAction,
TextEditAction,
UndoAction,
RedoAction,
PasteAction,
SettingsAction,
ThemeAction,
MemoryDebugAction,
SwitchLanguageAction,
ClipboardHistoryAction
enum class ActionCategory {
ActionKey,
PinnedKey,
Favorites,
More,
Disabled
}
fun ActionCategory.toSepName(): String {
return "_SEP_${name}"
}
val ActionCategorySepNames = List(ActionCategory.entries.size) { i ->
Pair(
ActionCategory.entries[i].toSepName(),
ActionCategory.entries[i],
)
}.toMap()
sealed class ActionEditorItem {
data class Item(val action: Action) : ActionEditorItem()
data class Separator(val category: ActionCategory) : ActionEditorItem()
}
fun ActionEditorItem.toKey(): String {
return when(this) {
is ActionEditorItem.Item -> this.action.name.toString()
is ActionEditorItem.Separator -> "sep " + this.category.name
}
}
fun ActionEditorItem.stringRepresentation(): String = when(this) {
is ActionEditorItem.Item -> AllActionsMap.entries.find { it.value == action }!!.key
is ActionEditorItem.Separator -> "_SEP_" + this.category.name
}
fun List<ActionEditorItem>.serializeActionEditorItemListToString(): String = joinToString { it.stringRepresentation() }
fun String.toActionEditorItems() : List<ActionEditorItem> = split(",").mapNotNull {
val v = it.trimStart()
when {
ActionCategorySepNames.containsKey(v) -> ActionEditorItem.Separator(ActionCategorySepNames[v]!!)
AllActionsMap.containsKey(v) -> ActionEditorItem.Item(AllActionsMap[v]!!)
else -> null
}
}
fun List<ActionEditorItem>.toActionMap(): Map<ActionCategory, List<Action>> {
val actionMap = mutableMapOf<ActionCategory, MutableList<Action>>()
var currentCategory: ActionCategory? = null
for (item in this) {
when (item) {
is ActionEditorItem.Item -> {
currentCategory?.let { category ->
actionMap.getOrPut(category) { mutableListOf() }.add(item.action)
}
}
is ActionEditorItem.Separator -> {
currentCategory = item.category
if (!actionMap.containsKey(currentCategory)) {
actionMap[currentCategory] = mutableListOf()
}
}
}
}
return actionMap
}
// Initializes any non-present categories to be empty
fun Map<ActionCategory, List<Action>>.ensureAllCategoriesPresent(): Map<ActionCategory, List<Action>> {
val map = toMutableMap()
ActionCategory.entries.forEach {
if(!map.containsKey(it)) {
map[it] = listOf()
}
}
return map
}
// Adds any missing actions to the "More" section
fun Map<ActionCategory, List<Action>>.ensureAllActionsPresent(): Map<ActionCategory, List<Action>> {
val map = toMutableMap()
val actionsPresent = mutableSetOf<Action>()
values.forEach { v -> actionsPresent.addAll(v) }
val actionsRequired = AllActions.toSet()
val actionsMissing = actionsRequired.subtract(actionsPresent)
if(actionsMissing.isNotEmpty()) {
map[ActionCategory.More] = map[ActionCategory.More]!! + actionsMissing
}
return map
}
// Flattens the map back to a list
fun Map<ActionCategory, List<Action>>.flattenToActionEditorItems(): List<ActionEditorItem> {
val result = mutableListOf<ActionEditorItem>()
for (category in ActionCategory.entries) {
if (this.containsKey(category)) {
result.add(ActionEditorItem.Separator(category))
this[category]?.let { actions ->
result.addAll(actions.map {
ActionEditorItem.Item(it)
})
}
}
}
return result
}
fun List<ActionEditorItem>.ensureWellFormed(): List<ActionEditorItem> {
var map = toActionMap()
// Ensure all categories are present
map = map.ensureAllCategoriesPresent()
// Ensure all actions are present
map = map.ensureAllActionsPresent()
// Return the flattened list
return map.flattenToActionEditorItems()
}
fun List<Action>.serializeActionListToString(): String = joinToString(separator = ",") { action ->
AllActionsMap.entries.find { it.value == action }!!.key
}
fun String.toActionList(): List<Action> = split(",").mapNotNull { AllActionsMap[it.trim()] }
val DefaultActionSettings = mapOf(
ActionCategory.ActionKey to listOf(EmojiAction),
ActionCategory.PinnedKey to listOf(VoiceInputAction),
ActionCategory.Favorites to listOf(SwitchLanguageAction, UndoAction, RedoAction, TextEditAction, ClipboardHistoryAction, ThemeAction),
ActionCategory.More to listOf(), // Remaining actions get populated automatically by ensureWellFormed
ActionCategory.Disabled to listOf(MemoryDebugAction)
)
val DefaultActionsString = ActionRegistry.actionsToString(DefaultActions)
val ActionsSettings = SettingsKey(
stringPreferencesKey("actions_settings_map"),
DefaultActionSettings.flattenToActionEditorItems().ensureWellFormed().serializeActionEditorItemListToString()
)
val PinnedActions = SettingsKey(
stringPreferencesKey("pinned_actions_s"),
DefaultActionSettings[ActionCategory.PinnedKey]!!.serializeActionListToString()
)
val FavoriteActions = SettingsKey(
stringPreferencesKey("favorite_actions_s"),
DefaultActionSettings[ActionCategory.Favorites]!!.serializeActionListToString()
)
val DefaultPinnedActions = listOf(VoiceInputAction)
val DefaultPinnedActionsString = ActionRegistry.actionsToString(DefaultPinnedActions)
fun Context.updateSettingsWithNewActions(newActions: Map<ActionCategory, List<Action>>) {
val map = newActions.ensureAllCategoriesPresent().ensureAllActionsPresent()
val actionKey = map[ActionCategory.ActionKey]?.firstOrNull()
val sharedPrefs = PreferenceUtils.getDefaultSharedPreferences(this)
sharedPrefs.edit {
putString(Settings.PREF_ACTION_KEY_ID, actionKey?.let {
ActionRegistry.actionToStringId(it)
} ?: "")
}
setSettingBlocking(ActionsSettings.key, map.flattenToActionEditorItems().serializeActionEditorItemListToString())
setSettingBlocking(PinnedActions.key, (map[ActionCategory.PinnedKey] ?: listOf()).serializeActionListToString())
setSettingBlocking(FavoriteActions.key, (map[ActionCategory.Favorites] ?: listOf()).serializeActionListToString())
}