diff --git a/app/src/main/java/com/anland/termux/MainActivity.java b/app/src/main/java/com/anland/termux/MainActivity.java
index 1cf0628..9df37f1 100644
--- a/app/src/main/java/com/anland/termux/MainActivity.java
+++ b/app/src/main/java/com/anland/termux/MainActivity.java
@@ -88,7 +88,7 @@ public class MainActivity extends Activity
// shrinking it: the bar rides up with the keyboard but the surface keeps
// its full size. See relayout() and buildExtraKeysBar().
private static final String KEY_KEYBOARD_FLOATING = "keyboard_floating";
- private boolean mKeyboardFloating = false;
+ private boolean mKeyboardFloating = true;
// Persistent "tap to open Settings" notification, toggleable in Settings > General.
private static final String KEY_NOTIFICATION_ENABLED = "settings_notification";
private static final String KEY_SCREEN_ORIENTATION = "screen_orientation";
@@ -380,14 +380,26 @@ public void onKeyUp(int scanCode) {
// the view starts GONE and a GONE view is never measured.
setContentView(root);
+ // Establish a focused descendant after attachment so the DecorView routes
+ // input through the surface even while the extra-keys bar is hidden.
+ surfaceView.requestFocus();
surfaceView.getHolder().addCallback(this);
root.setOnApplyWindowInsetsListener((v, insets) -> {
// When the IME hides by any means (toggle, system back, or the IME's
// own close button), release the hidden input so its focus state
// stays in sync — otherwise reopening needs a second press.
- if (!insets.isVisible(WindowInsets.Type.ime()))
+ // Ignore the initial hidden-inset dispatch while a show request is
+ // settling. That dispatch can arrive between requestFocus() and
+ // showSoftInput(), and disabling the target there makes the first
+ // bound-key press appear to do nothing.
+ boolean imeWasVisible = mImeBottom > 0;
+ if (!insets.isVisible(WindowInsets.Type.ime()) && imeWasVisible) {
+ View focused = getCurrentFocus();
systemIme.releaseHiddenInput();
+ if (focused == systemIme.getInputView() || getCurrentFocus() == null)
+ surfaceView.requestFocus();
+ }
applyDisplaySafeInsets(insets);
applyImeInset(insets);
return v.onApplyWindowInsets(insets);
@@ -593,6 +605,14 @@ && getResources().getConfiguration().orientation
cutout.right, Math.max(cutout.bottom, roundedCornerBottom));
}
+ // The raised desktop ends directly above the IME. Keeping the display's
+ // bottom safe inset there would leave a black strip between the desktop
+ // (or extra-keys bar) and the keyboard. Use this dispatch's IME state,
+ // rather than mImeBottom, because it has not been updated yet.
+ if (!mKeyboardFloating && insets.isVisible(WindowInsets.Type.ime())) {
+ safeInsets = Insets.of(safeInsets.left, safeInsets.top, safeInsets.right, 0);
+ }
+
if (mRoot.getPaddingLeft() != safeInsets.left || mRoot.getPaddingTop() != safeInsets.top
|| mRoot.getPaddingRight() != safeInsets.right
|| mRoot.getPaddingBottom() != safeInsets.bottom) {
@@ -1063,6 +1083,9 @@ private void buildExtraKeysBar() {
}
});
mBarHeight = Math.round(37.5f * mDensity * extraKeysBar.getRowCount());
+ // Keep the virtual screen height even when the bar is visible.
+ if ((mBarHeight & 1) != 0)
+ mBarHeight++;
extraKeysBar.setFloating(mKeyboardFloating);
extraKeysBar.setVisibility(View.GONE);
mRoot.addView(extraKeysBar, new FrameLayout.LayoutParams(
@@ -1093,6 +1116,25 @@ private void toggleExtraKeysBar() {
setExtraKeysBarVisible(!visible);
}
+ /**
+ * Handle the user-bound soft-keyboard toggle in every key dispatch path.
+ * Accessibility interception runs before the Activity, so keeping this in
+ * one helper keeps the setting consistent for both routes.
+ */
+ private boolean handleSoftKeyboardToggleKey(KeyEvent event) {
+ SharedPreferences prefs = getSharedPreferences(PREFS_NAME, MODE_PRIVATE);
+ int boundKeycode = prefs.getInt(KEY_BOUND_KEYCODE, -1);
+ if (boundKeycode == -1 || event.getKeyCode() != boundKeycode)
+ return false;
+
+ if (event.getAction() == KeyEvent.ACTION_DOWN && event.getRepeatCount() == 0)
+ systemIme.toggleSystemKeyboard();
+ // Consume both the press and release. Once the IME owns focus, allowing
+ // the release to continue through the normal dispatch path can send a
+ // stray key-up to Linux and leave its key state stuck.
+ return true;
+ }
+
// ---- SystemIME.Host ----
@Override
@@ -1178,13 +1220,8 @@ public boolean onGenericMotionEvent(MotionEvent event) {
@Override
public boolean onKeyDown(int keyCode, KeyEvent event) {
- SharedPreferences prefs = getSharedPreferences(PREFS_NAME, MODE_PRIVATE);
- int boundKeycode = prefs.getInt(KEY_BOUND_KEYCODE, -1);
- if (boundKeycode != -1 && keyCode == boundKeycode) {
- if (event.getRepeatCount() == 0)
- systemIme.toggleSystemKeyboard();
+ if (handleSoftKeyboardToggleKey(event))
return true;
- }
Boolean actionHandled = handleConfiguredUserAction(event);
if (actionHandled != null)
@@ -1210,6 +1247,9 @@ public void onBackPressed() {
// Called from KeyInterceptor (accessibility service) to handle keys that
// the normal onKeyDown/onKeyUp might miss (e.g. Fn combos).
public boolean handleAccessibilityKey(KeyEvent event) {
+ if (handleSoftKeyboardToggleKey(event))
+ return true;
+
// Some tablet keyboard layouts expose their physical Esc key as
// Android Back (Linux KEY_BACK / Browser Back). Convert it only on
// the accessibility-interception path so the normal Android Back and
@@ -1282,6 +1322,9 @@ public boolean isAccessibilityInterceptEnabled() {
@Override
public boolean onKeyUp(int keyCode, KeyEvent event) {
+ if (handleSoftKeyboardToggleKey(event))
+ return true;
+
Boolean actionHandled = handleConfiguredUserAction(event);
if (actionHandled != null)
return actionHandled || super.onKeyUp(keyCode, event);
diff --git a/app/src/main/java/com/anland/termux/SettingsActivity.java b/app/src/main/java/com/anland/termux/SettingsActivity.java
index 5a5f77a..e4b0a78 100644
--- a/app/src/main/java/com/anland/termux/SettingsActivity.java
+++ b/app/src/main/java/com/anland/termux/SettingsActivity.java
@@ -339,6 +339,24 @@ private void buildVirtualKeyboardSection(LinearLayout root) {
bindButton.setText(R.string.bind_key_button);
bindButton.setOnClickListener(v -> startListening());
root.addView(bindButton);
+
+ SharedPreferences prefs = getSharedPreferences(PREFS_NAME, MODE_PRIVATE);
+ Switch raiseDesktopSwitch = new Switch(this);
+ raiseDesktopSwitch.setText(R.string.raise_desktop_for_soft_keyboard);
+ raiseDesktopSwitch.setTextSize(14);
+ raiseDesktopSwitch.setPadding(0, dp(8), 0, 0);
+ raiseDesktopSwitch.setChecked(!prefs.getBoolean(KEY_KEYBOARD_FLOATING, true));
+ raiseDesktopSwitch.setOnCheckedChangeListener((v, checked) ->
+ getSharedPreferences(PREFS_NAME, MODE_PRIVATE).edit()
+ .putBoolean(KEY_KEYBOARD_FLOATING, !checked).apply());
+ root.addView(raiseDesktopSwitch);
+
+ TextView raiseDesktopHint = new TextView(this);
+ raiseDesktopHint.setText(R.string.raise_desktop_for_soft_keyboard_hint);
+ raiseDesktopHint.setTextSize(12);
+ raiseDesktopHint.setTextColor(getColor(R.color.settings_text_secondary));
+ raiseDesktopHint.setPadding(0, dp(4), 0, dp(8));
+ root.addView(raiseDesktopHint);
}
private void buildAccessibilitySection(LinearLayout root) {
@@ -414,23 +432,6 @@ private void buildExtraKeysSection(LinearLayout root) {
autoShowHint.setPadding(0, dp(4), 0, dp(8));
root.addView(autoShowHint);
- // === Keyboard floating ===
- Switch keyboardFloatingSwitch = new Switch(this);
- keyboardFloatingSwitch.setText(R.string.keyboard_floating_switch);
- keyboardFloatingSwitch.setTextSize(14);
- keyboardFloatingSwitch.setPadding(0, dp(16), 0, 0);
- keyboardFloatingSwitch.setChecked(prefs.getBoolean(KEY_KEYBOARD_FLOATING, true));
- keyboardFloatingSwitch.setOnCheckedChangeListener((v, checked) ->
- getSharedPreferences(PREFS_NAME, MODE_PRIVATE).edit()
- .putBoolean(KEY_KEYBOARD_FLOATING, checked).apply());
- root.addView(keyboardFloatingSwitch);
-
- TextView keyboardFloatingHint = new TextView(this);
- keyboardFloatingHint.setText(R.string.keyboard_floating_hint);
- keyboardFloatingHint.setTextSize(12);
- keyboardFloatingHint.setTextColor(getColor(R.color.settings_text_secondary));
- keyboardFloatingHint.setPadding(0, dp(4), 0, dp(8));
- root.addView(keyboardFloatingHint);
}
private void buildCustomLayoutSection(LinearLayout root) {
diff --git a/app/src/main/java/com/anland/termux/SystemIME.java b/app/src/main/java/com/anland/termux/SystemIME.java
index 2c9c5c9..5fcc3f3 100644
--- a/app/src/main/java/com/anland/termux/SystemIME.java
+++ b/app/src/main/java/com/anland/termux/SystemIME.java
@@ -6,6 +6,7 @@
import android.view.KeyEvent;
import android.view.View;
import android.view.WindowInsets;
+import android.view.WindowInsetsController;
import android.view.inputmethod.BaseInputConnection;
import android.view.inputmethod.EditorInfo;
import android.view.inputmethod.InputConnection;
@@ -25,6 +26,12 @@
*/
public final class SystemIME {
+ // A newly focused view may need a few UI-loop turns before IMM considers it
+ // the served editor. Keep the retry bounded so a failed request cannot leave
+ // a permanent callback chain behind.
+ private static final int SHOW_RETRY_LIMIT = 4;
+ private static final long SHOW_RETRY_DELAY_MS = 50L;
+
/** Callback surface SystemIME needs from its host (MainActivity). */
public interface Host {
/** The current extra-keys bar, or null if none is built. */
@@ -49,6 +56,8 @@ public interface Host {
private final Host host;
private InputMethodManager imm;
private EditText hiddenInput;
+ /** Incremented whenever a pending show becomes obsolete (for example, hide). */
+ private int showRequestId;
SystemIME(Activity activity, Host host) {
this.activity = activity;
@@ -355,12 +364,50 @@ void releaseHiddenInput() {
hiddenInput.setEnabled(false);
}
+ private void retryShow(int requestId, int attempt) {
+ if (attempt >= SHOW_RETRY_LIMIT || requestId != showRequestId
+ || !hiddenInput.isEnabled())
+ return;
+ hiddenInput.postDelayed(() -> requestShow(requestId, attempt + 1),
+ SHOW_RETRY_DELAY_MS);
+ }
+
+ private void requestShow(int requestId, int attempt) {
+ if (requestId != showRequestId || !hiddenInput.isEnabled()) return;
+ if (!hiddenInput.hasFocus()) hiddenInput.requestFocus();
+
+ android.os.ResultReceiver receiver = new android.os.ResultReceiver(
+ hiddenInput.getHandler()) {
+ @Override
+ protected void onReceiveResult(int resultCode, android.os.Bundle data) {
+ if (resultCode == InputMethodManager.RESULT_UNCHANGED_HIDDEN
+ || resultCode == InputMethodManager.RESULT_HIDDEN) {
+ retryShow(requestId, attempt);
+ }
+ }
+ };
+
+ // The insets API is the explicit user-driven show path on Android 11+
+ // and later. Keep IMM below as a compatibility/focus-registration
+ // fallback for devices that do not hand the view a controller yet.
+ WindowInsetsController controller = hiddenInput.getWindowInsetsController();
+ if (controller == null) controller = activity.getWindow().getInsetsController();
+ if (controller != null) controller.show(WindowInsets.Type.ime());
+
+ // showSoftInput() returns false when the view has not reached IMM's
+ // served-view state. In that case ResultReceiver is not guaranteed to
+ // run, so retry from the return value as well as from a hidden result.
+ if (!imm.showSoftInput(hiddenInput, InputMethodManager.SHOW_IMPLICIT, receiver))
+ retryShow(requestId, attempt);
+ }
+
// Toggle the system IME (soft keyboard). Driven by the ⌨ bar key tap and the
// user-bound hardware keycode.
void toggleSystemKeyboard() {
if (imm == null) imm = activity.getSystemService(InputMethodManager.class);
if (imm == null) return;
if (isImeVisible()) {
+ showRequestId++;
imm.hideSoftInputFromWindow(hiddenInput.getWindowToken(), 0);
releaseHiddenInput();
// In freeform mode the inset callback may not fire; hide the bar
@@ -371,7 +418,11 @@ void toggleSystemKeyboard() {
hiddenInput.setFocusable(true);
hiddenInput.setFocusableInTouchMode(true);
hiddenInput.requestFocus();
- imm.showSoftInput(hiddenInput, InputMethodManager.SHOW_IMPLICIT);
+ // A newly focused editor is registered with IMM asynchronously, so
+ // an immediate showSoftInput() can race that registration. Post the
+ // request and retry briefly if the IME did not accept it.
+ final int requestId = ++showRequestId;
+ hiddenInput.post(() -> requestShow(requestId, 0));
// In freeform / small-window mode the IME appears as a floating
// window that does NOT trigger window insets, so applyImeInset()
// is never called and the extra-keys bar stays hidden. Show it
diff --git a/app/src/main/res/values-zh-rTW/strings.xml b/app/src/main/res/values-zh-rTW/strings.xml
index 9d6bc7d..4a11137 100644
--- a/app/src/main/res/values-zh-rTW/strings.xml
+++ b/app/src/main/res/values-zh-rTW/strings.xml
@@ -28,6 +28,8 @@
綁定虛擬鍵盤按鍵
+ 軟鍵盤彈出時抬起桌面
+ 開啟後,桌面會縮小並向上移動,能看清正在輸入的內容。
正在監聽…(%1$d 秒)
目前:無
目前:%1$s
@@ -42,8 +44,6 @@
顯示類似 Termux 的底部按鍵列,包含 ESC/TAB/CTRL/ALT/方向鍵/PgUp 等。顯示區域會縮小以騰出空間,按鍵列會隨軟鍵盤一起升起。下次返回桌面檢視時生效。
隨軟鍵盤自動顯示擴充按鍵
開啟時,擴充按鍵列會跟隨軟鍵盤顯示或隱藏。關閉時,軟鍵盤不會改變按鍵列狀態;可在設定中或透過已設定的使用者操作手動切換。
- 鍵盤懸浮(覆蓋顯示區域)
- 開啟時,軟鍵盤和擴充按鍵列會懸浮在顯示區域之上,而不是縮小顯示區域;按鍵列使用半透明背景並隨鍵盤升起,但顯示區域不會被調整大小。關閉時保持原有行為(顯示區域縮小以騰出空間)。下次返回桌面時生效。
載入預設範本
diff --git a/app/src/main/res/values-zh/strings.xml b/app/src/main/res/values-zh/strings.xml
index a7b7a37..839f2db 100644
--- a/app/src/main/res/values-zh/strings.xml
+++ b/app/src/main/res/values-zh/strings.xml
@@ -28,6 +28,8 @@
绑定虚拟键盘按键
+ 软键盘弹出时抬起桌面
+ 开启后,桌面会缩小并向上移动,能看清正在输入的内容。
正在监听…(%1$d 秒)
当前:无
当前:%1$s
@@ -42,8 +44,6 @@
显示类似 Termux 的底部按键栏,包含 ESC/TAB/CTRL/ALT/方向键/PgUp 等。显示区域会缩小以腾出空间,按键栏会随软键盘一起升起。下次返回桌面视图时生效。
随软键盘自动显示扩展按键
开启时,扩展按键栏会跟随软键盘显示或隐藏。关闭时,软键盘不会改变按键栏状态;可在设置中或通过已配置的用户操作手动切换。
- 键盘悬浮(覆盖显示区域)
- 开启时,软键盘和扩展按键栏会悬浮在显示区域之上,而不是缩小显示区域;按键栏使用半透明背景并随键盘升起,但显示区域不会被调整大小。关闭时保持原有行为(显示区域缩小以腾出空间)。下次返回桌面时生效。
加载默认模板
diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml
index aa2ca5b..5fda39e 100644
--- a/app/src/main/res/values/strings.xml
+++ b/app/src/main/res/values/strings.xml
@@ -28,6 +28,8 @@
Bind Virtual Keyboard Key
+ Raise desktop when soft keyboard opens
+ When on, the desktop shrinks and moves up so you can see what you type.
Listening… (%1$ds)
Current: None
Current: %1$s
@@ -42,8 +44,6 @@
Show a Termux-style bottom bar with ESC/TAB/CTRL/ALT/arrows/PgUp etc. The display area shrinks to make room; the bar rises together with the soft keyboard. Takes effect on next return to the desktop view.
Auto-show extra keys with keyboard
When ON, the extra keys bar follows the soft keyboard. When OFF, keyboard visibility does not change the bar; toggle it manually in Settings or with a configured user action.
- Keyboard floating (overlay display)
- When ON, the soft keyboard and the extra keys bar float over the display area instead of shrinking it; the bar uses a translucent background and rises with the keyboard, but the display is not resized. When OFF, the original behaviour (display shrinks to make room) is kept. Takes effect on next return to the desktop.
Load default template