diff --git a/app/build.gradle b/app/build.gradle index 33c88d1d..5cc9a8bb 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -43,8 +43,8 @@ android { defaultConfig { minSdkVersion project.properties.minSdkVersion.toInteger() targetSdkVersion project.properties.targetSdkVersion.toInteger() - versionCode 118 - versionName "0.118.0" + versionCode 1023 + versionName "0.119.0-titan2.1" if (appVersionName) versionName = appVersionName validateVersionName(versionName) diff --git a/terminal-view/src/main/java/com/termux/view/TerminalView.java b/terminal-view/src/main/java/com/termux/view/TerminalView.java index 0b3f5156..34f05b10 100644 --- a/terminal-view/src/main/java/com/termux/view/TerminalView.java +++ b/terminal-view/src/main/java/com/termux/view/TerminalView.java @@ -68,6 +68,15 @@ public final class TerminalView extends View { /** The top row of text to display. Ranges from -activeTranscriptRows to 0. */ int mTopRow; + + /** TITAN2 PATCH: user scrolled away from the bottom; hold the viewport until they return. */ + boolean mUserScrollLock; + + /** TITAN2 PATCH: latched hardware Shift/Alt state. The Titan 2 keyboard layer re-injects + * synthetic key events (FLAG_FROM_SYSTEM) whose letter events often arrive without meta + * state, so the modifier state is tracked from the modifier key events themselves. */ + boolean mHardwareShiftDown; + boolean mHardwareAltDown; int[] mDefaultSelectors = new int[]{-1,-1,-1,-1}; float mScaleFactor = 1.f; @@ -290,6 +299,9 @@ public final class TerminalView extends View { public boolean attachSession(TerminalSession session) { if (session == mTermSession) return false; mTopRow = 0; + mUserScrollLock = false; // TITAN2 PATCH + mHardwareShiftDown = false; // TITAN2 PATCH + mHardwareAltDown = false; // TITAN2 PATCH mTermSession = session; mEmulator = null; @@ -479,6 +491,17 @@ public final class TerminalView extends View { mTopRow -= rowShift; decrementYTextSelectionCursors(rowShift); } + } else if (mUserScrollLock && mTopRow != 0) { + // TITAN2 PATCH: the user scrolled up, so hold the viewport instead of snapping + // back to the bottom on every screen update (e.g. while an app streams output). + // Keep the same content in view when new lines are appended. + int rowShift = mEmulator.getScrollCounter(); + if (-mTopRow + rowShift > rowsInHistory) { + mTopRow = -rowsInHistory; + } else { + mTopRow -= rowShift; + } + skipScrolling = true; } if (!skipScrolling && mTopRow != 0) { @@ -583,6 +606,9 @@ public final class TerminalView extends View { handleKeyCode(up ? KeyEvent.KEYCODE_DPAD_UP : KeyEvent.KEYCODE_DPAD_DOWN, 0); } else { mTopRow = Math.min(0, Math.max(-(mEmulator.getScreen().getActiveTranscriptRows()), mTopRow + (up ? -1 : 1))); + // TITAN2 PATCH: scrolling away from the bottom pauses auto-follow; + // scrolling back to the bottom (mTopRow == 0) resumes it. + mUserScrollLock = mTopRow != 0; if (!awakenScrollBars()) invalidate(); } } @@ -770,6 +796,21 @@ public final class TerminalView extends View { if (TERMINAL_VIEW_KEY_LOGGING_ENABLED) mClient.logInfo(LOG_TAG, "onKeyDown(keyCode=" + keyCode + ", isSystem()=" + event.isSystem() + ", event=" + event + ")"); if (mEmulator == null) return true; + + // TITAN2 PATCH: latch hardware Shift/Alt state from the modifier key events + // themselves. The synthetic letter events injected by the Titan 2 keyboard layer + // are flagged FLAG_FROM_SYSTEM and often arrive without meta state. + switch (keyCode) { + case KeyEvent.KEYCODE_SHIFT_LEFT: + case KeyEvent.KEYCODE_SHIFT_RIGHT: + mHardwareShiftDown = true; + break; + case KeyEvent.KEYCODE_ALT_LEFT: + case KeyEvent.KEYCODE_ALT_RIGHT: + mHardwareAltDown = true; + break; + } + if (isSelectingText()) { stopTextSelectionMode(); } @@ -777,7 +818,11 @@ public final class TerminalView extends View { if (mClient.onKeyDown(keyCode, event, mTermSession)) { invalidate(); return true; - } else if (event.isSystem() && (!mClient.shouldBackButtonBeMappedToEscape() || keyCode != KeyEvent.KEYCODE_BACK)) { + } else if (event.isSystem() && isRealSystemKey(keyCode) + && (!mClient.shouldBackButtonBeMappedToEscape() || keyCode != KeyEvent.KEYCODE_BACK)) { + // TITAN2 PATCH: only bubble *real* system keys (back, home, volume, ...). The + // Titan 2 keyboard layer re-injects letter events with FLAG_FROM_SYSTEM, which + // must reach the KCM path below instead of being dropped on the activity. return super.onKeyDown(keyCode, event); } else if (event.getAction() == KeyEvent.ACTION_MULTIPLE && keyCode == KeyEvent.KEYCODE_UNKNOWN) { mTermSession.write(event.getCharacters()); @@ -788,8 +833,9 @@ public final class TerminalView extends View { final int metaState = event.getMetaState(); final boolean controlDown = event.isCtrlPressed() || mClient.readControlKey(); - final boolean leftAltDown = (metaState & KeyEvent.META_ALT_LEFT_ON) != 0 || mClient.readAltKey(); - final boolean shiftDown = event.isShiftPressed() || mClient.readShiftKey(); + // TITAN2 PATCH: also use the latched hardware modifier state (injected events lack meta). + final boolean leftAltDown = (metaState & KeyEvent.META_ALT_LEFT_ON) != 0 || mClient.readAltKey() || mHardwareAltDown; + final boolean shiftDown = event.isShiftPressed() || mClient.readShiftKey() || mHardwareShiftDown; final boolean rightAltDownFromEvent = (metaState & KeyEvent.META_ALT_RIGHT_ON) != 0; int keyMod = 0; @@ -817,8 +863,19 @@ public final class TerminalView extends View { if (mClient.readFnKey()) effectiveMetaState |= KeyEvent.META_FUNCTION_ON; int result = event.getUnicodeChar(effectiveMetaState); + boolean altLayerUsed = false; + if (!rightAltDownFromEvent && leftAltDown && result != 0) { + // TITAN2 PATCH: consult the kcm alt layer (e.g. Alt+W -> '1' in TitanKey.kcm). + // The base lookup above strips left-alt from the meta state, so upstream never + // uses the alt layer and instead sends ESC + . + int altLayerResult = event.getUnicodeChar(effectiveMetaState | KeyEvent.META_ALT_LEFT_ON | KeyEvent.META_ALT_ON); + if (altLayerResult != 0 && altLayerResult != result) { + result = altLayerResult; + altLayerUsed = true; + } + } if (TERMINAL_VIEW_KEY_LOGGING_ENABLED) - mClient.logInfo(LOG_TAG, "KeyEvent#getUnicodeChar(" + effectiveMetaState + ") returned: " + result); + mClient.logInfo(LOG_TAG, "KeyEvent#getUnicodeChar(" + effectiveMetaState + ") returned: " + result + (altLayerUsed ? " (kcm alt layer)" : "")); if (result == 0) { return false; } @@ -835,7 +892,7 @@ public final class TerminalView extends View { if (combinedChar > 0) result = combinedChar; mCombiningAccent = 0; } - inputCodePoint(event.getDeviceId(), result, controlDown, leftAltDown); + inputCodePoint(event.getDeviceId(), result, controlDown, leftAltDown, altLayerUsed); } if (mCombiningAccent != oldCombiningAccent) invalidate(); @@ -844,6 +901,10 @@ public final class TerminalView extends View { } public void inputCodePoint(int eventSource, int codePoint, boolean controlDownFromEvent, boolean leftAltDownFromEvent) { + inputCodePoint(eventSource, codePoint, controlDownFromEvent, leftAltDownFromEvent, false); + } + + public void inputCodePoint(int eventSource, int codePoint, boolean controlDownFromEvent, boolean leftAltDownFromEvent, boolean altConsumedByKcm) { if (TERMINAL_VIEW_KEY_LOGGING_ENABLED) { mClient.logInfo(LOG_TAG, "inputCodePoint(eventSource=" + eventSource + ", codePoint=" + codePoint + ", controlDownFromEvent=" + controlDownFromEvent + ", leftAltDownFromEvent=" + leftAltDownFromEvent + ")"); @@ -856,7 +917,11 @@ public final class TerminalView extends View { mEmulator.setCursorBlinkState(true); final boolean controlDown = controlDownFromEvent || mClient.readControlKey(); - final boolean altDown = leftAltDownFromEvent || mClient.readAltKey(); + // TITAN2 PATCH: if the kcm alt layer already produced the character (altConsumedByKcm), + // do not also send the ESC alt-prefix. + final boolean altDown = !altConsumedByKcm && (leftAltDownFromEvent || mClient.readAltKey()); + + mUserScrollLock = false; // TITAN2 PATCH: input returns the view to the live bottom if (mClient.onCodePoint(codePoint, controlDown, mTermSession)) return; @@ -921,6 +986,7 @@ public final class TerminalView extends View { String code = KeyHandler.getCode(keyCode, keyMod, term.isCursorKeysApplicationMode(), term.isKeypadApplicationMode()); if (code == null) return false; mTermSession.write(code); + mUserScrollLock = false; // TITAN2 PATCH: input returns the view to the live bottom return true; } @@ -956,6 +1022,18 @@ public final class TerminalView extends View { if (TERMINAL_VIEW_KEY_LOGGING_ENABLED) mClient.logInfo(LOG_TAG, "onKeyUp(keyCode=" + keyCode + ", event=" + event + ")"); + // TITAN2 PATCH: release latched hardware modifier state (see onKeyDown). + switch (keyCode) { + case KeyEvent.KEYCODE_SHIFT_LEFT: + case KeyEvent.KEYCODE_SHIFT_RIGHT: + mHardwareShiftDown = false; + break; + case KeyEvent.KEYCODE_ALT_LEFT: + case KeyEvent.KEYCODE_ALT_RIGHT: + mHardwareAltDown = false; + break; + } + // Do not return for KEYCODE_BACK and send it to the client since user may be trying // to exit the activity. if (mEmulator == null && keyCode != KeyEvent.KEYCODE_BACK) return true; @@ -971,6 +1049,60 @@ public final class TerminalView extends View { return true; } + @Override + public void onWindowFocusChanged(boolean hasWindowFocus) { + super.onWindowFocusChanged(hasWindowFocus); + // TITAN2 PATCH: drop latched hardware modifier state when focus is lost, in case + // the keyup was delivered elsewhere (app switch, notification shade, dialog, ...). + if (!hasWindowFocus) { + mHardwareShiftDown = false; + mHardwareAltDown = false; + } + } + + /** + * TITAN2 PATCH: mirror of the @hide {@code KeyEvent.isSystemKey(int)}. Used to tell + * real system keys (back, home, volume, ...) apart from the synthetic letter events + * that the Titan 2 keyboard layer injects with FLAG_FROM_SYSTEM. + */ + private static boolean isRealSystemKey(int keyCode) { + switch (keyCode) { + case KeyEvent.KEYCODE_MENU: + case KeyEvent.KEYCODE_SOFT_RIGHT: + case KeyEvent.KEYCODE_HOME: + case KeyEvent.KEYCODE_BACK: + case KeyEvent.KEYCODE_CALL: + case KeyEvent.KEYCODE_ENDCALL: + case KeyEvent.KEYCODE_VOLUME_UP: + case KeyEvent.KEYCODE_VOLUME_DOWN: + case KeyEvent.KEYCODE_MUTE: + case KeyEvent.KEYCODE_HEADSETHOOK: + case KeyEvent.KEYCODE_MEDIA_PLAY: + case KeyEvent.KEYCODE_MEDIA_PAUSE: + case KeyEvent.KEYCODE_MEDIA_PLAY_PAUSE: + case KeyEvent.KEYCODE_MEDIA_STOP: + case KeyEvent.KEYCODE_MEDIA_NEXT: + case KeyEvent.KEYCODE_MEDIA_PREVIOUS: + case KeyEvent.KEYCODE_MEDIA_REWIND: + case KeyEvent.KEYCODE_MEDIA_RECORD: + case KeyEvent.KEYCODE_MEDIA_FAST_FORWARD: + case KeyEvent.KEYCODE_CAMERA: + case KeyEvent.KEYCODE_FOCUS: + case KeyEvent.KEYCODE_SEARCH: + case KeyEvent.KEYCODE_BRIGHTNESS_DOWN: + case KeyEvent.KEYCODE_BRIGHTNESS_UP: + case KeyEvent.KEYCODE_MEDIA_AUDIO_TRACK: + case KeyEvent.KEYCODE_SLEEP: + case KeyEvent.KEYCODE_WAKEUP: + case KeyEvent.KEYCODE_VOICE_ASSIST: + case KeyEvent.KEYCODE_ASSIST: + case KeyEvent.KEYCODE_APP_SWITCH: + return true; + default: + return false; + } + } + /** * This is called during layout when the size of this view has changed. If you were just added to the view * hierarchy, you're called with the old values of 0. @@ -1000,6 +1132,7 @@ public final class TerminalView extends View { mTerminalCursorBlinkerRunnable.setEmulator(mEmulator); mTopRow = 0; + mUserScrollLock = false; // TITAN2 PATCH scrollTo(0, 0); invalidate(); }