Edge-to-edge insets, quick bar redesign, multi-session focus, keyboard settings
- Add status bar insets to SessionTabBar and navigation bar insets to terminal Column for proper edge-to-edge rendering on Samsung S23 (Android 14+) - Redesign quick bar: ESC, TAB, :, ~, |, arrows, shift-tab (remove Ctrl+C) - Defer haptic in infinite scroll mode until tap is confirmed (no haptic on scroll) - Manage focus explicitly when switching sessions — clear focus on invisible views, request focus + restartInput on visible to fix Samsung IME routing - Wire space long-press gear to KeyboardSettingsDialog from terminal pane - Add diagnostic logging for writeInput drops and null session entries Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
70ab307294
commit
c4f60b05cb
9 changed files with 116 additions and 16 deletions
|
|
@ -988,7 +988,11 @@ class TerminalService : Service() {
|
|||
// ========================================================================
|
||||
|
||||
fun writeInput(sessionId: Long, bytes: ByteArray) {
|
||||
val entry = sessions[sessionId] ?: return
|
||||
val entry = sessions[sessionId]
|
||||
if (entry == null) {
|
||||
FileLogger.log(TAG, "writeInput[$sessionId]: entry NOT FOUND, dropping ${bytes.size} bytes (sessions=${sessions.keys})")
|
||||
return
|
||||
}
|
||||
entry.sshSession?.write(bytes)
|
||||
entry.localShellSession?.write(bytes)
|
||||
entry.telnetSession?.write(bytes)
|
||||
|
|
|
|||
|
|
@ -20,7 +20,10 @@ import androidx.compose.animation.fadeIn
|
|||
import androidx.compose.animation.fadeOut
|
||||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.fillMaxSize
|
||||
import androidx.compose.foundation.layout.navigationBars
|
||||
import androidx.compose.foundation.layout.windowInsetsPadding
|
||||
import androidx.compose.runtime.Composable
|
||||
import androidx.compose.runtime.LaunchedEffect
|
||||
import androidx.compose.runtime.getValue
|
||||
|
|
@ -38,6 +41,7 @@ import com.roundingmobile.sshworkbench.BuildConfig
|
|||
import com.roundingmobile.sshworkbench.R
|
||||
import com.roundingmobile.sshworkbench.auth.BiometricAuthManager
|
||||
import com.roundingmobile.sshworkbench.pro.ProFeatures
|
||||
import com.roundingmobile.sshworkbench.terminal.KeyboardSettingsDialog
|
||||
import com.roundingmobile.sshworkbench.terminal.TerminalService
|
||||
import com.roundingmobile.sshworkbench.ui.navigation.Routes
|
||||
import com.roundingmobile.sshworkbench.ui.navigation.SshWorkbenchNavGraph
|
||||
|
|
@ -170,7 +174,9 @@ class MainActivity : AppCompatActivity() {
|
|||
val showTerminal = appState.currentPane == Pane.TERMINAL
|
||||
Column(
|
||||
modifier = if (showTerminal) Modifier.fillMaxSize()
|
||||
.windowInsetsPadding(WindowInsets.navigationBars)
|
||||
else Modifier.fillMaxSize().alpha(0f)
|
||||
.windowInsetsPadding(WindowInsets.navigationBars)
|
||||
) {
|
||||
// Session tab bar
|
||||
if (showTerminal) {
|
||||
|
|
@ -236,7 +242,12 @@ class MainActivity : AppCompatActivity() {
|
|||
// TODO: show snippet picker dialog
|
||||
},
|
||||
onSettingsTap = {
|
||||
// TODO: show keyboard settings dialog
|
||||
KeyboardSettingsDialog.show(
|
||||
context = this@MainActivity,
|
||||
currentSettings = mainViewModel.getCurrentKeyboardSettings(),
|
||||
proFeatures = proFeatures,
|
||||
onSave = { newSettings -> mainViewModel.saveKeyboardSettings(newSettings) }
|
||||
)
|
||||
},
|
||||
onReconnect = {
|
||||
mainViewModel.reconnectSession(sid)
|
||||
|
|
|
|||
|
|
@ -18,6 +18,7 @@ import kotlinx.coroutines.flow.StateFlow
|
|||
import kotlinx.coroutines.flow.asStateFlow
|
||||
import kotlinx.coroutines.flow.first
|
||||
import kotlinx.coroutines.flow.update
|
||||
import kotlinx.coroutines.runBlocking
|
||||
import kotlinx.coroutines.launch
|
||||
import javax.inject.Inject
|
||||
|
||||
|
|
@ -569,4 +570,45 @@ class MainViewModel @Inject constructor(
|
|||
fun writeToSession(sessionId: Long, bytes: ByteArray) {
|
||||
terminalService?.writeInput(sessionId, bytes)
|
||||
}
|
||||
|
||||
// --- Keyboard settings (for space long-press gear) ---
|
||||
|
||||
fun getCurrentKeyboardSettings(): com.roundingmobile.sshworkbench.terminal.KeyboardDisplaySettings {
|
||||
val prefs = terminalPrefs
|
||||
return com.roundingmobile.sshworkbench.terminal.KeyboardDisplaySettings(
|
||||
language = runBlocking { prefs.keyboardLanguage.first() },
|
||||
heightPercent = runBlocking { prefs.keyboardHeightPercent.first() },
|
||||
heightLandscape = runBlocking { prefs.keyboardHeightLandscape.first() },
|
||||
sameSizeBoth = runBlocking { prefs.keyboardSameSizeBoth.first() },
|
||||
showPageIndicators = runBlocking { prefs.showPageIndicators.first() },
|
||||
keyColorPreset = runBlocking { prefs.keyColorPreset.first() },
|
||||
keyColorCustom = runBlocking { prefs.keyColorCustom.first() },
|
||||
showHints = runBlocking { prefs.showKeyHints.first() },
|
||||
keyRepeatDelay = runBlocking { prefs.keyRepeatDelay.first() },
|
||||
longPressDelay = runBlocking { prefs.longPressDelay.first() },
|
||||
quickBarPosition = runBlocking { prefs.quickBarPosition.first() },
|
||||
quickBarSize = runBlocking { prefs.quickBarSize.first() },
|
||||
qbColorPreset = runBlocking { prefs.qbColorPreset.first() },
|
||||
qbColorCustom = runBlocking { prefs.qbColorCustom.first() }
|
||||
)
|
||||
}
|
||||
|
||||
fun saveKeyboardSettings(settings: com.roundingmobile.sshworkbench.terminal.KeyboardDisplaySettings) {
|
||||
viewModelScope.launch {
|
||||
terminalPrefs.setKeyboardLanguage(settings.language)
|
||||
terminalPrefs.setKeyboardHeightPercent(settings.heightPercent)
|
||||
terminalPrefs.setKeyboardHeightLandscape(settings.heightLandscape)
|
||||
terminalPrefs.setKeyboardSameSizeBoth(settings.sameSizeBoth)
|
||||
terminalPrefs.setShowPageIndicators(settings.showPageIndicators)
|
||||
terminalPrefs.setKeyColorPreset(settings.keyColorPreset)
|
||||
terminalPrefs.setKeyColorCustom(settings.keyColorCustom)
|
||||
terminalPrefs.setShowKeyHints(settings.showHints)
|
||||
terminalPrefs.setKeyRepeatDelay(settings.keyRepeatDelay)
|
||||
terminalPrefs.setLongPressDelay(settings.longPressDelay)
|
||||
terminalPrefs.setQuickBarPosition(settings.quickBarPosition)
|
||||
terminalPrefs.setQuickBarSize(settings.quickBarSize)
|
||||
terminalPrefs.setQbColorPreset(settings.qbColorPreset)
|
||||
terminalPrefs.setQbColorCustom(settings.qbColorCustom)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -8,11 +8,14 @@ import androidx.compose.foundation.layout.Arrangement
|
|||
import androidx.compose.foundation.layout.Box
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.fillMaxWidth
|
||||
import androidx.compose.foundation.layout.height
|
||||
import androidx.compose.foundation.layout.padding
|
||||
import androidx.compose.foundation.layout.size
|
||||
import androidx.compose.foundation.layout.statusBars
|
||||
import androidx.compose.foundation.layout.width
|
||||
import androidx.compose.foundation.layout.windowInsetsPadding
|
||||
import androidx.compose.foundation.rememberScrollState
|
||||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
|
|
@ -90,8 +93,9 @@ fun SessionTabBar(
|
|||
Row(
|
||||
modifier = modifier
|
||||
.fillMaxWidth()
|
||||
.height(41.dp)
|
||||
.background(TabBarBg)
|
||||
.windowInsetsPadding(WindowInsets.statusBars)
|
||||
.height(41.dp)
|
||||
.padding(horizontal = 4.dp),
|
||||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
|
|
|
|||
|
|
@ -272,13 +272,33 @@ fun TerminalPane(
|
|||
root.visibility = if (visible) View.VISIBLE else View.INVISIBLE
|
||||
val tags = root.tag as? ViewTags ?: return@AndroidView
|
||||
|
||||
// Update screen buffer binding
|
||||
// Ensure the active session's terminal gets focus for IME input.
|
||||
// INVISIBLE views retain focus, so we must explicitly manage it.
|
||||
if (visible) {
|
||||
tags.terminalView.isFocusable = true
|
||||
tags.terminalView.isFocusableInTouchMode = true
|
||||
if (!tags.terminalView.hasFocus()) {
|
||||
tags.terminalView.requestFocus()
|
||||
// Force IME to re-create InputConnection for this view —
|
||||
// Samsung One UI can hold stale connections after view switch
|
||||
val imm = root.context.getSystemService(InputMethodManager::class.java)
|
||||
imm?.restartInput(tags.terminalView)
|
||||
}
|
||||
} else {
|
||||
tags.terminalView.clearFocus()
|
||||
tags.terminalView.isFocusable = false
|
||||
tags.terminalView.isFocusableInTouchMode = false
|
||||
}
|
||||
|
||||
// Update screen buffer binding + render callback
|
||||
val currentEntry = terminalService?.getSession(sessionId)
|
||||
if (currentEntry != null) {
|
||||
if (tags.terminalView.screenBuffer !== currentEntry.screenBuffer) {
|
||||
tags.terminalView.screenBuffer = currentEntry.screenBuffer
|
||||
}
|
||||
currentEntry.onScreenUpdated = { tags.terminalView.invalidateTerminal() }
|
||||
} else {
|
||||
FileLogger.log(TAG, "update[$sessionId]: entry NULL, visible=$visible")
|
||||
}
|
||||
|
||||
// Update keyboard visibility
|
||||
|
|
|
|||
|
|
@ -403,6 +403,12 @@ class QuickBarView(context: Context) : View(context) {
|
|||
isScrolling = false
|
||||
velocityTracker = VelocityTracker.obtain()
|
||||
velocityTracker?.addMovement(event)
|
||||
// In infinite scroll mode, defer haptic/onKeyDown until ACTION_UP
|
||||
// confirms this is a tap, not a scroll gesture.
|
||||
val key = findKeyAt(event.x, event.y) ?: return true
|
||||
pressedKeys.add(key.id)
|
||||
invalidate()
|
||||
return true
|
||||
}
|
||||
|
||||
val key = findKeyAt(event.x, event.y) ?: return true
|
||||
|
|
@ -424,7 +430,7 @@ class QuickBarView(context: Context) : View(context) {
|
|||
if (!isScrolling && Math.abs(event.y - scrollTouchStartY) > dpToPx(8f)) {
|
||||
isScrolling = true
|
||||
pressedKeys.clear()
|
||||
onKeyUp?.invoke()
|
||||
invalidate()
|
||||
}
|
||||
if (isScrolling) {
|
||||
scrollOffset -= dy
|
||||
|
|
@ -437,7 +443,7 @@ class QuickBarView(context: Context) : View(context) {
|
|||
if (!isScrolling && Math.abs(event.x - scrollTouchStartX) > dpToPx(8f)) {
|
||||
isScrolling = true
|
||||
pressedKeys.clear()
|
||||
onKeyUp?.invoke()
|
||||
invalidate()
|
||||
}
|
||||
if (isScrolling) {
|
||||
scrollOffset -= dx
|
||||
|
|
@ -469,8 +475,12 @@ class QuickBarView(context: Context) : View(context) {
|
|||
val key = findKeyAt(event.x, event.y)
|
||||
pressedKeys.clear()
|
||||
invalidate()
|
||||
if (key != null) {
|
||||
// Confirmed tap — fire haptic + action together
|
||||
onKeyDown?.invoke(key)
|
||||
onKeyTap?.invoke(key)
|
||||
}
|
||||
onKeyUp?.invoke()
|
||||
if (key != null) onKeyTap?.invoke(key)
|
||||
velocityTracker?.recycle()
|
||||
velocityTracker = null
|
||||
return true
|
||||
|
|
|
|||
|
|
@ -264,13 +264,16 @@
|
|||
"draggable": false,
|
||||
"height": 42,
|
||||
"keys": [
|
||||
{ "id": "qb_ctrlc", "label": "^C", "w": 1, "action": { "type": "combo", "mod": ["ctrl"], "key": "c" }, "style": "danger" },
|
||||
{ "id": "qb_esc", "label": "ESC", "w": 1, "action": { "type": "bytes", "value": [27] } },
|
||||
{ "id": "qb_tab", "label": "TAB", "w": 1, "action": { "type": "bytes", "value": [9] } },
|
||||
{ "id": "qb_colon", "label": ":", "w": 0.7, "action": { "type": "char", "primary": ":" } },
|
||||
{ "id": "qb_tilde", "label": "~", "w": 0.7, "action": { "type": "char", "primary": "~" } },
|
||||
{ "id": "qb_pipe", "label": "|", "w": 0.7, "action": { "type": "char", "primary": "|" } },
|
||||
{ "id": "qb_left", "label": "◀", "w": 0.8, "action": { "type": "esc_seq", "seq": "[D" }, "repeatable": true },
|
||||
{ "id": "qb_up", "label": "▲", "w": 0.8, "action": { "type": "esc_seq", "seq": "[A" }, "repeatable": true },
|
||||
{ "id": "qb_down", "label": "▼", "w": 0.8, "action": { "type": "esc_seq", "seq": "[B" }, "repeatable": true },
|
||||
{ "id": "qb_left", "label": "◀", "w": 0.8, "action": { "type": "esc_seq", "seq": "[D" }, "repeatable": true },
|
||||
{ "id": "qb_right", "label": "▶", "w": 0.8, "action": { "type": "esc_seq", "seq": "[C" }, "repeatable": true },
|
||||
{ "id": "qb_esc", "label": "ESC", "w": 1, "action": { "type": "bytes", "value": [27] } }
|
||||
{ "id": "qb_stab", "label": "⇤", "w": 0.8, "action": { "type": "esc_seq", "seq": "[Z" } }
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -265,13 +265,16 @@
|
|||
"draggable": false,
|
||||
"height": 42,
|
||||
"keys": [
|
||||
{ "id": "qb_ctrlc", "label": "^C", "w": 1, "action": { "type": "combo", "mod": ["ctrl"], "key": "c" }, "style": "danger" },
|
||||
{ "id": "qb_esc", "label": "ESC", "w": 1, "action": { "type": "bytes", "value": [27] } },
|
||||
{ "id": "qb_tab", "label": "TAB", "w": 1, "action": { "type": "bytes", "value": [9] } },
|
||||
{ "id": "qb_colon", "label": ":", "w": 0.7, "action": { "type": "char", "primary": ":" } },
|
||||
{ "id": "qb_tilde", "label": "~", "w": 0.7, "action": { "type": "char", "primary": "~" } },
|
||||
{ "id": "qb_pipe", "label": "|", "w": 0.7, "action": { "type": "char", "primary": "|" } },
|
||||
{ "id": "qb_left", "label": "◀", "w": 0.8, "action": { "type": "esc_seq", "seq": "[D" }, "repeatable": true },
|
||||
{ "id": "qb_up", "label": "▲", "w": 0.8, "action": { "type": "esc_seq", "seq": "[A" }, "repeatable": true },
|
||||
{ "id": "qb_down", "label": "▼", "w": 0.8, "action": { "type": "esc_seq", "seq": "[B" }, "repeatable": true },
|
||||
{ "id": "qb_left", "label": "◀", "w": 0.8, "action": { "type": "esc_seq", "seq": "[D" }, "repeatable": true },
|
||||
{ "id": "qb_right", "label": "▶", "w": 0.8, "action": { "type": "esc_seq", "seq": "[C" }, "repeatable": true },
|
||||
{ "id": "qb_esc", "label": "ESC", "w": 1, "action": { "type": "bytes", "value": [27] } }
|
||||
{ "id": "qb_stab", "label": "⇤", "w": 0.8, "action": { "type": "esc_seq", "seq": "[Z" } }
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -267,13 +267,16 @@
|
|||
"draggable": false,
|
||||
"height": 42,
|
||||
"keys": [
|
||||
{ "id": "qb_ctrlc", "label": "^C", "w": 1, "action": { "type": "combo", "mod": ["ctrl"], "key": "c" }, "style": "danger" },
|
||||
{ "id": "qb_esc", "label": "ESC", "w": 1, "action": { "type": "bytes", "value": [27] } },
|
||||
{ "id": "qb_tab", "label": "TAB", "w": 1, "action": { "type": "bytes", "value": [9] } },
|
||||
{ "id": "qb_colon", "label": ":", "w": 0.7, "action": { "type": "char", "primary": ":" } },
|
||||
{ "id": "qb_tilde", "label": "~", "w": 0.7, "action": { "type": "char", "primary": "~" } },
|
||||
{ "id": "qb_pipe", "label": "|", "w": 0.7, "action": { "type": "char", "primary": "|" } },
|
||||
{ "id": "qb_left", "label": "◀", "w": 0.8, "action": { "type": "esc_seq", "seq": "[D" }, "repeatable": true },
|
||||
{ "id": "qb_up", "label": "▲", "w": 0.8, "action": { "type": "esc_seq", "seq": "[A" }, "repeatable": true },
|
||||
{ "id": "qb_down", "label": "▼", "w": 0.8, "action": { "type": "esc_seq", "seq": "[B" }, "repeatable": true },
|
||||
{ "id": "qb_left", "label": "◀", "w": 0.8, "action": { "type": "esc_seq", "seq": "[D" }, "repeatable": true },
|
||||
{ "id": "qb_right", "label": "▶", "w": 0.8, "action": { "type": "esc_seq", "seq": "[C" }, "repeatable": true },
|
||||
{ "id": "qb_esc", "label": "ESC", "w": 1, "action": { "type": "bytes", "value": [27] } }
|
||||
{ "id": "qb_stab", "label": "⇤", "w": 0.8, "action": { "type": "esc_seq", "seq": "[Z" } }
|
||||
]
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue