Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
59 changes: 51 additions & 8 deletions app/src/main/java/com/anland/termux/MainActivity.java
Original file line number Diff line number Diff line change
Expand Up @@ -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";
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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) {
Expand Down Expand Up @@ -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(
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand All @@ -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
Expand Down Expand Up @@ -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);
Expand Down
35 changes: 18 additions & 17 deletions app/src/main/java/com/anland/termux/SettingsActivity.java
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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) {
Expand Down
53 changes: 52 additions & 1 deletion app/src/main/java/com/anland/termux/SystemIME.java
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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. */
Expand All @@ -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;
Expand Down Expand Up @@ -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
Expand All @@ -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
Expand Down
4 changes: 2 additions & 2 deletions app/src/main/res/values-zh-rTW/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@

<!-- Bind key -->
<string name="bind_key_button">綁定虛擬鍵盤按鍵</string>
<string name="raise_desktop_for_soft_keyboard">軟鍵盤彈出時抬起桌面</string>
<string name="raise_desktop_for_soft_keyboard_hint">開啟後,桌面會縮小並向上移動,能看清正在輸入的內容。</string>
<string name="listening_countdown">正在監聽…(%1$d 秒)</string>
<string name="status_current_none">目前:無</string>
<string name="status_current">目前:%1$s</string>
Expand All @@ -42,8 +44,6 @@
<string name="extra_keys_hint">顯示類似 Termux 的底部按鍵列,包含 ESC/TAB/CTRL/ALT/方向鍵/PgUp 等。顯示區域會縮小以騰出空間,按鍵列會隨軟鍵盤一起升起。下次返回桌面檢視時生效。</string>
<string name="auto_show_switch">隨軟鍵盤自動顯示擴充按鍵</string>
<string name="auto_show_hint">開啟時,擴充按鍵列會跟隨軟鍵盤顯示或隱藏。關閉時,軟鍵盤不會改變按鍵列狀態;可在設定中或透過已設定的使用者操作手動切換。</string>
<string name="keyboard_floating_switch">鍵盤懸浮(覆蓋顯示區域)</string>
<string name="keyboard_floating_hint">開啟時,軟鍵盤和擴充按鍵列會懸浮在顯示區域之上,而不是縮小顯示區域;按鍵列使用半透明背景並隨鍵盤升起,但顯示區域不會被調整大小。關閉時保持原有行為(顯示區域縮小以騰出空間)。下次返回桌面時生效。</string>

<!-- Custom layout editor -->
<string name="btn_load_default">載入預設範本</string>
Expand Down
4 changes: 2 additions & 2 deletions app/src/main/res/values-zh/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@

<!-- Bind key -->
<string name="bind_key_button">绑定虚拟键盘按键</string>
<string name="raise_desktop_for_soft_keyboard">软键盘弹出时抬起桌面</string>
<string name="raise_desktop_for_soft_keyboard_hint">开启后,桌面会缩小并向上移动,能看清正在输入的内容。</string>
<string name="listening_countdown">正在监听…(%1$d 秒)</string>
<string name="status_current_none">当前:无</string>
<string name="status_current">当前:%1$s</string>
Expand All @@ -42,8 +44,6 @@
<string name="extra_keys_hint">显示类似 Termux 的底部按键栏,包含 ESC/TAB/CTRL/ALT/方向键/PgUp 等。显示区域会缩小以腾出空间,按键栏会随软键盘一起升起。下次返回桌面视图时生效。</string>
<string name="auto_show_switch">随软键盘自动显示扩展按键</string>
<string name="auto_show_hint">开启时,扩展按键栏会跟随软键盘显示或隐藏。关闭时,软键盘不会改变按键栏状态;可在设置中或通过已配置的用户操作手动切换。</string>
<string name="keyboard_floating_switch">键盘悬浮(覆盖显示区域)</string>
<string name="keyboard_floating_hint">开启时,软键盘和扩展按键栏会悬浮在显示区域之上,而不是缩小显示区域;按键栏使用半透明背景并随键盘升起,但显示区域不会被调整大小。关闭时保持原有行为(显示区域缩小以腾出空间)。下次返回桌面时生效。</string>

<!-- Custom layout editor -->
<string name="btn_load_default">加载默认模板</string>
Expand Down
4 changes: 2 additions & 2 deletions app/src/main/res/values/strings.xml
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@

<!-- Bind key -->
<string name="bind_key_button">Bind Virtual Keyboard Key</string>
<string name="raise_desktop_for_soft_keyboard">Raise desktop when soft keyboard opens</string>
<string name="raise_desktop_for_soft_keyboard_hint">When on, the desktop shrinks and moves up so you can see what you type.</string>
<string name="listening_countdown">Listening… (%1$ds)</string>
<string name="status_current_none">Current: None</string>
<string name="status_current">Current: %1$s</string>
Expand All @@ -42,8 +44,6 @@
<string name="extra_keys_hint">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.</string>
<string name="auto_show_switch">Auto-show extra keys with keyboard</string>
<string name="auto_show_hint">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.</string>
<string name="keyboard_floating_switch">Keyboard floating (overlay display)</string>
<string name="keyboard_floating_hint">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.</string>

<!-- Custom layout editor -->
<string name="btn_load_default">Load default template</string>
Expand Down