Skip to content

Desktop tray functionality and controls#538

Open
epicleafies wants to merge 9 commits intobitcoin-core:qt6from
epicleafies:desktop-tray-with-settings
Open

Desktop tray functionality and controls#538
epicleafies wants to merge 9 commits intobitcoin-core:qt6from
epicleafies:desktop-tray-with-settings

Conversation

@epicleafies
Copy link
Copy Markdown
Contributor

@epicleafies epicleafies commented Mar 18, 2026

Added window behavior as a new option in settings. #513
Added window behavior page, allows the user to configure if the tray is visible, behavior on minimization and behavior on closing.
Tray icon matches dark and light mode in the app.
Tray icon gives option to show or quit when clicked on.
Added unit, qml and functional tests.

Screenshot from 2026-03-25 15-35-28 Screenshot from 2026-03-25 15-35-39

@epicleafies
Copy link
Copy Markdown
Contributor Author

Needs testing on Windows and Mac. Some of the behavior is OS dependent.

@hebasto
Copy link
Copy Markdown
Member

hebasto commented Mar 23, 2026

Please rebase.

@epicleafies epicleafies force-pushed the desktop-tray-with-settings branch from 342e670 to 3629563 Compare March 25, 2026 19:37
Copy link
Copy Markdown
Collaborator

@johnny9 johnny9 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like the settings options but lets try to use the QML SystemTrayIcon instead of implementing the platform-specific DBus notification apis. These will only work on linux platforms.

One problem with these tray icons is that Qt doesn't support the native versions on all platforms and this means we'll need to link Widgets in order to implement this on platforms that don't have native support which I think includes Windows

QSystemTrayIcon requires QApplication (not QGuiApplication), so switch
the main binary and the unit test runner to QApplication and add
Qt6::Widgets to the build. On Linux, Qt may route tray icon ownership
through the DBus StatusNotifierItem protocol, so add a WITH_DBUS option
(on by default for non-Windows/macOS/Android targets) that conditionally
finds and links Qt6::DBus. Wire in the two new unit test translation
units for the tray models so the test binary picks them up.
Exposes three boolean settings — showTrayIcon, minimizeToTray, and
minimizeOnClose — persisted via QSettings using the legacy Bitcoin Qt
keys (fHideTrayIcon, fMinimizeToTray, fMinimizeOnClose) for settings
file compatibility. On non-desktop platforms all three are forced to
false and ignore writes.

Cascade logic ensures that disabling showTrayIcon also clears the
dependent minimizeToTray and minimizeOnClose so the UI and persisted
state stay consistent. Two composite helpers,
shouldHideToTrayOnMinimize() and shouldMinimizeWindowOnClose(), provide
the combined conditions needed by main.qml.

A second constructor accepting explicit default values enables
deterministic unit testing without touching the on-disk QSettings.
Wraps QSystemTrayIcon with the extra machinery needed for reliable
cross-platform operation:

- Show is deferred to the first event-loop iteration so DBus/SNI is
  ready on Linux before we call QSystemTrayIcon::show().
- Up to four retries with increasing delays (100–400 ms) handle slow
  SNI host initialization on some compositors.
- A native QMenu with Show/Quit actions is installed before the first
  show() to satisfy AppIndicator/SNI ownership requirements.
- On Linux an XDG-compliant icon theme is written to a temporary
  directory so the system can resolve the icon by name, avoiding
  rendering artefacts from direct pixmap injection.
- Dark-mode support: on non-macOS platforms updateIcon() inverts pixel
  luminance when isDark is true; on macOS setIsMask(true) is used so
  the system composites the icon correctly in both modes.

Signals restoreRequested, quitRequested, and contextMenuRequested
decouple tray-event handling from the controller itself so QML can
connect freely.
The existing Setting component had no way to render a visually disabled
row while keeping it present in the layout — setting Qt's enabled:false
also suppresses the item entirely from focus and hit-testing in ways
that break parent layouts.

Add a disabled property (distinct from enabled) with a matching
DISABLED state that renders the header and description in a muted,
theme-aware color. Mouse-area handlers are guarded on !root.disabled
so hover/active states cannot fire while the row is disabled. A
stateDescriptionColor property is threaded through to the inner
SettingItem so the description text follows the same color transitions.
Adds a new settings page reachable from Node Settings → Window Behavior
(desktop only) with three toggles backed by desktopWindowBehaviorModel:

- Show tray icon: always enabled on desktop
- Minimize to tray: enabled only when show tray icon is on
- Minimize on close: enabled only when show tray icon is on

The two dependent switches use the new Setting.disabled property to
appear visually greyed out while showTrayIcon is off. A QML unit test
verifies the enabled/disabled transitions for each switch based on
platform and model state.
Instantiate DesktopWindowBehaviorModel and DesktopTrayIconController in
QmlGuiMain and expose them to QML via rootContext. The tray icon is
shown on startup only in desktop app-mode when showTrayIcon is
persisted as true; if the system tray turns out to be unsupported after
all retries, supportedChanged feeds back into the model so the setting
is cleared rather than left in an inconsistent state.

Switch the entry point from QGuiApplication to QApplication, required
by QSystemTrayIcon. Remove duplicate common/system.h and common/args.h
includes that were present in bitcoin.cpp. Honor -resetguisettings by
clearing the three window-behavior QSettings keys before the models
read their initial values.

In main.qml:

- onClosing either hides the window (if minimizeOnClose is set) or
  requests a node shutdown, replacing the previous unconditional quit.
- onVisibilityChanged tracks the previous visibility in m_prevVisibility
  to distinguish a genuine user minimize from the Hidden→Minimized
  transition that showNormal() emits internally; without this guard the
  window would immediately re-hide after a tray-icon restore.
- Connections to desktopTrayIconController handle restore, quit, and
  context-menu requests from the tray.
- A Menu provides Show/Hide and Quit items for the tray context menu.
- A Binding keeps the controller's isDark in sync with Theme.dark so
  the tray icon recolors automatically when the user switches themes.
Add qml_test_tray.py, an end-to-end functional test that navigates to
Settings → Window Behavior, asserts the three switches are present and
have the expected default states, exercises toggle round-trips, and
verifies that back navigation and re-opening the page work correctly.
Wire it into the GitHub Actions functional-tests job.
@epicleafies epicleafies force-pushed the desktop-tray-with-settings branch from 3629563 to 25accd4 Compare April 2, 2026 20:03
The original implementation included Linux-specific code paths to work
around StatusNotifierItem/AppIndicator behaviour: deferred show() with
exponential-backoff retries, XDG icon theme file management, and a
separate setIcon() entry point that wrote pixmaps into
~/.local/share/icons/hicolor/. These workarounds are platform-specific
and do not belong in a cross-platform tray implementation.

Replace with a straightforward QSystemTrayIcon wrapper: show()/hide()
are called synchronously, icon theming is handled through updateIcon()
as before (with macOS mask support), and the context menu is registered
without AppIndicator-specific ordering constraints.

Update unit tests to cover the simplified interface using googletest
(matching the project's current test framework) instead of QtTest, and
drop the Linux-only XDG icon theme tests that tested the removed code.
The WITH_DBUS option was introduced to link Qt6::DBus on Linux for the
StatusNotifierItem tray workarounds, which have now been removed from
DesktopTrayIconController. Qt's platform plugin handles any necessary
D-Bus communication internally; no explicit DBus linkage is needed.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants