mirror of
https://gitlab.futo.org/keyboard/latinime.git
synced 2024-09-28 14:54:30 +01:00
Allow re-arranging actionbar
This commit is contained in:
parent
d2de3dee38
commit
3ea6a08ccc
@ -1,6 +1,7 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<resources xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
|
||||
<string name="voice_input_action_title">Voice Input</string>
|
||||
<string name="system_voice_input_action_title">Voice Input (System)</string>
|
||||
<string name="theme_switcher_action_title">Theme Switcher</string>
|
||||
<string name="emoji_action_title">Emojis</string>
|
||||
<string name="clipboard_action_title">Paste from Clipboard</string>
|
||||
|
@ -4,10 +4,12 @@ import android.content.Context
|
||||
import android.os.Build
|
||||
import android.view.View
|
||||
import androidx.annotation.RequiresApi
|
||||
import androidx.compose.animation.AnimatedVisibility
|
||||
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.Row
|
||||
import androidx.compose.foundation.layout.RowScope
|
||||
@ -16,8 +18,10 @@ 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.offset
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.lazy.LazyItemScope
|
||||
import androidx.compose.foundation.lazy.LazyRow
|
||||
import androidx.compose.material3.ButtonDefaults
|
||||
import androidx.compose.material3.ColorScheme
|
||||
@ -34,6 +38,7 @@ import androidx.compose.material3.dynamicLightColorScheme
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.CompositionLocalProvider
|
||||
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.Companion.Center
|
||||
@ -47,9 +52,15 @@ import androidx.compose.ui.geometry.Offset
|
||||
import androidx.compose.ui.geometry.Size
|
||||
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.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.LocalView
|
||||
import androidx.compose.ui.res.painterResource
|
||||
import androidx.compose.ui.res.stringResource
|
||||
@ -63,9 +74,12 @@ import androidx.compose.ui.text.rememberTextMeasurer
|
||||
import androidx.compose.ui.text.style.TextOverflow
|
||||
import androidx.compose.ui.tooling.preview.Preview
|
||||
import androidx.compose.ui.unit.Constraints
|
||||
import androidx.compose.ui.unit.IntOffset
|
||||
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 org.futo.inputmethod.latin.R
|
||||
import org.futo.inputmethod.latin.SuggestedWords
|
||||
import org.futo.inputmethod.latin.SuggestedWords.SuggestedWordInfo
|
||||
@ -73,16 +87,11 @@ 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.ClipboardAction
|
||||
import org.futo.inputmethod.latin.uix.actions.EmojiAction
|
||||
import org.futo.inputmethod.latin.uix.actions.RedoAction
|
||||
import org.futo.inputmethod.latin.uix.actions.SettingsAction
|
||||
import org.futo.inputmethod.latin.uix.actions.SystemVoiceInputAction
|
||||
import org.futo.inputmethod.latin.uix.actions.TextEditAction
|
||||
import org.futo.inputmethod.latin.uix.actions.ThemeAction
|
||||
import org.futo.inputmethod.latin.uix.actions.UndoAction
|
||||
import org.futo.inputmethod.latin.uix.actions.ActionRegistry
|
||||
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.settings.useDataStore
|
||||
import org.futo.inputmethod.latin.uix.settings.useDataStoreValueNullable
|
||||
import org.futo.inputmethod.latin.uix.theme.DarkColorScheme
|
||||
import org.futo.inputmethod.latin.uix.theme.Typography
|
||||
import org.futo.inputmethod.latin.uix.theme.UixThemeWrapper
|
||||
@ -244,7 +253,9 @@ fun RowScope.SuggestionItem(words: SuggestedWords, idx: Int, isPrimary: Boolean,
|
||||
) {
|
||||
CompositionLocalProvider(LocalContentColor provides MaterialTheme.colorScheme.onBackground) {
|
||||
if (word != null) {
|
||||
AutoFitText(word, style = textStyle, modifier = textModifier.align(Center).padding(2.dp))
|
||||
AutoFitText(word, style = textStyle, modifier = textModifier
|
||||
.align(Center)
|
||||
.padding(2.dp))
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -331,22 +342,100 @@ fun RowScope.SuggestionItems(words: SuggestedWords, onClick: (i: Int) -> Unit, o
|
||||
}
|
||||
}
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
fun ActionItem(action: Action, onSelect: (Action) -> Unit) {
|
||||
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
|
||||
|
||||
val width = 64.dp
|
||||
val widthPx = with(LocalDensity.current) {
|
||||
width.toPx()
|
||||
}
|
||||
|
||||
val context = LocalContext.current
|
||||
|
||||
val isWindowAction = action.windowImpl != null
|
||||
|
||||
val col = MaterialTheme.colorScheme.secondaryContainer
|
||||
val contentCol = MaterialTheme.colorScheme.onSecondaryContainer
|
||||
IconButton(onClick = { onSelect(action) }, modifier = Modifier
|
||||
|
||||
val modifier = Modifier
|
||||
.offset { IntOffset(offsetX.floatValue.roundToInt(), offsetY.floatValue.roundToInt()) }
|
||||
.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
|
||||
|
||||
if (offsetX.floatValue >= widthPx) {
|
||||
offsetX.floatValue -= widthPx
|
||||
runBlocking {
|
||||
context.setSetting(
|
||||
ExpandableActionItems, ActionRegistry.moveElement(
|
||||
context.getSetting(ExpandableActionItems, DefaultActionsString),
|
||||
action,
|
||||
1
|
||||
)
|
||||
)
|
||||
}
|
||||
} else if (offsetX.floatValue <= -widthPx) {
|
||||
offsetX.floatValue += widthPx
|
||||
runBlocking {
|
||||
context.setSetting(
|
||||
ExpandableActionItems, ActionRegistry.moveElement(
|
||||
context.getSetting(ExpandableActionItems, DefaultActionsString),
|
||||
action,
|
||||
-1
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
.width(width)
|
||||
.let {
|
||||
if (!dragging.value) {
|
||||
it.animateItemPlacement()
|
||||
} else {
|
||||
it.zIndex(10.0f)
|
||||
}
|
||||
}
|
||||
.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)
|
||||
cornerRadius = CornerRadius(radius, radius),
|
||||
style = if(isWindowAction) {
|
||||
Fill
|
||||
} else {
|
||||
Stroke(width = 4.0f)
|
||||
}
|
||||
)
|
||||
}
|
||||
.width(50.dp)
|
||||
.fillMaxHeight(),
|
||||
.fillMaxHeight()
|
||||
|
||||
val contentCol = if(isWindowAction) {
|
||||
MaterialTheme.colorScheme.onSecondaryContainer
|
||||
} else {
|
||||
MaterialTheme.colorScheme.onBackground
|
||||
}
|
||||
|
||||
IconButton(onClick = { onSelect(action) }, modifier = modifier,
|
||||
colors = IconButtonDefaults.iconButtonColors(contentColor = contentCol)
|
||||
) {
|
||||
Icon(
|
||||
@ -371,17 +460,18 @@ fun ActionItemSmall(action: Action, onSelect: (Action) -> Unit) {
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun RowScope.ActionItems(onSelect: (Action) -> Unit) {
|
||||
val systemVoiceInput = useDataStore(key = USE_SYSTEM_VOICE_INPUT.key, default = USE_SYSTEM_VOICE_INPUT.default)
|
||||
fun ActionItems(onSelect: (Action) -> Unit) {
|
||||
val actions = useDataStoreValueNullable(key = ExpandableActionItems, default = DefaultActionsString)
|
||||
|
||||
ActionItem(SettingsAction, onSelect)
|
||||
ActionItem(EmojiAction, onSelect)
|
||||
ActionItem(if(systemVoiceInput.value) { SystemVoiceInputAction } else { VoiceInputAction }, onSelect)
|
||||
ActionItem(ThemeAction, onSelect)
|
||||
ActionItem(UndoAction, onSelect)
|
||||
ActionItem(RedoAction, onSelect)
|
||||
ActionItem(ClipboardAction, onSelect)
|
||||
ActionItem(TextEditAction, onSelect)
|
||||
if(actions != null) {
|
||||
val actionItems = ActionRegistry.stringToActions(actions)
|
||||
|
||||
LazyRow {
|
||||
items(actionItems.size, key = { actionItems[it].name }) {
|
||||
ActionItem(it, actionItems[it], onSelect)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -389,12 +479,6 @@ fun RowScope.ActionItems(onSelect: (Action) -> Unit) {
|
||||
fun ExpandActionsButton(isActionsOpen: Boolean, onClick: () -> Unit) {
|
||||
val moreActionsColor = MaterialTheme.colorScheme.primary
|
||||
|
||||
val moreActionsFill = if(isActionsOpen) {
|
||||
MaterialTheme.colorScheme.primary
|
||||
} else {
|
||||
MaterialTheme.colorScheme.background
|
||||
}
|
||||
|
||||
val actionsContent = if(isActionsOpen) {
|
||||
MaterialTheme.colorScheme.onPrimary
|
||||
} else {
|
||||
@ -414,8 +498,15 @@ fun ExpandActionsButton(isActionsOpen: Boolean, onClick: () -> Unit) {
|
||||
)
|
||||
.fillMaxHeight()
|
||||
.drawBehind {
|
||||
drawCircle(color = moreActionsColor, radius = size.width / 3.0f + 1.0f)
|
||||
drawCircle(color = moreActionsFill, radius = size.width / 3.0f - 2.0f)
|
||||
drawCircle(
|
||||
color = moreActionsColor,
|
||||
radius = size.width / 3.0f + 1.0f,
|
||||
style = if(!isActionsOpen) {
|
||||
Stroke(3.0f)
|
||||
} else {
|
||||
Fill
|
||||
}
|
||||
)
|
||||
},
|
||||
|
||||
colors = IconButtonDefaults.iconButtonColors(contentColor = actionsContent)
|
||||
@ -479,7 +570,11 @@ fun ActionBar(
|
||||
val view = LocalView.current
|
||||
val context = LocalContext.current
|
||||
val isActionsOpen = remember { mutableStateOf(forceOpenActionsInitially) }
|
||||
val systemVoiceInput = useDataStore(key = USE_SYSTEM_VOICE_INPUT.key, default = USE_SYSTEM_VOICE_INPUT.default)
|
||||
|
||||
val activateActionWithHaptic: (Action) -> Unit = {
|
||||
keyboardManagerForAction?.performHapticAndAudioFeedback(Constants.CODE_TAB, view)
|
||||
onActionActivated(it)
|
||||
}
|
||||
|
||||
Surface(modifier = Modifier
|
||||
.fillMaxWidth()
|
||||
@ -498,30 +593,34 @@ fun ActionBar(
|
||||
ImportantNoticeView(importantNotice)
|
||||
}else {
|
||||
|
||||
if (isActionsOpen.value) {
|
||||
LazyRow {
|
||||
item {
|
||||
ActionItems {
|
||||
keyboardManagerForAction?.performHapticAndAudioFeedback(Constants.CODE_TAB, view)
|
||||
onActionActivated(it)
|
||||
}
|
||||
}
|
||||
}
|
||||
} else if (inlineSuggestions.isNotEmpty() && Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
InlineSuggestions(inlineSuggestions)
|
||||
} else if (words != null) {
|
||||
SuggestionItems(words, onClick = {
|
||||
suggestionStripListener.pickSuggestionManually(
|
||||
words.getInfo(it)
|
||||
)
|
||||
keyboardManagerForAction?.performHapticAndAudioFeedback(Constants.CODE_TAB, view)
|
||||
}, onLongClick = { suggestionStripListener.requestForgetWord(words.getInfo(it)) })
|
||||
} else {
|
||||
Spacer(modifier = Modifier.weight(1.0f))
|
||||
AnimatedVisibility(isActionsOpen.value) {
|
||||
ActionItems(activateActionWithHaptic)
|
||||
}
|
||||
|
||||
if (!isActionsOpen.value) {
|
||||
ActionItemSmall(if(systemVoiceInput.value) { SystemVoiceInputAction } else { VoiceInputAction }, onActionActivated)
|
||||
if(!isActionsOpen.value) {
|
||||
if (inlineSuggestions.isNotEmpty() && Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
|
||||
InlineSuggestions(inlineSuggestions)
|
||||
} else if (words != null) {
|
||||
SuggestionItems(
|
||||
words,
|
||||
onClick = {
|
||||
suggestionStripListener.pickSuggestionManually(
|
||||
words.getInfo(it)
|
||||
)
|
||||
keyboardManagerForAction?.performHapticAndAudioFeedback(
|
||||
Constants.CODE_TAB,
|
||||
view
|
||||
)
|
||||
},
|
||||
onLongClick = {
|
||||
suggestionStripListener.requestForgetWord(
|
||||
words.getInfo(it)
|
||||
)
|
||||
})
|
||||
} else {
|
||||
Spacer(modifier = Modifier.weight(1.0f))
|
||||
}
|
||||
ActionItemSmall(VoiceInputAction, activateActionWithHaptic)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -53,6 +53,7 @@ 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.ActionRegistry
|
||||
import org.futo.inputmethod.latin.uix.actions.EmojiAction
|
||||
import org.futo.inputmethod.latin.uix.settings.SettingsActivity
|
||||
import org.futo.inputmethod.latin.uix.theme.ThemeOption
|
||||
@ -202,9 +203,13 @@ class UixManager(private val latinIME: LatinIME) {
|
||||
|
||||
val isMainKeyboardHidden get() = mainKeyboardHidden
|
||||
|
||||
private fun onActionActivated(action: Action) {
|
||||
private fun onActionActivated(rawAction: Action) {
|
||||
latinIME.inputLogic.finishInput()
|
||||
|
||||
val action = runBlocking {
|
||||
ActionRegistry.getActionOverride(latinIME, rawAction)
|
||||
}
|
||||
|
||||
if (action.windowImpl != null) {
|
||||
enterActionWindowView(action)
|
||||
} else if (action.simplePressImpl != null) {
|
||||
|
73
java/src/org/futo/inputmethod/latin/uix/actions/Registry.kt
Normal file
73
java/src/org/futo/inputmethod/latin/uix/actions/Registry.kt
Normal file
@ -0,0 +1,73 @@
|
||||
package org.futo.inputmethod.latin.uix.actions
|
||||
|
||||
import android.content.Context
|
||||
import androidx.datastore.preferences.core.stringPreferencesKey
|
||||
import org.futo.inputmethod.latin.uix.Action
|
||||
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")
|
||||
|
||||
// Note: indices must stay stable
|
||||
val AllActions = listOf(
|
||||
EmojiAction,
|
||||
SettingsAction,
|
||||
ClipboardAction,
|
||||
TextEditAction,
|
||||
ThemeAction,
|
||||
UndoAction,
|
||||
RedoAction,
|
||||
VoiceInputAction,
|
||||
SystemVoiceInputAction,
|
||||
)
|
||||
|
||||
|
||||
object ActionRegistry {
|
||||
fun stringToActions(string: String): List<Action> {
|
||||
return string.split(",").mapNotNull { idx ->
|
||||
idx.toIntOrNull()?.let { AllActions.getOrNull(it) }
|
||||
}
|
||||
}
|
||||
|
||||
fun actionsToString(actions: List<Action>): String {
|
||||
return actions.map { AllActions.indexOf(it) }.joinToString(separator = ",")
|
||||
}
|
||||
|
||||
fun moveElement(string: String, action: Action, direction: Int): String {
|
||||
val actions = stringToActions(string)
|
||||
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)
|
||||
if(useSystemVoiceInput) {
|
||||
SystemVoiceInputAction
|
||||
} else {
|
||||
VoiceInputAction
|
||||
}
|
||||
} else {
|
||||
action
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
val DefaultActions = listOf(
|
||||
EmojiAction,
|
||||
SettingsAction,
|
||||
TextEditAction,
|
||||
ThemeAction,
|
||||
ClipboardAction,
|
||||
UndoAction,
|
||||
RedoAction,
|
||||
)
|
||||
|
||||
val DefaultActionsString = ActionRegistry.actionsToString(DefaultActions)
|
||||
|
||||
|
||||
val DefaultPinnedActions = listOf(VoiceInputAction)
|
||||
val DefaultPinnedActionsString = ActionRegistry.actionsToString(DefaultPinnedActions)
|
@ -52,7 +52,7 @@ import java.util.Locale
|
||||
|
||||
val SystemVoiceInputAction = Action(
|
||||
icon = R.drawable.mic_fill,
|
||||
name = R.string.voice_input_action_title,
|
||||
name = R.string.system_voice_input_action_title,
|
||||
simplePressImpl = { it, _ ->
|
||||
it.triggerSystemVoiceInput()
|
||||
},
|
||||
|
@ -22,10 +22,32 @@ import kotlinx.coroutines.Job
|
||||
import kotlinx.coroutines.flow.Flow
|
||||
import kotlinx.coroutines.flow.map
|
||||
import kotlinx.coroutines.launch
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.coroutines.withContext
|
||||
import org.futo.inputmethod.latin.uix.dataStore
|
||||
import org.futo.inputmethod.latin.uix.getSetting
|
||||
|
||||
data class DataStoreItem<T>(val value: T, val setValue: (T) -> Job)
|
||||
|
||||
@Composable
|
||||
fun <T> useDataStoreValueNullable(key: Preferences.Key<T>, default: T): T? {
|
||||
val context = LocalContext.current
|
||||
|
||||
val initialValue = remember {
|
||||
runBlocking {
|
||||
context.getSetting(key, default)
|
||||
}
|
||||
}
|
||||
|
||||
val valueFlow: Flow<T> = remember {
|
||||
context.dataStore.data.map { preferences ->
|
||||
preferences[key] ?: default
|
||||
}
|
||||
}
|
||||
|
||||
return valueFlow.collectAsState(initial = initialValue).value
|
||||
}
|
||||
|
||||
@Composable
|
||||
fun <T> useDataStore(key: Preferences.Key<T>, default: T): DataStoreItem<T> {
|
||||
val context = LocalContext.current
|
||||
|
Loading…
Reference in New Issue
Block a user