diff --git a/java/res/drawable/arrow_down.xml b/java/res/drawable/arrow_down.xml
new file mode 100644
index 000000000..9c8ef5e6c
--- /dev/null
+++ b/java/res/drawable/arrow_down.xml
@@ -0,0 +1,20 @@
+
+
+
+
diff --git a/java/res/drawable/arrow_left.xml b/java/res/drawable/arrow_left.xml
index f53ed4d37..51868088b 100644
--- a/java/res/drawable/arrow_left.xml
+++ b/java/res/drawable/arrow_left.xml
@@ -1,20 +1,20 @@
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="24"
+ android:viewportHeight="24">
diff --git a/java/res/drawable/arrow_left_26.xml b/java/res/drawable/arrow_left_26.xml
new file mode 100644
index 000000000..f53ed4d37
--- /dev/null
+++ b/java/res/drawable/arrow_left_26.xml
@@ -0,0 +1,20 @@
+
+
+
+
diff --git a/java/res/drawable/arrow_right.xml b/java/res/drawable/arrow_right.xml
new file mode 100644
index 000000000..5109cd87b
--- /dev/null
+++ b/java/res/drawable/arrow_right.xml
@@ -0,0 +1,20 @@
+
+
+
+
diff --git a/java/res/drawable/arrow_up.xml b/java/res/drawable/arrow_up.xml
new file mode 100644
index 000000000..8d338651a
--- /dev/null
+++ b/java/res/drawable/arrow_up.xml
@@ -0,0 +1,20 @@
+
+
+
+
diff --git a/java/res/drawable/edit_text.xml b/java/res/drawable/edit_text.xml
new file mode 100644
index 000000000..f2d53814c
--- /dev/null
+++ b/java/res/drawable/edit_text.xml
@@ -0,0 +1,20 @@
+
+
+
+
diff --git a/java/res/values/strings-uix.xml b/java/res/values/strings-uix.xml
index ebd092013..ae8682877 100644
--- a/java/res/values/strings-uix.xml
+++ b/java/res/values/strings-uix.xml
@@ -2,9 +2,11 @@
Voice Input
Theme Switcher
+ Emojis
Paste from Clipboard
Undo
Redo
+ Text Editor
AMOLED Dark Purple
AOSP Material Dark
diff --git a/java/res/values/strings.xml b/java/res/values/strings.xml
index d3a7f826c..e7b51e048 100644
--- a/java/res/values/strings.xml
+++ b/java/res/values/strings.xml
@@ -573,5 +573,4 @@ Tip: You can download and remove dictionaries by going to <b>Languages &
This resource is copied from packages/apps/Settings/res/values/strings.xml -->
\u0020ABCDEFGHIJKLMNOPQRSTUVWXYZ
- Emojis
diff --git a/java/src/org/futo/inputmethod/latin/LatinIME.kt b/java/src/org/futo/inputmethod/latin/LatinIME.kt
index c0e8827e0..2f0310894 100644
--- a/java/src/org/futo/inputmethod/latin/LatinIME.kt
+++ b/java/src/org/futo/inputmethod/latin/LatinIME.kt
@@ -5,8 +5,6 @@ import android.content.res.Configuration
import android.inputmethodservice.InputMethodService
import android.os.Build
import android.os.Bundle
-import android.os.SystemClock
-import android.view.KeyCharacterMap
import android.view.KeyEvent
import android.view.View
import android.view.inputmethod.CompletionInfo
@@ -359,7 +357,7 @@ class LatinIME : InputMethodService(), LifecycleOwner, ViewModelStoreOwner, Save
returnBackToMainKeyboardViewFromAction()
}) {
Icon(
- painter = painterResource(id = R.drawable.arrow_left),
+ painter = painterResource(id = R.drawable.arrow_left_26),
contentDescription = "Back"
)
}
diff --git a/java/src/org/futo/inputmethod/latin/uix/ActionBar.kt b/java/src/org/futo/inputmethod/latin/uix/ActionBar.kt
index 8ffbe8a81..b81164b3f 100644
--- a/java/src/org/futo/inputmethod/latin/uix/ActionBar.kt
+++ b/java/src/org/futo/inputmethod/latin/uix/ActionBar.kt
@@ -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.EmojiAction
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.UndoAction
import org.futo.inputmethod.latin.uix.actions.VoiceInputAction
@@ -324,6 +325,7 @@ fun RowScope.ActionItems(onSelect: (Action) -> Unit) {
ActionItem(UndoAction, onSelect)
ActionItem(RedoAction, onSelect)
ActionItem(ClipboardAction, onSelect)
+ ActionItem(TextEditAction, onSelect)
}
diff --git a/java/src/org/futo/inputmethod/latin/uix/actions/EmojiAction.kt b/java/src/org/futo/inputmethod/latin/uix/actions/EmojiAction.kt
index 8cf36d093..174a69295 100644
--- a/java/src/org/futo/inputmethod/latin/uix/actions/EmojiAction.kt
+++ b/java/src/org/futo/inputmethod/latin/uix/actions/EmojiAction.kt
@@ -267,7 +267,7 @@ class PersistentEmojiState: PersistentActionState {
val EmojiAction = Action(
icon = R.drawable.smile,
- name = R.string.title_emojis,
+ name = R.string.emoji_action_title,
simplePressImpl = null,
persistentState = { manager ->
val state = PersistentEmojiState()
@@ -283,7 +283,7 @@ val EmojiAction = Action(
object : ActionWindow {
@Composable
override fun windowName(): String {
- return stringResource(R.string.title_emojis)
+ return stringResource(R.string.emoji_action_title)
}
@Composable
diff --git a/java/src/org/futo/inputmethod/latin/uix/actions/TextEditAction.kt b/java/src/org/futo/inputmethod/latin/uix/actions/TextEditAction.kt
new file mode 100644
index 000000000..1dc1bb0cd
--- /dev/null
+++ b/java/src/org/futo/inputmethod/latin/uix/actions/TextEditAction.kt
@@ -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() {
+ }
+ }
+ }
+)
\ No newline at end of file