Number row modes, mini numpad, font scaling, tablet defaults
- Number row setting: top (legacy), left/right (mini numpad), hidden (long-press) - Mini numpad: JSON-defined side section (3x3+1 grid), separate KeyboardPageView with touch handling, LinearLayout split in AndroidView, clipChildren fix for SurfaceView rendering - Font scaling: proportional 45%/25% of tallest key height, 10sp/7sp floor (was 13sp/8sp clamping to theme minimum, too big on small keyboards) - Tablet defaults: sameSizeBoth=false, landscape height min 20% (phones 30%) - Disconnect notification tap navigates to session, red badge on connection cards - CKB hide/show via tab bar kebab menu - KeyboardDisplaySettings Eagerly shared to avoid stale initial values Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
d2f925cc4d
commit
293fbcba1e
23 changed files with 533 additions and 93 deletions
|
|
@ -2,5 +2,5 @@ package com.roundingmobile.sshworkbench
|
|||
|
||||
// Auto-generated — do not edit
|
||||
object BuildTimestamp {
|
||||
const val TIME = "2026-04-04 00:39:45"
|
||||
const val TIME = "2026-04-04 20:29:33"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -31,6 +31,7 @@ object TerminalPrefsKeys {
|
|||
val KEYBOARD_HEIGHT_PERCENT = floatPreferencesKey("keyboard_height_percent")
|
||||
val KEYBOARD_HEIGHT_LANDSCAPE = floatPreferencesKey("keyboard_height_landscape")
|
||||
val KEYBOARD_SAME_SIZE_BOTH = booleanPreferencesKey("keyboard_same_size_both")
|
||||
val NUMBER_ROW_MODE = stringPreferencesKey("number_row_mode")
|
||||
val QUICK_BAR_POSITION = stringPreferencesKey("quick_bar_position")
|
||||
val QUICK_BAR_SIZE = intPreferencesKey("quick_bar_size") // dp
|
||||
val SHOW_PAGE_INDICATORS = booleanPreferencesKey("show_page_indicators")
|
||||
|
|
@ -70,7 +71,7 @@ object TerminalPrefsKeys {
|
|||
val RECOVERY_PROTOCOL = stringPreferencesKey("recovery_protocol")
|
||||
}
|
||||
|
||||
class TerminalPreferences(private val dataStore: DataStore<Preferences>) {
|
||||
class TerminalPreferences(private val dataStore: DataStore<Preferences>, private val isTablet: Boolean = false) {
|
||||
val fontSizeSp: Flow<Float> = dataStore.data.map { prefs ->
|
||||
prefs[TerminalPrefsKeys.FONT_SIZE_SP] ?: 10f
|
||||
}
|
||||
|
|
@ -176,13 +177,21 @@ class TerminalPreferences(private val dataStore: DataStore<Preferences>) {
|
|||
}
|
||||
|
||||
val keyboardSameSizeBoth: Flow<Boolean> = dataStore.data.map { prefs ->
|
||||
prefs[TerminalPrefsKeys.KEYBOARD_SAME_SIZE_BOTH] ?: true
|
||||
prefs[TerminalPrefsKeys.KEYBOARD_SAME_SIZE_BOTH] ?: !isTablet
|
||||
}
|
||||
|
||||
suspend fun setKeyboardSameSizeBoth(same: Boolean) {
|
||||
dataStore.edit { prefs -> prefs[TerminalPrefsKeys.KEYBOARD_SAME_SIZE_BOTH] = same }
|
||||
}
|
||||
|
||||
val numberRowMode: Flow<String> = dataStore.data.map { prefs ->
|
||||
prefs[TerminalPrefsKeys.NUMBER_ROW_MODE] ?: "left"
|
||||
}
|
||||
|
||||
suspend fun setNumberRowMode(mode: String) {
|
||||
dataStore.edit { prefs -> prefs[TerminalPrefsKeys.NUMBER_ROW_MODE] = mode }
|
||||
}
|
||||
|
||||
val quickBarPosition: Flow<String> = dataStore.data.map { prefs ->
|
||||
prefs[TerminalPrefsKeys.QUICK_BAR_POSITION] ?: "above_keyboard"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -55,6 +55,7 @@ object DatabaseModule {
|
|||
@Provides
|
||||
@Singleton
|
||||
fun provideTerminalPreferences(@ApplicationContext context: Context): TerminalPreferences {
|
||||
return TerminalPreferences(context.terminalDataStore)
|
||||
val isTablet = context.resources.configuration.smallestScreenWidthDp >= 600
|
||||
return TerminalPreferences(context.terminalDataStore, isTablet)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ data class KeyboardDisplaySettings(
|
|||
val showPageIndicators: Boolean = true,
|
||||
val keyColorPreset: String = "default",
|
||||
val keyColorCustom: String = "", // hex #RRGGBB or empty
|
||||
val numberRowMode: String = "left",
|
||||
val showHints: Boolean = true,
|
||||
val keyRepeatDelay: Int = 400,
|
||||
val longPressDelay: Int = 350,
|
||||
|
|
@ -31,6 +32,7 @@ data class KeyboardDisplaySettings(
|
|||
put("showPageIndicators", showPageIndicators)
|
||||
put("keyColorPreset", keyColorPreset)
|
||||
put("keyColorCustom", keyColorCustom)
|
||||
put("numberRowMode", numberRowMode)
|
||||
put("showHints", showHints)
|
||||
put("keyRepeatDelay", keyRepeatDelay)
|
||||
put("longPressDelay", longPressDelay)
|
||||
|
|
@ -54,6 +56,7 @@ data class KeyboardDisplaySettings(
|
|||
showPageIndicators = j.optBoolean("showPageIndicators", defaults.showPageIndicators),
|
||||
keyColorPreset = j.optString("keyColorPreset", defaults.keyColorPreset),
|
||||
keyColorCustom = j.optString("keyColorCustom", defaults.keyColorCustom),
|
||||
numberRowMode = j.optString("numberRowMode", defaults.numberRowMode),
|
||||
showHints = j.optBoolean("showHints", defaults.showHints),
|
||||
keyRepeatDelay = j.optInt("keyRepeatDelay", defaults.keyRepeatDelay),
|
||||
longPressDelay = j.optInt("longPressDelay", defaults.longPressDelay),
|
||||
|
|
|
|||
|
|
@ -98,6 +98,7 @@ fun KeyboardSettingsScreen(
|
|||
onCustomizeQb: (() -> Unit)? = null,
|
||||
onResetKeys: (() -> Unit)? = null
|
||||
) {
|
||||
val context = LocalContext.current
|
||||
val isPro = proFeatures == null || proFeatures.keyboardCustomization
|
||||
|
||||
// Mutable state for all settings
|
||||
|
|
@ -105,6 +106,7 @@ fun KeyboardSettingsScreen(
|
|||
var heightPercent by remember { mutableFloatStateOf(currentSettings.heightPercent) }
|
||||
var heightLandscape by remember { mutableFloatStateOf(currentSettings.heightLandscape) }
|
||||
var sameSizeBoth by remember { mutableStateOf(currentSettings.sameSizeBoth) }
|
||||
var numberRowMode by remember { mutableStateOf(currentSettings.numberRowMode) }
|
||||
var showPageIndicators by remember { mutableStateOf(currentSettings.showPageIndicators) }
|
||||
var keyColorPreset by remember { mutableStateOf(currentSettings.keyColorPreset) }
|
||||
var keyColorCustom by remember { mutableStateOf(currentSettings.keyColorCustom) }
|
||||
|
|
@ -121,9 +123,10 @@ fun KeyboardSettingsScreen(
|
|||
|
||||
fun currentState() = KeyboardDisplaySettings(
|
||||
language, heightPercent, heightLandscape, sameSizeBoth, showPageIndicators,
|
||||
keyColorPreset, keyColorCustom, showHints,
|
||||
keyRepeatDelay, longPressDelay, quickBarPosition, quickBarSize,
|
||||
qbColorPreset, qbColorCustom
|
||||
keyColorPreset, keyColorCustom, numberRowMode = numberRowMode, showHints = showHints,
|
||||
keyRepeatDelay = keyRepeatDelay, longPressDelay = longPressDelay,
|
||||
quickBarPosition = quickBarPosition, quickBarSize = quickBarSize,
|
||||
qbColorPreset = qbColorPreset, qbColorCustom = qbColorCustom
|
||||
)
|
||||
|
||||
fun hasChanges() = currentState() != currentSettings
|
||||
|
|
@ -225,6 +228,8 @@ fun KeyboardSettingsScreen(
|
|||
onSameSizeBothChange = { sameSizeBoth = it },
|
||||
heightLandscape = heightLandscape,
|
||||
onHeightLandscapeChange = { heightLandscape = it },
|
||||
numberRowMode = numberRowMode,
|
||||
onNumberRowModeChange = { numberRowMode = it },
|
||||
showPageIndicators = showPageIndicators,
|
||||
onShowPageIndicatorsChange = { showPageIndicators = it },
|
||||
keyColorPreset = keyColorPreset,
|
||||
|
|
@ -238,10 +243,12 @@ fun KeyboardSettingsScreen(
|
|||
longPressDelay = longPressDelay,
|
||||
onLongPressDelayChange = { longPressDelay = it },
|
||||
onReset = {
|
||||
val isTab = context.resources.configuration.smallestScreenWidthDp >= 600
|
||||
val d = KeyboardDisplaySettings()
|
||||
heightPercent = d.heightPercent
|
||||
heightLandscape = d.heightLandscape
|
||||
sameSizeBoth = d.sameSizeBoth
|
||||
sameSizeBoth = if (isTab) false else d.sameSizeBoth
|
||||
numberRowMode = d.numberRowMode
|
||||
showPageIndicators = d.showPageIndicators
|
||||
keyColorPreset = d.keyColorPreset
|
||||
keyColorCustom = d.keyColorCustom
|
||||
|
|
@ -683,6 +690,8 @@ private fun KeyboardTab(
|
|||
onSameSizeBothChange: (Boolean) -> Unit,
|
||||
heightLandscape: Float,
|
||||
onHeightLandscapeChange: (Float) -> Unit,
|
||||
numberRowMode: String,
|
||||
onNumberRowModeChange: (String) -> Unit,
|
||||
showPageIndicators: Boolean,
|
||||
onShowPageIndicatorsChange: (Boolean) -> Unit,
|
||||
keyColorPreset: String,
|
||||
|
|
@ -734,16 +743,35 @@ private fun KeyboardTab(
|
|||
|
||||
// Size (landscape) — conditionally visible
|
||||
if (!sameSizeBoth) {
|
||||
val isTab = LocalContext.current.resources.configuration.smallestScreenWidthDp >= 600
|
||||
val landscapeMin = if (isTab) 20 else 30
|
||||
SectionLabel(stringResource(R.string.size_landscape))
|
||||
SliderRow(
|
||||
value = (heightLandscape * 100).roundToInt(),
|
||||
min = 15, max = 50,
|
||||
min = landscapeMin, max = 50,
|
||||
enabled = isPro,
|
||||
label = "${(heightLandscape * 100).roundToInt()}%",
|
||||
onValueChange = { onHeightLandscapeChange(it / 100f) }
|
||||
)
|
||||
}
|
||||
|
||||
// Number row mode
|
||||
SectionLabel(stringResource(R.string.number_row))
|
||||
val numberRowOptions = remember {
|
||||
listOf(
|
||||
"top" to R.string.number_row_top,
|
||||
"left" to R.string.number_row_left,
|
||||
"right" to R.string.number_row_right,
|
||||
"hidden" to R.string.number_row_hidden
|
||||
)
|
||||
}
|
||||
LanguageDropdown(
|
||||
options = numberRowOptions.map { it.first to stringResource(it.second) },
|
||||
selected = numberRowMode,
|
||||
enabled = isPro,
|
||||
onSelected = onNumberRowModeChange
|
||||
)
|
||||
|
||||
// Page indicators
|
||||
SwitchRow(
|
||||
label = stringResource(R.string.show_page_indicators),
|
||||
|
|
|
|||
|
|
@ -0,0 +1,74 @@
|
|||
package com.roundingmobile.sshworkbench.terminal
|
||||
|
||||
import com.roundingmobile.keyboard.model.*
|
||||
|
||||
/**
|
||||
* Transforms a [KeyboardLayout] based on the number row mode setting.
|
||||
*
|
||||
* Modes:
|
||||
* - "top": no change (default — dedicated number row)
|
||||
* - "hidden": remove number row, add number hints + long-press to QWERTY keys
|
||||
* - "left": remove number row, prepend number keys to each row as a side column
|
||||
* - "right": remove number row, append number keys to each row as a side column
|
||||
*/
|
||||
fun transformNumberRow(layout: KeyboardLayout, mode: String): KeyboardLayout {
|
||||
if (mode == "top") return layout
|
||||
|
||||
val alphaPageIndex = layout.pages.indexOfFirst { it.id == "alpha" }
|
||||
if (alphaPageIndex < 0) return layout
|
||||
|
||||
val alphaPage = layout.pages[alphaPageIndex]
|
||||
val rows = alphaPage.rows
|
||||
if (rows.isEmpty()) return layout
|
||||
|
||||
// Find the number row (first row, height 0.75, keys are digits)
|
||||
val numberRow = rows.firstOrNull() ?: return layout
|
||||
val numberKeys = numberRow.keys
|
||||
if (numberKeys.size != 10) return layout
|
||||
|
||||
// Remaining rows (QWERTY, ASDF, ZXCV, bottom)
|
||||
val letterRows = rows.drop(1)
|
||||
|
||||
val newRows = when (mode) {
|
||||
"hidden" -> transformHidden(letterRows, numberKeys)
|
||||
"left", "right" -> letterRows // just remove number row; mini pad handles numbers
|
||||
else -> return layout
|
||||
}
|
||||
|
||||
val newPage = alphaPage.copy(rows = newRows)
|
||||
val newPages = layout.pages.toMutableList()
|
||||
newPages[alphaPageIndex] = newPage
|
||||
return layout.copy(pages = newPages)
|
||||
}
|
||||
|
||||
/**
|
||||
* Hidden mode: remove number row entirely, add number hints and long-press to QWERTY keys.
|
||||
* q→1, w→2, e→3, r→4, t→5, y→6, u→7, i→8, o→9, p→0
|
||||
*/
|
||||
private fun transformHidden(
|
||||
letterRows: List<KeyRow>,
|
||||
numberKeys: List<KeyDefinition>
|
||||
): List<KeyRow> {
|
||||
if (letterRows.isEmpty()) return letterRows
|
||||
|
||||
// Map: position index → number character
|
||||
val numberChars = numberKeys.map { key ->
|
||||
val action = key.action
|
||||
if (action is KeyAction.Char) action.primary else key.label
|
||||
}
|
||||
|
||||
// First letter row is QWERTY — add number hints
|
||||
val qwertyRow = letterRows[0]
|
||||
val newQwertyKeys = qwertyRow.keys.mapIndexed { i, key ->
|
||||
if (i < numberChars.size) {
|
||||
val num = numberChars[i]
|
||||
val existingLong = key.longPress?.toMutableList() ?: mutableListOf()
|
||||
if (num !in existingLong) existingLong.add(0, num)
|
||||
key.copy(hint = num, longPress = existingLong)
|
||||
} else key
|
||||
}
|
||||
val newQwertyRow = qwertyRow.copy(keys = newQwertyKeys)
|
||||
|
||||
return listOf(newQwertyRow) + letterRows.drop(1)
|
||||
}
|
||||
|
||||
|
|
@ -48,9 +48,15 @@ class SessionNotifier(
|
|||
/** Labels awaiting batch flush (within 5s window) */
|
||||
private val pending = mutableListOf<String>()
|
||||
|
||||
/** Session IDs awaiting batch flush — parallel to [pending] */
|
||||
private val pendingSessionIds = mutableListOf<Long>()
|
||||
|
||||
/** Labels currently shown as disconnected in the live notification */
|
||||
private val active = mutableSetOf<String>()
|
||||
|
||||
/** First session ID to navigate to when notification is tapped */
|
||||
private var targetSessionId: Long = -1L
|
||||
|
||||
private var batchJob: Job? = null
|
||||
|
||||
/** Create the notification channel. Call once from Service.onCreate(). */
|
||||
|
|
@ -66,9 +72,10 @@ class SessionNotifier(
|
|||
}
|
||||
|
||||
/** A session disconnected. Batched for 5s. */
|
||||
fun onDisconnect(label: String) {
|
||||
fun onDisconnect(label: String, sessionId: Long = -1L) {
|
||||
synchronized(lock) {
|
||||
pending.add(label)
|
||||
pendingSessionIds.add(sessionId)
|
||||
}
|
||||
scheduleBatchFlush()
|
||||
}
|
||||
|
|
@ -77,7 +84,12 @@ class SessionNotifier(
|
|||
fun onReconnect(label: String) {
|
||||
val wasPending: Boolean
|
||||
synchronized(lock) {
|
||||
wasPending = pending.remove(label)
|
||||
val idx = pending.indexOf(label)
|
||||
wasPending = idx >= 0
|
||||
if (wasPending) {
|
||||
pending.removeAt(idx)
|
||||
pendingSessionIds.removeAt(idx)
|
||||
}
|
||||
}
|
||||
if (wasPending) return // still in batch window — just removed, done
|
||||
|
||||
|
|
@ -112,7 +124,12 @@ class SessionNotifier(
|
|||
val flushed: List<String>
|
||||
synchronized(lock) {
|
||||
flushed = pending.toList()
|
||||
// Use the first pending session ID as navigation target
|
||||
if (targetSessionId < 0 && pendingSessionIds.isNotEmpty()) {
|
||||
targetSessionId = pendingSessionIds.first { it >= 0 }
|
||||
}
|
||||
pending.clear()
|
||||
pendingSessionIds.clear()
|
||||
active.addAll(flushed)
|
||||
}
|
||||
if (flushed.isEmpty()) return@launch
|
||||
|
|
@ -127,6 +144,7 @@ class SessionNotifier(
|
|||
|
||||
val labels = synchronized(lock) { active.toList() }
|
||||
if (labels.isEmpty()) {
|
||||
synchronized(lock) { targetSessionId = -1L }
|
||||
nm.cancel(NOTIFICATION_ID)
|
||||
FileLogger.log(TAG, "notification cancelled (all reconnected)") // TRACE
|
||||
return
|
||||
|
|
@ -136,8 +154,10 @@ class SessionNotifier(
|
|||
val text = if (labels.size == 1) labels[0]
|
||||
else context.getString(R.string.notification_sessions_disconnected, labels.size)
|
||||
|
||||
val navSessionId = synchronized(lock) { targetSessionId }
|
||||
val intent = Intent(context, MainActivity::class.java).apply {
|
||||
flags = Intent.FLAG_ACTIVITY_SINGLE_TOP or Intent.FLAG_ACTIVITY_CLEAR_TOP
|
||||
if (navSessionId >= 0) putExtra("sessionId", navSessionId)
|
||||
}
|
||||
val pi = PendingIntent.getActivity(
|
||||
context, 1, intent,
|
||||
|
|
|
|||
|
|
@ -519,7 +519,7 @@ class TerminalService : Service() {
|
|||
releaseWifiLock(entry)
|
||||
if (state.reason != "User disconnected" && !state.cleanExit) {
|
||||
dumpDisconnectInfo(entry, state.reason, isError = false)
|
||||
sessionNotifier.onDisconnect(entry.customLabel ?: entry.host ?: getString(R.string.session_fallback_label, sessionId))
|
||||
sessionNotifier.onDisconnect(entry.customLabel ?: entry.host ?: getString(R.string.session_fallback_label, sessionId), sessionId)
|
||||
}
|
||||
if (state.reason != "User disconnected"
|
||||
&& !state.cleanExit
|
||||
|
|
@ -532,7 +532,7 @@ class TerminalService : Service() {
|
|||
is SessionState.Error -> {
|
||||
releaseWifiLock(entry)
|
||||
dumpDisconnectInfo(entry, state.message, isError = true)
|
||||
sessionNotifier.onDisconnect(entry.customLabel ?: entry.host ?: getString(R.string.session_fallback_label, sessionId))
|
||||
sessionNotifier.onDisconnect(entry.customLabel ?: entry.host ?: getString(R.string.session_fallback_label, sessionId), sessionId)
|
||||
// Dump SSH debug log to terminal on failure
|
||||
dumpDebugLog(entry, session)
|
||||
if (entry.autoReconnectEnabled) {
|
||||
|
|
@ -954,14 +954,14 @@ class TerminalService : Service() {
|
|||
releaseWifiLock(entry)
|
||||
if (state.reason != "User disconnected" && !state.cleanExit) {
|
||||
dumpDisconnectInfo(entry, state.reason, isError = false)
|
||||
sessionNotifier.onDisconnect(entry.customLabel ?: entry.host ?: getString(R.string.session_fallback_label, sessionId))
|
||||
sessionNotifier.onDisconnect(entry.customLabel ?: entry.host ?: getString(R.string.session_fallback_label, sessionId), sessionId)
|
||||
}
|
||||
cleanupDisconnectedSession(sessionId)
|
||||
}
|
||||
is SessionState.Error -> {
|
||||
releaseWifiLock(entry)
|
||||
dumpDisconnectInfo(entry, state.message, isError = true)
|
||||
sessionNotifier.onDisconnect(entry.customLabel ?: entry.host ?: getString(R.string.session_fallback_label, sessionId))
|
||||
sessionNotifier.onDisconnect(entry.customLabel ?: entry.host ?: getString(R.string.session_fallback_label, sessionId), sessionId)
|
||||
cleanupDisconnectedSession(sessionId)
|
||||
}
|
||||
else -> {}
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ import androidx.compose.material3.Snackbar
|
|||
import androidx.compose.material3.SnackbarHost
|
||||
import androidx.compose.material3.SnackbarHostState
|
||||
import androidx.compose.foundation.layout.Column
|
||||
import androidx.compose.ui.platform.LocalConfiguration
|
||||
import androidx.compose.foundation.layout.WindowInsets
|
||||
import androidx.compose.foundation.layout.Row
|
||||
import androidx.compose.foundation.layout.fillMaxHeight
|
||||
|
|
@ -162,6 +163,12 @@ class MainActivity : AppCompatActivity() {
|
|||
mainViewModel.connectByProfile(it)
|
||||
pendingProfile = null
|
||||
}
|
||||
// Navigate to session from disconnect notification
|
||||
if (pendingSessionId >= 0) {
|
||||
FileLogger.log(TAG, "onServiceConnected — navigating to session $pendingSessionId") // TRACE
|
||||
mainViewModel.switchToTerminal(pendingSessionId)
|
||||
pendingSessionId = -1L
|
||||
}
|
||||
}
|
||||
|
||||
override fun onServiceDisconnected(name: ComponentName?) {
|
||||
|
|
@ -234,9 +241,15 @@ class MainActivity : AppCompatActivity() {
|
|||
val keyboardType by mainViewModel.terminalPrefs.keyboardType.collectAsStateWithLifecycle("")
|
||||
val hapticEnabled by mainViewModel.terminalPrefs.hapticFeedback.collectAsStateWithLifecycle(true)
|
||||
val quickBarVisible by mainViewModel.terminalPrefs.quickBarVisible.collectAsStateWithLifecycle(true)
|
||||
val keyboardHeightPercent by mainViewModel.terminalPrefs.keyboardHeightPercent.collectAsStateWithLifecycle(0.27f)
|
||||
val keyboardHeightPortrait by mainViewModel.terminalPrefs.keyboardHeightPercent.collectAsStateWithLifecycle(0.27f)
|
||||
val keyboardHeightLandscape by mainViewModel.terminalPrefs.keyboardHeightLandscape.collectAsStateWithLifecycle(0.27f)
|
||||
val isTablet = LocalContext.current.resources.configuration.smallestScreenWidthDp >= 600
|
||||
val sameSizeBoth by mainViewModel.terminalPrefs.keyboardSameSizeBoth.collectAsStateWithLifecycle(!isTablet)
|
||||
val isLandscape = LocalConfiguration.current.orientation == android.content.res.Configuration.ORIENTATION_LANDSCAPE
|
||||
val keyboardHeightPercent = if (isLandscape && !sameSizeBoth) keyboardHeightLandscape else keyboardHeightPortrait
|
||||
val keyboardLanguage by mainViewModel.terminalPrefs.keyboardLanguage.collectAsStateWithLifecycle("en")
|
||||
val showKeyHints by mainViewModel.terminalPrefs.showKeyHints.collectAsStateWithLifecycle(true)
|
||||
val numberRowMode by mainViewModel.terminalPrefs.numberRowMode.collectAsStateWithLifecycle("left")
|
||||
val showPageIndicators by mainViewModel.terminalPrefs.showPageIndicators.collectAsStateWithLifecycle(true)
|
||||
val cqbSize by mainViewModel.terminalPrefs.quickBarSize.collectAsStateWithLifecycle(42)
|
||||
val aqbSize by mainViewModel.terminalPrefs.aqbSize.collectAsStateWithLifecycle(42)
|
||||
|
|
@ -260,7 +273,6 @@ class MainActivity : AppCompatActivity() {
|
|||
// Keyboard settings dialog state (opened from CKB gear menu)
|
||||
var showKbSettings by remember { mutableStateOf(false) }
|
||||
var showAqbSettings by remember { mutableStateOf(false) }
|
||||
var showGearMenu by remember { mutableStateOf(false) }
|
||||
var showQbCustomizer by remember { mutableStateOf(false) }
|
||||
|
||||
// Resolve keyboard layout and language resource IDs
|
||||
|
|
@ -275,12 +287,16 @@ class MainActivity : AppCompatActivity() {
|
|||
else -> com.roundingmobile.terminalkeyboard.R.raw.lang_en
|
||||
}
|
||||
|
||||
// Single shared keyboard instance — recreated only when language changes
|
||||
// Single shared keyboard instance — recreated when language or number row mode changes
|
||||
val context = LocalContext.current
|
||||
val keyboard = remember(keyboardLanguage) {
|
||||
val keyboard = remember(keyboardLanguage, numberRowMode) {
|
||||
val baseLayout = com.roundingmobile.keyboard.parser.LayoutParser.fromResource(
|
||||
context, layoutResId,
|
||||
com.roundingmobile.keyboard.parser.LanguageParser.fromResource(context, langResId)
|
||||
)
|
||||
val transformedLayout = com.roundingmobile.sshworkbench.terminal.transformNumberRow(baseLayout, numberRowMode)
|
||||
TerminalKeyboard.Builder(context)
|
||||
.fromJson(layoutResId)
|
||||
.language(langResId)
|
||||
.layout(transformedLayout)
|
||||
.hapticFeedback(hapticEnabled)
|
||||
.showHints(showKeyHints)
|
||||
.build()
|
||||
|
|
@ -323,10 +339,9 @@ class MainActivity : AppCompatActivity() {
|
|||
}
|
||||
}
|
||||
|
||||
// Wire snippet and settings callbacks (resolve active session dynamically)
|
||||
// Wire snippet callback (resolve active session dynamically)
|
||||
LaunchedEffect(keyboard) {
|
||||
keyboard.onSnippetsTap = { showSnippetPicker() }
|
||||
keyboard.onSettingsTap = { showGearMenu = true }
|
||||
}
|
||||
|
||||
// Update dynamic keyboard settings
|
||||
|
|
@ -390,27 +405,7 @@ class MainActivity : AppCompatActivity() {
|
|||
}
|
||||
}
|
||||
|
||||
// CKB gear long-press menu (Hide Keyboard / Keyboard Settings)
|
||||
if (showGearMenu) {
|
||||
val hideLabel = getString(R.string.hide_keyboard)
|
||||
val settingsLabel = getString(R.string.keyboard_settings_title)
|
||||
androidx.compose.material3.AlertDialog(
|
||||
onDismissRequest = { showGearMenu = false },
|
||||
confirmButton = {},
|
||||
text = {
|
||||
Column {
|
||||
androidx.compose.material3.TextButton(onClick = { showGearMenu = false; ckbHidden = true }) {
|
||||
androidx.compose.material3.Text(hideLabel)
|
||||
}
|
||||
androidx.compose.material3.TextButton(onClick = { showGearMenu = false; showKbSettings = true }) {
|
||||
androidx.compose.material3.Text(settingsLabel)
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
// Keyboard settings dialog (opened from CKB gear → Keyboard Settings)
|
||||
// Keyboard settings dialog (opened from tab bar kebab → KB Settings)
|
||||
if (showKbSettings) {
|
||||
val cachedKbSettings by mainViewModel.currentKeyboardSettings.collectAsStateWithLifecycle()
|
||||
KeyboardSettingsScreen(
|
||||
|
|
@ -549,7 +544,9 @@ class MainActivity : AppCompatActivity() {
|
|||
},
|
||||
kbSettingsLabel = if (isCustomKeyboard)
|
||||
getString(R.string.keyboard_settings_title)
|
||||
else getString(R.string.quick_bar_settings)
|
||||
else getString(R.string.quick_bar_settings),
|
||||
ckbVisible = isCustomKeyboard && !ckbHidden,
|
||||
onToggleCkb = { ckbHidden = !ckbHidden }
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -683,17 +680,64 @@ class MainActivity : AppCompatActivity() {
|
|||
}
|
||||
|
||||
// --- Shared keyboard (one instance for all sessions) ---
|
||||
val showMini = (numberRowMode == "left" || numberRowMode == "right") && keyboard.miniSection != null
|
||||
val miniWeight = if (showMini) (keyboard.miniSection?.widthPercent ?: 10) / 100f else 0f
|
||||
val kbWeight = if (showMini) 1f - miniWeight else 1f
|
||||
|
||||
key(keyboardLanguage, numberRowMode) {
|
||||
AndroidView(
|
||||
factory = { ctx ->
|
||||
FrameLayout(ctx).apply {
|
||||
clipChildren = false
|
||||
clipToPadding = false
|
||||
keyboard.attachTo(this)
|
||||
if (showMini) {
|
||||
android.widget.LinearLayout(ctx).apply {
|
||||
orientation = android.widget.LinearLayout.HORIZONTAL
|
||||
clipChildren = false
|
||||
clipToPadding = false
|
||||
|
||||
val miniFrame = FrameLayout(ctx).apply {
|
||||
clipChildren = false; clipToPadding = false
|
||||
}
|
||||
keyboard.attachMiniTo(miniFrame)
|
||||
|
||||
val kbFrame = FrameLayout(ctx).apply {
|
||||
clipChildren = false; clipToPadding = false
|
||||
}
|
||||
keyboard.attachTo(kbFrame)
|
||||
|
||||
if (numberRowMode == "left") {
|
||||
addView(miniFrame, android.widget.LinearLayout.LayoutParams(0, android.view.ViewGroup.LayoutParams.MATCH_PARENT, miniWeight))
|
||||
addView(kbFrame, android.widget.LinearLayout.LayoutParams(0, android.view.ViewGroup.LayoutParams.MATCH_PARENT, kbWeight))
|
||||
} else {
|
||||
addView(kbFrame, android.widget.LinearLayout.LayoutParams(0, android.view.ViewGroup.LayoutParams.MATCH_PARENT, kbWeight))
|
||||
addView(miniFrame, android.widget.LinearLayout.LayoutParams(0, android.view.ViewGroup.LayoutParams.MATCH_PARENT, miniWeight))
|
||||
}
|
||||
// attachTo/attachMiniTo propagate clipChildren=false up the
|
||||
// entire parent chain which breaks SurfaceView rendering.
|
||||
// Re-enable clipping from the LinearLayout upward.
|
||||
post {
|
||||
var v: android.view.ViewGroup? = this@apply
|
||||
while (v != null) {
|
||||
v.clipChildren = true
|
||||
v.clipToPadding = true
|
||||
v = v.parent as? android.view.ViewGroup
|
||||
}
|
||||
// Keep clipChildren=false only on the direct keyboard containers
|
||||
kbFrame.clipChildren = false
|
||||
kbFrame.clipToPadding = false
|
||||
miniFrame.clipChildren = false
|
||||
miniFrame.clipToPadding = false
|
||||
}
|
||||
}
|
||||
} else {
|
||||
FrameLayout(ctx).apply {
|
||||
clipChildren = false
|
||||
clipToPadding = false
|
||||
keyboard.attachTo(this)
|
||||
}
|
||||
}
|
||||
},
|
||||
update = { container ->
|
||||
val isTermTab = tabTypes[appState.activeSessionId] != TabType.SFTP
|
||||
container.visibility = if (isCustomKeyboard && !ckbHidden && showTerminal && isTermTab) View.VISIBLE else View.GONE
|
||||
container.visibility = if (isCustomKeyboard && !ckbHidden && showTerminal && isTermTab) android.view.View.VISIBLE else android.view.View.GONE
|
||||
val screenHeight = container.resources.displayMetrics.heightPixels
|
||||
val kbHeightPx = (screenHeight * keyboardHeightPercent).toInt()
|
||||
val params = container.layoutParams
|
||||
|
|
@ -703,6 +747,7 @@ class MainActivity : AppCompatActivity() {
|
|||
}
|
||||
}
|
||||
)
|
||||
} // key(keyboardLanguage, numberRowMode)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -896,8 +941,20 @@ class MainActivity : AppCompatActivity() {
|
|||
|
||||
override fun onNewIntent(intent: Intent) {
|
||||
super.onNewIntent(intent)
|
||||
FileLogger.log(TAG, "onNewIntent action=${intent.action}") // TRACE
|
||||
FileLogger.log(TAG, "onNewIntent action=${intent.action} extras=${intent.extras?.keySet()}") // TRACE
|
||||
handleSharedImage(intent)
|
||||
handleSessionNavigation(intent)
|
||||
}
|
||||
|
||||
/** Navigate to a specific session when launched from disconnect notification. */
|
||||
private fun handleSessionNavigation(intent: Intent?) {
|
||||
val sessionId = intent?.getLongExtra("sessionId", -1L) ?: -1L
|
||||
if (sessionId >= 0) {
|
||||
FileLogger.log(TAG, "handleSessionNavigation: sessionId=$sessionId") // TRACE
|
||||
// Clear the extra so we don't re-navigate on config change
|
||||
intent?.removeExtra("sessionId")
|
||||
mainViewModel.switchToTerminal(sessionId)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onResume() {
|
||||
|
|
@ -977,6 +1034,8 @@ class MainActivity : AppCompatActivity() {
|
|||
|
||||
/** Profile name from launch intent — connected once the service is bound. */
|
||||
private var pendingProfile: String? = null
|
||||
/** Session ID from disconnect notification — navigated once the service is bound. */
|
||||
private var pendingSessionId: Long = -1L
|
||||
|
||||
private fun handleLaunchExtras(intent: Intent?) {
|
||||
intent ?: return
|
||||
|
|
@ -997,6 +1056,19 @@ class MainActivity : AppCompatActivity() {
|
|||
pendingProfile = null
|
||||
}
|
||||
}
|
||||
|
||||
// Navigate to session from disconnect notification (fresh Activity launch)
|
||||
val sessionId = intent.getLongExtra("sessionId", -1L)
|
||||
if (sessionId >= 0) {
|
||||
FileLogger.log(TAG, "handleLaunchExtras: sessionId=$sessionId") // TRACE
|
||||
pendingSessionId = sessionId
|
||||
intent.removeExtra("sessionId")
|
||||
// If service is already bound, navigate now; otherwise onServiceBound handles it
|
||||
if (serviceBound) {
|
||||
mainViewModel.switchToTerminal(sessionId)
|
||||
pendingSessionId = -1L
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ========================================================================
|
||||
|
|
|
|||
|
|
@ -42,10 +42,11 @@ enum class TabType { TERMINAL, TELNET, LOCAL, SFTP }
|
|||
data class ConnectionSessionInfo(
|
||||
val terminalCount: Int = 0,
|
||||
val sftpCount: Int = 0,
|
||||
val disconnectedCount: Int = 0,
|
||||
val terminalEarliestStart: Long = 0L,
|
||||
val sftpEarliestStart: Long = 0L
|
||||
) {
|
||||
val totalCount: Int get() = terminalCount + sftpCount
|
||||
val totalCount: Int get() = terminalCount + sftpCount + disconnectedCount
|
||||
val earliestStart: Long get() = when {
|
||||
terminalEarliestStart > 0 && sftpEarliestStart > 0 -> minOf(terminalEarliestStart, sftpEarliestStart)
|
||||
terminalEarliestStart > 0 -> terminalEarliestStart
|
||||
|
|
@ -533,12 +534,14 @@ class MainViewModel @Inject constructor(
|
|||
// Terminal/Telnet sessions
|
||||
for (entry in svc.getAllSessions()) {
|
||||
val state = _activeSessions.value[entry.sessionId]
|
||||
val connId = entry.savedConnectionId
|
||||
val prev = infos[connId] ?: ConnectionSessionInfo()
|
||||
if (state is com.roundingmobile.ssh.SessionState.Connected || state is com.roundingmobile.ssh.SessionState.Connecting) {
|
||||
val connId = entry.savedConnectionId
|
||||
val prev = infos[connId] ?: ConnectionSessionInfo()
|
||||
val earliest = if (prev.terminalEarliestStart == 0L) entry.createdAt
|
||||
else minOf(prev.terminalEarliestStart, entry.createdAt)
|
||||
infos[connId] = prev.copy(terminalCount = prev.terminalCount + 1, terminalEarliestStart = earliest)
|
||||
} else if (state is com.roundingmobile.ssh.SessionState.Disconnected || state is com.roundingmobile.ssh.SessionState.Error) {
|
||||
infos[connId] = prev.copy(disconnectedCount = prev.disconnectedCount + 1)
|
||||
}
|
||||
}
|
||||
// SFTP tabs
|
||||
|
|
@ -847,13 +850,14 @@ class MainViewModel @Inject constructor(
|
|||
prefs.keyRepeatDelay,
|
||||
prefs.longPressDelay
|
||||
) { preset, custom, hints, repeat, longPress -> listOf(preset, custom, hints, repeat, longPress) },
|
||||
prefs.numberRowMode,
|
||||
combine(
|
||||
prefs.quickBarPosition,
|
||||
prefs.quickBarSize,
|
||||
prefs.qbColorPreset,
|
||||
prefs.qbColorCustom
|
||||
) { pos, size, preset, custom -> listOf(pos, size, preset, custom) }
|
||||
) { kb, colors, qb ->
|
||||
) { kb, colors, numMode, qb ->
|
||||
com.roundingmobile.sshworkbench.terminal.KeyboardDisplaySettings(
|
||||
language = kb[0] as String,
|
||||
heightPercent = kb[1] as Float,
|
||||
|
|
@ -862,6 +866,7 @@ class MainViewModel @Inject constructor(
|
|||
showPageIndicators = kb[4] as Boolean,
|
||||
keyColorPreset = colors[0] as String,
|
||||
keyColorCustom = colors[1] as String,
|
||||
numberRowMode = numMode,
|
||||
showHints = colors[2] as Boolean,
|
||||
keyRepeatDelay = colors[3] as Int,
|
||||
longPressDelay = colors[4] as Int,
|
||||
|
|
@ -870,7 +875,7 @@ class MainViewModel @Inject constructor(
|
|||
qbColorPreset = qb[2] as String,
|
||||
qbColorCustom = qb[3] as String
|
||||
)
|
||||
}.stateIn(viewModelScope, SharingStarted.WhileSubscribed(5000),
|
||||
}.stateIn(viewModelScope, SharingStarted.Eagerly,
|
||||
com.roundingmobile.sshworkbench.terminal.KeyboardDisplaySettings())
|
||||
}
|
||||
|
||||
|
|
@ -917,6 +922,7 @@ class MainViewModel @Inject constructor(
|
|||
terminalPrefs.setShowPageIndicators(settings.showPageIndicators)
|
||||
terminalPrefs.setKeyColorPreset(settings.keyColorPreset)
|
||||
terminalPrefs.setKeyColorCustom(settings.keyColorCustom)
|
||||
terminalPrefs.setNumberRowMode(settings.numberRowMode)
|
||||
terminalPrefs.setShowKeyHints(settings.showHints)
|
||||
terminalPrefs.setKeyRepeatDelay(settings.keyRepeatDelay)
|
||||
terminalPrefs.setLongPressDelay(settings.longPressDelay)
|
||||
|
|
|
|||
|
|
@ -46,6 +46,7 @@ import kotlinx.coroutines.delay
|
|||
|
||||
internal val ActiveGreen = Color(0xFF4CAF50)
|
||||
private val AccentAmber = Color(0xFFFFB020)
|
||||
private val DisconnectedRed = Color(0xFFE53935)
|
||||
|
||||
@OptIn(ExperimentalFoundationApi::class)
|
||||
@Composable
|
||||
|
|
@ -76,9 +77,10 @@ internal fun ConnectionItem(
|
|||
.fillMaxWidth()
|
||||
.then(
|
||||
if (isActive) {
|
||||
val barColor = if ((sessionInfo?.terminalCount ?: 0) > 0 || (sessionInfo?.sftpCount ?: 0) > 0) ActiveGreen else DisconnectedRed
|
||||
Modifier.drawBehind {
|
||||
drawRect(
|
||||
color = ActiveGreen,
|
||||
color = barColor,
|
||||
topLeft = Offset.Zero,
|
||||
size = androidx.compose.ui.geometry.Size(4.dp.toPx(), size.height)
|
||||
)
|
||||
|
|
@ -100,8 +102,11 @@ internal fun ConnectionItem(
|
|||
verticalAlignment = Alignment.CenterVertically
|
||||
) {
|
||||
// Status dot + colored circle
|
||||
val circleColor = if (isActive) {
|
||||
val hasConnected = (sessionInfo?.terminalCount ?: 0) > 0 || (sessionInfo?.sftpCount ?: 0) > 0
|
||||
val circleColor = if (isActive && hasConnected) {
|
||||
ActiveGreen
|
||||
} else if (isActive) {
|
||||
DisconnectedRed
|
||||
} else if (connection.color != 0) {
|
||||
Color(connection.color)
|
||||
} else {
|
||||
|
|
@ -165,6 +170,15 @@ internal fun ConnectionItem(
|
|||
is24h = is24h
|
||||
)
|
||||
}
|
||||
// Disconnected sessions line
|
||||
if (sessionInfo.disconnectedCount > 0) {
|
||||
if (sessionInfo.terminalCount > 0 || sessionInfo.sftpCount > 0) Spacer(modifier = Modifier.height(2.dp))
|
||||
SessionTypeLabel(
|
||||
count = sessionInfo.disconnectedCount,
|
||||
color = DisconnectedRed,
|
||||
label = stringResource(R.string.notification_count_disconnected, sessionInfo.disconnectedCount)
|
||||
)
|
||||
}
|
||||
} else if (isActive && sessionInfo != null) {
|
||||
// Session tracking disabled — just show count per type
|
||||
if (sessionInfo.terminalCount > 0) {
|
||||
|
|
@ -174,6 +188,10 @@ internal fun ConnectionItem(
|
|||
if (sessionInfo.terminalCount > 0) Spacer(modifier = Modifier.height(2.dp))
|
||||
SessionTypeDot(sessionInfo.sftpCount, AccentAmber)
|
||||
}
|
||||
if (sessionInfo.disconnectedCount > 0) {
|
||||
if (sessionInfo.terminalCount > 0 || sessionInfo.sftpCount > 0) Spacer(modifier = Modifier.height(2.dp))
|
||||
SessionTypeDot(sessionInfo.disconnectedCount, DisconnectedRed)
|
||||
}
|
||||
} else {
|
||||
Text(
|
||||
text = "${connection.username}@${connection.host}:${connection.port}",
|
||||
|
|
@ -206,12 +224,13 @@ internal fun ConnectionItem(
|
|||
Spacer(modifier = Modifier.width(8.dp))
|
||||
|
||||
if (isActive) {
|
||||
// Simple green dot — per-type details are in the text lines
|
||||
// Status dot — green if any connected, red if only disconnected
|
||||
val dotColor = if (hasConnected) ActiveGreen else DisconnectedRed
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(12.dp)
|
||||
.clip(CircleShape)
|
||||
.background(ActiveGreen)
|
||||
.background(dotColor)
|
||||
)
|
||||
} else {
|
||||
Icon(
|
||||
|
|
@ -282,6 +301,33 @@ private fun SessionTypeLine(
|
|||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SessionTypeLabel(count: Int, color: Color, label: String) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
Box(
|
||||
modifier = Modifier
|
||||
.size(18.dp)
|
||||
.clip(CircleShape)
|
||||
.background(color),
|
||||
contentAlignment = Alignment.Center
|
||||
) {
|
||||
Text(
|
||||
text = "$count",
|
||||
color = Color.White,
|
||||
style = MaterialTheme.typography.labelSmall,
|
||||
fontWeight = FontWeight.Bold
|
||||
)
|
||||
}
|
||||
Spacer(modifier = Modifier.width(6.dp))
|
||||
Text(
|
||||
text = label,
|
||||
style = MaterialTheme.typography.bodySmall,
|
||||
fontWeight = FontWeight.Medium,
|
||||
color = color.copy(alpha = 0.85f)
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@Composable
|
||||
private fun SessionTypeDot(count: Int, color: Color) {
|
||||
Row(verticalAlignment = Alignment.CenterVertically) {
|
||||
|
|
|
|||
|
|
@ -22,6 +22,8 @@ import androidx.compose.foundation.shape.CircleShape
|
|||
import androidx.compose.foundation.shape.RoundedCornerShape
|
||||
import androidx.compose.material.icons.Icons
|
||||
import androidx.compose.material.icons.filled.Add
|
||||
import androidx.compose.material.icons.filled.Keyboard
|
||||
import androidx.compose.material.icons.filled.KeyboardHide
|
||||
import androidx.compose.material.icons.filled.MoreVert
|
||||
import androidx.compose.material.icons.filled.Settings
|
||||
import androidx.compose.material3.AlertDialog
|
||||
|
|
@ -96,6 +98,8 @@ fun SessionTabBar(
|
|||
onPlusTap: () -> Unit,
|
||||
onKbSettings: () -> Unit = {},
|
||||
kbSettingsLabel: String = "",
|
||||
ckbVisible: Boolean = false,
|
||||
onToggleCkb: () -> Unit = {},
|
||||
onDuplicate: (Long) -> Unit = {},
|
||||
onSftp: (Long) -> Unit = {},
|
||||
onConnectTerminal: (Long) -> Unit = {},
|
||||
|
|
@ -178,6 +182,19 @@ fun SessionTabBar(
|
|||
onClick = { showBarMenu = false; onPlusTap() },
|
||||
leadingIcon = { Icon(Icons.Filled.Add, contentDescription = null, tint = AccentTeal) }
|
||||
)
|
||||
if (ckbVisible) {
|
||||
DropdownMenuItem(
|
||||
text = { Text(stringResource(R.string.hide_keyboard)) },
|
||||
onClick = { showBarMenu = false; onToggleCkb() },
|
||||
leadingIcon = { Icon(Icons.Filled.KeyboardHide, contentDescription = null) }
|
||||
)
|
||||
} else {
|
||||
DropdownMenuItem(
|
||||
text = { Text(stringResource(R.string.show_keyboard)) },
|
||||
onClick = { showBarMenu = false; onToggleCkb() },
|
||||
leadingIcon = { Icon(Icons.Filled.Keyboard, contentDescription = null) }
|
||||
)
|
||||
}
|
||||
DropdownMenuItem(
|
||||
text = { Text(kbSettingsLabel.ifEmpty { stringResource(R.string.quick_bar_settings) }) },
|
||||
onClick = { showBarMenu = false; onKbSettings() },
|
||||
|
|
|
|||
|
|
@ -214,6 +214,7 @@
|
|||
|
||||
<!-- Keyboard Settings Dialog -->
|
||||
<string name="hide_keyboard">Ocultar teclado</string>
|
||||
<string name="show_keyboard">Mostrar teclado</string>
|
||||
<string name="keyboard_settings_title">Ajustes de teclado</string>
|
||||
<string name="unsaved_changes_title">Cambios sin guardar</string>
|
||||
<string name="unsaved_changes_message">¿Guardar los cambios?</string>
|
||||
|
|
@ -221,6 +222,11 @@
|
|||
<string name="size_portrait">Tamaño (vertical)</string>
|
||||
<string name="size_landscape">Tamaño (horizontal)</string>
|
||||
<string name="same_size_both">Mismo tamaño en ambas orientaciones</string>
|
||||
<string name="number_row">Fila de números</string>
|
||||
<string name="number_row_top">Fila superior</string>
|
||||
<string name="number_row_left">Lado izquierdo</string>
|
||||
<string name="number_row_right">Lado derecho</string>
|
||||
<string name="number_row_hidden">Oculta (pulsación larga)</string>
|
||||
<string name="show_page_indicators">Mostrar indicadores de página</string>
|
||||
<string name="key_color">Color de teclas</string>
|
||||
<string name="show_key_hints">Mostrar sugerencias de teclas</string>
|
||||
|
|
|
|||
|
|
@ -213,6 +213,7 @@
|
|||
<string name="agent_forwarding_feature">Agentvidarebefordran</string>
|
||||
<!-- Keyboard Settings Dialog -->
|
||||
<string name="hide_keyboard">Dölj tangentbord</string>
|
||||
<string name="show_keyboard">Visa tangentbord</string>
|
||||
<string name="keyboard_settings_title">Tangentbordsinställningar</string>
|
||||
<string name="unsaved_changes_title">Osparade ändringar</string>
|
||||
<string name="unsaved_changes_message">Spara dina ändringar?</string>
|
||||
|
|
@ -220,6 +221,11 @@
|
|||
<string name="size_portrait">Storlek (stående)</string>
|
||||
<string name="size_landscape">Storlek (liggande)</string>
|
||||
<string name="same_size_both">Samma storlek i båda orienteringarna</string>
|
||||
<string name="number_row">Sifferrad</string>
|
||||
<string name="number_row_top">Översta raden</string>
|
||||
<string name="number_row_left">Vänster sida</string>
|
||||
<string name="number_row_right">Höger sida</string>
|
||||
<string name="number_row_hidden">Dold (långtryck)</string>
|
||||
<string name="show_page_indicators">Visa sidindikatorer</string>
|
||||
<string name="key_color">Tangentfärg</string>
|
||||
<string name="show_key_hints">Visa tangenttips</string>
|
||||
|
|
|
|||
|
|
@ -215,6 +215,7 @@
|
|||
|
||||
<!-- Keyboard Settings Dialog -->
|
||||
<string name="hide_keyboard">Hide keyboard</string>
|
||||
<string name="show_keyboard">Show keyboard</string>
|
||||
<string name="keyboard_settings_title">Keyboard Settings</string>
|
||||
<string name="unsaved_changes_title">Unsaved changes</string>
|
||||
<string name="unsaved_changes_message">Save your changes?</string>
|
||||
|
|
@ -222,6 +223,11 @@
|
|||
<string name="size_portrait">Size (portrait)</string>
|
||||
<string name="size_landscape">Size (landscape)</string>
|
||||
<string name="same_size_both">Same size for both orientations</string>
|
||||
<string name="number_row">Number row</string>
|
||||
<string name="number_row_top">Top row</string>
|
||||
<string name="number_row_left">Left side</string>
|
||||
<string name="number_row_right">Right side</string>
|
||||
<string name="number_row_hidden">Hidden (long-press)</string>
|
||||
<string name="show_page_indicators">Show page indicators</string>
|
||||
<string name="key_color">Key color</string>
|
||||
<string name="show_key_hints">Show key hints</string>
|
||||
|
|
|
|||
|
|
@ -154,7 +154,7 @@ Key files:
|
|||
|
||||
## Temporary Hide (CKB only)
|
||||
|
||||
- Gear long-press → "Hide Keyboard" sets `ckbHidden` Compose state (not a pref change)
|
||||
- Tab bar kebab menu → "Hide keyboard" / "Show keyboard" toggles `ckbHidden` Compose state (not a pref change)
|
||||
- CKB view hidden, QB stays visible
|
||||
- Tapping TV fires `onTapShowKeyboard` → resets `ckbHidden`
|
||||
- Does not affect AKB mode (system IME has its own show/hide)
|
||||
|
|
|
|||
|
|
@ -51,6 +51,9 @@ class TerminalKeyboard private constructor(
|
|||
|
||||
// --- Views ---
|
||||
private var keyboardView: KeyboardView? = null
|
||||
|
||||
/** Returns the keyboard view for external layout adjustments (padding, margins). */
|
||||
fun getKeyboardView(): KeyboardView? = keyboardView
|
||||
var quickBarView: QuickBarView? = null
|
||||
private set
|
||||
private var hintPopup: HintPopupView? = null
|
||||
|
|
@ -257,6 +260,31 @@ class TerminalKeyboard private constructor(
|
|||
}
|
||||
}
|
||||
|
||||
// --- Mini number pad ---
|
||||
private var miniPageView: KeyboardPageView? = null
|
||||
private var miniContainer: ViewGroup? = null
|
||||
|
||||
/** The mini section definition from the layout, if any. */
|
||||
val miniSection get() = layout.mini
|
||||
|
||||
@SuppressLint("ClickableViewAccessibility")
|
||||
fun attachMiniTo(container: ViewGroup) {
|
||||
miniPageView?.let { (it.parent as? ViewGroup)?.removeView(it) }
|
||||
|
||||
val mini = layout.mini ?: return
|
||||
if (mini.rows.isEmpty()) return
|
||||
|
||||
this.miniContainer = container
|
||||
val page = KeyboardPage(id = "mini", name = "Mini", rows = mini.rows)
|
||||
val pv = KeyboardPageView(context, page, theme)
|
||||
pv.setOnTouchListener { _, event -> handlePageTouch(pv, event) }
|
||||
container.addView(pv, ViewGroup.LayoutParams(
|
||||
ViewGroup.LayoutParams.MATCH_PARENT,
|
||||
ViewGroup.LayoutParams.MATCH_PARENT
|
||||
))
|
||||
miniPageView = pv
|
||||
}
|
||||
|
||||
fun detach() {
|
||||
modStateJob?.cancel()
|
||||
keyRepeatHandler.destroy()
|
||||
|
|
@ -266,11 +294,14 @@ class TerminalKeyboard private constructor(
|
|||
quickBarView?.let { (it.parent as? ViewGroup)?.removeView(it) }
|
||||
hintPopup?.let { (it.parent as? ViewGroup)?.removeView(it) }
|
||||
longPressPopup?.let { (it.parent as? ViewGroup)?.removeView(it) }
|
||||
miniPageView?.let { (it.parent as? ViewGroup)?.removeView(it) }
|
||||
keyboardView = null
|
||||
quickBarView = null
|
||||
hintPopup = null
|
||||
longPressPopup = null
|
||||
miniPageView = null
|
||||
container = null
|
||||
miniContainer = null
|
||||
}
|
||||
|
||||
// --- Touch handling ---
|
||||
|
|
|
|||
|
|
@ -1,10 +1,16 @@
|
|||
package com.roundingmobile.keyboard.model
|
||||
|
||||
data class MiniSection(
|
||||
val widthPercent: Int = 10,
|
||||
val rows: List<KeyRow> = emptyList()
|
||||
)
|
||||
|
||||
data class KeyboardLayout(
|
||||
val id: String,
|
||||
val name: String,
|
||||
val pages: List<KeyboardPage>,
|
||||
val quickBar: QuickBarConfig? = null,
|
||||
val mini: MiniSection? = null,
|
||||
val settings: KeyboardSettings = KeyboardSettings()
|
||||
)
|
||||
|
||||
|
|
|
|||
|
|
@ -14,6 +14,7 @@ object LayoutParser {
|
|||
name = obj.getString("name"),
|
||||
pages = parsePages(obj.getJSONArray("pages"), languagePack),
|
||||
quickBar = obj.optJSONObject("quickBar")?.let { parseQuickBar(it, languagePack) },
|
||||
mini = obj.optJSONObject("mini")?.let { parseMiniSection(it, languagePack) },
|
||||
settings = obj.optJSONObject("settings")?.let { parseSettings(it) } ?: KeyboardSettings()
|
||||
)
|
||||
}
|
||||
|
|
@ -147,6 +148,12 @@ object LayoutParser {
|
|||
infiniteScroll = obj.optBoolean("infiniteScroll", true)
|
||||
)
|
||||
|
||||
private fun parseMiniSection(obj: JSONObject, lang: LanguagePack?): MiniSection =
|
||||
MiniSection(
|
||||
widthPercent = obj.optInt("widthPercent", 10),
|
||||
rows = obj.optJSONArray("rows")?.let { parseRows(it, lang) } ?: emptyList()
|
||||
)
|
||||
|
||||
private fun parseSettings(obj: JSONObject): KeyboardSettings =
|
||||
KeyboardSettings(
|
||||
showHints = obj.optBoolean("showHints", true),
|
||||
|
|
|
|||
|
|
@ -41,6 +41,8 @@ class KeyboardPageView(
|
|||
private var gapPx = 0f
|
||||
private var radiusPx = 0f
|
||||
private var paddingPx = 0f
|
||||
/** Cached typical key height from last layout — drives font scaling */
|
||||
private var typicalKeyHeight = 0f
|
||||
|
||||
init {
|
||||
updateMetrics()
|
||||
|
|
@ -50,8 +52,22 @@ class KeyboardPageView(
|
|||
gapPx = dpToPx(theme.keyGapDp.toFloat())
|
||||
radiusPx = dpToPx(theme.keyRadiusDp.toFloat())
|
||||
paddingPx = dpToPx(4f)
|
||||
labelPaint.textSize = spToPx(theme.mainLabelSizeSp.toFloat())
|
||||
hintPaint.textSize = spToPx(theme.hintLabelSizeSp.toFloat())
|
||||
updateFontSizes()
|
||||
}
|
||||
|
||||
/** Scale label and hint fonts to a fraction of the typical key height.
|
||||
* Low absolute floor (6sp/4sp) prevents unreadable text; theme minimums
|
||||
* are only used as fallback before the first layout pass. */
|
||||
private fun updateFontSizes() {
|
||||
if (typicalKeyHeight > 0f) {
|
||||
val floorLabel = spToPx(10f)
|
||||
val floorHint = spToPx(7f)
|
||||
labelPaint.textSize = maxOf(typicalKeyHeight * 0.45f, floorLabel)
|
||||
hintPaint.textSize = maxOf(typicalKeyHeight * 0.25f, floorHint)
|
||||
} else {
|
||||
labelPaint.textSize = spToPx(theme.mainLabelSizeSp.toFloat())
|
||||
hintPaint.textSize = spToPx(theme.hintLabelSizeSp.toFloat())
|
||||
}
|
||||
}
|
||||
|
||||
fun setTheme(newTheme: KeyboardTheme) {
|
||||
|
|
@ -113,6 +129,13 @@ class KeyboardPageView(
|
|||
}
|
||||
y += rowH + gapPx
|
||||
}
|
||||
|
||||
// Update font sizes based on the tallest key (skip short number-row keys)
|
||||
val maxKeyH = allKeys.maxOfOrNull { it.bounds.height() } ?: 0f
|
||||
if (maxKeyH > 0f && maxKeyH != typicalKeyHeight) {
|
||||
typicalKeyHeight = maxKeyH
|
||||
updateFontSizes()
|
||||
}
|
||||
}
|
||||
|
||||
override fun onDraw(canvas: Canvas) {
|
||||
|
|
@ -148,17 +171,24 @@ class KeyboardPageView(
|
|||
labelPaint.color = theme.resolveKeyText(key.style)
|
||||
labelPaint.isFakeBoldText = pressed
|
||||
val displayLabel = resolveDisplayLabel(key)
|
||||
val fm = labelPaint.fontMetrics
|
||||
val labelY = if (key.hint != null) {
|
||||
rect.centerY() - dpToPx(2f)
|
||||
// Label + hint: label in upper portion, hint below
|
||||
val totalH = -fm.ascent + fm.descent + hintPaint.textSize
|
||||
val startY = rect.centerY() - totalH / 2f
|
||||
startY - fm.ascent
|
||||
} else {
|
||||
rect.centerY() + labelPaint.textSize / 3f
|
||||
// Label only: vertically centered using font metrics
|
||||
rect.centerY() - (fm.ascent + fm.descent) / 2f
|
||||
}
|
||||
canvas.drawText(displayLabel, rect.centerX(), labelY, labelPaint)
|
||||
|
||||
// Hint label
|
||||
if (key.hint != null) {
|
||||
hintPaint.color = theme.keyHintText
|
||||
canvas.drawText(key.hint, rect.centerX(), labelY + labelPaint.textSize * 0.9f, hintPaint)
|
||||
val hintFm = hintPaint.fontMetrics
|
||||
val hintY = labelY + fm.descent + (-hintFm.ascent)
|
||||
canvas.drawText(key.hint, rect.centerX(), hintY, hintPaint)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -84,7 +84,7 @@
|
|||
{ "id": "slash", "label": "/", "w": 0.9, "action": { "type": "char", "primary": "/" } },
|
||||
{ "id": "dash", "label": "-", "w": 0.9, "action": { "type": "char", "primary": "-" }, "longPress": ["-", "_", "=", "+"] },
|
||||
{ "id": "comma", "label": ",", "w": 1, "action": { "type": "char", "primary": "," }, "longPress": [",", "<", ">", "(", ")"] },
|
||||
{ "id": "space", "label": " ", "w": 2, "action": { "type": "char", "primary": " " }, "longPress": ["\u2699"] },
|
||||
{ "id": "space", "label": " ", "w": 2, "action": { "type": "char", "primary": " " } },
|
||||
{ "id": "dot", "label": ".", "w": 1, "action": { "type": "char", "primary": "." }, "longPress": [".", ":", ";", "!", "?"] },
|
||||
{ "id": "enter", "label": "⏎", "w": 1.8, "action": { "type": "bytes", "value": [13] }, "style": "confirm" }
|
||||
]
|
||||
|
|
@ -165,18 +165,18 @@
|
|||
{ "id": "f2", "label": "F2", "w": 1, "action": { "type": "esc_seq", "seq": "OQ" }, "style": "fn" },
|
||||
{ "id": "f3", "label": "F3", "w": 1, "action": { "type": "esc_seq", "seq": "OR" }, "style": "fn" },
|
||||
{ "id": "f4", "label": "F4", "w": 1, "action": { "type": "esc_seq", "seq": "OS" }, "style": "fn" },
|
||||
{ "id": "f5", "label": "F5", "w": 1, "action": { "type": "esc_seq", "seq": "[15~" }, "style": "fn" },
|
||||
{ "id": "f6", "label": "F6", "w": 1, "action": { "type": "esc_seq", "seq": "[17~" }, "style": "fn" }
|
||||
{ "id": "f5", "label": "F5", "w": 1, "action": { "type": "esc_seq", "seq": "[15~" } },
|
||||
{ "id": "f6", "label": "F6", "w": 1, "action": { "type": "esc_seq", "seq": "[17~" } }
|
||||
]
|
||||
},
|
||||
{
|
||||
"keys": [
|
||||
{ "id": "f7", "label": "F7", "w": 1, "action": { "type": "esc_seq", "seq": "[18~" }, "style": "fn" },
|
||||
{ "id": "f8", "label": "F8", "w": 1, "action": { "type": "esc_seq", "seq": "[19~" }, "style": "fn" },
|
||||
{ "id": "f9", "label": "F9", "w": 1, "action": { "type": "esc_seq", "seq": "[20~" }, "style": "fn" },
|
||||
{ "id": "f10", "label": "F10", "w": 1, "action": { "type": "esc_seq", "seq": "[21~" }, "style": "fn" },
|
||||
{ "id": "f11", "label": "F11", "w": 1, "action": { "type": "esc_seq", "seq": "[23~" }, "style": "fn" },
|
||||
{ "id": "f12", "label": "F12", "w": 1, "action": { "type": "esc_seq", "seq": "[24~" }, "style": "fn" }
|
||||
{ "id": "f7", "label": "F7", "w": 1, "action": { "type": "esc_seq", "seq": "[18~" } },
|
||||
{ "id": "f8", "label": "F8", "w": 1, "action": { "type": "esc_seq", "seq": "[19~" } },
|
||||
{ "id": "f9", "label": "F9", "w": 1, "action": { "type": "esc_seq", "seq": "[20~" } },
|
||||
{ "id": "f10", "label": "F10", "w": 1, "action": { "type": "esc_seq", "seq": "[21~" } },
|
||||
{ "id": "f11", "label": "F11", "w": 1, "action": { "type": "esc_seq", "seq": "[23~" } },
|
||||
{ "id": "f12", "label": "F12", "w": 1, "action": { "type": "esc_seq", "seq": "[24~" } }
|
||||
]
|
||||
},
|
||||
{
|
||||
|
|
@ -201,6 +201,30 @@
|
|||
}
|
||||
],
|
||||
|
||||
"mini": {
|
||||
"widthPercent": 10,
|
||||
"rows": [
|
||||
{ "keys": [
|
||||
{ "id": "m1", "label": "1", "action": { "type": "char", "primary": "1", "shift": "!" } },
|
||||
{ "id": "m2", "label": "2", "action": { "type": "char", "primary": "2", "shift": "@" } },
|
||||
{ "id": "m3", "label": "3", "action": { "type": "char", "primary": "3", "shift": "#" } }
|
||||
]},
|
||||
{ "keys": [
|
||||
{ "id": "m4", "label": "4", "action": { "type": "char", "primary": "4", "shift": "$" } },
|
||||
{ "id": "m5", "label": "5", "action": { "type": "char", "primary": "5", "shift": "%" } },
|
||||
{ "id": "m6", "label": "6", "action": { "type": "char", "primary": "6", "shift": "^" } }
|
||||
]},
|
||||
{ "keys": [
|
||||
{ "id": "m7", "label": "7", "action": { "type": "char", "primary": "7", "shift": "&" } },
|
||||
{ "id": "m8", "label": "8", "action": { "type": "char", "primary": "8", "shift": "*" } },
|
||||
{ "id": "m9", "label": "9", "action": { "type": "char", "primary": "9", "shift": "(" } }
|
||||
]},
|
||||
{ "keys": [
|
||||
{ "id": "m0", "label": "0", "action": { "type": "char", "primary": "0", "shift": ")" } }
|
||||
]}
|
||||
]
|
||||
},
|
||||
|
||||
"quickBar": {
|
||||
"position": "top",
|
||||
"orientation": "horizontal",
|
||||
|
|
|
|||
|
|
@ -85,7 +85,7 @@
|
|||
{ "id": "slash", "label": "/", "w": 0.9, "action": { "type": "char", "primary": "/" } },
|
||||
{ "id": "dash", "label": "-", "w": 0.9, "action": { "type": "char", "primary": "-" }, "longPress": ["-", "_", "=", "+"] },
|
||||
{ "id": "comma", "label": ",", "w": 1, "action": { "type": "char", "primary": "," }, "longPress": [",", "<", ">", "(", ")"] },
|
||||
{ "id": "space", "label": " ", "w": 2, "action": { "type": "char", "primary": " " }, "longPress": ["\u2699"] },
|
||||
{ "id": "space", "label": " ", "w": 2, "action": { "type": "char", "primary": " " } },
|
||||
{ "id": "dot", "label": ".", "w": 1, "action": { "type": "char", "primary": "." }, "longPress": [".", ":", ";", "!", "?"] },
|
||||
{ "id": "enter", "label": "⏎", "w": 1.8, "action": { "type": "bytes", "value": [13] }, "style": "confirm" }
|
||||
]
|
||||
|
|
@ -166,18 +166,18 @@
|
|||
{ "id": "f2", "label": "F2", "w": 1, "action": { "type": "esc_seq", "seq": "OQ" }, "style": "fn" },
|
||||
{ "id": "f3", "label": "F3", "w": 1, "action": { "type": "esc_seq", "seq": "OR" }, "style": "fn" },
|
||||
{ "id": "f4", "label": "F4", "w": 1, "action": { "type": "esc_seq", "seq": "OS" }, "style": "fn" },
|
||||
{ "id": "f5", "label": "F5", "w": 1, "action": { "type": "esc_seq", "seq": "[15~" }, "style": "fn" },
|
||||
{ "id": "f6", "label": "F6", "w": 1, "action": { "type": "esc_seq", "seq": "[17~" }, "style": "fn" }
|
||||
{ "id": "f5", "label": "F5", "w": 1, "action": { "type": "esc_seq", "seq": "[15~" } },
|
||||
{ "id": "f6", "label": "F6", "w": 1, "action": { "type": "esc_seq", "seq": "[17~" } }
|
||||
]
|
||||
},
|
||||
{
|
||||
"keys": [
|
||||
{ "id": "f7", "label": "F7", "w": 1, "action": { "type": "esc_seq", "seq": "[18~" }, "style": "fn" },
|
||||
{ "id": "f8", "label": "F8", "w": 1, "action": { "type": "esc_seq", "seq": "[19~" }, "style": "fn" },
|
||||
{ "id": "f9", "label": "F9", "w": 1, "action": { "type": "esc_seq", "seq": "[20~" }, "style": "fn" },
|
||||
{ "id": "f10", "label": "F10", "w": 1, "action": { "type": "esc_seq", "seq": "[21~" }, "style": "fn" },
|
||||
{ "id": "f11", "label": "F11", "w": 1, "action": { "type": "esc_seq", "seq": "[23~" }, "style": "fn" },
|
||||
{ "id": "f12", "label": "F12", "w": 1, "action": { "type": "esc_seq", "seq": "[24~" }, "style": "fn" }
|
||||
{ "id": "f7", "label": "F7", "w": 1, "action": { "type": "esc_seq", "seq": "[18~" } },
|
||||
{ "id": "f8", "label": "F8", "w": 1, "action": { "type": "esc_seq", "seq": "[19~" } },
|
||||
{ "id": "f9", "label": "F9", "w": 1, "action": { "type": "esc_seq", "seq": "[20~" } },
|
||||
{ "id": "f10", "label": "F10", "w": 1, "action": { "type": "esc_seq", "seq": "[21~" } },
|
||||
{ "id": "f11", "label": "F11", "w": 1, "action": { "type": "esc_seq", "seq": "[23~" } },
|
||||
{ "id": "f12", "label": "F12", "w": 1, "action": { "type": "esc_seq", "seq": "[24~" } }
|
||||
]
|
||||
},
|
||||
{
|
||||
|
|
@ -202,6 +202,30 @@
|
|||
}
|
||||
],
|
||||
|
||||
"mini": {
|
||||
"widthPercent": 10,
|
||||
"rows": [
|
||||
{ "keys": [
|
||||
{ "id": "m1", "label": "1", "action": { "type": "char", "primary": "1", "shift": "!" } },
|
||||
{ "id": "m2", "label": "2", "action": { "type": "char", "primary": "2", "shift": "@" } },
|
||||
{ "id": "m3", "label": "3", "action": { "type": "char", "primary": "3", "shift": "#" } }
|
||||
]},
|
||||
{ "keys": [
|
||||
{ "id": "m4", "label": "4", "action": { "type": "char", "primary": "4", "shift": "$" } },
|
||||
{ "id": "m5", "label": "5", "action": { "type": "char", "primary": "5", "shift": "%" } },
|
||||
{ "id": "m6", "label": "6", "action": { "type": "char", "primary": "6", "shift": "^" } }
|
||||
]},
|
||||
{ "keys": [
|
||||
{ "id": "m7", "label": "7", "action": { "type": "char", "primary": "7", "shift": "&" } },
|
||||
{ "id": "m8", "label": "8", "action": { "type": "char", "primary": "8", "shift": "*" } },
|
||||
{ "id": "m9", "label": "9", "action": { "type": "char", "primary": "9", "shift": "(" } }
|
||||
]},
|
||||
{ "keys": [
|
||||
{ "id": "m0", "label": "0", "action": { "type": "char", "primary": "0", "shift": ")" } }
|
||||
]}
|
||||
]
|
||||
},
|
||||
|
||||
"quickBar": {
|
||||
"position": "top",
|
||||
"orientation": "horizontal",
|
||||
|
|
|
|||
|
|
@ -87,7 +87,7 @@
|
|||
{ "id": "slash", "label": "/", "w": 0.9, "action": { "type": "char", "primary": "/" } },
|
||||
{ "id": "dash", "label": "-", "w": 0.9, "action": { "type": "char", "primary": "-" }, "longPress": ["-", "_", "=", "+"] },
|
||||
{ "id": "comma", "label": ",", "w": 1, "action": { "type": "char", "primary": "," }, "longPress": [",", "<", ">", "(", ")"] },
|
||||
{ "id": "space", "label": " ", "w": 2, "action": { "type": "char", "primary": " " }, "longPress": ["\u2699"] },
|
||||
{ "id": "space", "label": " ", "w": 2, "action": { "type": "char", "primary": " " } },
|
||||
{ "id": "dot", "label": ".", "w": 1, "action": { "type": "char", "primary": "." }, "longPress": [".", ":", ";", "!", "?"] },
|
||||
{ "id": "enter", "label": "⏎", "w": 1.8, "action": { "type": "bytes", "value": [13] }, "style": "confirm" }
|
||||
]
|
||||
|
|
@ -168,18 +168,18 @@
|
|||
{ "id": "f2", "label": "F2", "w": 1, "action": { "type": "esc_seq", "seq": "OQ" }, "style": "fn" },
|
||||
{ "id": "f3", "label": "F3", "w": 1, "action": { "type": "esc_seq", "seq": "OR" }, "style": "fn" },
|
||||
{ "id": "f4", "label": "F4", "w": 1, "action": { "type": "esc_seq", "seq": "OS" }, "style": "fn" },
|
||||
{ "id": "f5", "label": "F5", "w": 1, "action": { "type": "esc_seq", "seq": "[15~" }, "style": "fn" },
|
||||
{ "id": "f6", "label": "F6", "w": 1, "action": { "type": "esc_seq", "seq": "[17~" }, "style": "fn" }
|
||||
{ "id": "f5", "label": "F5", "w": 1, "action": { "type": "esc_seq", "seq": "[15~" } },
|
||||
{ "id": "f6", "label": "F6", "w": 1, "action": { "type": "esc_seq", "seq": "[17~" } }
|
||||
]
|
||||
},
|
||||
{
|
||||
"keys": [
|
||||
{ "id": "f7", "label": "F7", "w": 1, "action": { "type": "esc_seq", "seq": "[18~" }, "style": "fn" },
|
||||
{ "id": "f8", "label": "F8", "w": 1, "action": { "type": "esc_seq", "seq": "[19~" }, "style": "fn" },
|
||||
{ "id": "f9", "label": "F9", "w": 1, "action": { "type": "esc_seq", "seq": "[20~" }, "style": "fn" },
|
||||
{ "id": "f10", "label": "F10", "w": 1, "action": { "type": "esc_seq", "seq": "[21~" }, "style": "fn" },
|
||||
{ "id": "f11", "label": "F11", "w": 1, "action": { "type": "esc_seq", "seq": "[23~" }, "style": "fn" },
|
||||
{ "id": "f12", "label": "F12", "w": 1, "action": { "type": "esc_seq", "seq": "[24~" }, "style": "fn" }
|
||||
{ "id": "f7", "label": "F7", "w": 1, "action": { "type": "esc_seq", "seq": "[18~" } },
|
||||
{ "id": "f8", "label": "F8", "w": 1, "action": { "type": "esc_seq", "seq": "[19~" } },
|
||||
{ "id": "f9", "label": "F9", "w": 1, "action": { "type": "esc_seq", "seq": "[20~" } },
|
||||
{ "id": "f10", "label": "F10", "w": 1, "action": { "type": "esc_seq", "seq": "[21~" } },
|
||||
{ "id": "f11", "label": "F11", "w": 1, "action": { "type": "esc_seq", "seq": "[23~" } },
|
||||
{ "id": "f12", "label": "F12", "w": 1, "action": { "type": "esc_seq", "seq": "[24~" } }
|
||||
]
|
||||
},
|
||||
{
|
||||
|
|
@ -204,6 +204,30 @@
|
|||
}
|
||||
],
|
||||
|
||||
"mini": {
|
||||
"widthPercent": 10,
|
||||
"rows": [
|
||||
{ "keys": [
|
||||
{ "id": "m1", "label": "1", "action": { "type": "char", "primary": "1", "shift": "!" } },
|
||||
{ "id": "m2", "label": "2", "action": { "type": "char", "primary": "2", "shift": "@" } },
|
||||
{ "id": "m3", "label": "3", "action": { "type": "char", "primary": "3", "shift": "#" } }
|
||||
]},
|
||||
{ "keys": [
|
||||
{ "id": "m4", "label": "4", "action": { "type": "char", "primary": "4", "shift": "$" } },
|
||||
{ "id": "m5", "label": "5", "action": { "type": "char", "primary": "5", "shift": "%" } },
|
||||
{ "id": "m6", "label": "6", "action": { "type": "char", "primary": "6", "shift": "^" } }
|
||||
]},
|
||||
{ "keys": [
|
||||
{ "id": "m7", "label": "7", "action": { "type": "char", "primary": "7", "shift": "&" } },
|
||||
{ "id": "m8", "label": "8", "action": { "type": "char", "primary": "8", "shift": "*" } },
|
||||
{ "id": "m9", "label": "9", "action": { "type": "char", "primary": "9", "shift": "(" } }
|
||||
]},
|
||||
{ "keys": [
|
||||
{ "id": "m0", "label": "0", "action": { "type": "char", "primary": "0", "shift": ")" } }
|
||||
]}
|
||||
]
|
||||
},
|
||||
|
||||
"quickBar": {
|
||||
"position": "top",
|
||||
"orientation": "horizontal",
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue