Add undo, redo, paste actions

This commit is contained in:
Aleksandras Kostarevas 2024-01-07 16:20:20 +02:00
parent 9cccdcb79d
commit e2999ada34
11 changed files with 142 additions and 19 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="M16,4h2a2,2 0,0 1,2 2v14a2,2 0,0 1,-2 2H6a2,2 0,0 1,-2 -2V6a2,2 0,0 1,2 -2h2"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="#FFFFFF"
android:strokeLineCap="round"/>
<path
android:pathData="M9,2L15,2A1,1 0,0 1,16 3L16,5A1,1 0,0 1,15 6L9,6A1,1 0,0 1,8 5L8,3A1,1 0,0 1,9 2z"
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="M15,14l5,-5l-5,-5"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="#ffffff"
android:strokeLineCap="round"/>
<path
android:pathData="M4,20v-7a4,4 0,0 1,4 -4h12"
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="M9,14l-5,-5l5,-5"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="#ffffff"
android:strokeLineCap="round"/>
<path
android:pathData="M20,20v-7a4,4 0,0 0,-4 -4H4"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="#ffffff"
android:strokeLineCap="round"/>
</vector>

View File

@ -2,6 +2,9 @@
<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="clipboard_action_title">Paste from Clipboard</string>
<string name="undo_action_title">Undo</string>
<string name="redo_action_title">Redo</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

@ -5,6 +5,8 @@ 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
@ -277,7 +279,9 @@ class LatinIME : InputMethodService(), LifecycleOwner, ViewModelStoreOwner, Save
@Composable @Composable
private fun LegacyKeyboardView(hidden: Boolean) { private fun LegacyKeyboardView(hidden: Boolean) {
val modifier = if(hidden) { val modifier = if(hidden) {
Modifier.clipToBounds().size(0.dp) Modifier
.clipToBounds()
.size(0.dp)
} else { } else {
Modifier.onSizeChanged { Modifier.onSizeChanged {
inputViewHeight = it.height inputViewHeight = it.height
@ -703,6 +707,15 @@ class LatinIME : InputMethodService(), LifecycleOwner, ViewModelStoreOwner, Save
} }
} }
override fun sendCodePointEvent(codePoint: Int) {
latinIMELegacy.onCodeInput(codePoint, NOT_A_COORDINATE, NOT_A_COORDINATE, false)
}
override fun sendKeyEvent(keyCode: Int, metaState: Int) {
latinIMELegacy.mInputLogic.sendDownUpKeyEvent(keyCode, metaState)
}
@RequiresApi(Build.VERSION_CODES.R) @RequiresApi(Build.VERSION_CODES.R)
override fun onCreateInlineSuggestionsRequest(uiExtras: Bundle): InlineSuggestionsRequest { override fun onCreateInlineSuggestionsRequest(uiExtras: Bundle): InlineSuggestionsRequest {
return createInlineSuggestionsRequest(this, this.activeColorScheme) return createInlineSuggestionsRequest(this, this.activeColorScheme)

View File

@ -1132,7 +1132,7 @@ public final class InputLogic {
// As for the case where we don't know the cursor position, it can happen // As for the case where we don't know the cursor position, it can happen
// because of bugs in the framework. But the framework should know, so the next // because of bugs in the framework. But the framework should know, so the next
// best thing is to leave it to whatever it thinks is best. // best thing is to leave it to whatever it thinks is best.
sendDownUpKeyEvent(KeyEvent.KEYCODE_DEL); sendDownUpKeyEvent(KeyEvent.KEYCODE_DEL, 0);
int totalDeletedLength = 1; int totalDeletedLength = 1;
if (mDeleteCount > Constants.DELETE_ACCELERATE_AT) { if (mDeleteCount > Constants.DELETE_ACCELERATE_AT) {
// If this is an accelerated (i.e., double) deletion, then we need to // If this is an accelerated (i.e., double) deletion, then we need to
@ -1140,7 +1140,7 @@ public final class InputLogic {
// the previous word, and will lose it after next deletion. // the previous word, and will lose it after next deletion.
hasUnlearnedWordBeingDeleted |= unlearnWordBeingDeleted( hasUnlearnedWordBeingDeleted |= unlearnWordBeingDeleted(
inputTransaction.mSettingsValues, currentKeyboardScriptId); inputTransaction.mSettingsValues, currentKeyboardScriptId);
sendDownUpKeyEvent(KeyEvent.KEYCODE_DEL); sendDownUpKeyEvent(KeyEvent.KEYCODE_DEL, 0);
totalDeletedLength++; totalDeletedLength++;
} }
StatsUtils.onBackspacePressed(totalDeletedLength); StatsUtils.onBackspacePressed(totalDeletedLength);
@ -2021,13 +2021,13 @@ public final class InputLogic {
* *
* @param keyCode the key code to send inside the key event. * @param keyCode the key code to send inside the key event.
*/ */
private void sendDownUpKeyEvent(final int keyCode) { public void sendDownUpKeyEvent(final int keyCode, final int metaState) {
final long eventTime = SystemClock.uptimeMillis(); final long eventTime = SystemClock.uptimeMillis();
mConnection.sendKeyEvent(new KeyEvent(eventTime, eventTime, mConnection.sendKeyEvent(new KeyEvent(eventTime, eventTime,
KeyEvent.ACTION_DOWN, keyCode, 0, 0, KeyCharacterMap.VIRTUAL_KEYBOARD, 0, KeyEvent.ACTION_DOWN, keyCode, 0, metaState, KeyCharacterMap.VIRTUAL_KEYBOARD, 0,
KeyEvent.FLAG_SOFT_KEYBOARD | KeyEvent.FLAG_KEEP_TOUCH_MODE)); KeyEvent.FLAG_SOFT_KEYBOARD | KeyEvent.FLAG_KEEP_TOUCH_MODE));
mConnection.sendKeyEvent(new KeyEvent(SystemClock.uptimeMillis(), eventTime, mConnection.sendKeyEvent(new KeyEvent(SystemClock.uptimeMillis(), eventTime,
KeyEvent.ACTION_UP, keyCode, 0, 0, KeyCharacterMap.VIRTUAL_KEYBOARD, 0, KeyEvent.ACTION_UP, keyCode, 0, metaState, KeyCharacterMap.VIRTUAL_KEYBOARD, 0,
KeyEvent.FLAG_SOFT_KEYBOARD | KeyEvent.FLAG_KEEP_TOUCH_MODE)); KeyEvent.FLAG_SOFT_KEYBOARD | KeyEvent.FLAG_KEEP_TOUCH_MODE));
} }
@ -2045,7 +2045,7 @@ public final class InputLogic {
// TODO: Remove this special handling of digit letters. // TODO: Remove this special handling of digit letters.
// For backward compatibility. See {@link InputMethodService#sendKeyChar(char)}. // For backward compatibility. See {@link InputMethodService#sendKeyChar(char)}.
if (codePoint >= '0' && codePoint <= '9') { if (codePoint >= '0' && codePoint <= '9') {
sendDownUpKeyEvent(codePoint - '0' + KeyEvent.KEYCODE_0); sendDownUpKeyEvent(codePoint - '0' + KeyEvent.KEYCODE_0, 0);
return; return;
} }
@ -2055,7 +2055,7 @@ public final class InputLogic {
// a hardware keyboard event on pressing enter or delete. This is bad for many // a hardware keyboard event on pressing enter or delete. This is bad for many
// reasons (there are race conditions with commits) but some applications are // reasons (there are race conditions with commits) but some applications are
// relying on this behavior so we continue to support it for older apps. // relying on this behavior so we continue to support it for older apps.
sendDownUpKeyEvent(KeyEvent.KEYCODE_ENTER); sendDownUpKeyEvent(KeyEvent.KEYCODE_ENTER, 0);
} else { } else {
mConnection.commitText(StringUtils.newSingleCodePointString(codePoint), 1); mConnection.commitText(StringUtils.newSingleCodePointString(codePoint), 1);
} }

View File

@ -29,6 +29,9 @@ interface KeyboardManagerForAction {
fun triggerSystemVoiceInput() fun triggerSystemVoiceInput()
fun updateTheme(newTheme: ThemeOption) fun updateTheme(newTheme: ThemeOption)
fun sendCodePointEvent(codePoint: Int)
fun sendKeyEvent(keyCode: Int, metaState: Int)
} }
interface ActionWindow { interface ActionWindow {

View File

@ -13,6 +13,7 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.width import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.ColorScheme import androidx.compose.material3.ColorScheme
import androidx.compose.material3.Icon import androidx.compose.material3.Icon
@ -60,8 +61,11 @@ import org.futo.inputmethod.latin.SuggestedWords
import org.futo.inputmethod.latin.SuggestedWords.SuggestedWordInfo import org.futo.inputmethod.latin.SuggestedWords.SuggestedWordInfo
import org.futo.inputmethod.latin.SuggestedWords.SuggestedWordInfo.KIND_TYPED import org.futo.inputmethod.latin.SuggestedWords.SuggestedWordInfo.KIND_TYPED
import org.futo.inputmethod.latin.suggestions.SuggestionStripView 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.EmojiAction
import org.futo.inputmethod.latin.uix.actions.RedoAction
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.VoiceInputAction import org.futo.inputmethod.latin.uix.actions.VoiceInputAction
import org.futo.inputmethod.latin.uix.theme.DarkColorScheme import org.futo.inputmethod.latin.uix.theme.DarkColorScheme
import org.futo.inputmethod.latin.uix.theme.UixThemeWrapper import org.futo.inputmethod.latin.uix.theme.UixThemeWrapper
@ -287,7 +291,7 @@ fun ActionItem(action: Action, onSelect: (Action) -> Unit) {
cornerRadius = CornerRadius(radius, radius) cornerRadius = CornerRadius(radius, radius)
) )
} }
.width(64.dp) .width(50.dp)
.fillMaxHeight(), .fillMaxHeight(),
colors = IconButtonDefaults.iconButtonColors(contentColor = contentCol) colors = IconButtonDefaults.iconButtonColors(contentColor = contentCol)
) { ) {
@ -317,12 +321,9 @@ fun RowScope.ActionItems(onSelect: (Action) -> Unit) {
ActionItem(EmojiAction, onSelect) ActionItem(EmojiAction, onSelect)
ActionItem(VoiceInputAction, onSelect) ActionItem(VoiceInputAction, onSelect)
ActionItem(ThemeAction, onSelect) ActionItem(ThemeAction, onSelect)
ActionItem(UndoAction, onSelect)
Box(modifier = Modifier ActionItem(RedoAction, onSelect)
.fillMaxHeight() ActionItem(ClipboardAction, onSelect)
.weight(1.0f)) {
}
} }
@ -386,7 +387,11 @@ fun ActionBar(
ExpandActionsButton(isActionsOpen.value) { isActionsOpen.value = !isActionsOpen.value } ExpandActionsButton(isActionsOpen.value) { isActionsOpen.value = !isActionsOpen.value }
if(isActionsOpen.value) { if(isActionsOpen.value) {
ActionItems(onActionActivated) LazyRow {
item {
ActionItems(onActionActivated)
}
}
} else if(inlineSuggestions.isNotEmpty() && Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) { } else if(inlineSuggestions.isNotEmpty() && Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
InlineSuggestions(inlineSuggestions) InlineSuggestions(inlineSuggestions)
} else if(words != null) { } else if(words != null) {
@ -399,7 +404,9 @@ fun ActionBar(
Spacer(modifier = Modifier.weight(1.0f)) Spacer(modifier = Modifier.weight(1.0f))
} }
ActionItemSmall(VoiceInputAction, onActionActivated) if(!isActionsOpen.value) {
ActionItemSmall(VoiceInputAction, onActionActivated)
}
} }
} }
} }

View File

@ -0,0 +1,14 @@
package org.futo.inputmethod.latin.uix.actions
import android.view.KeyEvent
import org.futo.inputmethod.latin.R
import org.futo.inputmethod.latin.uix.Action
val ClipboardAction = Action(
icon = R.drawable.clipboard,
name = R.string.clipboard_action_title,
simplePressImpl = { manager, _ ->
manager.sendKeyEvent(KeyEvent.KEYCODE_V, KeyEvent.META_CTRL_ON)
},
windowImpl = null,
)

View File

@ -55,6 +55,7 @@ import kotlinx.serialization.json.jsonArray
import kotlinx.serialization.json.jsonObject import kotlinx.serialization.json.jsonObject
import kotlinx.serialization.json.jsonPrimitive import kotlinx.serialization.json.jsonPrimitive
import org.futo.inputmethod.latin.R import org.futo.inputmethod.latin.R
import org.futo.inputmethod.latin.common.Constants
import org.futo.inputmethod.latin.uix.Action import org.futo.inputmethod.latin.uix.Action
import org.futo.inputmethod.latin.uix.ActionWindow import org.futo.inputmethod.latin.uix.ActionWindow
import org.futo.inputmethod.latin.uix.PersistentActionState import org.futo.inputmethod.latin.uix.PersistentActionState
@ -293,9 +294,9 @@ val EmojiAction = Action(
}, onExit = { }, onExit = {
manager.closeActionWindow() manager.closeActionWindow()
}, onSpace = { }, onSpace = {
manager.typeText(" ") manager.sendCodePointEvent(Constants.CODE_SPACE)
}, onBackspace = { }, onBackspace = {
manager.backspace(1) manager.sendCodePointEvent(Constants.CODE_DELETE)
}, bitmaps = state.bitmaps, emojis = emojis) }, bitmaps = state.bitmaps, emojis = emojis)
} }
} }

View File

@ -0,0 +1,22 @@
package org.futo.inputmethod.latin.uix.actions
import android.view.KeyEvent
import org.futo.inputmethod.latin.R
import org.futo.inputmethod.latin.uix.Action
val UndoAction = Action(
icon = R.drawable.undo,
name = R.string.undo_action_title,
simplePressImpl = { manager, _ ->
manager.sendKeyEvent(KeyEvent.KEYCODE_Z, KeyEvent.META_CTRL_ON)
},
windowImpl = null,
)
val RedoAction = Action(
icon = R.drawable.redo,
name = R.string.redo_action_title,
simplePressImpl = { manager, _ ->
manager.sendKeyEvent(KeyEvent.KEYCODE_Y, KeyEvent.META_CTRL_ON)
},
windowImpl = null,
)