Skip to content

Add minimize to system tray#789

Open
SubliminalsTV wants to merge 1 commit into
WhiteMagic:developfrom
SubliminalsTV:feature/minimize-to-tray
Open

Add minimize to system tray#789
SubliminalsTV wants to merge 1 commit into
WhiteMagic:developfrom
SubliminalsTV:feature/minimize-to-tray

Conversation

@SubliminalsTV

Copy link
Copy Markdown
Contributor

Restores the pre-R14 minimize-to-tray behaviour as an opt-in option.

What

  • New minimize-to-tray option (off by default). When enabled, minimizing the main window hides it to a tray icon instead of the taskbar; clicking the icon restores and focuses it.
  • Tray icon created only when QSystemTrayIcon.isSystemTrayAvailable().
  • Context menu offers profile activate/deactivate (label + icon reflect the live state via Backend.activityChanged) and quit — quit routes through the window's normal close path so the unsaved-changes prompt still fires.

Notes

  • Self-contained in gremlin/ui/tray.py; joystick_gremlin.py only registers the option and creates the icon. No behaviour change when the option is off.
  • The tray icon is always present (when a tray exists) so activate/quit stay reachable while minimized — the option only governs hide-on-minimize. Happy to gate the icon's presence on the option instead if you'd prefer.
  • Reuses existing gfx/icon*.ico assets and Backend signals; nothing added to signal.

@WhiteMagic

Copy link
Copy Markdown
Owner

The issue with all the systray solutions LLM's come up with is that they don't understand Gremlin's constraints well. As a result they always want to use QtWidget's based solutions which will work right now, but once the input viewer is reworked nothing in Gremlin requires QtWidgets and they will be dropped. As such adding new components that requires that won't work. There is a QML system tray icon but that falls back onto QtWidgets, making it not suitable. This effectively leaves a solution that directly uses Window's systray interface via pywin32 which Gremlin requires as a dependency anyway.

Restores the pre-R14 minimize-to-tray behaviour as an opt-in option. When
'minimize-to-tray' is enabled, minimizing the window hides it to a tray
icon instead of the taskbar; clicking the icon restores it. The tray menu
offers profile activate/deactivate and quit (via the window's close path
so unsaved changes are still flagged), and the icon reflects the active
state.

Implemented directly against the Win32 Shell_NotifyIcon API via pywin32
(already a Gremlin dependency) rather than QtWidgets' QSystemTrayIcon, so
it adds no QtWidgets dependency. Window show/hide goes through the QML
QWindow (QtGui). Self-contained in gremlin/ui/tray.py plus the option
registration and tray creation in joystick_gremlin.py.
@SubliminalsTV SubliminalsTV force-pushed the feature/minimize-to-tray branch from 76e91ef to d508e36 Compare June 24, 2026 10:11
@SubliminalsTV

Copy link
Copy Markdown
Contributor Author

Reworked to use the Win32 Shell_NotifyIcon API directly via pywin32 (already a Gremlin dependency) instead of QtWidgets — QSystemTrayIcon and the QML tray both pull in QtWidgets, which this avoids entirely.

A hidden helper window handles the icon's mouse/menu messages (its wndproc is driven by Qt's running event loop), and window show/hide still goes through the QML QWindow (QtGui). Verified live: the icon appears, minimize-to-tray hides the window, left-click restores it, and the right-click menu (Show / Activate-Deactivate / Quit) works with the icon reflecting the active state.

@WhiteMagic

WhiteMagic commented Jun 29, 2026

Copy link
Copy Markdown
Owner

Ran it through my usual merge steps, the LLM comes up with these things it thinks may be troublesome:

0. remove() double-deletes the tray icon, and _on_destroy is a confused fallback

remove() calls NIM_DELETE, then DestroyWindow. DestroyWindow sends WM_DESTROY synchronously, which dispatches to _on_destroy, which calls NIM_DELETE again. The second call silently fails (the icon is already gone), but the two methods now share responsibility for the same side-effect. The intent of _on_destroy is clearly a defensive fallback for abnormal window destruction — it should be the only place NIM_DELETE lives, and remove() should just call DestroyWindow.

1. Upgrade to NOTIFYICON_VERSION_4

The current code uses the pre-Vista notification protocol. Calling NIM_SETVERSION after NIM_ADD unlocks: Cursor position packed into wparam (eliminates the GetCursorPos call) NIN_SELECT for keyboard-driven activation (accessibility)
NIN_KEYMAPMENU for keyboard-driven context menu Better multi-monitor menu positioning The catch: with version 4, lparam is (icon_id << 16) | notification_event, so _on_tray_event must extract lparam & 0xFFFF instead of comparing lparam directly — the current code would silently break if you added the version call without that fix.

2. Make the helper window invisible to the shell

The current CreateWindow uses WS_OVERLAPPED (= 0) at zero size, which works but relies on the window being invisible by accident. A zero-size overlapped window can briefly flicker in the taskbar on some DWM configurations. Use reateWindowEx with WS_EX_TOOLWINDOW | WS_EX_NOACTIVATE to explicitly exclude it from the taskbar and Alt+Tab.

3. Add NIF_SHOWTIP to the notification data

NIF_SHOWTIP (added in Vista) opts into the modern rich tooltip and ensures the tip text shows even when the user has balloon tips suppressed by group policy.

Probably giving these files as context to an LLM will make it understand these inputs:

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants