From 94bee115a92049355ab4cc93db73b1bc21042765 Mon Sep 17 00:00:00 2001 From: Twaik Yont <9674930+twaik@users.noreply.github.com> Date: Tue, 14 Jan 2025 08:22:48 +0200 Subject: [PATCH] enhancement(LorieView.java): improve input handling --- app/src/main/cpp/lorie/activity.c | 12 +- app/src/main/cpp/lorie/cmdentrypoint.c | 7 + app/src/main/cpp/lorie/lorie.h | 1 + .../main/cpp/patches/.xserver.patch.kate-swp | Bin 0 -> 77 bytes app/src/main/cpp/patches/xserver.patch | 20 +++ .../main/java/com/termux/x11/LorieView.java | 156 +++++++++++------- .../java/com/termux/x11/MainActivity.java | 12 +- app/src/main/res/values/strings.xml | 2 - app/src/main/res/xml/preferences.xml | 1 - 9 files changed, 132 insertions(+), 79 deletions(-) create mode 100644 app/src/main/cpp/patches/.xserver.patch.kate-swp diff --git a/app/src/main/cpp/lorie/activity.c b/app/src/main/cpp/lorie/activity.c index 6e50b23d8..8d04a0fb6 100644 --- a/app/src/main/cpp/lorie/activity.c +++ b/app/src/main/cpp/lorie/activity.c @@ -26,7 +26,7 @@ extern volatile int conn_fd; // The only variable from shared with X server code static struct { jclass self; - jmethodID getInstance, clientConnectedStateChanged; + jmethodID getInstance, clientConnectedStateChanged, resetIme; } MainActivity = {0}; static struct { @@ -115,6 +115,7 @@ static void nativeInit(JNIEnv *env, jobject thiz) { MainActivity.self = FindClassOrDie(env, "com/termux/x11/MainActivity"); MainActivity.getInstance = FindMethodOrDie(env, MainActivity.self, "getInstance", "()Lcom/termux/x11/MainActivity;", JNI_TRUE); MainActivity.clientConnectedStateChanged = FindMethodOrDie(env, MainActivity.self, "clientConnectedStateChanged", "()V", JNI_FALSE); + MainActivity.resetIme = FindMethodOrDie(env, (*env)->GetObjectClass(env, thiz), "resetIme", "()V", JNI_FALSE); } (*env)->GetJavaVM(env, &vm); @@ -205,6 +206,9 @@ static int xcallback(int fd, int events, __unused void* data) { #endif LorieBuffer_release(buffer); } + case EVENT_WINDOW_FOCUS_CHANGED: { + (*env)->CallVoidMethod(env, thiz, MainActivity.resetIme); + } } } @@ -290,6 +294,8 @@ static void sendWindowChange(__unused JNIEnv* env, __unused jobject cls, jint wi static void sendMouseEvent(__unused JNIEnv* env, __unused jobject cls, jfloat x, jfloat y, jint which_button, jboolean button_down, jboolean relative) { if (conn_fd != -1) { + if (which_button > 0) + (*env)->CallVoidMethod(env, globalThiz, MainActivity.resetIme); lorieEvent e = { .mouse = { .t = EVENT_MOUSE, .x = x, .y = y, .detail = which_button, .down = button_down, .relative = relative } }; write(conn_fd, &e, sizeof(e)); } @@ -306,6 +312,7 @@ static void sendStylusEvent(__unused JNIEnv *env, __unused jobject thiz, jfloat jint pressure, jint tilt_x, jint tilt_y, jint orientation, jint buttons, jboolean eraser, jboolean mouse) { if (conn_fd != -1) { + (*env)->CallVoidMethod(env, globalThiz, MainActivity.resetIme); lorieEvent e = { .stylus = { .t = EVENT_STYLUS, .x = x, .y = y, .pressure = pressure, .tilt_x = tilt_x, .tilt_y = tilt_y, .orientation = orientation, .buttons = buttons, .eraser = eraser, .mouse = mouse } }; write(conn_fd, &e, sizeof(e)); } @@ -320,6 +327,7 @@ static void requestStylusEnabled(__unused JNIEnv *env, __unused jclass clazz, jb static jboolean sendKeyEvent(__unused JNIEnv* env, __unused jobject cls, jint scan_code, jint key_code, jboolean key_down) { if (conn_fd != -1) { + (*env)->CallVoidMethod(env, globalThiz, MainActivity.resetIme); int code = (scan_code) ?: android_to_linux_keycode[key_code]; log(DEBUG, "Sending key: %d (%d %d %d)", code + 8, scan_code, key_code, key_down); lorieEvent e = { .key = { .t = EVENT_KEY, .key = code + 8, .state = key_down } }; @@ -358,7 +366,7 @@ static void sendTextEvent(JNIEnv *env, __unused jobject thiz, jbyteArray text) { p += len; if (p - (char*) str >= length) break; - usleep(30000); + usleep(2500); } (*env)->ReleaseByteArrayElements(env, text, str, JNI_ABORT); diff --git a/app/src/main/cpp/lorie/cmdentrypoint.c b/app/src/main/cpp/lorie/cmdentrypoint.c index 479065229..3af7db734 100644 --- a/app/src/main/cpp/lorie/cmdentrypoint.c +++ b/app/src/main/cpp/lorie/cmdentrypoint.c @@ -437,6 +437,13 @@ void lorieSendRootWindowBuffer(LorieBuffer* buffer) { } } +void DDXNotifyFocusChanged(void) { + if (conn_fd != -1) { + lorieEvent e = { .type = EVENT_WINDOW_FOCUS_CHANGED }; + write(conn_fd, &e, sizeof(e)); + } +} + JNIEXPORT jobject JNICALL Java_com_termux_x11_CmdEntryPoint_getXConnection(JNIEnv *env, __unused jobject cls) { int client[2]; diff --git a/app/src/main/cpp/lorie/lorie.h b/app/src/main/cpp/lorie/lorie.h index 861c93365..83d263cad 100644 --- a/app/src/main/cpp/lorie/lorie.h +++ b/app/src/main/cpp/lorie/lorie.h @@ -104,6 +104,7 @@ typedef enum { EVENT_CLIPBOARD_ANNOUNCE, EVENT_CLIPBOARD_REQUEST, EVENT_CLIPBOARD_SEND, + EVENT_WINDOW_FOCUS_CHANGED, } eventType; typedef union { diff --git a/app/src/main/cpp/patches/.xserver.patch.kate-swp b/app/src/main/cpp/patches/.xserver.patch.kate-swp new file mode 100644 index 0000000000000000000000000000000000000000..c947786f77f6d93150b984def0dadca299930d21 GIT binary patch literal 77 zcmZQzU=Z?7EJ;-eE>A2_aLdd|RWQ;sU|?Vn(f#|S>tX1b&Pdi*oLf6Neob)y5*-}G Vz`&>rlw|>8R@Y!pBrc<#D*$MV69fPN literal 0 HcmV?d00001 diff --git a/app/src/main/cpp/patches/xserver.patch b/app/src/main/cpp/patches/xserver.patch index 89ac781cd..2dfdf48ae 100644 --- a/app/src/main/cpp/patches/xserver.patch +++ b/app/src/main/cpp/patches/xserver.patch @@ -299,3 +299,23 @@ index f9b7b06d9..f4b2aeddc 100644 /* display is initialized to "0" by main(). It is then set to the display * number if specified on the command line. */ ++++ b/dix/enterleave.c +@@ -1540,6 +1540,8 @@ DeviceFocusEvents(DeviceIntPtr dev, WindowPtr from, WindowPtr to, int mode) + } + } + ++extern void DDXNotifyFocusChanged(void); ++ + /** + * Figure out if focus events are necessary and send them to the + * appropriate windows. +@@ -1550,6 +1552,9 @@ DeviceFocusEvents(DeviceIntPtr dev, WindowPtr from, WindowPtr to, int mode) + void + DoFocusEvents(DeviceIntPtr pDev, WindowPtr from, WindowPtr to, int mode) + { ++ if (from != to) ++ DDXNotifyFocusChanged(); ++ + if (!IsKeyboardDevice(pDev)) + return; + diff --git a/app/src/main/java/com/termux/x11/LorieView.java b/app/src/main/java/com/termux/x11/LorieView.java index bc28f8c81..d78a14fe1 100644 --- a/app/src/main/java/com/termux/x11/LorieView.java +++ b/app/src/main/java/com/termux/x11/LorieView.java @@ -24,7 +24,7 @@ import android.view.inputmethod.EditorInfo; import android.view.inputmethod.InputConnection; import android.view.inputmethod.InputMethodManager; -import android.view.inputmethod.InputMethodSubtype; +import android.view.inputmethod.TextSnapshot; import androidx.annotation.Keep; import androidx.annotation.NonNull; @@ -32,9 +32,10 @@ import com.termux.x11.input.InputStub; import com.termux.x11.input.TouchInputHandler; -import java.nio.charset.StandardCharsets; import java.util.regex.PatternSyntaxException; +import static java.nio.charset.StandardCharsets.UTF_8; + import dalvik.annotation.optimization.CriticalNative; import dalvik.annotation.optimization.FastNative; @@ -54,11 +55,77 @@ interface PixelFormat { private static boolean clipboardSyncEnabled = false; private static boolean hardwareKbdScancodesWorkaround = false; private final InputMethodManager mIMM = (InputMethodManager)getContext().getSystemService(Context.INPUT_METHOD_SERVICE); - private String mImeLang; - private boolean mImeCJK; - public boolean enableGboardCJK; private Callback mCallback; private final Point p = new Point(); + CharSequence currentComposingText = null; + private final InputConnection mConnection = new BaseInputConnection(this, true) { + private final MainActivity a = MainActivity.getInstance(); + private final CharSequence seq = " "; + + @Override public Editable getEditable() { + return null; + } + + // Needed to send arrow keys with IME's cursor control feature + @Override public CharSequence getTextBeforeCursor(int length, int flags) { return seq; } + @Override public CharSequence getTextAfterCursor(int length, int flags) { return seq; } + @Override public boolean setComposingRegion(int start, int end) { return true; } + + void sendKey(int k) { + LorieView.this.sendKeyEvent(0, k, true); + LorieView.this.sendKeyEvent(0, k, false); + } + + @Override public boolean deleteSurroundingText(int beforeLength, int afterLength) { + for (int i=0; i 0 && newLen > 0 && (currentComposingText.toString().startsWith(newText.toString()) + || newText.toString().startsWith(currentComposingText.toString()))) { + for (int i=0; i < oldLen - newLen; i++) + sendKey(KeyEvent.KEYCODE_DEL); + for (int i=oldLen; i= 2 && !languageTag.substring(0, 2).equals(mImeLang)) - mIMM.restartInput(this); - else if (recheck) { // recheck needed because sometimes requestCursorUpdates() is called too fast, before InputMethodManager detect change in IM subtype - MainActivity.handler.postDelayed(() -> checkRestartInput(false), 40); - } - } - @Override public InputConnection onCreateInputConnection(EditorInfo outAttrs) { - outAttrs.inputType = InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS | InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD; - + outAttrs.inputType = InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS | InputType.TYPE_TEXT_VARIATION_NORMAL; + outAttrs.actionLabel = "↵"; // Note that IME_ACTION_NONE cannot be used as that makes it impossible to input newlines using the on-screen // keyboard on Android TV (see https://github.com/termux/termux-app/issues/221). outAttrs.imeOptions = EditorInfo.IME_FLAG_NO_FULLSCREEN; + return mConnection; + } - if (enableGboardCJK) { - InputMethodSubtype methodSubtype = mIMM.getCurrentInputMethodSubtype(); - mImeLang = methodSubtype == null ? null : methodSubtype.getLanguageTag(); - if (mImeLang != null && mImeLang.length() > 2) - mImeLang = mImeLang.substring(0, 2); - mImeCJK = mImeLang != null && (mImeLang.equals("zh") || mImeLang.equals("ko") || mImeLang.equals("ja")); - outAttrs.inputType = InputType.TYPE_CLASS_TEXT | InputType.TYPE_TEXT_FLAG_NO_SUGGESTIONS | - (mImeCJK ? InputType.TYPE_TEXT_VARIATION_NORMAL : InputType.TYPE_TEXT_VARIATION_VISIBLE_PASSWORD); - return new BaseInputConnection(this, false) { - // workaround for Gboard - // Gboard calls requestCursorUpdates() whenever switching language - // check and then restart keyboard in different inputType when needed - @Override - public Editable getEditable() { - checkRestartInput(true); - return super.getEditable(); - } - @Override - public boolean requestCursorUpdates(int cursorUpdateMode) { - checkRestartInput(true); - return super.requestCursorUpdates(cursorUpdateMode); - } + /** + * Unfortunately there is no direct way to focus inside X windows. + * As a workaround we will reset IME on X window focus change and any user interaction + * with LorieView except sending text (Unicode) and mouse movements. + * We must reset IME to get rid of pending composing, autocomplete and other status related stuff. + * It is called from native code, not from Java. + * @noinspection unused + */ + @Keep void resetIme() { + if (currentComposingText == null) + return; - @Override - public boolean commitText(CharSequence text, int newCursorPosition) { - boolean result = super.commitText(text, newCursorPosition); - if (mImeCJK) - // suppress Gboard CJK keyboard suggestion - // this workaround does not work well for non-CJK keyboards - // , when typing fast and two keypresses (commitText) are close in time - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) - mIMM.invalidateInput(LorieView.this); - else - mIMM.restartInput(LorieView.this); - return result; - } - }; - } else { - return super.onCreateInputConnection(outAttrs); - } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) + mIMM.invalidateInput(this); + else + mIMM.restartInput(this); } static native boolean renderingInActivity(); diff --git a/app/src/main/java/com/termux/x11/MainActivity.java b/app/src/main/java/com/termux/x11/MainActivity.java index 6a61c4a44..bed6ecba0 100644 --- a/app/src/main/java/com/termux/x11/MainActivity.java +++ b/app/src/main/java/com/termux/x11/MainActivity.java @@ -90,7 +90,7 @@ public class MainActivity extends AppCompatActivity implements View.OnApplyWindo private static boolean externalKeyboardConnected = false; private View.OnKeyListener mLorieKeyListener; private boolean filterOutWinKey = false; - private boolean useTermuxEKBarBehaviour = false; + boolean useTermuxEKBarBehaviour = false; private boolean isInPictureInPictureMode = false; public static Prefs prefs = null; @@ -232,7 +232,6 @@ else if (SamsungDexUtils.checkDeXEnabled(this)) onPreferencesChanged(""); toggleExtraKeys(false, false); - checkRestartInput(); initStylusAuxButtons(); initMouseAuxButtons(); @@ -879,15 +878,6 @@ public static boolean isConnected() { return LorieView.connected(); } - private void checkRestartInput() { - // an imperfect workaround for Gboard CJK keyboard in DeX soft keyboard mode - // in that particular mode during language switching, InputConnection#requestCursorUpdates() is not called and no signal can be picked up. - // therefore, check to activate CJK keyboard is done upon a keypress. - if (getLorieView().enableGboardCJK && SamsungDexUtils.checkDeXEnabled(this)) - getLorieView().checkRestartInput(false); - handler.postDelayed(this::checkRestartInput, 300); - } - public static void getRealMetrics(DisplayMetrics m) { if (getInstance() != null && getInstance().getLorieView() != null && diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 051ad26f1..a94d120f2 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -84,8 +84,6 @@ E.g. META for META key, \n Pause key intercepting with Esc key Filter out intercepted Win (Meta/Mod4) key. Allows you to use Dex shortcuts while intercepting. Requires Accessibility service to work. - Workaround to enable CJK Gboard - May require Android 14 and Gboard 14 Clipboard sharing Request notification permission diff --git a/app/src/main/res/xml/preferences.xml b/app/src/main/res/xml/preferences.xml index 0015779ba..6520cffd1 100644 --- a/app/src/main/res/xml/preferences.xml +++ b/app/src/main/res/xml/preferences.xml @@ -49,7 +49,6 @@ -