Add basic text edit mode

This commit is contained in:
Aleksandras Kostarevas 2024-01-08 20:23:54 +02:00
parent 7f17d66072
commit b237e49ece
12 changed files with 425 additions and 14 deletions

View File

@ -0,0 +1,20 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M12,5L12,19"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="#ffffff"
android:strokeLineCap="round"/>
<path
android:pathData="M19,12l-7,7l-7,-7"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="#ffffff"
android:strokeLineCap="round"/>
</vector>

View File

@ -1,20 +1,20 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android" <vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="26dp" android:width="24dp"
android:height="26dp" android:height="24dp"
android:viewportWidth="26" android:viewportWidth="24"
android:viewportHeight="26"> android:viewportHeight="24">
<path <path
android:pathData="M20,13L6,13" android:pathData="M19,12L5,12"
android:strokeLineJoin="round" android:strokeLineJoin="round"
android:strokeWidth="2" android:strokeWidth="2"
android:fillColor="#00000000" android:fillColor="#00000000"
android:strokeColor="#FFFFFF" android:strokeColor="#ffffff"
android:strokeLineCap="round"/> android:strokeLineCap="round"/>
<path <path
android:pathData="M13,20L6,13L13,6" android:pathData="M12,19l-7,-7l7,-7"
android:strokeLineJoin="round" android:strokeLineJoin="round"
android:strokeWidth="2" android:strokeWidth="2"
android:fillColor="#00000000" android:fillColor="#00000000"
android:strokeColor="#FFFFFF" android:strokeColor="#ffffff"
android:strokeLineCap="round"/> android:strokeLineCap="round"/>
</vector> </vector>

View File

@ -0,0 +1,20 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="26dp"
android:height="26dp"
android:viewportWidth="26"
android:viewportHeight="26">
<path
android:pathData="M20,13L6,13"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="#FFFFFF"
android:strokeLineCap="round"/>
<path
android:pathData="M13,20L6,13L13,6"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="#FFFFFF"
android:strokeLineCap="round"/>
</vector>

View File

@ -0,0 +1,20 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M5,12L19,12"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="#ffffff"
android:strokeLineCap="round"/>
<path
android:pathData="M12,5l7,7l-7,7"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="#ffffff"
android:strokeLineCap="round"/>
</vector>

View File

@ -0,0 +1,20 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M12,19L12,5"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="#ffffff"
android:strokeLineCap="round"/>
<path
android:pathData="M5,12l7,-7l7,7"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="#ffffff"
android:strokeLineCap="round"/>
</vector>

View File

@ -0,0 +1,20 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M11,4H4a2,2 0,0 0,-2 2v14a2,2 0,0 0,2 2h14a2,2 0,0 0,2 -2v-7"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="#ffffff"
android:strokeLineCap="round"/>
<path
android:pathData="M18.5,2.5a2.121,2.121 0,0 1,3 3L12,15l-4,1 1,-4 9.5,-9.5z"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="#ffffff"
android:strokeLineCap="round"/>
</vector>

View File

@ -2,9 +2,11 @@
<resources> <resources>
<string name="voice_input_action_title">Voice Input</string> <string name="voice_input_action_title">Voice Input</string>
<string name="theme_switcher_action_title">Theme Switcher</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> <string name="clipboard_action_title">Paste from Clipboard</string>
<string name="undo_action_title">Undo</string> <string name="undo_action_title">Undo</string>
<string name="redo_action_title">Redo</string> <string name="redo_action_title">Redo</string>
<string name="text_edit_action_title">Text Editor</string>
<string name="amoled_dark_theme_name">AMOLED Dark Purple</string> <string name="amoled_dark_theme_name">AMOLED Dark Purple</string>
<string name="classic_material_dark_theme_name">AOSP Material Dark</string> <string name="classic_material_dark_theme_name">AOSP Material Dark</string>

View File

@ -573,5 +573,4 @@ Tip: You can download and remove dictionaries by going to &lt;b>Languages&#160;&
This resource is copied from packages/apps/Settings/res/values/strings.xml --> This resource is copied from packages/apps/Settings/res/values/strings.xml -->
<!-- This resource is corresponding to msgid="5433275485499039199" --> <!-- This resource is corresponding to msgid="5433275485499039199" -->
<string name="user_dict_fast_scroll_alphabet">\u0020ABCDEFGHIJKLMNOPQRSTUVWXYZ</string> <string name="user_dict_fast_scroll_alphabet">\u0020ABCDEFGHIJKLMNOPQRSTUVWXYZ</string>
<string name="title_emojis">Emojis</string>
</resources> </resources>

View File

@ -5,8 +5,6 @@ import android.content.res.Configuration
import android.inputmethodservice.InputMethodService import android.inputmethodservice.InputMethodService
import android.os.Build import android.os.Build
import android.os.Bundle import android.os.Bundle
import android.os.SystemClock
import android.view.KeyCharacterMap
import android.view.KeyEvent import android.view.KeyEvent
import android.view.View import android.view.View
import android.view.inputmethod.CompletionInfo import android.view.inputmethod.CompletionInfo
@ -359,7 +357,7 @@ class LatinIME : InputMethodService(), LifecycleOwner, ViewModelStoreOwner, Save
returnBackToMainKeyboardViewFromAction() returnBackToMainKeyboardViewFromAction()
}) { }) {
Icon( Icon(
painter = painterResource(id = R.drawable.arrow_left), painter = painterResource(id = R.drawable.arrow_left_26),
contentDescription = "Back" contentDescription = "Back"
) )
} }

View File

@ -64,6 +64,7 @@ import org.futo.inputmethod.latin.suggestions.SuggestionStripView
import org.futo.inputmethod.latin.uix.actions.ClipboardAction import org.futo.inputmethod.latin.uix.actions.ClipboardAction
import org.futo.inputmethod.latin.uix.actions.EmojiAction import org.futo.inputmethod.latin.uix.actions.EmojiAction
import org.futo.inputmethod.latin.uix.actions.RedoAction import org.futo.inputmethod.latin.uix.actions.RedoAction
import org.futo.inputmethod.latin.uix.actions.TextEditAction
import org.futo.inputmethod.latin.uix.actions.ThemeAction import org.futo.inputmethod.latin.uix.actions.ThemeAction
import org.futo.inputmethod.latin.uix.actions.UndoAction import org.futo.inputmethod.latin.uix.actions.UndoAction
import org.futo.inputmethod.latin.uix.actions.VoiceInputAction import org.futo.inputmethod.latin.uix.actions.VoiceInputAction
@ -324,6 +325,7 @@ fun RowScope.ActionItems(onSelect: (Action) -> Unit) {
ActionItem(UndoAction, onSelect) ActionItem(UndoAction, onSelect)
ActionItem(RedoAction, onSelect) ActionItem(RedoAction, onSelect)
ActionItem(ClipboardAction, onSelect) ActionItem(ClipboardAction, onSelect)
ActionItem(TextEditAction, onSelect)
} }

View File

@ -267,7 +267,7 @@ class PersistentEmojiState: PersistentActionState {
val EmojiAction = Action( val EmojiAction = Action(
icon = R.drawable.smile, icon = R.drawable.smile,
name = R.string.title_emojis, name = R.string.emoji_action_title,
simplePressImpl = null, simplePressImpl = null,
persistentState = { manager -> persistentState = { manager ->
val state = PersistentEmojiState() val state = PersistentEmojiState()
@ -283,7 +283,7 @@ val EmojiAction = Action(
object : ActionWindow { object : ActionWindow {
@Composable @Composable
override fun windowName(): String { override fun windowName(): String {
return stringResource(R.string.title_emojis) return stringResource(R.string.emoji_action_title)
} }
@Composable @Composable

View File

@ -0,0 +1,310 @@
package org.futo.inputmethod.latin.uix.actions
import android.view.KeyEvent
import androidx.annotation.DrawableRes
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.interaction.collectIsPressedAsState
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.material3.Button
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.graphics.drawscope.translate
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import kotlinx.coroutines.delay
import org.futo.inputmethod.latin.R
import org.futo.inputmethod.latin.uix.Action
import org.futo.inputmethod.latin.uix.ActionWindow
@Composable
fun IconWithColor(@DrawableRes iconId: Int, iconColor: Color, modifier: Modifier = Modifier) {
val icon = painterResource(id = iconId)
Canvas(modifier = modifier) {
translate(
left = this.size.width / 2.0f - icon.intrinsicSize.width / 2.0f,
top = this.size.height / 2.0f - icon.intrinsicSize.height / 2.0f
) {
with(icon) {
draw(
icon.intrinsicSize,
colorFilter = ColorFilter.tint(
iconColor
)
)
}
}
}
}
@Composable
fun RepeatableActionKey(
onTrigger: () -> Unit,
modifier: Modifier = Modifier,
contents: @Composable () -> Unit
) {
val interactionSource = remember { MutableInteractionSource() }
val isPressed by interactionSource.collectIsPressedAsState()
LaunchedEffect(isPressed) {
if(isPressed) {
onTrigger()
delay(670L)
while(isPressed) {
onTrigger()
delay(50L)
}
}
}
Surface(
modifier = modifier.clickable(
interactionSource = interactionSource,
indication = null,
onClick = { }
),
color = MaterialTheme.colorScheme.primary
) {
contents()
}
}
@Composable
@Preview(showBackground = true)
fun TextEditScreen(onCodePoint: (Int) -> Unit = { _ -> }, onEvent: (Int, Int) -> Unit = { _, _ -> }) {
var shiftState by remember { mutableStateOf(false) }
var ctrlState by remember { mutableStateOf(false) }
val metaState = 0 or
(if(shiftState) { KeyEvent.META_SHIFT_ON } else { 0 }) or
(if(ctrlState) { KeyEvent.META_CTRL_ON } else { 0 })
val buttonColorsUntoggled = ButtonDefaults.buttonColors(
containerColor = MaterialTheme.colorScheme.secondaryContainer,
contentColor = MaterialTheme.colorScheme.onSecondaryContainer
)
val buttonColorsToggled = ButtonDefaults.buttonColors(
containerColor = MaterialTheme.colorScheme.secondary,
contentColor = MaterialTheme.colorScheme.onSecondary
)
val sendEvent = { keycode: Int -> onEvent(keycode, metaState) }
val keySize = 48.dp
Column {
Row(modifier = Modifier.fillMaxWidth()) {
Spacer(modifier = Modifier.weight(1.0f))
Box(
modifier = Modifier
.height(keySize * 2)
.width(keySize * 3)
) {
RepeatableActionKey(
modifier = Modifier
.align(Alignment.TopStart)
.width(keySize)
.height(keySize),
onTrigger = { sendEvent(KeyEvent.KEYCODE_INSERT) }
) {
Text("Ins", modifier = Modifier.padding(4.dp))
}
RepeatableActionKey(
modifier = Modifier
.align(Alignment.BottomStart)
.width(keySize)
.height(keySize),
onTrigger = { sendEvent(KeyEvent.KEYCODE_DEL) }
) {
Text("Del", modifier = Modifier.padding(4.dp))
}
RepeatableActionKey(
modifier = Modifier
.align(Alignment.TopCenter)
.width(keySize)
.height(keySize),
onTrigger = { sendEvent(KeyEvent.KEYCODE_MOVE_HOME) }
) {
Text("Home", modifier = Modifier.padding(4.dp))
}
RepeatableActionKey(
modifier = Modifier
.align(Alignment.BottomCenter)
.width(keySize)
.height(keySize),
onTrigger = { sendEvent(KeyEvent.KEYCODE_MOVE_END) }
) {
Text("End", modifier = Modifier.padding(4.dp))
}
RepeatableActionKey(
modifier = Modifier
.align(Alignment.TopEnd)
.width(keySize)
.height(keySize),
onTrigger = { sendEvent(KeyEvent.KEYCODE_PAGE_UP) }
) {
Text("PgUp", modifier = Modifier.padding(4.dp))
}
RepeatableActionKey(
modifier = Modifier
.align(Alignment.BottomEnd)
.width(keySize)
.height(keySize),
onTrigger = { sendEvent(KeyEvent.KEYCODE_PAGE_DOWN) }
) {
Text("PgDn", modifier = Modifier.padding(4.dp))
}
}
}
Spacer(modifier = Modifier.height(32.dp))
Row(modifier = Modifier.fillMaxWidth()) {
Box(modifier = Modifier.height(92.dp)) {
Button(
onClick = { shiftState = !shiftState },
colors = if (shiftState) {
buttonColorsToggled
} else {
buttonColorsUntoggled
},
modifier = Modifier.align(Alignment.TopStart).width(keySize * 3)
) {
Row {
Text("Shift")
}
}
Button(
onClick = { ctrlState = !ctrlState },
colors = if (ctrlState) {
buttonColorsToggled
} else {
buttonColorsUntoggled
},
modifier = Modifier.align(Alignment.BottomStart).width(keySize * 2)
) {
Text("Ctrl")
}
}
Spacer(modifier = Modifier.weight(1.0f))
Box(
modifier = Modifier
.height(keySize * 2)
.width(keySize * 3)
) {
RepeatableActionKey(
modifier = Modifier
.align(Alignment.TopCenter)
.width(keySize)
.height(keySize),
onTrigger = { sendEvent(KeyEvent.KEYCODE_DPAD_UP) }
) {
IconWithColor(
iconId = R.drawable.arrow_up,
iconColor = MaterialTheme.colorScheme.onPrimary
)
}
RepeatableActionKey(
modifier = Modifier
.align(Alignment.BottomCenter)
.width(keySize)
.height(keySize),
onTrigger = { sendEvent(KeyEvent.KEYCODE_DPAD_DOWN) }
) {
IconWithColor(
iconId = R.drawable.arrow_down,
iconColor = MaterialTheme.colorScheme.onPrimary
)
}
RepeatableActionKey(
modifier = Modifier
.align(Alignment.BottomStart)
.width(keySize)
.height(keySize),
onTrigger = { sendEvent(KeyEvent.KEYCODE_DPAD_LEFT) }
) {
IconWithColor(
iconId = R.drawable.arrow_left,
iconColor = MaterialTheme.colorScheme.onPrimary
)
}
RepeatableActionKey(
modifier = Modifier
.align(Alignment.BottomEnd)
.width(keySize)
.height(keySize),
onTrigger = { sendEvent(KeyEvent.KEYCODE_DPAD_RIGHT) }
) {
IconWithColor(
iconId = R.drawable.arrow_right,
iconColor = MaterialTheme.colorScheme.onPrimary
)
}
}
}
}
}
val TextEditAction = Action(
icon = R.drawable.edit_text,
name = R.string.text_edit_action_title,
simplePressImpl = null,
persistentState = null,
windowImpl = { manager, persistentState ->
object : ActionWindow {
@Composable
override fun windowName(): String {
return stringResource(R.string.text_edit_action_title)
}
@Composable
override fun WindowContents() {
TextEditScreen(onCodePoint = { a -> manager.sendCodePointEvent(a)}, onEvent = { a, b -> manager.sendKeyEvent(a, b) })
}
override fun close() {
}
}
}
)