From 4af8637c8e4aedf2d170970ee7a80ba46c76a4b7 Mon Sep 17 00:00:00 2001 From: James Rich Date: Fri, 10 Jul 2026 14:20:05 -0500 Subject: [PATCH 1/2] fix(desktop): align tray, shortcuts, and macOS chrome with current CMP desktop guidance Audit of the desktop shell against JetBrains' official Compose Multiplatform desktop docs (CMP 1.11.1, our pin) surfaced three gaps: - Hide-to-tray dropped the Window from composition, destroying the navigation backstack and scroll positions; use the documented Window(visible=) pattern so the UI tree survives a tray round trip - Keyboard shortcuts checked only the meta key, leaving them dead on the Windows/Linux distributions; use the platform-conventional modifier (Cmd on macOS, Ctrl elsewhere) - Add -Dapple.awt.application.appearance=system so the macOS title bar follows the system dark theme Co-Authored-By: Claude Fable 5 --- desktopApp/build.gradle.kts | 2 ++ .../kotlin/org/meshtastic/desktop/Main.kt | 20 +++++++++++++++---- 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/desktopApp/build.gradle.kts b/desktopApp/build.gradle.kts index b3f142482c..6cd94f2d49 100644 --- a/desktopApp/build.gradle.kts +++ b/desktopApp/build.gradle.kts @@ -112,6 +112,8 @@ compose.desktop { val desktopJvmArgs = listOf( "-Xmx2G", + // Let the macOS title bar follow the system light/dark theme (ignored on other OSes). + "-Dapple.awt.application.appearance=system", "-Dapple.awt.application.name=Meshtastic Desktop", "-Dcom.apple.mrj.application.apple.menu.about.name=Meshtastic Desktop", "-Dcom.apple.bundle.identifier=org.meshtastic.MeshtasticDesktop", diff --git a/desktopApp/src/main/kotlin/org/meshtastic/desktop/Main.kt b/desktopApp/src/main/kotlin/org/meshtastic/desktop/Main.kt index 3ff4f111c0..63b8f8025e 100644 --- a/desktopApp/src/main/kotlin/org/meshtastic/desktop/Main.kt +++ b/desktopApp/src/main/kotlin/org/meshtastic/desktop/Main.kt @@ -31,6 +31,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.input.key.Key import androidx.compose.ui.input.key.KeyEventType +import androidx.compose.ui.input.key.isCtrlPressed import androidx.compose.ui.input.key.isMetaPressed import androidx.compose.ui.input.key.key import androidx.compose.ui.input.key.type @@ -86,6 +87,7 @@ import org.meshtastic.core.ui.viewmodel.UIViewModel import org.meshtastic.desktop.data.DesktopPreferencesDataSource import org.meshtastic.desktop.di.desktopModule import org.meshtastic.desktop.di.desktopPlatformModule +import org.meshtastic.desktop.notification.DesktopOS import org.meshtastic.desktop.ui.DesktopMainScreen import java.awt.Desktop import java.util.Locale @@ -232,8 +234,10 @@ private fun ApplicationScope.MeshtasticDesktopApp(uiViewModel: UIViewModel, isDa }, ) - if (isWindowReady && isAppVisible) { - MeshtasticWindow(uiViewModel, isDarkTheme, appIcon, windowState) { + if (isWindowReady) { + // Hide via `visible` rather than dropping the Window from composition so the UI tree + // (navigation backstack, scroll positions) survives a hide-to-tray round trip. + MeshtasticWindow(uiViewModel, isDarkTheme, appIcon, windowState, visible = isAppVisible) { // Minimize to the tray on close — but only where a tray exists. On platforms without a // system tray (e.g. some Linux desktop environments) there's nowhere to minimize to, so // quit instead; otherwise the process would be stranded with no window and no tray icon. @@ -293,6 +297,7 @@ private fun ApplicationScope.MeshtasticWindow( isDarkTheme: Boolean, appIcon: Painter, windowState: WindowState, + visible: Boolean, onCloseRequest: () -> Unit, ) { val multiBackstack = @@ -310,6 +315,7 @@ private fun ApplicationScope.MeshtasticWindow( title = "Meshtastic Desktop", icon = appIcon, state = windowState, + visible = visible, onPreviewKeyEvent = { event -> handleKeyboardShortcut(event, multiBackstack, ::exitApplication) }, ) { val eventEdition by uiViewModel.eventEdition.collectAsState() @@ -352,13 +358,19 @@ private fun CoilImageLoaderSetup() { // ----- Keyboard shortcuts ----- -/** Handles Cmd-key shortcuts. Returns `true` if the event was consumed. */ +private val isMacOS = DesktopOS.current() == DesktopOS.MacOS + +/** Platform-conventional shortcut modifier: Cmd on macOS, Ctrl on Windows/Linux. */ +private val androidx.compose.ui.input.key.KeyEvent.isShortcutModifierPressed: Boolean + get() = if (isMacOS) isMetaPressed else isCtrlPressed + +/** Handles Cmd/Ctrl-key shortcuts. Returns `true` if the event was consumed. */ private fun handleKeyboardShortcut( event: androidx.compose.ui.input.key.KeyEvent, multiBackstack: MultiBackstack, exitApplication: () -> Unit, ): Boolean { - if (event.type != KeyEventType.KeyDown || !event.isMetaPressed) return false + if (event.type != KeyEventType.KeyDown || !event.isShortcutModifierPressed) return false val backStack = multiBackstack.activeBackStack return when (event.key) { Key.Q -> { From d9f87f3516aec7bead968a9e3af92ef751d8ca1e Mon Sep 17 00:00:00 2001 From: James Rich Date: Fri, 10 Jul 2026 14:38:24 -0500 Subject: [PATCH 2/2] fix(desktop): exclude AltGr from Ctrl shortcuts, import KeyEvent MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Windows reports AltGr as Ctrl+Alt, so typing AltGr characters (e.g. @ on German layouts = AltGr+Q) would have fired shortcuts — including quit. Require Alt up on the Ctrl path. Also import KeyEvent instead of repeating the FQN (no clash exists in this file). Co-Authored-By: Claude Fable 5 --- .../src/main/kotlin/org/meshtastic/desktop/Main.kt | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/desktopApp/src/main/kotlin/org/meshtastic/desktop/Main.kt b/desktopApp/src/main/kotlin/org/meshtastic/desktop/Main.kt index 63b8f8025e..f71126606a 100644 --- a/desktopApp/src/main/kotlin/org/meshtastic/desktop/Main.kt +++ b/desktopApp/src/main/kotlin/org/meshtastic/desktop/Main.kt @@ -30,7 +30,9 @@ import androidx.compose.runtime.snapshotFlow import androidx.compose.ui.Alignment import androidx.compose.ui.graphics.painter.Painter import androidx.compose.ui.input.key.Key +import androidx.compose.ui.input.key.KeyEvent import androidx.compose.ui.input.key.KeyEventType +import androidx.compose.ui.input.key.isAltPressed import androidx.compose.ui.input.key.isCtrlPressed import androidx.compose.ui.input.key.isMetaPressed import androidx.compose.ui.input.key.key @@ -360,13 +362,17 @@ private fun CoilImageLoaderSetup() { private val isMacOS = DesktopOS.current() == DesktopOS.MacOS -/** Platform-conventional shortcut modifier: Cmd on macOS, Ctrl on Windows/Linux. */ -private val androidx.compose.ui.input.key.KeyEvent.isShortcutModifierPressed: Boolean - get() = if (isMacOS) isMetaPressed else isCtrlPressed +/** + * Platform-conventional shortcut modifier: Cmd on macOS, Ctrl on Windows/Linux. Alt must be up on the Ctrl path because + * Windows reports AltGr as Ctrl+Alt — otherwise typing AltGr characters (e.g. @ on German layouts, which is AltGr+Q) + * would trigger shortcuts like quit. + */ +private val KeyEvent.isShortcutModifierPressed: Boolean + get() = if (isMacOS) isMetaPressed else isCtrlPressed && !isAltPressed /** Handles Cmd/Ctrl-key shortcuts. Returns `true` if the event was consumed. */ private fun handleKeyboardShortcut( - event: androidx.compose.ui.input.key.KeyEvent, + event: KeyEvent, multiBackstack: MultiBackstack, exitApplication: () -> Unit, ): Boolean {