mirror of
https://gitlab.futo.org/keyboard/latinime.git
synced 2024-09-28 14:54:30 +01:00
Implement new ActionBar and editor
This commit is contained in:
parent
e306c29ccd
commit
5aaad74d3c
@ -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'
|
||||
|
27
java/res/drawable/more_horizontal.xml
Normal file
27
java/res/drawable/more_horizontal.xml
Normal 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>
|
@ -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>
|
||||
|
@ -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
|
||||
|
@ -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"
|
||||
|
@ -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";
|
||||
|
||||
|
@ -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);
|
||||
|
@ -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 {
|
||||
|
@ -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 = { }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
@ -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 {
|
||||
|
@ -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 {
|
||||
|
@ -6,6 +6,7 @@ import androidx.annotation.ColorInt
|
||||
|
||||
interface DynamicThemeProvider {
|
||||
val primaryKeyboardColor: Int
|
||||
val keyColor: Int
|
||||
|
||||
val keyboardBackground: Drawable
|
||||
val keyBackground: Drawable
|
||||
|
@ -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)
|
||||
}
|
||||
}
|
239
java/src/org/futo/inputmethod/latin/uix/actions/MoreActions.kt
Normal file
239
java/src/org/futo/inputmethod/latin/uix/actions/MoreActions.kt
Normal 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() {
|
||||
|
||||
}
|
||||
}
|
||||
},
|
||||
)
|
@ -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())
|
||||
}
|
Loading…
Reference in New Issue
Block a user