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