Skip to content

Commit

Permalink
enhancement(LorieView.java): improve input handling
Browse files Browse the repository at this point in the history
  • Loading branch information
twaik committed Jan 14, 2025
1 parent 989d08a commit 94bee11
Show file tree
Hide file tree
Showing 9 changed files with 132 additions and 79 deletions.
12 changes: 10 additions & 2 deletions app/src/main/cpp/lorie/activity.c
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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);
}
}
}

Expand Down Expand Up @@ -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));
}
Expand All @@ -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));
}
Expand All @@ -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 } };
Expand Down Expand Up @@ -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);
Expand Down
7 changes: 7 additions & 0 deletions app/src/main/cpp/lorie/cmdentrypoint.c
Original file line number Diff line number Diff line change
Expand Up @@ -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];
Expand Down
1 change: 1 addition & 0 deletions app/src/main/cpp/lorie/lorie.h
Original file line number Diff line number Diff line change
Expand Up @@ -104,6 +104,7 @@ typedef enum {
EVENT_CLIPBOARD_ANNOUNCE,
EVENT_CLIPBOARD_REQUEST,
EVENT_CLIPBOARD_SEND,
EVENT_WINDOW_FOCUS_CHANGED,
} eventType;

typedef union {
Expand Down
Binary file added app/src/main/cpp/patches/.xserver.patch.kate-swp
Binary file not shown.
20 changes: 20 additions & 0 deletions app/src/main/cpp/patches/xserver.patch
Original file line number Diff line number Diff line change
Expand Up @@ -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;

156 changes: 93 additions & 63 deletions app/src/main/java/com/termux/x11/LorieView.java
Original file line number Diff line number Diff line change
Expand Up @@ -24,17 +24,18 @@
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;

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;

Expand All @@ -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<beforeLength; i++) sendKey(KeyEvent.KEYCODE_DEL);
for (int i=0; i<afterLength; i++) sendKey(KeyEvent.KEYCODE_FORWARD_DEL);
return true;
}

/**
* X server itself does not provide any way to compose text.
* But we can simply send text we want and erase it in the case if user does not need it.
*
* @noinspection SameReturnValue*/
boolean replaceText(CharSequence newText, boolean reuse) {
int oldLen = currentComposingText != null ? currentComposingText.length() : 0;
int newLen = newText != null ? newText.length() : 0;
if (oldLen > 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<newLen; i++)
sendTextEvent(String.valueOf(newText.charAt(i)).getBytes(UTF_8));
} else {
for (int i = 0; i < oldLen; i++)
sendKey(KeyEvent.KEYCODE_DEL);
if (newText != null)
sendTextEvent(newText.toString().getBytes(UTF_8));
}

currentComposingText = reuse ? newText : null;

if (a.useTermuxEKBarBehaviour && a.mExtraKeys != null)
a.mExtraKeys.unsetSpecialKeys();
return true;
}

@Override public boolean setComposingText(CharSequence text, int newCursorPosition) {
return replaceText(text, true);
}

@Override
public boolean commitText(CharSequence text, int newCursorPosition) {
return replaceText(text, false);
}

@Override
public boolean finishComposingText() {
// We do not implement real composing, so no need to finish it.
currentComposingText = null;
return true;
}
};
private final SurfaceHolder.Callback mSurfaceCallback = new SurfaceHolder.Callback() {
@Override public void surfaceCreated(@NonNull SurfaceHolder holder) {
holder.setFormat(PixelFormat.BGRA_8888);
Expand Down Expand Up @@ -235,8 +302,6 @@ public void reloadPreferences(Prefs p) {
clipboardSyncEnabled = p.clipboardEnable.get();
setClipboardSyncEnabled(clipboardSyncEnabled, clipboardSyncEnabled);
TouchInputHandler.refreshInputDevices();
enableGboardCJK = p.enableGboardCJK.get();
mIMM.restartInput(this);
}

// It is used in native code
Expand All @@ -252,14 +317,14 @@ void setClipboardText(String text) {
/** @noinspection unused*/ // It is used in native code
void requestClipboard() {
if (!clipboardSyncEnabled) {
sendClipboardEvent("".getBytes(StandardCharsets.UTF_8));
sendClipboardEvent("".getBytes(UTF_8));
return;
}

CharSequence clip = clipboard.getText();
if (clip != null) {
String text = String.valueOf(clipboard.getText());
sendClipboardEvent(text.getBytes(StandardCharsets.UTF_8));
sendClipboardEvent(text.getBytes(UTF_8));
Log.d("CLIP", "sending clipboard contents: " + text);
}
}
Expand Down Expand Up @@ -298,67 +363,32 @@ public void onWindowFocusChanged(boolean hasFocus) {
TouchInputHandler.refreshInputDevices();
}

public void checkRestartInput(boolean recheck) {
if (!enableGboardCJK)
return;

InputMethodSubtype methodSubtype = mIMM.getCurrentInputMethodSubtype();
String languageTag = methodSubtype == null ? null : methodSubtype.getLanguageTag();
if (languageTag != null && languageTag.length() >= 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();
Expand Down
12 changes: 1 addition & 11 deletions app/src/main/java/com/termux/x11/MainActivity.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -232,7 +232,6 @@ else if (SamsungDexUtils.checkDeXEnabled(this))
onPreferencesChanged("");

toggleExtraKeys(false, false);
checkRestartInput();

initStylusAuxButtons();
initMouseAuxButtons();
Expand Down Expand Up @@ -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 &&
Expand Down
2 changes: 0 additions & 2 deletions app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -84,8 +84,6 @@ E.g. <b>META</b> for <i>META key</i>, \n
<string name="pref_pauseKeyInterceptingWithEsc">Pause key intercepting with Esc key</string>
<string name="pref_filterOutWinkey">Filter out intercepted Win (Meta/Mod4) key.</string>
<string name="pref_filterOutWinkey_summary">Allows you to use Dex shortcuts while intercepting. Requires Accessibility service to work.</string>
<string name="pref_enableGboardCJK">Workaround to enable CJK Gboard</string>
<string name="pref_enableGboardCJK_summary">May require Android 14 and Gboard 14</string>

<string name="pref_clipboardEnable">Clipboard sharing</string>
<string name="pref_requestNotificationPermission">Request notification permission</string>
Expand Down
1 change: 0 additions & 1 deletion app/src/main/res/xml/preferences.xml
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,6 @@
<SwitchPreferenceCompat app:key="enableAccessibilityServiceAutomatically" app:defaultValue="false" />
<SwitchPreferenceCompat app:key="pauseKeyInterceptingWithEsc" app:defaultValue="false" />
<SwitchPreferenceCompat app:key="filterOutWinkey" app:defaultValue="false" />
<SwitchPreferenceCompat app:key="enableGboardCJK" app:defaultValue="false" />
</PreferenceScreen>
<PreferenceScreen app:key="other">
<SwitchPreferenceCompat app:key="clipboardEnable" app:defaultValue="true" />
Expand Down

0 comments on commit 94bee11

Please sign in to comment.