Titan 2 Elite: physical keyboard fix + streaming scroll-hold

- Only bubble real system keys (isSystem() synthetic letter events
  injected by the Titan 2 keyboard layer now reach the KCM path).
- Latch hardware Shift/Alt from modifier key events; injected letter
  events arrive without meta state. Cleared on keyup, focus loss and
  session attach.
- Consult the kcm alt layer for Alt+<key> (Alt+W -> '1') and send the
  resolved char without the ESC alt-prefix.
- Hold the scroll viewport when the user scrolled up while output is
  streaming; auto-follow resumes on scroll-to-bottom or typing.
- versionCode 1023 / versionName 0.119.0-titan2.1
This commit is contained in:
2026-08-18 13:44:33 +02:00
parent 3df69d1da1
commit 4d9b758646
2 changed files with 141 additions and 8 deletions

View File

@@ -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)

View File

@@ -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 + <char>.
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();
}