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"
android:width="26dp"
android:height="26dp"
android:viewportWidth="26"
android:viewportHeight="26">
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M20,13L6,13"
android:pathData="M19,12L5,12"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="#FFFFFF"
android:strokeColor="#ffffff"
android:strokeLineCap="round"/>
<path
android:pathData="M13,20L6,13L13,6"
android:pathData="M12,19l-7,-7l7,-7"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="#FFFFFF"
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="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" />
<!-- Color to use for the label in a key when in inactivated state. -->
<attr name="keyTextInactivatedColor" format="color" />
<attr name="keyPressedTextColor" format="color" />
<!-- Color to use for the label in a key that has followFunctionalTextColor keyLabelFlags. -->
<attr name="functionalTextColor" format="color" />
<!-- Key hint letter (= one character hint label) color -->

View File

@ -2,6 +2,11 @@
<resources>
<string name="voice_input_action_title">Voice Input</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="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 corresponding to msgid="5433275485499039199" -->
<string name="user_dict_fast_scroll_alphabet">\u0020ABCDEFGHIJKLMNOPQRSTUVWXYZ</string>
<string name="title_emojis">Emojis</string>
</resources>

View File

@ -48,6 +48,7 @@
<item name="spacebarBackground">@drawable/btn_keyboard_spacebar_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="keyPressedTextColor">@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="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="keyTextColor">@color/key_text_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="keyHintLetterColor">@color/key_hint_letter_color_lxx_light</item>
<item name="keyHintLabelColor">@color/key_text_inactive_color_lxx_light</item>

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -101,6 +101,11 @@ public interface KeyboardActionListener {
*/
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 class Adapter implements KeyboardActionListener {
@ -125,8 +130,14 @@ public interface KeyboardActionListener {
@Override
public void onFinishSlidingInput() {}
@Override
public boolean onCustomRequest(int requestCode) {
return false;
}
public boolean onCustomRequest(int requestCode) { 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);
mDefaultKeyLabelFlags = keyAttr.getInt(R.styleable.Keyboard_Key_keyLabelFlags, 0);
mKeyVisualAttributes = KeyVisualAttributes.newInstance(keyAttr, mDrawableProvider);
if(isMoreKeys && mKeyVisualAttributes != null) {
mKeyVisualAttributes.mTextColor = mDrawableProvider.getMoreKeysTextColor();
}
keyAttr.recycle();
mPaint.setAntiAlias(true);

View File

@ -85,6 +85,8 @@ public final class PointerTracker implements PointerTrackerQueue.Element,
// Parameters for pointer handling.
private static PointerTrackerParams sParams;
private static final int sPointerStep = (int)(16.0 * Resources.getSystem().getDisplayMetrics().density);
private static GestureStrokeRecognitionParams sGestureStrokeRecognitionParams;
private static GestureStrokeDrawingParams sGestureStrokeDrawingParams;
private static boolean sNeedsPhantomSuddenMoveEventHack;
@ -128,6 +130,11 @@ public final class PointerTracker implements PointerTrackerQueue.Element,
private int mLastX;
private int mLastY;
private int mStartX;
private int mStartY;
private long mStartTime;
private boolean mCursorMoved = false;
// true if keyboard layout has been changed.
private boolean mKeyboardLayoutHasBeenChanged;
@ -691,6 +698,10 @@ public final class PointerTracker implements PointerTrackerQueue.Element,
startRepeatKey(key);
startLongPressTimer(key);
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 lastY = mLastY;
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);
if (sGestureEnabler.shouldHandleGesture()) {
@ -966,6 +1000,14 @@ public final class PointerTracker implements PointerTrackerQueue.Element,
// Release the last pressed key.
setReleasedKeyGraphics(currentKey, true /* withAnimation */);
if(mCursorMoved && currentKey.getCode() == Constants.CODE_DELETE) {
sListener.onUpWithDeletePointerActive();
}
if(mCursorMoved) {
sListener.onUpWithPointerActive();
}
if (isShowingMoreKeysPanel()) {
if (!mIsTrackingForActionDisabled) {
final int translatedX = mMoreKeysPanel.translateX(x);
@ -988,6 +1030,10 @@ public final class PointerTracker implements PointerTrackerQueue.Element,
return;
}
if (mCursorMoved) {
mCursorMoved = false;
return;
}
if (mIsTrackingForActionDisabled) {
return;
}
@ -1018,6 +1064,9 @@ public final class PointerTracker implements PointerTrackerQueue.Element,
if (isShowingMoreKeysPanel()) {
return;
}
if (mCursorMoved) {
return;
}
final Key key = getKey();
if (key == null) {
return;

View File

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

View File

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

View File

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

View File

@ -1,6 +1,5 @@
package org.futo.inputmethod.latin
import android.content.Context
import android.content.res.Configuration
import android.inputmethodservice.InputMethodService
import android.os.Build
@ -13,34 +12,17 @@ import android.view.inputmethod.InlineSuggestionsRequest
import android.view.inputmethod.InlineSuggestionsResponse
import android.view.inputmethod.InputMethodSubtype
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.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.MutableState
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.key
import androidx.compose.ui.Alignment.Companion.CenterVertically
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.clipToBounds
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.viewinterop.AndroidView
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.LifecycleCoroutineScope
import androidx.lifecycle.LifecycleOwner
import androidx.lifecycle.LifecycleRegistry
import androidx.lifecycle.ViewModelStore
@ -56,34 +38,24 @@ import androidx.savedstate.SavedStateRegistryOwner
import androidx.savedstate.findViewTreeSavedStateRegistryOwner
import androidx.savedstate.setViewTreeSavedStateRegistryOwner
import kotlinx.coroutines.Job
import kotlinx.coroutines.launch
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.DynamicThemeProvider
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.UixManager
import org.futo.inputmethod.latin.uix.createInlineSuggestionsRequest
import org.futo.inputmethod.latin.uix.deferGetSetting
import org.futo.inputmethod.latin.uix.deferSetSetting
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.ThemeOption
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.xlm.LanguageModelFacilitator
class LatinIME : InputMethodService(), LifecycleOwner, ViewModelStoreOwner, SavedStateRegistryOwner,
LatinIMELegacy.SuggestionStripController, DynamicThemeProviderOwner, KeyboardManagerForAction {
LatinIMELegacy.SuggestionStripController, DynamicThemeProviderOwner {
private val mSavedStateRegistryController = SavedStateRegistryController.create(this)
@ -101,7 +73,7 @@ class LatinIME : InputMethodService(), LifecycleOwner, ViewModelStoreOwner, Save
override val viewModelStore
get() = store
private fun setOwners() {
fun setOwners() {
val decorView = window.window?.decorView
if (decorView?.findViewTreeLifecycleOwner() == null) {
decorView?.setViewTreeLifecycleOwner(this)
@ -114,14 +86,14 @@ class LatinIME : InputMethodService(), LifecycleOwner, ViewModelStoreOwner, Save
}
}
private var composeView: ComposeView? = null
private val latinIMELegacy = LatinIMELegacy(
val latinIMELegacy = LatinIMELegacy(
this as InputMethodService,
this as LatinIMELegacy.SuggestionStripController
)
public val languageModelFacilitator = LanguageModelFacilitator(
val inputLogic get() = latinIMELegacy.mInputLogic
val languageModelFacilitator = LanguageModelFacilitator(
this,
latinIMELegacy.mInputLogic,
latinIMELegacy.mDictionaryFacilitator,
@ -130,21 +102,18 @@ class LatinIME : InputMethodService(), LifecycleOwner, ViewModelStoreOwner, Save
lifecycleScope
)
val uixManager = UixManager(this)
private var activeThemeOption: ThemeOption? = null
private var activeColorScheme = DarkColorScheme
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 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
// 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)
window.window?.navigationBarColor = drawableProvider!!.primaryKeyboardColor
setContent()
uixManager.onColorSchemeChanged()
}
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() {
super.onCreate()
@ -241,41 +236,38 @@ class LatinIME : InputMethodService(), LifecycleOwner, ViewModelStoreOwner, Save
private var touchableHeight: Int = 0
override fun onCreateInputView(): View {
legacyInputView = latinIMELegacy.onCreateInputView()
composeView = ComposeView(this).apply {
setViewCompositionStrategy(ViewCompositionStrategy.DisposeOnViewTreeLifecycleDestroyed)
setParentCompositionContext(null)
this@LatinIME.setOwners()
}
setContent()
val composeView = uixManager.createComposeView()
latinIMELegacy.setComposeInputView(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")
}
return composeView
}
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
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) {
Modifier.clipToBounds().size(0.dp)
Modifier
.clipToBounds()
.size(0.dp)
} else {
Modifier.onSizeChanged {
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
fun updateLegacyView(newView: View) {
legacyInputView = newView
setContent()
if (composeView != null) {
latinIMELegacy.setComposeInputView(composeView)
uixManager.setContent()
uixManager.getComposeView()?.let {
latinIMELegacy.setComposeInputView(it)
}
latinIMELegacy.setInputView(legacyInputView)
@ -417,8 +295,8 @@ class LatinIME : InputMethodService(), LifecycleOwner, ViewModelStoreOwner, Save
override fun setInputView(view: View?) {
super.setInputView(view)
if (composeView != null) {
latinIMELegacy.setComposeInputView(composeView)
uixManager.getComposeView()?.let {
latinIMELegacy.setComposeInputView(it)
}
latinIMELegacy.setInputView(legacyInputView)
@ -443,15 +321,14 @@ class LatinIME : InputMethodService(), LifecycleOwner, ViewModelStoreOwner, Save
override fun onFinishInputView(finishingInput: Boolean) {
super.onFinishInputView(finishingInput)
latinIMELegacy.onFinishInputView(finishingInput)
closeActionWindow()
uixManager.onInputFinishing()
}
override fun onFinishInput() {
super.onFinishInput()
latinIMELegacy.onFinishInput()
closeActionWindow()
uixManager.onInputFinishing()
languageModelFacilitator.saveHistoryLog()
}
@ -471,7 +348,7 @@ class LatinIME : InputMethodService(), LifecycleOwner, ViewModelStoreOwner, Save
super.onWindowHidden()
latinIMELegacy.onWindowHidden()
closeActionWindow()
uixManager.onInputFinishing()
}
override fun onUpdateSelection(
@ -521,12 +398,14 @@ class LatinIME : InputMethodService(), LifecycleOwner, ViewModelStoreOwner, Save
}
override fun onComputeInsets(outInsets: Insets?) {
val composeView = uixManager.getComposeView()
// This method may be called before {@link #setInputView(View)}.
if (legacyInputView == null || composeView == null) {
return
}
val inputHeight: Int = composeView!!.height
val inputHeight: Int = composeView.height
if (latinIMELegacy.isImeSuppressedByHardwareKeyboard && !legacyInputView!!.isShown) {
// If there is a hardware keyboard and a visible software keyboard view has been hidden,
// no visual element will be shown on the screen.
@ -541,7 +420,7 @@ class LatinIME : InputMethodService(), LifecycleOwner, ViewModelStoreOwner, Save
val touchLeft = 0
val touchTop = visibleTopY
val touchRight = composeView!!.width
val touchRight = composeView.width
val touchBottom = inputHeight
latinIMELegacy.setInsets(outInsets!!.apply {
@ -581,124 +460,25 @@ class LatinIME : InputMethodService(), LifecycleOwner, ViewModelStoreOwner, Save
}
override fun updateVisibility(shouldShowSuggestionsStrip: Boolean, fullscreenMode: Boolean) {
this.shouldShowSuggestionStrip = shouldShowSuggestionsStrip
setContent()
uixManager.updateVisibility(shouldShowSuggestionsStrip, fullscreenMode)
}
override fun setSuggestions(suggestedWords: SuggestedWords?, rtlSubtype: Boolean) {
this.suggestedWords = suggestedWords
setContent()
uixManager.setSuggestions(suggestedWords, rtlSubtype)
}
override fun maybeShowImportantNoticeTitle(): Boolean {
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() {
super.onLowMemory()
cleanUpPersistentStates()
uixManager.cleanUpPersistentStates()
}
override fun onTrimMemory(level: Int) {
super.onTrimMemory(level)
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()
}
}
uixManager.cleanUpPersistentStates()
}
@RequiresApi(Build.VERSION_CODES.R)
@ -708,12 +488,7 @@ class LatinIME : InputMethodService(), LifecycleOwner, ViewModelStoreOwner, Save
@RequiresApi(Build.VERSION_CODES.R)
override fun onInlineSuggestionsResponse(response: InlineSuggestionsResponse): Boolean {
inlineSuggestions = response.inlineSuggestions.map {
inflateInlineSuggestion(it)
}
setContent()
return true
return uixManager.onInlineSuggestionsResponse(response)
}
fun postUpdateSuggestionStrip(inputStyle: Int) {

View File

@ -41,6 +41,7 @@ import android.os.IBinder;
import android.os.Message;
import android.preference.PreferenceManager;
import android.text.InputType;
import android.text.TextUtils;
import android.util.Log;
import android.util.PrintWriterPrinter;
import android.util.Printer;
@ -158,6 +159,7 @@ public class LatinIMELegacy implements KeyboardActionListener,
private static final String SCHEME_PACKAGE = "package";
final Settings mSettings;
private Locale mLocale;
final DictionaryFacilitator mDictionaryFacilitator =
DictionaryFacilitatorProvider.getDictionaryFacilitator(
false /* isNeededForSpellChecking */);
@ -671,18 +673,18 @@ public class LatinIMELegacy implements KeyboardActionListener,
// Has to be package-visible for unit tests
@UsedForTesting
void loadSettings() {
final Locale locale = mRichImm.getCurrentSubtypeLocale();
mLocale = mRichImm.getCurrentSubtypeLocale();
final EditorInfo editorInfo = mInputMethodService.getCurrentInputEditorInfo();
final InputAttributes inputAttributes = new InputAttributes(
editorInfo, mInputMethodService.isFullscreenMode(), mInputMethodService.getPackageName());
mSettings.loadSettings(mInputMethodService, locale, inputAttributes);
mSettings.loadSettings(mInputMethodService, mLocale, inputAttributes);
final SettingsValues currentSettingsValues = mSettings.getCurrent();
AudioAndHapticFeedbackManager.getInstance().onSettingsChanged(currentSettingsValues);
// This method is called on startup and language switch, before the new layout has
// been displayed. Opening dictionaries never affects responsivity as dictionaries are
// asynchronously loaded.
if (!mHandler.hasPendingReopenDictionaries()) {
resetDictionaryFacilitator(locale);
resetDictionaryFacilitator(mLocale);
}
refreshPersonalizationDictionarySession(currentSettingsValues);
resetDictionaryFacilitatorIfNecessary();
@ -1365,6 +1367,54 @@ public class LatinIMELegacy implements KeyboardActionListener,
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() {
return mOptionsDialog != null && mOptionsDialog.isShowing();
}

View File

@ -1044,4 +1044,41 @@ public final class RichInputConnection implements PrivateCommandPerformer {
return InputConnectionCompatUtils.requestCursorUpdates(
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
// 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.
sendDownUpKeyEvent(KeyEvent.KEYCODE_DEL);
sendDownUpKeyEvent(KeyEvent.KEYCODE_DEL, 0);
int totalDeletedLength = 1;
if (mDeleteCount > Constants.DELETE_ACCELERATE_AT) {
// 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.
hasUnlearnedWordBeingDeleted |= unlearnWordBeingDeleted(
inputTransaction.mSettingsValues, currentKeyboardScriptId);
sendDownUpKeyEvent(KeyEvent.KEYCODE_DEL);
sendDownUpKeyEvent(KeyEvent.KEYCODE_DEL, 0);
totalDeletedLength++;
}
StatsUtils.onBackspacePressed(totalDeletedLength);
@ -2021,13 +2021,13 @@ public final class InputLogic {
*
* @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();
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));
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));
}
@ -2045,7 +2045,7 @@ public final class InputLogic {
// TODO: Remove this special handling of digit letters.
// For backward compatibility. See {@link InputMethodService#sendKeyChar(char)}.
if (codePoint >= '0' && codePoint <= '9') {
sendDownUpKeyEvent(codePoint - '0' + KeyEvent.KEYCODE_0);
sendDownUpKeyEvent(codePoint - '0' + KeyEvent.KEYCODE_0, 0);
return;
}
@ -2055,7 +2055,7 @@ public final class InputLogic {
// 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
// relying on this behavior so we continue to support it for older apps.
sendDownUpKeyEvent(KeyEvent.KEYCODE_ENTER);
sendDownUpKeyEvent(KeyEvent.KEYCODE_ENTER, 0);
} else {
mConnection.commitText(StringUtils.newSingleCodePointString(codePoint), 1);
}

View File

@ -29,6 +29,9 @@ interface KeyboardManagerForAction {
fun triggerSystemVoiceInput()
fun updateTheme(newTheme: ThemeOption)
fun sendCodePointEvent(codePoint: Int)
fun sendKeyEvent(keyCode: Int, metaState: Int)
}
interface ActionWindow {
@ -36,7 +39,7 @@ interface ActionWindow {
fun windowName(): String
@Composable
fun WindowContents()
fun WindowContents(keyboardShown: Boolean)
fun close()
}
@ -54,6 +57,8 @@ interface PersistentActionState {
data class Action(
@DrawableRes val icon: Int,
@StringRes val name: Int,
val canShowKeyboard: Boolean = false,
val windowImpl: ((KeyboardManagerForAction, PersistentActionState?) -> ActionWindow)?,
val simplePressImpl: ((KeyboardManagerForAction, PersistentActionState?) -> Unit)?,
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.height
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.lazy.LazyRow
import androidx.compose.material3.ButtonDefaults
import androidx.compose.material3.ColorScheme
import androidx.compose.material3.Icon
@ -20,6 +21,7 @@ import androidx.compose.material3.IconButton
import androidx.compose.material3.IconButtonDefaults
import androidx.compose.material3.MaterialTheme
import androidx.compose.material3.Surface
import androidx.compose.material3.Text
import androidx.compose.material3.TextButton
import androidx.compose.material3.dynamicDarkColorScheme
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.KIND_TYPED
import org.futo.inputmethod.latin.suggestions.SuggestionStripView
import org.futo.inputmethod.latin.uix.actions.ClipboardAction
import org.futo.inputmethod.latin.uix.actions.EmojiAction
import org.futo.inputmethod.latin.uix.actions.RedoAction
import org.futo.inputmethod.latin.uix.actions.TextEditAction
import org.futo.inputmethod.latin.uix.actions.ThemeAction
import org.futo.inputmethod.latin.uix.actions.UndoAction
import org.futo.inputmethod.latin.uix.actions.VoiceInputAction
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 java.lang.Integer.min
import kotlin.math.ceil
@ -287,7 +294,7 @@ fun ActionItem(action: Action, onSelect: (Action) -> Unit) {
cornerRadius = CornerRadius(radius, radius)
)
}
.width(64.dp)
.width(50.dp)
.fillMaxHeight(),
colors = IconButtonDefaults.iconButtonColors(contentColor = contentCol)
) {
@ -317,12 +324,10 @@ fun RowScope.ActionItems(onSelect: (Action) -> Unit) {
ActionItem(EmojiAction, onSelect)
ActionItem(VoiceInputAction, onSelect)
ActionItem(ThemeAction, onSelect)
Box(modifier = Modifier
.fillMaxHeight()
.weight(1.0f)) {
}
ActionItem(UndoAction, onSelect)
ActionItem(RedoAction, onSelect)
ActionItem(ClipboardAction, onSelect)
ActionItem(TextEditAction, onSelect)
}
@ -386,7 +391,11 @@ fun ActionBar(
ExpandActionsButton(isActionsOpen.value) { isActionsOpen.value = !isActionsOpen.value }
if(isActionsOpen.value) {
LazyRow {
item {
ActionItems(onActionActivated)
}
}
} else if(inlineSuggestions.isNotEmpty() && Build.VERSION.SDK_INT >= Build.VERSION_CODES.R) {
InlineSuggestions(inlineSuggestions)
} else if(words != null) {
@ -399,10 +408,113 @@ fun ActionBar(
Spacer(modifier = Modifier.weight(1.0f))
}
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
@Preview

View File

@ -31,6 +31,7 @@ class BasicThemeProvider(val context: Context, val overrideColorScheme: ColorSch
override val keyFeedback: Drawable
override val moreKeysTextColor: Int
override val moreKeysKeyboardBackground: Drawable
override val popupKey: Drawable
@ -107,6 +108,10 @@ class BasicThemeProvider(val context: Context, val overrideColorScheme: ColorSch
val surface = colorScheme.background.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 onBackground = colorScheme.onBackground.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_keyTextInactivatedColor] = onBackgroundHalf
colors[R.styleable.Keyboard_Key_keyPressedTextColor] = onPrimary
colors[R.styleable.Keyboard_Key_keyTextShadowColor] = 0
colors[R.styleable.Keyboard_Key_functionalTextColor] = onBackground
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())
}
moreKeysKeyboardBackground = coloredRoundedRectangle(surface, dp(8.dp))
moreKeysTextColor = onPrimaryContainer
moreKeysKeyboardBackground = coloredRoundedRectangle(primaryContainer, dp(8.dp))
popupKey = StateListDrawable().apply {
addStateWithHighlightLayerOnPressed(highlight, intArrayOf(),
coloredRoundedRectangle(surface, dp(8.dp))
addStateWithHighlightLayerOnPressed(primary, intArrayOf(),
coloredRoundedRectangle(primaryContainer, dp(8.dp))
)
}
}

View File

@ -13,6 +13,7 @@ interface DynamicThemeProvider {
val keyFeedback: Drawable
val moreKeysTextColor: Int
val moreKeysKeyboardBackground: 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.jsonPrimitive
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
import org.futo.inputmethod.latin.uix.PersistentActionState
@ -161,7 +162,7 @@ data class BitmapRecycler(
}
@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 spToDp = context.resources.displayMetrics.scaledDensity / context.resources.displayMetrics.density
@ -181,22 +182,28 @@ fun EmojiGrid(onClick: (EmojiItem) -> Unit, onExit: () -> Unit, onBackspace: ()
}
}
}
Surface(color = MaterialTheme.colorScheme.background, modifier = Modifier
if(!keyboardShown) {
Surface(
color = MaterialTheme.colorScheme.background, modifier = Modifier
.fillMaxWidth()
.height(48.dp)) {
.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
Button(
onClick = { onSpace() }, modifier = Modifier
.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)) {
), shape = RoundedCornerShape(32.dp)
) {
Text("")
}
@ -224,6 +231,7 @@ fun EmojiGrid(onClick: (EmojiItem) -> Unit, onExit: () -> Unit, onBackspace: ()
}
}
}
}
/*
@Preview(showBackground = true)
@ -266,7 +274,8 @@ class PersistentEmojiState: PersistentActionState {
val EmojiAction = Action(
icon = R.drawable.smile,
name = R.string.title_emojis,
name = R.string.emoji_action_title,
canShowKeyboard = true,
simplePressImpl = null,
persistentState = { manager ->
val state = PersistentEmojiState()
@ -282,21 +291,21 @@ val EmojiAction = Action(
object : ActionWindow {
@Composable
override fun windowName(): String {
return stringResource(R.string.title_emojis)
return stringResource(R.string.emoji_action_title)
}
@Composable
override fun WindowContents() {
override fun WindowContents(keyboardShown: Boolean) {
state.emojis.value?.let { emojis ->
EmojiGrid(onClick = {
manager.typeText(it.emoji)
}, onExit = {
manager.closeActionWindow()
}, onSpace = {
manager.typeText(" ")
manager.sendCodePointEvent(Constants.CODE_SPACE)
}, onBackspace = {
manager.backspace(1)
}, bitmaps = state.bitmaps, emojis = emojis)
manager.sendCodePointEvent(Constants.CODE_DELETE)
}, 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
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.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.res.stringResource
import androidx.compose.ui.unit.dp
import org.futo.inputmethod.latin.R
import org.futo.inputmethod.latin.uix.Action
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
val ThemeAction = Action(
icon = R.drawable.eye,
name = R.string.theme_switcher_action_title,
simplePressImpl = null,
canShowKeyboard = true,
windowImpl = { manager, _ ->
object : ActionWindow {
@Composable
@ -30,33 +20,8 @@ val ThemeAction = Action(
}
@Composable
override fun WindowContents() {
val context = LocalContext.current
override fun WindowContents(keyboardShown: Boolean) {
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() {

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
override fun WindowContents() {
override fun WindowContents(keyboardShown: Boolean) {
Box(modifier = Modifier
.fillMaxSize()
.clickable(enabled = true,

View File

@ -651,6 +651,9 @@ namespace latinime {
std::vector<TokenMix> mixes;
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]);
for(float &f : proportions) {
if(f < 0.05f) f = 0.0f;
@ -701,12 +704,12 @@ namespace latinime {
results.x = ((float)xCoordinates[i]) / ((float)pInfo->getKeyboardWidth());
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],
results.x, results.y,
(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[2].second)), (float)(index_value[2].first)
);
//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,
// (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[2].second)), (float)(index_value[2].first)
// );
for(int j=0; j<NUM_TOKEN_MIX; j++) {