Merge branch 'lm-2-finetuning-whisperggml' into 'lm-2-finetuning'

Add additional actions and cursor sliding

See merge request alex/latinime!5
This commit is contained in:
Aleksandras Kostarevas 2024-01-09 17:00:24 +00:00
commit 05f5f9fe8f
46 changed files with 1558 additions and 428 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,13 @@
<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="M6,9l6,6l6,-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="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="M18,6L6,18"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="#ffffff"
android:strokeLineCap="round"/>
<path
android:pathData="M6,6L18,18"
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,9L20,9A2,2 0,0 1,22 11L22,20A2,2 0,0 1,20 22L11,22A2,2 0,0 1,9 20L9,11A2,2 0,0 1,11 9z"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="#ffffff"
android:strokeLineCap="round"/>
<path
android:pathData="M5,15H4a2,2 0,0 1,-2 -2V4a2,2 0,0 1,2 -2h9a2,2 0,0 1,2 2v1"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="#ffffff"
android:strokeLineCap="round"/>
</vector>

View File

@ -0,0 +1,27 @@
<vector android:height="24dp" android:viewportHeight="26"
android:viewportWidth="26" android:width="24dp" xmlns:android="http://schemas.android.com/apk/res/android">
<path android:fillColor="#00000000"
android:pathData="M1.995,15.995L23.995,15.995"
android:strokeColor="#FFFFFF" android:strokeLineCap="round"
android:strokeLineJoin="round" android:strokeWidth="2"/>
<path android:fillColor="#00000000"
android:pathData="M24.005,11.995L24.005,15.995"
android:strokeColor="#FFFFFF" android:strokeLineCap="round"
android:strokeLineJoin="round" android:strokeWidth="2"/>
<path android:fillColor="#00000000"
android:pathData="M5.995,10.995a1,1 0,1 0,2 0a1,1 0,1 0,-2 0z"
android:strokeColor="#FFFFFF" android:strokeLineCap="round"
android:strokeLineJoin="round" android:strokeWidth="2"/>
<path android:fillColor="#00000000"
android:pathData="M11.995,10.995a1,1 0,1 0,2 0a1,1 0,1 0,-2 0z"
android:strokeColor="#FFFFFF" android:strokeLineCap="round"
android:strokeLineJoin="round" android:strokeWidth="2"/>
<path android:fillColor="#00000000"
android:pathData="M2.005,11.995L2.005,15.995"
android:strokeColor="#FFFFFF" android:strokeLineCap="round"
android:strokeLineJoin="round" android:strokeWidth="2"/>
<path android:fillColor="#00000000"
android:pathData="M17.995,10.995a1,1 0,1 0,2 0a1,1 0,1 0,-2 0z"
android:strokeColor="#FFFFFF" android:strokeLineCap="round"
android:strokeLineJoin="round" android:strokeWidth="2"/>
</vector>

41
java/res/drawable/cut.xml Normal file
View File

@ -0,0 +1,41 @@
<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="M6,6m-3,0a3,3 0,1 1,6 0a3,3 0,1 1,-6 0"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="#ffffff"
android:strokeLineCap="round"/>
<path
android:pathData="M6,18m-3,0a3,3 0,1 1,6 0a3,3 0,1 1,-6 0"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="#ffffff"
android:strokeLineCap="round"/>
<path
android:pathData="M20,4L8.12,15.88"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="#ffffff"
android:strokeLineCap="round"/>
<path
android:pathData="M14.47,14.48L20,20"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="#ffffff"
android:strokeLineCap="round"/>
<path
android:pathData="M8.12,8.12L12,12"
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

@ -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

@ -421,6 +421,7 @@
<attr name="keyTextShadowColor" format="color" /> <attr name="keyTextShadowColor" format="color" />
<!-- Color to use for the label in a key when in inactivated state. --> <!-- Color to use for the label in a key when in inactivated state. -->
<attr name="keyTextInactivatedColor" format="color" /> <attr name="keyTextInactivatedColor" format="color" />
<attr name="keyPressedTextColor" format="color" />
<!-- Color to use for the label in a key that has followFunctionalTextColor keyLabelFlags. --> <!-- Color to use for the label in a key that has followFunctionalTextColor keyLabelFlags. -->
<attr name="functionalTextColor" format="color" /> <attr name="functionalTextColor" format="color" />
<!-- Key hint letter (= one character hint label) color --> <!-- Key hint letter (= one character hint label) color -->

View File

@ -2,6 +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="undo_action_title">Undo</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

@ -48,6 +48,7 @@
<item name="spacebarBackground">@drawable/btn_keyboard_spacebar_lxx_dark</item> <item name="spacebarBackground">@drawable/btn_keyboard_spacebar_lxx_dark</item>
<item name="keyTextColor">@color/key_text_color_lxx_dark</item> <item name="keyTextColor">@color/key_text_color_lxx_dark</item>
<item name="keyTextInactivatedColor">@color/key_functional_text_color_lxx_dark</item> <item name="keyTextInactivatedColor">@color/key_functional_text_color_lxx_dark</item>
<item name="keyPressedTextColor">@color/key_text_color_lxx_dark</item>
<item name="functionalTextColor">@color/key_text_color_lxx_dark</item> <item name="functionalTextColor">@color/key_text_color_lxx_dark</item>
<item name="keyHintLetterColor">@color/key_hint_letter_color_lxx_dark</item> <item name="keyHintLetterColor">@color/key_hint_letter_color_lxx_dark</item>
<item name="keyHintLabelColor">@color/key_text_inactive_color_lxx_dark</item> <item name="keyHintLabelColor">@color/key_text_inactive_color_lxx_dark</item>

View File

@ -48,6 +48,7 @@
<item name="spacebarBackground">@drawable/btn_keyboard_spacebar_lxx_light</item> <item name="spacebarBackground">@drawable/btn_keyboard_spacebar_lxx_light</item>
<item name="keyTextColor">@color/key_text_color_lxx_light</item> <item name="keyTextColor">@color/key_text_color_lxx_light</item>
<item name="keyTextInactivatedColor">@color/key_text_inactive_color_lxx_light</item> <item name="keyTextInactivatedColor">@color/key_text_inactive_color_lxx_light</item>
<item name="keyPressedTextColor">@color/key_text_color_lxx_light</item>
<item name="functionalTextColor">@color/key_functional_text_color_lxx_light</item> <item name="functionalTextColor">@color/key_functional_text_color_lxx_light</item>
<item name="keyHintLetterColor">@color/key_hint_letter_color_lxx_light</item> <item name="keyHintLetterColor">@color/key_hint_letter_color_lxx_light</item>
<item name="keyHintLabelColor">@color/key_text_inactive_color_lxx_light</item> <item name="keyHintLabelColor">@color/key_text_inactive_color_lxx_light</item>

View File

@ -23,16 +23,26 @@
> >
<Key <Key
latin:keySpec="a" latin:keySpec="a"
latin:keyHintLabel="\@"
latin:additionalMoreKeys="\\@"
latin:moreKeys="!text/morekeys_a" /> latin:moreKeys="!text/morekeys_a" />
<Key <Key
latin:keySpec="s" latin:keySpec="s"
latin:keyHintLabel="#"
latin:additionalMoreKeys="#"
latin:moreKeys="!text/morekeys_s" /> latin:moreKeys="!text/morekeys_s" />
<Key <Key
latin:keySpec="d" latin:keySpec="d"
latin:keyHintLabel="$"
latin:additionalMoreKeys="&#x24;,&#x20AC;,&#x00A3;,&#x00A5;,&#x00A2;"
latin:moreKeys="!text/morekeys_d" /> latin:moreKeys="!text/morekeys_d" />
<Key <Key
latin:keySpec="f" /> latin:keySpec="f"
latin:keyHintLabel="%"
latin:additionalMoreKeys="%" />
<Key <Key
latin:keySpec="g" latin:keySpec="g"
latin:keyHintLabel="&amp;"
latin:additionalMoreKeys="&amp;,|"
latin:moreKeys="!text/morekeys_g" /> latin:moreKeys="!text/morekeys_g" />
</merge> </merge>

View File

@ -23,14 +23,22 @@
> >
<Key <Key
latin:keySpec="h" latin:keySpec="h"
latin:keyHintLabel="-"
latin:additionalMoreKeys="-,&#x2013;,&#x2014;,_"
latin:moreKeys="!text/morekeys_h" /> latin:moreKeys="!text/morekeys_h" />
<Key <Key
latin:keySpec="j" latin:keySpec="j"
latin:keyHintLabel="+"
latin:additionalMoreKeys="+,="
latin:moreKeys="!text/morekeys_j" /> latin:moreKeys="!text/morekeys_j" />
<Key <Key
latin:keySpec="k" latin:keySpec="k"
latin:keyHintLabel="("
latin:additionalMoreKeys="(,[,{,&#x3c;"
latin:moreKeys="!text/morekeys_k" /> latin:moreKeys="!text/morekeys_k" />
<Key <Key
latin:keySpec="l" latin:keySpec="l"
latin:keyHintLabel=")"
latin:additionalMoreKeys="),],},&#x3e;"
latin:moreKeys="!text/morekeys_l" /> latin:moreKeys="!text/morekeys_l" />
</merge> </merge>

View File

@ -23,14 +23,22 @@
> >
<Key <Key
latin:keySpec="z" latin:keySpec="z"
latin:keyHintLabel="*"
latin:additionalMoreKeys="*"
latin:moreKeys="!text/morekeys_z" /> latin:moreKeys="!text/morekeys_z" />
<Key <Key
latin:keySpec="!text/keyspec_x" latin:keySpec="!text/keyspec_x"
latin:keyHintLabel="&quot;"
latin:additionalMoreKeys="&quot;"
latin:moreKeys="!text/morekeys_x" /> latin:moreKeys="!text/morekeys_x" />
<Key <Key
latin:keySpec="c" latin:keySpec="c"
latin:keyHintLabel="'"
latin:additionalMoreKeys="'"
latin:moreKeys="!text/morekeys_c" /> latin:moreKeys="!text/morekeys_c" />
<Key <Key
latin:keySpec="v" latin:keySpec="v"
latin:keyHintLabel=":"
latin:additionalMoreKeys=":"
latin:moreKeys="!text/morekeys_v" /> latin:moreKeys="!text/morekeys_v" />
</merge> </merge>

View File

@ -22,10 +22,16 @@
xmlns:latin="http://schemas.android.com/apk/res/org.futo.inputmethod.latin" xmlns:latin="http://schemas.android.com/apk/res/org.futo.inputmethod.latin"
> >
<Key <Key
latin:keySpec="b" /> latin:keySpec="b"
latin:keyHintLabel=";"
latin:additionalMoreKeys=";" />
<Key <Key
latin:keySpec="n" latin:keySpec="n"
latin:keyHintLabel="!"
latin:additionalMoreKeys="!"
latin:moreKeys="!text/morekeys_n" /> latin:moreKeys="!text/morekeys_n" />
<Key <Key
latin:keySpec="m" /> latin:keySpec="m"
latin:keyHintLabel="?"
latin:additionalMoreKeys="?,/" />
</merge> </merge>

View File

@ -665,6 +665,10 @@ public class Key implements Comparable<Key> {
if ((mLabelFlags & LABEL_FLAGS_FOLLOW_FUNCTIONAL_TEXT_COLOR) != 0) { if ((mLabelFlags & LABEL_FLAGS_FOLLOW_FUNCTIONAL_TEXT_COLOR) != 0) {
return params.mFunctionalTextColor; return params.mFunctionalTextColor;
} }
if (mPressed) {
return params.mPressedTextColor;
}
return isShiftedLetterActivated() ? params.mTextInactivatedColor : params.mTextColor; return isShiftedLetterActivated() ? params.mTextInactivatedColor : params.mTextColor;
} }

View File

@ -101,6 +101,11 @@ public interface KeyboardActionListener {
*/ */
public boolean onCustomRequest(int requestCode); public boolean onCustomRequest(int requestCode);
public void onMovePointer(int steps);
public void onMoveDeletePointer(int steps);
public void onUpWithDeletePointerActive();
public void onUpWithPointerActive();
public static final KeyboardActionListener EMPTY_LISTENER = new Adapter(); public static final KeyboardActionListener EMPTY_LISTENER = new Adapter();
public static class Adapter implements KeyboardActionListener { public static class Adapter implements KeyboardActionListener {
@ -125,8 +130,14 @@ public interface KeyboardActionListener {
@Override @Override
public void onFinishSlidingInput() {} public void onFinishSlidingInput() {}
@Override @Override
public boolean onCustomRequest(int requestCode) { public boolean onCustomRequest(int requestCode) { return false; }
return false; @Override
} public void onMovePointer(int steps) {}
@Override
public void onMoveDeletePointer(int steps) {}
@Override
public void onUpWithDeletePointerActive() {}
@Override
public void onUpWithPointerActive() {}
} }
} }

View File

@ -176,6 +176,11 @@ public class KeyboardView extends View {
R.styleable.Keyboard_Key, defStyle, R.style.KeyboardView); R.styleable.Keyboard_Key, defStyle, R.style.KeyboardView);
mDefaultKeyLabelFlags = keyAttr.getInt(R.styleable.Keyboard_Key_keyLabelFlags, 0); mDefaultKeyLabelFlags = keyAttr.getInt(R.styleable.Keyboard_Key_keyLabelFlags, 0);
mKeyVisualAttributes = KeyVisualAttributes.newInstance(keyAttr, mDrawableProvider); mKeyVisualAttributes = KeyVisualAttributes.newInstance(keyAttr, mDrawableProvider);
if(isMoreKeys && mKeyVisualAttributes != null) {
mKeyVisualAttributes.mTextColor = mDrawableProvider.getMoreKeysTextColor();
}
keyAttr.recycle(); keyAttr.recycle();
mPaint.setAntiAlias(true); mPaint.setAntiAlias(true);

View File

@ -85,6 +85,8 @@ public final class PointerTracker implements PointerTrackerQueue.Element,
// Parameters for pointer handling. // Parameters for pointer handling.
private static PointerTrackerParams sParams; private static PointerTrackerParams sParams;
private static final int sPointerStep = (int)(16.0 * Resources.getSystem().getDisplayMetrics().density);
private static GestureStrokeRecognitionParams sGestureStrokeRecognitionParams; private static GestureStrokeRecognitionParams sGestureStrokeRecognitionParams;
private static GestureStrokeDrawingParams sGestureStrokeDrawingParams; private static GestureStrokeDrawingParams sGestureStrokeDrawingParams;
private static boolean sNeedsPhantomSuddenMoveEventHack; private static boolean sNeedsPhantomSuddenMoveEventHack;
@ -128,6 +130,11 @@ public final class PointerTracker implements PointerTrackerQueue.Element,
private int mLastX; private int mLastX;
private int mLastY; private int mLastY;
private int mStartX;
private int mStartY;
private long mStartTime;
private boolean mCursorMoved = false;
// true if keyboard layout has been changed. // true if keyboard layout has been changed.
private boolean mKeyboardLayoutHasBeenChanged; private boolean mKeyboardLayoutHasBeenChanged;
@ -691,6 +698,10 @@ public final class PointerTracker implements PointerTrackerQueue.Element,
startRepeatKey(key); startRepeatKey(key);
startLongPressTimer(key); startLongPressTimer(key);
setPressedKeyGraphics(key, eventTime); setPressedKeyGraphics(key, eventTime);
mStartX = x;
mStartY = y;
mStartTime = System.currentTimeMillis();
} }
} }
@ -892,6 +903,29 @@ public final class PointerTracker implements PointerTrackerQueue.Element,
final int lastX = mLastX; final int lastX = mLastX;
final int lastY = mLastY; final int lastY = mLastY;
final Key oldKey = mCurrentKey; final Key oldKey = mCurrentKey;
if (oldKey != null && oldKey.getCode() == Constants.CODE_SPACE) {
int steps = (x - mStartX) / sPointerStep;
final int swipeIgnoreTime = Settings.getInstance().getCurrent().mKeyLongpressTimeout / MULTIPLIER_FOR_LONG_PRESS_TIMEOUT_IN_SLIDING_INPUT;
if (steps != 0 && mStartTime + swipeIgnoreTime < System.currentTimeMillis()) {
mCursorMoved = true;
mStartX += steps * sPointerStep;
sListener.onMovePointer(steps);
}
return;
}
if (oldKey != null && oldKey.getCode() == Constants.CODE_DELETE) {
int steps = (x - mStartX) / sPointerStep;
if (steps != 0) {
sTimerProxy.cancelKeyTimersOf(this);
mCursorMoved = true;
mStartX += steps * sPointerStep;
sListener.onMoveDeletePointer(steps);
}
return;
}
final Key newKey = onMoveKey(x, y); final Key newKey = onMoveKey(x, y);
if (sGestureEnabler.shouldHandleGesture()) { if (sGestureEnabler.shouldHandleGesture()) {
@ -966,6 +1000,14 @@ public final class PointerTracker implements PointerTrackerQueue.Element,
// Release the last pressed key. // Release the last pressed key.
setReleasedKeyGraphics(currentKey, true /* withAnimation */); setReleasedKeyGraphics(currentKey, true /* withAnimation */);
if(mCursorMoved && currentKey.getCode() == Constants.CODE_DELETE) {
sListener.onUpWithDeletePointerActive();
}
if(mCursorMoved) {
sListener.onUpWithPointerActive();
}
if (isShowingMoreKeysPanel()) { if (isShowingMoreKeysPanel()) {
if (!mIsTrackingForActionDisabled) { if (!mIsTrackingForActionDisabled) {
final int translatedX = mMoreKeysPanel.translateX(x); final int translatedX = mMoreKeysPanel.translateX(x);
@ -988,6 +1030,10 @@ public final class PointerTracker implements PointerTrackerQueue.Element,
return; return;
} }
if (mCursorMoved) {
mCursorMoved = false;
return;
}
if (mIsTrackingForActionDisabled) { if (mIsTrackingForActionDisabled) {
return; return;
} }
@ -1018,6 +1064,9 @@ public final class PointerTracker implements PointerTrackerQueue.Element,
if (isShowingMoreKeysPanel()) { if (isShowingMoreKeysPanel()) {
return; return;
} }
if (mCursorMoved) {
return;
}
final Key key = getKey(); final Key key = getKey();
if (key == null) { if (key == null) {
return; return;

View File

@ -37,6 +37,7 @@ public final class KeyDrawParams {
public int mTextColor; public int mTextColor;
public int mTextInactivatedColor; public int mTextInactivatedColor;
public int mPressedTextColor;
public int mTextShadowColor; public int mTextShadowColor;
public int mFunctionalTextColor; public int mFunctionalTextColor;
public int mHintLetterColor; public int mHintLetterColor;
@ -66,6 +67,7 @@ public final class KeyDrawParams {
mTextColor = copyFrom.mTextColor; mTextColor = copyFrom.mTextColor;
mTextInactivatedColor = copyFrom.mTextInactivatedColor; mTextInactivatedColor = copyFrom.mTextInactivatedColor;
mPressedTextColor = copyFrom.mPressedTextColor;
mTextShadowColor = copyFrom.mTextShadowColor; mTextShadowColor = copyFrom.mTextShadowColor;
mFunctionalTextColor = copyFrom.mFunctionalTextColor; mFunctionalTextColor = copyFrom.mFunctionalTextColor;
mHintLetterColor = copyFrom.mHintLetterColor; mHintLetterColor = copyFrom.mHintLetterColor;
@ -103,6 +105,7 @@ public final class KeyDrawParams {
mTextColor = selectColor(attr.mTextColor, mTextColor); mTextColor = selectColor(attr.mTextColor, mTextColor);
mTextInactivatedColor = selectColor(attr.mTextInactivatedColor, mTextInactivatedColor); mTextInactivatedColor = selectColor(attr.mTextInactivatedColor, mTextInactivatedColor);
mPressedTextColor = selectColor(attr.mPressedTextColor, mPressedTextColor);
mTextShadowColor = selectColor(attr.mTextShadowColor, mTextShadowColor); mTextShadowColor = selectColor(attr.mTextShadowColor, mTextShadowColor);
mFunctionalTextColor = selectColor(attr.mFunctionalTextColor, mFunctionalTextColor); mFunctionalTextColor = selectColor(attr.mFunctionalTextColor, mFunctionalTextColor);
mHintLetterColor = selectColor(attr.mHintLetterColor, mHintLetterColor); mHintLetterColor = selectColor(attr.mHintLetterColor, mHintLetterColor);

View File

@ -41,8 +41,9 @@ public final class KeyVisualAttributes {
public final float mHintLabelRatio; public final float mHintLabelRatio;
public final float mPreviewTextRatio; public final float mPreviewTextRatio;
public final int mTextColor; public int mTextColor;
public final int mTextInactivatedColor; public final int mTextInactivatedColor;
public final int mPressedTextColor;
public final int mTextShadowColor; public final int mTextShadowColor;
public final int mFunctionalTextColor; public final int mFunctionalTextColor;
public final int mHintLetterColor; public final int mHintLetterColor;
@ -130,6 +131,8 @@ public final class KeyVisualAttributes {
R.styleable.Keyboard_Key_keyTextColor, 0, keyAttr, provider); R.styleable.Keyboard_Key_keyTextColor, 0, keyAttr, provider);
mTextInactivatedColor = DynamicThemeProvider.Companion.getColorOrDefault( mTextInactivatedColor = DynamicThemeProvider.Companion.getColorOrDefault(
R.styleable.Keyboard_Key_keyTextInactivatedColor, 0, keyAttr, provider); R.styleable.Keyboard_Key_keyTextInactivatedColor, 0, keyAttr, provider);
mPressedTextColor = DynamicThemeProvider.Companion.getColorOrDefault(
R.styleable.Keyboard_Key_keyPressedTextColor, 0, keyAttr, provider);
mTextShadowColor = DynamicThemeProvider.Companion.getColorOrDefault( mTextShadowColor = DynamicThemeProvider.Companion.getColorOrDefault(
R.styleable.Keyboard_Key_keyTextShadowColor, 0, keyAttr, provider); R.styleable.Keyboard_Key_keyTextShadowColor, 0, keyAttr, provider);
mFunctionalTextColor = DynamicThemeProvider.Companion.getColorOrDefault( mFunctionalTextColor = DynamicThemeProvider.Companion.getColorOrDefault(

View File

@ -302,7 +302,7 @@ public final class KeyboardTextsTable {
/* ~ additional_morekeys_symbols_0 */ /* ~ additional_morekeys_symbols_0 */
/* morekeys_tablet_period */ "!text/morekeys_tablet_punctuation", /* morekeys_tablet_period */ "!text/morekeys_tablet_punctuation",
/* morekeys_nordic_row2_11 */ EMPTY, /* morekeys_nordic_row2_11 */ EMPTY,
/* morekeys_punctuation */ "!autoColumnOrder!8,\\,,?,!,#,!text/keyspec_right_parenthesis,!text/keyspec_left_parenthesis,/,;,',@,:,-,\",+,\\%,&", /* morekeys_punctuation */ "_,\\\\,|,=",
/* keyspec_tablet_comma */ ",", /* keyspec_tablet_comma */ ",",
// Period key // Period key
/* keyspec_period */ ".", /* keyspec_period */ ".",

View File

@ -1,6 +1,5 @@
package org.futo.inputmethod.latin package org.futo.inputmethod.latin
import android.content.Context
import android.content.res.Configuration import android.content.res.Configuration
import android.inputmethodservice.InputMethodService import android.inputmethodservice.InputMethodService
import android.os.Build import android.os.Build
@ -13,34 +12,17 @@ import android.view.inputmethod.InlineSuggestionsRequest
import android.view.inputmethod.InlineSuggestionsResponse import android.view.inputmethod.InlineSuggestionsResponse
import android.view.inputmethod.InputMethodSubtype import android.view.inputmethod.InputMethodSubtype
import androidx.annotation.RequiresApi import androidx.annotation.RequiresApi
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.size import androidx.compose.foundation.layout.size
import androidx.compose.material3.ColorScheme import androidx.compose.material3.ColorScheme
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.key import androidx.compose.runtime.key
import androidx.compose.ui.Alignment.Companion.CenterVertically
import androidx.compose.ui.Modifier import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clipToBounds import androidx.compose.ui.draw.clipToBounds
import androidx.compose.ui.layout.onSizeChanged import androidx.compose.ui.layout.onSizeChanged
import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.ViewCompositionStrategy
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.unit.dp import androidx.compose.ui.unit.dp
import androidx.compose.ui.viewinterop.AndroidView import androidx.compose.ui.viewinterop.AndroidView
import androidx.lifecycle.Lifecycle import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleCoroutineScope
import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.LifecycleRegistry import androidx.lifecycle.LifecycleRegistry
import androidx.lifecycle.ViewModelStore import androidx.lifecycle.ViewModelStore
@ -56,34 +38,24 @@ import androidx.savedstate.SavedStateRegistryOwner
import androidx.savedstate.findViewTreeSavedStateRegistryOwner import androidx.savedstate.findViewTreeSavedStateRegistryOwner
import androidx.savedstate.setViewTreeSavedStateRegistryOwner import androidx.savedstate.setViewTreeSavedStateRegistryOwner
import kotlinx.coroutines.Job import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
import kotlinx.coroutines.runBlocking import kotlinx.coroutines.runBlocking
import org.futo.inputmethod.latin.common.Constants
import org.futo.inputmethod.latin.uix.Action
import org.futo.inputmethod.latin.uix.ActionBar
import org.futo.inputmethod.latin.uix.ActionInputTransaction
import org.futo.inputmethod.latin.uix.ActionWindow
import org.futo.inputmethod.latin.uix.BasicThemeProvider import org.futo.inputmethod.latin.uix.BasicThemeProvider
import org.futo.inputmethod.latin.uix.DynamicThemeProvider import org.futo.inputmethod.latin.uix.DynamicThemeProvider
import org.futo.inputmethod.latin.uix.DynamicThemeProviderOwner import org.futo.inputmethod.latin.uix.DynamicThemeProviderOwner
import org.futo.inputmethod.latin.uix.KeyboardManagerForAction
import org.futo.inputmethod.latin.uix.PersistentActionState
import org.futo.inputmethod.latin.uix.THEME_KEY import org.futo.inputmethod.latin.uix.THEME_KEY
import org.futo.inputmethod.latin.uix.UixManager
import org.futo.inputmethod.latin.uix.createInlineSuggestionsRequest import org.futo.inputmethod.latin.uix.createInlineSuggestionsRequest
import org.futo.inputmethod.latin.uix.deferGetSetting import org.futo.inputmethod.latin.uix.deferGetSetting
import org.futo.inputmethod.latin.uix.deferSetSetting import org.futo.inputmethod.latin.uix.deferSetSetting
import org.futo.inputmethod.latin.uix.differsFrom import org.futo.inputmethod.latin.uix.differsFrom
import org.futo.inputmethod.latin.uix.inflateInlineSuggestion
import org.futo.inputmethod.latin.uix.theme.DarkColorScheme import org.futo.inputmethod.latin.uix.theme.DarkColorScheme
import org.futo.inputmethod.latin.uix.theme.ThemeOption import org.futo.inputmethod.latin.uix.theme.ThemeOption
import org.futo.inputmethod.latin.uix.theme.ThemeOptions import org.futo.inputmethod.latin.uix.theme.ThemeOptions
import org.futo.inputmethod.latin.uix.theme.Typography
import org.futo.inputmethod.latin.uix.theme.UixThemeWrapper
import org.futo.inputmethod.latin.uix.theme.presets.VoiceInputTheme import org.futo.inputmethod.latin.uix.theme.presets.VoiceInputTheme
import org.futo.inputmethod.latin.xlm.LanguageModelFacilitator import org.futo.inputmethod.latin.xlm.LanguageModelFacilitator
class LatinIME : InputMethodService(), LifecycleOwner, ViewModelStoreOwner, SavedStateRegistryOwner, class LatinIME : InputMethodService(), LifecycleOwner, ViewModelStoreOwner, SavedStateRegistryOwner,
LatinIMELegacy.SuggestionStripController, DynamicThemeProviderOwner, KeyboardManagerForAction { LatinIMELegacy.SuggestionStripController, DynamicThemeProviderOwner {
private val mSavedStateRegistryController = SavedStateRegistryController.create(this) private val mSavedStateRegistryController = SavedStateRegistryController.create(this)
@ -101,7 +73,7 @@ class LatinIME : InputMethodService(), LifecycleOwner, ViewModelStoreOwner, Save
override val viewModelStore override val viewModelStore
get() = store get() = store
private fun setOwners() { fun setOwners() {
val decorView = window.window?.decorView val decorView = window.window?.decorView
if (decorView?.findViewTreeLifecycleOwner() == null) { if (decorView?.findViewTreeLifecycleOwner() == null) {
decorView?.setViewTreeLifecycleOwner(this) decorView?.setViewTreeLifecycleOwner(this)
@ -114,14 +86,14 @@ class LatinIME : InputMethodService(), LifecycleOwner, ViewModelStoreOwner, Save
} }
} }
private var composeView: ComposeView? = null val latinIMELegacy = LatinIMELegacy(
private val latinIMELegacy = LatinIMELegacy(
this as InputMethodService, this as InputMethodService,
this as LatinIMELegacy.SuggestionStripController this as LatinIMELegacy.SuggestionStripController
) )
public val languageModelFacilitator = LanguageModelFacilitator( val inputLogic get() = latinIMELegacy.mInputLogic
val languageModelFacilitator = LanguageModelFacilitator(
this, this,
latinIMELegacy.mInputLogic, latinIMELegacy.mInputLogic,
latinIMELegacy.mDictionaryFacilitator, latinIMELegacy.mDictionaryFacilitator,
@ -130,21 +102,18 @@ class LatinIME : InputMethodService(), LifecycleOwner, ViewModelStoreOwner, Save
lifecycleScope lifecycleScope
) )
val uixManager = UixManager(this)
private var activeThemeOption: ThemeOption? = null private var activeThemeOption: ThemeOption? = null
private var activeColorScheme = DarkColorScheme private var activeColorScheme = DarkColorScheme
private var colorSchemeLoaderJob: Job? = null private var colorSchemeLoaderJob: Job? = null
private var pendingRecreateKeyboard: Boolean = false
val themeOption get() = activeThemeOption
val colorScheme get() = activeColorScheme
private var drawableProvider: DynamicThemeProvider? = null private var drawableProvider: DynamicThemeProvider? = null
private var currWindowAction: Action? = null
private var currWindowActionWindow: ActionWindow? = null
private var persistentStates: HashMap<Action, PersistentActionState?> = hashMapOf()
private fun isActionWindowOpen(): Boolean {
return currWindowActionWindow != null
}
private var inlineSuggestions: List<MutableState<View?>> = listOf()
private var lastEditorInfo: EditorInfo? = null private var lastEditorInfo: EditorInfo? = null
// TODO: Calling this repeatedly as the theme changes tends to slow everything to a crawl // TODO: Calling this repeatedly as the theme changes tends to slow everything to a crawl
@ -158,7 +127,7 @@ class LatinIME : InputMethodService(), LifecycleOwner, ViewModelStoreOwner, Save
drawableProvider = BasicThemeProvider(this, overrideColorScheme = colorScheme) drawableProvider = BasicThemeProvider(this, overrideColorScheme = colorScheme)
window.window?.navigationBarColor = drawableProvider!!.primaryKeyboardColor window.window?.navigationBarColor = drawableProvider!!.primaryKeyboardColor
setContent() uixManager.onColorSchemeChanged()
} }
override fun getDrawableProvider(): DynamicThemeProvider { override fun getDrawableProvider(): DynamicThemeProvider {
@ -193,6 +162,32 @@ class LatinIME : InputMethodService(), LifecycleOwner, ViewModelStoreOwner, Save
} }
} }
fun updateTheme(newTheme: ThemeOption) {
assert(newTheme.available(this))
if (activeThemeOption != newTheme) {
activeThemeOption = newTheme
updateDrawableProvider(newTheme.obtainColors(this))
deferSetSetting(THEME_KEY, newTheme.key)
if(!uixManager.isMainKeyboardHidden) {
recreateKeyboard()
} else {
pendingRecreateKeyboard = true
}
}
}
// Called by UixManager when the intention is to subsequently call LegacyKeyboardView with hidden=false
// Maybe this can be changed to LaunchedEffect
fun onKeyboardShown() {
//if(pendingRecreateKeyboard) {
// pendingRecreateKeyboard = false
// recreateKeyboard()
//}
}
override fun onCreate() { override fun onCreate() {
super.onCreate() super.onCreate()
@ -241,41 +236,38 @@ class LatinIME : InputMethodService(), LifecycleOwner, ViewModelStoreOwner, Save
private var touchableHeight: Int = 0 private var touchableHeight: Int = 0
override fun onCreateInputView(): View { override fun onCreateInputView(): View {
legacyInputView = latinIMELegacy.onCreateInputView() legacyInputView = latinIMELegacy.onCreateInputView()
composeView = ComposeView(this).apply {
setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
setParentCompositionContext(null)
this@LatinIME.setOwners()
}
setContent()
val composeView = uixManager.createComposeView()
latinIMELegacy.setComposeInputView(composeView) latinIMELegacy.setComposeInputView(composeView)
return composeView!! return composeView
}
private fun onActionActivated(action: Action) {
// Finish what we are typing so far
latinIMELegacy.onFinishInputViewInternal(false)
if (action.windowImpl != null) {
enterActionWindowView(action)
} else if (action.simplePressImpl != null) {
action.simplePressImpl.invoke(this, persistentStates[action])
} else {
throw IllegalStateException("An action must have either a window implementation or a simple press implementation")
}
} }
private var inputViewHeight: Int = -1 private var inputViewHeight: Int = -1
private var shouldShowSuggestionStrip: Boolean = true
private var suggestedWords: SuggestedWords? = null
// Both called by UixManager
fun updateTouchableHeight(to: Int) { touchableHeight = to }
fun getInputViewHeight(): Int = inputViewHeight
// The keyboard view really doesn't like being detached, so it's always
// shown, but resized to 0 if an action window is open
@Composable @Composable
private fun LegacyKeyboardView(hidden: Boolean) { internal fun LegacyKeyboardView(hidden: Boolean) {
LaunchedEffect(hidden) {
if(hidden) {
latinIMELegacy.mKeyboardSwitcher.saveKeyboardState()
} else {
if(pendingRecreateKeyboard) {
pendingRecreateKeyboard = false
recreateKeyboard()
}
}
}
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
@ -288,127 +280,13 @@ class LatinIME : InputMethodService(), LifecycleOwner, ViewModelStoreOwner, Save
} }
} }
@Composable
private fun MainKeyboardViewWithActionBar() {
Column {
// Don't show suggested words when it's not meant to be shown
val suggestedWordsOrNull = if(shouldShowSuggestionStrip) {
suggestedWords
} else {
null
}
ActionBar(
suggestedWordsOrNull,
latinIMELegacy,
inlineSuggestions = inlineSuggestions,
onActionActivated = { onActionActivated(it) }
)
}
}
private fun enterActionWindowView(action: Action) {
assert(action.windowImpl != null)
latinIMELegacy.mKeyboardSwitcher.saveKeyboardState()
currWindowAction = action
if (persistentStates[action] == null) {
persistentStates[action] = action.persistentState?.let { it(this) }
}
currWindowActionWindow = action.windowImpl?.let { it(this, persistentStates[action]) }
setContent()
}
private fun returnBackToMainKeyboardViewFromAction() {
if(currWindowActionWindow == null) return
currWindowActionWindow!!.close()
currWindowAction = null
currWindowActionWindow = null
if(hasThemeChanged) {
hasThemeChanged = false
recreateKeyboard()
}
setContent()
}
@Composable
private fun ActionViewWithHeader(windowImpl: ActionWindow) {
Column {
Surface(
modifier = Modifier
.fillMaxWidth()
.height(40.dp), color = MaterialTheme.colorScheme.background
)
{
Row {
IconButton(onClick = {
returnBackToMainKeyboardViewFromAction()
}) {
Icon(
painter = painterResource(id = R.drawable.arrow_left),
contentDescription = "Back"
)
}
Text(
windowImpl.windowName(),
style = Typography.titleMedium,
modifier = Modifier.align(CenterVertically)
)
}
}
Box(modifier = Modifier
.fillMaxWidth()
.height(with(LocalDensity.current) { inputViewHeight.toDp() })
) {
windowImpl.WindowContents()
}
}
}
private fun setContent() {
composeView?.setContent {
UixThemeWrapper(activeColorScheme) {
Column {
Spacer(modifier = Modifier.weight(1.0f))
Surface(modifier = Modifier.onSizeChanged {
touchableHeight = it.height
}) {
Column {
when {
isActionWindowOpen() -> ActionViewWithHeader(
currWindowActionWindow!!
)
else -> MainKeyboardViewWithActionBar()
}
// The keyboard view really doesn't like being detached, so it's always
// shown, but resized to 0 if an action window is open
LegacyKeyboardView(hidden = isActionWindowOpen())
}
}
}
}
}
}
// necessary for when KeyboardSwitcher updates the theme // necessary for when KeyboardSwitcher updates the theme
fun updateLegacyView(newView: View) { fun updateLegacyView(newView: View) {
legacyInputView = newView legacyInputView = newView
setContent()
if (composeView != null) { uixManager.setContent()
latinIMELegacy.setComposeInputView(composeView) uixManager.getComposeView()?.let {
latinIMELegacy.setComposeInputView(it)
} }
latinIMELegacy.setInputView(legacyInputView) latinIMELegacy.setInputView(legacyInputView)
@ -417,8 +295,8 @@ class LatinIME : InputMethodService(), LifecycleOwner, ViewModelStoreOwner, Save
override fun setInputView(view: View?) { override fun setInputView(view: View?) {
super.setInputView(view) super.setInputView(view)
if (composeView != null) { uixManager.getComposeView()?.let {
latinIMELegacy.setComposeInputView(composeView) latinIMELegacy.setComposeInputView(it)
} }
latinIMELegacy.setInputView(legacyInputView) latinIMELegacy.setInputView(legacyInputView)
@ -443,15 +321,14 @@ class LatinIME : InputMethodService(), LifecycleOwner, ViewModelStoreOwner, Save
override fun onFinishInputView(finishingInput: Boolean) { override fun onFinishInputView(finishingInput: Boolean) {
super.onFinishInputView(finishingInput) super.onFinishInputView(finishingInput)
latinIMELegacy.onFinishInputView(finishingInput) latinIMELegacy.onFinishInputView(finishingInput)
uixManager.onInputFinishing()
closeActionWindow()
} }
override fun onFinishInput() { override fun onFinishInput() {
super.onFinishInput() super.onFinishInput()
latinIMELegacy.onFinishInput() latinIMELegacy.onFinishInput()
closeActionWindow() uixManager.onInputFinishing()
languageModelFacilitator.saveHistoryLog() languageModelFacilitator.saveHistoryLog()
} }
@ -471,7 +348,7 @@ class LatinIME : InputMethodService(), LifecycleOwner, ViewModelStoreOwner, Save
super.onWindowHidden() super.onWindowHidden()
latinIMELegacy.onWindowHidden() latinIMELegacy.onWindowHidden()
closeActionWindow() uixManager.onInputFinishing()
} }
override fun onUpdateSelection( override fun onUpdateSelection(
@ -521,12 +398,14 @@ class LatinIME : InputMethodService(), LifecycleOwner, ViewModelStoreOwner, Save
} }
override fun onComputeInsets(outInsets: Insets?) { override fun onComputeInsets(outInsets: Insets?) {
val composeView = uixManager.getComposeView()
// This method may be called before {@link #setInputView(View)}. // This method may be called before {@link #setInputView(View)}.
if (legacyInputView == null || composeView == null) { if (legacyInputView == null || composeView == null) {
return return
} }
val inputHeight: Int = composeView!!.height val inputHeight: Int = composeView.height
if (latinIMELegacy.isImeSuppressedByHardwareKeyboard && !legacyInputView!!.isShown) { if (latinIMELegacy.isImeSuppressedByHardwareKeyboard && !legacyInputView!!.isShown) {
// If there is a hardware keyboard and a visible software keyboard view has been hidden, // If there is a hardware keyboard and a visible software keyboard view has been hidden,
// no visual element will be shown on the screen. // no visual element will be shown on the screen.
@ -541,7 +420,7 @@ class LatinIME : InputMethodService(), LifecycleOwner, ViewModelStoreOwner, Save
val touchLeft = 0 val touchLeft = 0
val touchTop = visibleTopY val touchTop = visibleTopY
val touchRight = composeView!!.width val touchRight = composeView.width
val touchBottom = inputHeight val touchBottom = inputHeight
latinIMELegacy.setInsets(outInsets!!.apply { latinIMELegacy.setInsets(outInsets!!.apply {
@ -581,124 +460,25 @@ class LatinIME : InputMethodService(), LifecycleOwner, ViewModelStoreOwner, Save
} }
override fun updateVisibility(shouldShowSuggestionsStrip: Boolean, fullscreenMode: Boolean) { override fun updateVisibility(shouldShowSuggestionsStrip: Boolean, fullscreenMode: Boolean) {
this.shouldShowSuggestionStrip = shouldShowSuggestionsStrip uixManager.updateVisibility(shouldShowSuggestionsStrip, fullscreenMode)
setContent()
} }
override fun setSuggestions(suggestedWords: SuggestedWords?, rtlSubtype: Boolean) { override fun setSuggestions(suggestedWords: SuggestedWords?, rtlSubtype: Boolean) {
this.suggestedWords = suggestedWords uixManager.setSuggestions(suggestedWords, rtlSubtype)
setContent()
} }
override fun maybeShowImportantNoticeTitle(): Boolean { override fun maybeShowImportantNoticeTitle(): Boolean {
return false return false
} }
private fun cleanUpPersistentStates() {
println("Cleaning up persistent states")
for((key, value) in persistentStates.entries) {
if(currWindowAction != key) {
lifecycleScope.launch { value?.cleanUp() }
}
}
}
override fun onLowMemory() { override fun onLowMemory() {
super.onLowMemory() super.onLowMemory()
cleanUpPersistentStates() uixManager.cleanUpPersistentStates()
} }
override fun onTrimMemory(level: Int) { override fun onTrimMemory(level: Int) {
super.onTrimMemory(level) super.onTrimMemory(level)
cleanUpPersistentStates() uixManager.cleanUpPersistentStates()
}
override fun getContext(): Context {
return this
}
override fun getLifecycleScope(): LifecycleCoroutineScope {
return lifecycleScope
}
override fun triggerContentUpdate() {
setContent()
}
private class LatinIMEActionInputTransaction(
private val latinIME: LatinIME,
shouldApplySpace: Boolean
): ActionInputTransaction {
private val isSpaceNecessary: Boolean
init {
val priorText = latinIME.latinIMELegacy.mInputLogic.mConnection.getTextBeforeCursor(1, 0)
isSpaceNecessary = shouldApplySpace && !priorText.isNullOrEmpty() && !priorText.last().isWhitespace()
}
private fun transformText(text: String): String {
return if(isSpaceNecessary) { " $text" } else { text }
}
override fun updatePartial(text: String) {
latinIME.latinIMELegacy.mInputLogic.mConnection.setComposingText(
transformText(text),
1
)
}
override fun commit(text: String) {
latinIME.latinIMELegacy.mInputLogic.mConnection.commitText(
transformText(text),
1
)
}
override fun cancel() {
// TODO: Do we want to leave the composing text as-is, or delete it?
latinIME.latinIMELegacy.mInputLogic.mConnection.finishComposingText()
}
}
override fun createInputTransaction(applySpaceIfNeeded: Boolean): ActionInputTransaction {
return LatinIMEActionInputTransaction(this, applySpaceIfNeeded)
}
override fun typeText(v: String) {
latinIMELegacy.mInputLogic.mConnection.commitText(v, 1)
}
override fun backspace(amount: Int) {
latinIMELegacy.mInputLogic.mConnection.deleteTextBeforeCursor(amount)
}
override fun closeActionWindow() {
if(currWindowActionWindow == null) return
returnBackToMainKeyboardViewFromAction()
}
override fun triggerSystemVoiceInput() {
latinIMELegacy.onCodeInput(
Constants.CODE_SHORTCUT,
Constants.SUGGESTION_STRIP_COORDINATE,
Constants.SUGGESTION_STRIP_COORDINATE,
false
);
}
private var hasThemeChanged: Boolean = false
override fun updateTheme(newTheme: ThemeOption) {
assert(newTheme.available(this))
if (activeThemeOption != newTheme) {
activeThemeOption = newTheme
updateDrawableProvider(newTheme.obtainColors(this))
deferSetSetting(THEME_KEY, newTheme.key)
hasThemeChanged = true
if(!isActionWindowOpen()) {
recreateKeyboard()
}
}
} }
@RequiresApi(Build.VERSION_CODES.R) @RequiresApi(Build.VERSION_CODES.R)
@ -708,12 +488,7 @@ class LatinIME : InputMethodService(), LifecycleOwner, ViewModelStoreOwner, Save
@RequiresApi(Build.VERSION_CODES.R) @RequiresApi(Build.VERSION_CODES.R)
override fun onInlineSuggestionsResponse(response: InlineSuggestionsResponse): Boolean { override fun onInlineSuggestionsResponse(response: InlineSuggestionsResponse): Boolean {
inlineSuggestions = response.inlineSuggestions.map { return uixManager.onInlineSuggestionsResponse(response)
inflateInlineSuggestion(it)
}
setContent()
return true
} }
fun postUpdateSuggestionStrip(inputStyle: Int) { fun postUpdateSuggestionStrip(inputStyle: Int) {

View File

@ -41,6 +41,7 @@ import android.os.IBinder;
import android.os.Message; import android.os.Message;
import android.preference.PreferenceManager; import android.preference.PreferenceManager;
import android.text.InputType; import android.text.InputType;
import android.text.TextUtils;
import android.util.Log; import android.util.Log;
import android.util.PrintWriterPrinter; import android.util.PrintWriterPrinter;
import android.util.Printer; import android.util.Printer;
@ -158,6 +159,7 @@ public class LatinIMELegacy implements KeyboardActionListener,
private static final String SCHEME_PACKAGE = "package"; private static final String SCHEME_PACKAGE = "package";
final Settings mSettings; final Settings mSettings;
private Locale mLocale;
final DictionaryFacilitator mDictionaryFacilitator = final DictionaryFacilitator mDictionaryFacilitator =
DictionaryFacilitatorProvider.getDictionaryFacilitator( DictionaryFacilitatorProvider.getDictionaryFacilitator(
false /* isNeededForSpellChecking */); false /* isNeededForSpellChecking */);
@ -671,18 +673,18 @@ public class LatinIMELegacy implements KeyboardActionListener,
// Has to be package-visible for unit tests // Has to be package-visible for unit tests
@UsedForTesting @UsedForTesting
void loadSettings() { void loadSettings() {
final Locale locale = mRichImm.getCurrentSubtypeLocale(); mLocale = mRichImm.getCurrentSubtypeLocale();
final EditorInfo editorInfo = mInputMethodService.getCurrentInputEditorInfo(); final EditorInfo editorInfo = mInputMethodService.getCurrentInputEditorInfo();
final InputAttributes inputAttributes = new InputAttributes( final InputAttributes inputAttributes = new InputAttributes(
editorInfo, mInputMethodService.isFullscreenMode(), mInputMethodService.getPackageName()); editorInfo, mInputMethodService.isFullscreenMode(), mInputMethodService.getPackageName());
mSettings.loadSettings(mInputMethodService, locale, inputAttributes); mSettings.loadSettings(mInputMethodService, mLocale, inputAttributes);
final SettingsValues currentSettingsValues = mSettings.getCurrent(); final SettingsValues currentSettingsValues = mSettings.getCurrent();
AudioAndHapticFeedbackManager.getInstance().onSettingsChanged(currentSettingsValues); AudioAndHapticFeedbackManager.getInstance().onSettingsChanged(currentSettingsValues);
// This method is called on startup and language switch, before the new layout has // This method is called on startup and language switch, before the new layout has
// been displayed. Opening dictionaries never affects responsivity as dictionaries are // been displayed. Opening dictionaries never affects responsivity as dictionaries are
// asynchronously loaded. // asynchronously loaded.
if (!mHandler.hasPendingReopenDictionaries()) { if (!mHandler.hasPendingReopenDictionaries()) {
resetDictionaryFacilitator(locale); resetDictionaryFacilitator(mLocale);
} }
refreshPersonalizationDictionarySession(currentSettingsValues); refreshPersonalizationDictionarySession(currentSettingsValues);
resetDictionaryFacilitatorIfNecessary(); resetDictionaryFacilitatorIfNecessary();
@ -1365,6 +1367,54 @@ public class LatinIMELegacy implements KeyboardActionListener,
return false; return false;
} }
@Override
public void onMovePointer(int steps) {
if (mInputLogic.mConnection.hasCursorPosition()) {
if (TextUtils.getLayoutDirectionFromLocale(mLocale) == View.LAYOUT_DIRECTION_RTL)
steps = -steps;
steps = mInputLogic.mConnection.getUnicodeSteps(steps, true);
final int end = mInputLogic.mConnection.getExpectedSelectionEnd() + steps;
final int start = mInputLogic.mConnection.hasSelection() ? mInputLogic.mConnection.getExpectedSelectionStart() : end;
mInputLogic.finishInput();
mInputLogic.mConnection.setSelection(start, end);
} else {
for (; steps < 0; steps++)
mInputLogic.sendDownUpKeyEvent(KeyEvent.KEYCODE_DPAD_LEFT, 0);
for (; steps > 0; steps--)
mInputLogic.sendDownUpKeyEvent(KeyEvent.KEYCODE_DPAD_RIGHT, 0);
}
}
@Override
public void onMoveDeletePointer(int steps) {
if (mInputLogic.mConnection.hasCursorPosition()) {
steps = mInputLogic.mConnection.getUnicodeSteps(steps, false);
final int end = mInputLogic.mConnection.getExpectedSelectionEnd();
final int start = mInputLogic.mConnection.getExpectedSelectionStart() + steps;
if (start > end)
return;
mInputLogic.finishInput();
mInputLogic.mConnection.setSelection(start, end);
} else {
for (; steps < 0; steps++)
mInputLogic.sendDownUpKeyEvent(KeyEvent.KEYCODE_DEL, 0);
}
}
@Override
public void onUpWithDeletePointerActive() {
if (mInputLogic.mConnection.hasSelection())
mInputLogic.sendDownUpKeyEvent(KeyEvent.KEYCODE_DEL, 0);
}
@Override
public void onUpWithPointerActive() {
mInputLogic.restartSuggestionsOnWordTouchedByCursor(mSettings.getCurrent(), false, mKeyboardSwitcher.getCurrentKeyboardScriptId());
}
private boolean isShowingOptionDialog() { private boolean isShowingOptionDialog() {
return mOptionsDialog != null && mOptionsDialog.isShowing(); return mOptionsDialog != null && mOptionsDialog.isShowing();
} }

View File

@ -1044,4 +1044,41 @@ public final class RichInputConnection implements PrivateCommandPerformer {
return InputConnectionCompatUtils.requestCursorUpdates( return InputConnectionCompatUtils.requestCursorUpdates(
mIC, enableMonitor, requestImmediateCallback); mIC, enableMonitor, requestImmediateCallback);
} }
public boolean hasCursorPosition() {
return mExpectedSelStart != INVALID_CURSOR_POSITION && mExpectedSelEnd != INVALID_CURSOR_POSITION;
}
/**
* Some chars, such as emoji consist of 2 chars (surrogate pairs). We should treat them as one character.
*/
public int getUnicodeSteps(int chars, boolean rightSidePointer) {
int steps = 0;
if (chars < 0) {
CharSequence charsBeforeCursor = rightSidePointer && hasSelection() ?
getSelectedText(0) :
getTextBeforeCursor(-chars * 2, 0);
if (charsBeforeCursor != null) {
for (int i = charsBeforeCursor.length() - 1; i >= 0 && chars < 0; i--, chars++, steps--) {
if (Character.isSurrogate(charsBeforeCursor.charAt(i))) {
steps--;
i--;
}
}
}
} else if (chars > 0) {
CharSequence charsAfterCursor = !rightSidePointer && hasSelection() ?
getSelectedText(0) :
getTextAfterCursor(chars * 2, 0);
if (charsAfterCursor != null) {
for (int i = 0; i < charsAfterCursor.length() && chars > 0; i++, chars--, steps++) {
if (Character.isSurrogate(charsAfterCursor.charAt(i))) {
steps++;
i++;
}
}
}
}
return steps;
}
} }

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 {
@ -36,7 +39,7 @@ interface ActionWindow {
fun windowName(): String fun windowName(): String
@Composable @Composable
fun WindowContents() fun WindowContents(keyboardShown: Boolean)
fun close() fun close()
} }
@ -54,6 +57,8 @@ interface PersistentActionState {
data class Action( data class Action(
@DrawableRes val icon: Int, @DrawableRes val icon: Int,
@StringRes val name: Int, @StringRes val name: Int,
val canShowKeyboard: Boolean = false,
val windowImpl: ((KeyboardManagerForAction, PersistentActionState?) -> ActionWindow)?, val windowImpl: ((KeyboardManagerForAction, PersistentActionState?) -> ActionWindow)?,
val simplePressImpl: ((KeyboardManagerForAction, PersistentActionState?) -> Unit)?, val simplePressImpl: ((KeyboardManagerForAction, PersistentActionState?) -> Unit)?,
val persistentState: ((KeyboardManagerForAction) -> PersistentActionState)? = null, val persistentState: ((KeyboardManagerForAction) -> PersistentActionState)? = null,

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
@ -20,6 +21,7 @@ import androidx.compose.material3.IconButton
import androidx.compose.material3.IconButtonDefaults import androidx.compose.material3.IconButtonDefaults
import androidx.compose.material3.MaterialTheme import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton import androidx.compose.material3.TextButton
import androidx.compose.material3.dynamicDarkColorScheme import androidx.compose.material3.dynamicDarkColorScheme
import androidx.compose.material3.dynamicLightColorScheme import androidx.compose.material3.dynamicLightColorScheme
@ -60,10 +62,15 @@ 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.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.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.Typography
import org.futo.inputmethod.latin.uix.theme.UixThemeWrapper import org.futo.inputmethod.latin.uix.theme.UixThemeWrapper
import java.lang.Integer.min import java.lang.Integer.min
import kotlin.math.ceil import kotlin.math.ceil
@ -287,7 +294,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 +324,10 @@ 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)) { ActionItem(TextEditAction, onSelect)
}
} }
@ -386,7 +391,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 +408,110 @@ fun ActionBar(
Spacer(modifier = Modifier.weight(1.0f)) Spacer(modifier = Modifier.weight(1.0f))
} }
ActionItemSmall(VoiceInputAction, onActionActivated) if(!isActionsOpen.value) {
ActionItemSmall(VoiceInputAction, onActionActivated)
}
}
}
}
@Composable
fun ActionWindowBar(
windowName: String,
canExpand: Boolean,
onBack: () -> Unit,
onExpand: () -> Unit
) {
Surface(
modifier = Modifier
.fillMaxWidth()
.height(40.dp), color = MaterialTheme.colorScheme.background
)
{
Row {
IconButton(onClick = onBack) {
Icon(
painter = painterResource(id = R.drawable.arrow_left_26),
contentDescription = "Back"
)
}
Text(
windowName,
style = Typography.titleMedium,
modifier = Modifier.align(CenterVertically)
)
Spacer(modifier = Modifier.weight(1.0f))
if(canExpand) {
IconButton(onClick = onExpand) {
Icon(
painter = painterResource(id = R.drawable.arrow_up),
contentDescription = "Show Keyboard"
)
}
}
}
}
}
@Composable
fun CollapsibleSuggestionsBar(
onClose: () -> Unit,
onCollapse: () -> Unit,
words: SuggestedWords?,
suggestionStripListener: SuggestionStripView.Listener,
inlineSuggestions: List<MutableState<View?>>,
) {
Surface(modifier = Modifier
.fillMaxWidth()
.height(40.dp), color = MaterialTheme.colorScheme.background)
{
Row {
val color = MaterialTheme.colorScheme.primary
IconButton(
onClick = onClose,
modifier = Modifier
.width(42.dp)
.fillMaxHeight()
.drawBehind {
drawCircle(color = color, radius = size.width / 3.0f + 1.0f)
},
colors = IconButtonDefaults.iconButtonColors(contentColor = MaterialTheme.colorScheme.onPrimary)
) {
Icon(
painter = painterResource(id = R.drawable.close),
contentDescription = "Close"
)
}
if(inlineSuggestions.isNotEmpty() && Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
InlineSuggestions(inlineSuggestions)
} else if(words != null) {
SuggestionItems(words) {
suggestionStripListener.pickSuggestionManually(
words.getInfo(it)
)
}
} else {
Spacer(modifier = Modifier.weight(1.0f))
}
IconButton(
onClick = onCollapse,
modifier = Modifier
.width(42.dp)
.fillMaxHeight(),
colors = IconButtonDefaults.iconButtonColors(contentColor = MaterialTheme.colorScheme.onBackground)
) {
Icon(
painter = painterResource(id = R.drawable.arrow_down),
contentDescription = "Collapse"
)
}
} }
} }
} }
@ -491,6 +603,18 @@ fun PreviewExpandedActionBar(colorScheme: ColorScheme = DarkColorScheme) {
} }
} }
@Composable
@Preview
fun PreviewCollapsibleBar(colorScheme: ColorScheme = DarkColorScheme) {
CollapsibleSuggestionsBar(
onCollapse = { },
onClose = { },
words = exampleSuggestedWords,
suggestionStripListener = ExampleListener(),
inlineSuggestions = listOf()
)
}
@Composable @Composable
@Preview @Preview

View File

@ -31,6 +31,7 @@ class BasicThemeProvider(val context: Context, val overrideColorScheme: ColorSch
override val keyFeedback: Drawable override val keyFeedback: Drawable
override val moreKeysTextColor: Int
override val moreKeysKeyboardBackground: Drawable override val moreKeysKeyboardBackground: Drawable
override val popupKey: Drawable override val popupKey: Drawable
@ -107,6 +108,10 @@ class BasicThemeProvider(val context: Context, val overrideColorScheme: ColorSch
val surface = colorScheme.background.toArgb() val surface = colorScheme.background.toArgb()
val outline = colorScheme.outline.toArgb() val outline = colorScheme.outline.toArgb()
val primaryContainer = colorScheme.primaryContainer.toArgb()
val onPrimaryContainer = colorScheme.onPrimaryContainer.toArgb()
val onPrimary = colorScheme.onPrimary.toArgb()
val onSecondary = colorScheme.onSecondary.toArgb() val onSecondary = colorScheme.onSecondary.toArgb()
val onBackground = colorScheme.onBackground.toArgb() val onBackground = colorScheme.onBackground.toArgb()
val onBackgroundHalf = colorScheme.onBackground.copy(alpha = 0.5f).toArgb() val onBackgroundHalf = colorScheme.onBackground.copy(alpha = 0.5f).toArgb()
@ -115,6 +120,7 @@ class BasicThemeProvider(val context: Context, val overrideColorScheme: ColorSch
colors[R.styleable.Keyboard_Key_keyTextColor] = onBackground colors[R.styleable.Keyboard_Key_keyTextColor] = onBackground
colors[R.styleable.Keyboard_Key_keyTextInactivatedColor] = onBackgroundHalf colors[R.styleable.Keyboard_Key_keyTextInactivatedColor] = onBackgroundHalf
colors[R.styleable.Keyboard_Key_keyPressedTextColor] = onPrimary
colors[R.styleable.Keyboard_Key_keyTextShadowColor] = 0 colors[R.styleable.Keyboard_Key_keyTextShadowColor] = 0
colors[R.styleable.Keyboard_Key_functionalTextColor] = onBackground colors[R.styleable.Keyboard_Key_functionalTextColor] = onBackground
colors[R.styleable.Keyboard_Key_keyHintLetterColor] = onBackgroundHalf colors[R.styleable.Keyboard_Key_keyHintLetterColor] = onBackgroundHalf
@ -208,10 +214,11 @@ class BasicThemeProvider(val context: Context, val overrideColorScheme: ColorSch
setPadding(0, 0, 0, dp(50.dp).roundToInt()) setPadding(0, 0, 0, dp(50.dp).roundToInt())
} }
moreKeysKeyboardBackground = coloredRoundedRectangle(surface, dp(8.dp)) moreKeysTextColor = onPrimaryContainer
moreKeysKeyboardBackground = coloredRoundedRectangle(primaryContainer, dp(8.dp))
popupKey = StateListDrawable().apply { popupKey = StateListDrawable().apply {
addStateWithHighlightLayerOnPressed(highlight, intArrayOf(), addStateWithHighlightLayerOnPressed(primary, intArrayOf(),
coloredRoundedRectangle(surface, dp(8.dp)) coloredRoundedRectangle(primaryContainer, dp(8.dp))
) )
} }
} }

View File

@ -13,6 +13,7 @@ interface DynamicThemeProvider {
val keyFeedback: Drawable val keyFeedback: Drawable
val moreKeysTextColor: Int
val moreKeysKeyboardBackground: Drawable val moreKeysKeyboardBackground: Drawable
val popupKey: Drawable val popupKey: Drawable

View File

@ -0,0 +1,344 @@
package org.futo.inputmethod.latin.uix
import android.content.Context
import android.os.Build
import android.view.View
import android.view.inputmethod.InlineSuggestionsResponse
import androidx.annotation.RequiresApi
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.material3.Surface
import androidx.compose.runtime.Composable
import androidx.compose.runtime.MutableState
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.onSizeChanged
import androidx.compose.ui.platform.ComposeView
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.ViewCompositionStrategy
import androidx.lifecycle.LifecycleCoroutineScope
import androidx.lifecycle.lifecycleScope
import kotlinx.coroutines.launch
import org.futo.inputmethod.latin.LatinIME
import org.futo.inputmethod.latin.SuggestedWords
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.theme.ThemeOption
import org.futo.inputmethod.latin.uix.theme.UixThemeWrapper
private class LatinIMEActionInputTransaction(
private val inputLogic: InputLogic,
shouldApplySpace: Boolean
): ActionInputTransaction {
private val isSpaceNecessary: Boolean
init {
val priorText = inputLogic.mConnection.getTextBeforeCursor(1, 0)
isSpaceNecessary = shouldApplySpace && !priorText.isNullOrEmpty() && !priorText.last().isWhitespace()
}
private fun transformText(text: String): String {
return if(isSpaceNecessary) { " $text" } else { text }
}
override fun updatePartial(text: String) {
inputLogic.mConnection.setComposingText(
transformText(text),
1
)
}
override fun commit(text: String) {
inputLogic.mConnection.commitText(
transformText(text),
1
)
}
override fun cancel() {
inputLogic.mConnection.finishComposingText()
}
}
class UixActionKeyboardManager(val uixManager: UixManager, val latinIME: LatinIME) : KeyboardManagerForAction {
override fun getContext(): Context {
return latinIME
}
override fun getLifecycleScope(): LifecycleCoroutineScope {
return latinIME.lifecycleScope
}
override fun triggerContentUpdate() {
uixManager.setContent()
}
override fun createInputTransaction(applySpaceIfNeeded: Boolean): ActionInputTransaction {
return LatinIMEActionInputTransaction(latinIME.inputLogic, applySpaceIfNeeded)
}
override fun typeText(v: String) {
latinIME.latinIMELegacy.onTextInput(v)
}
override fun backspace(amount: Int) {
latinIME.latinIMELegacy.onCodeInput(
Constants.CODE_DELETE,
Constants.NOT_A_COORDINATE,
Constants.NOT_A_COORDINATE, false)
}
override fun closeActionWindow() {
if(uixManager.currWindowActionWindow == null) return
uixManager.returnBackToMainKeyboardViewFromAction()
}
override fun triggerSystemVoiceInput() {
latinIME.latinIMELegacy.onCodeInput(
Constants.CODE_SHORTCUT,
Constants.SUGGESTION_STRIP_COORDINATE,
Constants.SUGGESTION_STRIP_COORDINATE,
false
);
}
override fun updateTheme(newTheme: ThemeOption) {
latinIME.updateTheme(newTheme)
}
override fun sendCodePointEvent(codePoint: Int) {
latinIME.latinIMELegacy.onCodeInput(codePoint,
Constants.NOT_A_COORDINATE,
Constants.NOT_A_COORDINATE, false)
}
override fun sendKeyEvent(keyCode: Int, metaState: Int) {
latinIME.inputLogic.sendDownUpKeyEvent(keyCode, metaState)
}
}
class UixManager(private val latinIME: LatinIME) {
private var shouldShowSuggestionStrip: Boolean = true
private var suggestedWords: SuggestedWords? = null
private var composeView: ComposeView? = null
private var currWindowAction: Action? = null
private var persistentStates: HashMap<Action, PersistentActionState?> = hashMapOf()
private var inlineSuggestions: List<MutableState<View?>> = listOf()
private val keyboardManagerForAction = UixActionKeyboardManager(this, latinIME)
private var mainKeyboardHidden = false
var currWindowActionWindow: ActionWindow? = null
val isMainKeyboardHidden get() = mainKeyboardHidden
private fun onActionActivated(action: Action) {
latinIME.inputLogic.finishInput()
if (action.windowImpl != null) {
enterActionWindowView(action)
} else if (action.simplePressImpl != null) {
action.simplePressImpl.invoke(keyboardManagerForAction, persistentStates[action])
} else {
throw IllegalStateException("An action must have either a window implementation or a simple press implementation")
}
}
@Composable
private fun MainKeyboardViewWithActionBar() {
Column {
// Don't show suggested words when it's not meant to be shown
val suggestedWordsOrNull = if(shouldShowSuggestionStrip) {
suggestedWords
} else {
null
}
ActionBar(
suggestedWordsOrNull,
latinIME.latinIMELegacy as SuggestionStripView.Listener,
inlineSuggestions = inlineSuggestions,
onActionActivated = { onActionActivated(it) }
)
}
}
private fun enterActionWindowView(action: Action) {
assert(action.windowImpl != null)
mainKeyboardHidden = true
currWindowAction = action
if (persistentStates[action] == null) {
persistentStates[action] = action.persistentState?.let { it(keyboardManagerForAction) }
}
currWindowActionWindow = action.windowImpl?.let { it(keyboardManagerForAction, persistentStates[action]) }
setContent()
}
fun returnBackToMainKeyboardViewFromAction() {
if(currWindowActionWindow == null) return
currWindowActionWindow!!.close()
currWindowAction = null
currWindowActionWindow = null
mainKeyboardHidden = false
latinIME.onKeyboardShown()
setContent()
}
private fun toggleExpandAction() {
mainKeyboardHidden = !mainKeyboardHidden
if(!mainKeyboardHidden) {
latinIME.onKeyboardShown()
}
setContent()
}
@Composable
private fun ActionViewWithHeader(windowImpl: ActionWindow) {
val heightDiv = if(mainKeyboardHidden) {
1
} else {
1.5
}
Column {
if(mainKeyboardHidden) {
ActionWindowBar(
onBack = { returnBackToMainKeyboardViewFromAction() },
canExpand = currWindowAction!!.canShowKeyboard,
onExpand = { toggleExpandAction() },
windowName = windowImpl.windowName()
)
}
Box(modifier = Modifier
.fillMaxWidth()
.height(with(LocalDensity.current) {
(latinIME.getInputViewHeight().toFloat() / heightDiv.toFloat()).toDp()
})
) {
windowImpl.WindowContents(keyboardShown = !isMainKeyboardHidden)
}
if(!mainKeyboardHidden) {
val suggestedWordsOrNull = if (shouldShowSuggestionStrip) {
suggestedWords
} else {
null
}
CollapsibleSuggestionsBar(
onCollapse = { toggleExpandAction() },
onClose = { returnBackToMainKeyboardViewFromAction() },
words = suggestedWordsOrNull,
suggestionStripListener = latinIME.latinIMELegacy as SuggestionStripView.Listener,
inlineSuggestions = inlineSuggestions
)
}
}
}
fun setContent() {
composeView?.setContent {
UixThemeWrapper(latinIME.colorScheme) {
Column {
Spacer(modifier = Modifier.weight(1.0f))
Surface(modifier = Modifier.onSizeChanged {
latinIME.updateTouchableHeight(it.height)
}) {
Column {
when {
currWindowActionWindow != null -> ActionViewWithHeader(
currWindowActionWindow!!
)
else -> MainKeyboardViewWithActionBar()
}
latinIME.LegacyKeyboardView(hidden = isMainKeyboardHidden)
}
}
}
}
}
}
fun createComposeView(): View {
if(composeView != null) {
composeView = null
//throw IllegalStateException("Attempted to create compose view, when one is already created!")
}
composeView = ComposeView(latinIME).apply {
setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
setParentCompositionContext(null)
latinIME.setOwners()
}
setContent()
return composeView!!
}
fun getComposeView(): View? {
return composeView
}
fun onColorSchemeChanged() {
setContent()
}
fun onInputFinishing() {
closeActionWindow()
}
fun cleanUpPersistentStates() {
println("Cleaning up persistent states")
for((key, value) in persistentStates.entries) {
if(currWindowAction != key) {
latinIME.lifecycleScope.launch { value?.cleanUp() }
}
}
}
fun closeActionWindow() {
if(currWindowActionWindow == null) return
returnBackToMainKeyboardViewFromAction()
}
fun updateVisibility(shouldShowSuggestionsStrip: Boolean, fullscreenMode: Boolean) {
this.shouldShowSuggestionStrip = shouldShowSuggestionsStrip
setContent()
}
fun setSuggestions(suggestedWords: SuggestedWords?, rtlSubtype: Boolean) {
this.suggestedWords = suggestedWords
setContent()
}
@RequiresApi(Build.VERSION_CODES.R)
fun onInlineSuggestionsResponse(response: InlineSuggestionsResponse): Boolean {
inlineSuggestions = response.inlineSuggestions.map {
latinIME.inflateInlineSuggestion(it)
}
setContent()
return true
}
}

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
@ -161,7 +162,7 @@ data class BitmapRecycler(
} }
@Composable @Composable
fun EmojiGrid(onClick: (EmojiItem) -> Unit, onExit: () -> Unit, onBackspace: () -> Unit, onSpace: () -> Unit, bitmaps: BitmapRecycler, emojis: List<EmojiItem>) { fun EmojiGrid(onClick: (EmojiItem) -> Unit, onExit: () -> Unit, onBackspace: () -> Unit, onSpace: () -> Unit, bitmaps: BitmapRecycler, emojis: List<EmojiItem>, keyboardShown: Boolean) {
val context = LocalContext.current val context = LocalContext.current
val spToDp = context.resources.displayMetrics.scaledDensity / context.resources.displayMetrics.density val spToDp = context.resources.displayMetrics.scaledDensity / context.resources.displayMetrics.density
@ -181,41 +182,48 @@ fun EmojiGrid(onClick: (EmojiItem) -> Unit, onExit: () -> Unit, onBackspace: ()
} }
} }
} }
Surface(color = MaterialTheme.colorScheme.background, modifier = Modifier
.fillMaxWidth()
.height(48.dp)) {
Row(modifier = Modifier.padding(2.dp, 8.dp, 2.dp, 0.dp)) {
IconButton(onClick = { onExit() }) {
Text("ABC", fontSize = 14.sp)
}
Button(onClick = { onSpace() }, modifier = Modifier if(!keyboardShown) {
.weight(1.0f) Surface(
.padding(8.dp, 2.dp), colors = ButtonDefaults.buttonColors( color = MaterialTheme.colorScheme.background, modifier = Modifier
containerColor = MaterialTheme.colorScheme.outline.copy(alpha = 0.33f), .fillMaxWidth()
contentColor = MaterialTheme.colorScheme.onBackground, .height(48.dp)
disabledContainerColor = MaterialTheme.colorScheme.outline, ) {
disabledContentColor = MaterialTheme.colorScheme.onBackground, Row(modifier = Modifier.padding(2.dp, 8.dp, 2.dp, 0.dp)) {
), shape = RoundedCornerShape(32.dp)) { IconButton(onClick = { onExit() }) {
Text("") Text("ABC", fontSize = 14.sp)
} }
IconButton(onClick = { onBackspace() }) { Button(
val icon = painterResource(id = R.drawable.delete) onClick = { onSpace() }, modifier = Modifier
val iconColor = MaterialTheme.colorScheme.onBackground .weight(1.0f)
.padding(8.dp, 2.dp), colors = ButtonDefaults.buttonColors(
containerColor = MaterialTheme.colorScheme.outline.copy(alpha = 0.33f),
contentColor = MaterialTheme.colorScheme.onBackground,
disabledContainerColor = MaterialTheme.colorScheme.outline,
disabledContentColor = MaterialTheme.colorScheme.onBackground,
), shape = RoundedCornerShape(32.dp)
) {
Text("")
}
Canvas(modifier = Modifier.fillMaxSize()) { IconButton(onClick = { onBackspace() }) {
translate( val icon = painterResource(id = R.drawable.delete)
left = this.size.width / 2.0f - icon.intrinsicSize.width / 2.0f, val iconColor = MaterialTheme.colorScheme.onBackground
top = this.size.height / 2.0f - icon.intrinsicSize.height / 2.0f
) { Canvas(modifier = Modifier.fillMaxSize()) {
with(icon) { translate(
draw( left = this.size.width / 2.0f - icon.intrinsicSize.width / 2.0f,
icon.intrinsicSize, top = this.size.height / 2.0f - icon.intrinsicSize.height / 2.0f
colorFilter = androidx.compose.ui.graphics.ColorFilter.tint( ) {
iconColor with(icon) {
draw(
icon.intrinsicSize,
colorFilter = androidx.compose.ui.graphics.ColorFilter.tint(
iconColor
)
) )
) }
} }
} }
} }
@ -266,7 +274,8 @@ 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,
canShowKeyboard = true,
simplePressImpl = null, simplePressImpl = null,
persistentState = { manager -> persistentState = { manager ->
val state = PersistentEmojiState() val state = PersistentEmojiState()
@ -282,21 +291,21 @@ 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
override fun WindowContents() { override fun WindowContents(keyboardShown: Boolean) {
state.emojis.value?.let { emojis -> state.emojis.value?.let { emojis ->
EmojiGrid(onClick = { EmojiGrid(onClick = {
manager.typeText(it.emoji) manager.typeText(it.emoji)
}, 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, keyboardShown = keyboardShown)
} }
} }

View File

@ -0,0 +1,379 @@
package org.futo.inputmethod.latin.uix.actions
import android.view.KeyEvent
import androidx.annotation.DrawableRes
import androidx.compose.foundation.Canvas
import androidx.compose.foundation.LocalIndication
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.interaction.collectIsPressedAsState
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
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.padding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.MutableState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
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.common.Constants
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 TogglableKey(
onToggle: (Boolean) -> Unit,
toggled: Boolean,
modifier: Modifier = Modifier,
contents: @Composable (color: Color) -> Unit
) {
val interactionSource = remember { MutableInteractionSource() }
val isPressed by interactionSource.collectIsPressedAsState()
LaunchedEffect(isPressed) {
if(isPressed) {
onToggle(!toggled)
}
}
Surface(
modifier = modifier
.padding(4.dp)
.clickable(
interactionSource = interactionSource,
indication = LocalIndication.current,
onClick = { }
),
shape = RoundedCornerShape(8.dp),
color = if(toggled) { MaterialTheme.colorScheme.secondary } else { MaterialTheme.colorScheme.secondaryContainer }
) {
contents(if(toggled) { MaterialTheme.colorScheme.onSecondary } else { MaterialTheme.colorScheme.onSecondaryContainer })
}
}
@Composable
fun ActionKey(
onTrigger: () -> Unit,
modifier: Modifier = Modifier,
repeatable: Boolean = true,
color: Color = MaterialTheme.colorScheme.primary,
contents: @Composable () -> Unit
) {
val interactionSource = remember { MutableInteractionSource() }
val isPressed by interactionSource.collectIsPressedAsState()
LaunchedEffect(isPressed) {
if(isPressed) {
onTrigger()
if(repeatable) {
delay(670L)
while (isPressed) {
onTrigger()
delay(50L)
}
}
}
}
Surface(
modifier = modifier
.padding(4.dp)
.clickable(
interactionSource = interactionSource,
indication = LocalIndication.current,
onClick = { }
),
shape = RoundedCornerShape(8.dp),
color = color
) {
contents()
}
}
@Composable
fun ArrowKeys(modifier: Modifier, sendEvent: (Int) -> Unit) {
Row(modifier = modifier) {
ActionKey(
modifier = Modifier
.weight(1.0f)
.fillMaxHeight(),
onTrigger = { sendEvent(KeyEvent.KEYCODE_DPAD_LEFT) }
) {
IconWithColor(
iconId = R.drawable.arrow_left,
iconColor = MaterialTheme.colorScheme.onPrimary
)
}
Column(modifier = Modifier
.weight(1.0f)
.fillMaxHeight()) {
ActionKey(
modifier = Modifier
.weight(1.0f)
.fillMaxWidth(),
onTrigger = { sendEvent(KeyEvent.KEYCODE_DPAD_UP) }
) {
IconWithColor(
iconId = R.drawable.arrow_up,
iconColor = MaterialTheme.colorScheme.onPrimary
)
}
ActionKey(
modifier = Modifier
.weight(1.0f)
.fillMaxWidth(),
onTrigger = { sendEvent(KeyEvent.KEYCODE_DPAD_DOWN) }
) {
IconWithColor(
iconId = R.drawable.arrow_down,
iconColor = MaterialTheme.colorScheme.onPrimary
)
}
}
ActionKey(
modifier = Modifier
.weight(1.0f)
.fillMaxHeight(),
onTrigger = { sendEvent(KeyEvent.KEYCODE_DPAD_RIGHT) }
) {
IconWithColor(
iconId = R.drawable.arrow_right,
iconColor = MaterialTheme.colorScheme.onPrimary
)
}
}
}
@Composable
fun CtrlShiftMetaKeys(modifier: Modifier, ctrlState: MutableState<Boolean>, shiftState: MutableState<Boolean>) {
Row(modifier = modifier) {
TogglableKey(
onToggle = { ctrlState.value = it },
toggled = ctrlState.value,
modifier = Modifier
.weight(1.0f)
.fillMaxHeight()
) {
IconWithColor(
iconId = R.drawable.ctrl,
iconColor = it
)
}
TogglableKey(
onToggle = { shiftState.value = it },
toggled = shiftState.value,
modifier = Modifier
.weight(1.0f)
.fillMaxHeight()
) {
IconWithColor(
iconId = R.drawable.shift,
iconColor = it
)
}
}
}
@Composable
fun SideKeys(modifier: Modifier, onEvent: (Int, Int) -> Unit, onCodePoint: (Int) -> Unit, keyboardShown: Boolean) {
Column(modifier = modifier) {
ActionKey(
modifier = Modifier
.weight(1.0f)
.fillMaxWidth(),
repeatable = false,
color = MaterialTheme.colorScheme.primaryContainer,
onTrigger = { onEvent(KeyEvent.KEYCODE_C, KeyEvent.META_CTRL_ON) }
) {
IconWithColor(
iconId = R.drawable.copy,
iconColor = MaterialTheme.colorScheme.onPrimaryContainer
)
}
ActionKey(
modifier = Modifier
.weight(1.0f)
.fillMaxWidth(),
repeatable = false,
color = MaterialTheme.colorScheme.primaryContainer,
onTrigger = { onEvent(KeyEvent.KEYCODE_V, KeyEvent.META_CTRL_ON) }
) {
IconWithColor(
iconId = R.drawable.clipboard,
iconColor = MaterialTheme.colorScheme.onPrimaryContainer
)
}
if(!keyboardShown) {
ActionKey(
modifier = Modifier
.weight(1.0f)
.fillMaxWidth(),
repeatable = true,
color = MaterialTheme.colorScheme.primaryContainer,
onTrigger = { onCodePoint(Constants.CODE_DELETE) }
) {
IconWithColor(
iconId = R.drawable.delete,
iconColor = MaterialTheme.colorScheme.onPrimaryContainer
)
}
}
Row(modifier = Modifier
.weight(1.0f)
.fillMaxWidth()) {
ActionKey(
modifier = Modifier
.weight(1.0f)
.fillMaxHeight(),
repeatable = false,
color = MaterialTheme.colorScheme.primaryContainer,
onTrigger = { onEvent(KeyEvent.KEYCODE_Z, KeyEvent.META_CTRL_ON) }
) {
IconWithColor(
iconId = R.drawable.undo,
iconColor = MaterialTheme.colorScheme.onPrimaryContainer
)
}
ActionKey(
modifier = Modifier
.weight(1.0f)
.fillMaxHeight(),
repeatable = false,
color = MaterialTheme.colorScheme.primaryContainer,
onTrigger = { onEvent(KeyEvent.KEYCODE_Y, KeyEvent.META_CTRL_ON) }
) {
IconWithColor(
iconId = R.drawable.redo,
iconColor = MaterialTheme.colorScheme.onPrimaryContainer
)
}
}
}
}
@Composable
fun TextEditScreen(onCodePoint: (Int) -> Unit, onEvent: (Int, Int) -> Unit, keyboardShown: Boolean) {
val shiftState = remember { mutableStateOf(false) }
val ctrlState = remember { mutableStateOf(false) }
val metaState = 0 or
(if(shiftState.value) { KeyEvent.META_SHIFT_ON } else { 0 }) or
(if(ctrlState.value) { KeyEvent.META_CTRL_ON } else { 0 })
val sendEvent = { keycode: Int -> onEvent(keycode, metaState) }
Row(modifier = Modifier.fillMaxSize()) {
Column(modifier = Modifier
.fillMaxHeight()
.weight(3.0f)) {
ArrowKeys(
modifier = Modifier
.weight(3.0f)
.fillMaxWidth(),
sendEvent = sendEvent
)
CtrlShiftMetaKeys(
modifier = Modifier
.weight(1.0f)
.fillMaxWidth(),
ctrlState = ctrlState,
shiftState = shiftState
)
}
SideKeys(
modifier = Modifier
.fillMaxHeight()
.weight(1.0f),
onEvent = onEvent,
onCodePoint = onCodePoint,
keyboardShown = keyboardShown
)
}
}
val TextEditAction = Action(
icon = R.drawable.edit_text,
name = R.string.text_edit_action_title,
simplePressImpl = null,
persistentState = null,
canShowKeyboard = true,
windowImpl = { manager, persistentState ->
object : ActionWindow {
@Composable
override fun windowName(): String {
return stringResource(R.string.text_edit_action_title)
}
@Composable
override fun WindowContents(keyboardShown: Boolean) {
TextEditScreen(onCodePoint = { a -> manager.sendCodePointEvent(a)}, onEvent = { a, b -> manager.sendKeyEvent(a, b) }, keyboardShown = keyboardShown)
}
override fun close() {
}
}
}
)
@Composable
@Preview(showBackground = true)
fun TextEditScreenPreview() {
Surface(modifier = Modifier.height(256.dp)) {
TextEditScreen(onCodePoint = { }, onEvent = { _, _ -> }, keyboardShown = false)
}
}
@Composable
@Preview(showBackground = true)
fun TextEditScreenPreviewWithKb() {
Surface(modifier = Modifier.height(256.dp)) {
TextEditScreen(onCodePoint = { }, onEvent = { _, _ -> }, keyboardShown = true)
}
}

View File

@ -1,27 +1,17 @@
package org.futo.inputmethod.latin.uix.actions package org.futo.inputmethod.latin.uix.actions
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.material3.Button
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import org.futo.inputmethod.latin.R import org.futo.inputmethod.latin.R
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.KeyboardManagerForAction
import org.futo.inputmethod.latin.uix.theme.ThemeOptionKeys
import org.futo.inputmethod.latin.uix.theme.ThemeOptions
import org.futo.inputmethod.latin.uix.theme.selector.ThemePicker import org.futo.inputmethod.latin.uix.theme.selector.ThemePicker
val ThemeAction = Action( val ThemeAction = Action(
icon = R.drawable.eye, icon = R.drawable.eye,
name = R.string.theme_switcher_action_title, name = R.string.theme_switcher_action_title,
simplePressImpl = null, simplePressImpl = null,
canShowKeyboard = true,
windowImpl = { manager, _ -> windowImpl = { manager, _ ->
object : ActionWindow { object : ActionWindow {
@Composable @Composable
@ -30,33 +20,8 @@ val ThemeAction = Action(
} }
@Composable @Composable
override fun WindowContents() { override fun WindowContents(keyboardShown: Boolean) {
val context = LocalContext.current
ThemePicker { manager.updateTheme(it) } ThemePicker { manager.updateTheme(it) }
/*
LazyColumn(
modifier = Modifier
.padding(8.dp, 0.dp)
.fillMaxWidth()
)
{
items(ThemeOptionKeys.count()) {
val key = ThemeOptionKeys[it]
val themeOption = ThemeOptions[key]
if (themeOption != null && themeOption.available(context)) {
Button(onClick = {
manager.updateTheme(
themeOption
)
}) {
Text(stringResource(themeOption.name))
}
}
}
}
*/
} }
override fun close() { override fun close() {

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,
)

View File

@ -180,7 +180,7 @@ private class VoiceInputActionWindow(
} }
@Composable @Composable
override fun WindowContents() { override fun WindowContents(keyboardShown: Boolean) {
Box(modifier = Modifier Box(modifier = Modifier
.fillMaxSize() .fillMaxSize()
.clickable(enabled = true, .clickable(enabled = true,

View File

@ -651,6 +651,9 @@ namespace latinime {
std::vector<TokenMix> mixes; std::vector<TokenMix> mixes;
for(int i=0; i<inputSize; i++) { for(int i=0; i<inputSize; i++) {
char wc = partialWordString[i];
if (!(wc >= 'a' && wc <= 'z') && !(wc >= 'A' && wc <= 'Z')) continue;
std::vector<float> proportions = pInfo->decomposeTapPosition(xCoordinates[i], yCoordinates[i]); std::vector<float> proportions = pInfo->decomposeTapPosition(xCoordinates[i], yCoordinates[i]);
for(float &f : proportions) { for(float &f : proportions) {
if(f < 0.05f) f = 0.0f; if(f < 0.05f) f = 0.0f;
@ -701,12 +704,12 @@ namespace latinime {
results.x = ((float)xCoordinates[i]) / ((float)pInfo->getKeyboardWidth()); results.x = ((float)xCoordinates[i]) / ((float)pInfo->getKeyboardWidth());
results.y = ((float)yCoordinates[i]) / ((float)pInfo->getKeyboardHeight()); results.y = ((float)yCoordinates[i]) / ((float)pInfo->getKeyboardHeight());
AKLOGI("%d | Char %c, pos %.6f %.6f, nearest is %c at %.2f, then %c at %.2f, finally %c at %.2f", i, partialWordString[i], //AKLOGI("%d | Char %c, pos %.6f %.6f, nearest is %c at %.2f, then %c at %.2f, finally %c at %.2f", i, partialWordString[i],
results.x, results.y, // results.x, results.y,
(char)(pInfo->getKeyCodePoint(index_value[0].second)), (float)(index_value[0].first), // (char)(pInfo->getKeyCodePoint(index_value[0].second)), (float)(index_value[0].first),
(char)(pInfo->getKeyCodePoint(index_value[1].second)), (float)(index_value[1].first), // (char)(pInfo->getKeyCodePoint(index_value[1].second)), (float)(index_value[1].first),
(char)(pInfo->getKeyCodePoint(index_value[2].second)), (float)(index_value[2].first) // (char)(pInfo->getKeyCodePoint(index_value[2].second)), (float)(index_value[2].first)
); // );
for(int j=0; j<NUM_TOKEN_MIX; j++) { for(int j=0; j<NUM_TOKEN_MIX; j++) {