Skip to content

System-tray handling overhaul (closes #334) - #574

Merged
mrjimenez merged 7 commits into
amule-project:masterfrom
got3nks:fix/close-to-tray-clean
May 11, 2026
Merged

System-tray handling overhaul (closes #334)#574
mrjimenez merged 7 commits into
amule-project:masterfrom
got3nks:fix/close-to-tray-clean

Conversation

@got3nks

@got3nks got3nks commented May 10, 2026

Copy link
Copy Markdown
Contributor

This PR is a comprehensive overhaul of aMule's system-tray handling across macOS, Windows and Linux. What started as the reporter's specific ask in #334 ("Close to tray for the close button") grew into a sweep of long-standing gaps and inconsistencies in the tray flow on every platform — close-button behaviour, Quit-menu interception, Dock-icon handling, minimize-to-tray, splitter side-effects, Linux-specific compositor limitations.

Closes #334.

The original ask: aMule had the close-to-tray logic already (CamuleDlg::OnClose checks thePrefs::HideOnClose()) but the preference UI was gated to macOS only, so Linux/Windows users couldn't opt in. Exposing the checkbox on every platform turned out to be just step one — the surrounding plumbing the feature actually needs to be useful in practice (tray-icon gate, Quit-menu interception, iconize state tracking, platform-specific Dock/Wayland behaviours) was the bulk of the work.

Commits

  1. PrefsUnifiedDlg: expose 'Hide on close' option cross-platform, gated on tray-icon — drop the #ifdef __WXMAC__ that hid the checkbox on Linux/Windows and force-cleared the runtime pref. To keep the option safe, gate it on the tray-icon master switch being on (the tray is the only recovery surface when the window is hidden via the close button). Gate updates live when the user toggles the tray-icon master.

  2. amuleDlg/MuleTrayIcon: drop Dock icon while hidden via tray (macOS) — when HideOnClose triggers on macOS, plain Show(false) leaves a Dock thumbnail and Iconize(false) re-shows the window via the deminiaturize animation. Toggle NSApp.activationPolicy to NSApplicationActivationPolicyAccessory instead — that drops the Dock icon entirely, leaving the NSStatusItem as the only surface. Restored to Regular + activateIgnoringOtherApps: when un-hiding. Small Obj-C++ helper (MacAppHelper.{h,mm}); AppKit comes in transitively via wxWidgets so no extra -framework.

  3. HideOnClose: only intercept the close button, decouple confirm-exit — HideOnClose previously consumed every path that reached OnClose with a vetoable event, including the tray Exit menu, Cmd+Q, and the macOS Dock right-click → Quit. Three signals now bypass the hide-on-close branch: Close(true) (Cmd+Q via wxID_EXIT), an IsQuitting() flag set by CMuleTrayIcon::DoExit for the tray Exit, and the same flag set from CamuleGuiApp::OnQueryEndSession for the Dock Quit on macOS. The flag lives on CamuleAppCommon so both CamuleApp (monolithic) and CamuleRemoteGuiApp (amulegui) expose the same accessors. Same commit decouples the "Show confirmation on exit" pref from HideOnClose — they're orthogonal now that the clean exit paths can still ask for confirmation through the OnClose veto.

  4. Tray-icon UX polish: iconize-aware menu, single-left toggle, robust ShowGUIIsShown() is true even when the window is iconized to Dock/taskbar, so the menu offered "Hide aMule" in those states and clicking would Show(false) the already-hidden window, destroying the only recovery surface. Track iconize separately from wxIconizeEvent::IsIconized() (reliable cross-platform — unlike wxFrame::IsIconized()), require IsShown() && !IsTrayLogicallyIconized() for the menu label and the DoShowHide branch. Also: Iconize(false) before Show(true) on every platform when un-hiding via the tray (without it a previously-iconized window comes back as a taskbar-iconized entry); single-left tray-icon click toggles the window on Windows (matches Discord/Slack/Telegram/qBittorrent); SNI tray menu refresh on every show/hide via EVT_SHOW; GuiEvents::ShowGUI now restores from every hidden state (Show(true) + Iconize(false) + Raise() + Mac accessory-mode restore) so the duplicate-launch RAISE_DIALOG signal actually brings the window back when the user clicks a pinned taskbar shortcut.

  5. ServerWnd: anchor sash to top, persist only on user drag — orthogonal fix for a separate bug surfaced while testing HideOnClose. The Network tab's splitter had SetSashGravity(0.5f) set, which made it the only splitter in aMule that proportionally re-laid-out children on parent resize. During the minimize/restore reflow on Mac/Windows that proportional recalculation fired with transient parent dimensions and produced wxEVT_SPLITTER_SASH_POS_CHANGED events with positions outside the visible range, stomping the saved sash with garbage that collapsed the log pane the next time the window came back. Drop gravity (other splitters use the default 0.0); also add a CHANGING/CHANGED protocol so only real user drags persist the sash position.

  6. amule/PrefsUnifiedDlg: gate Linux-specific tray-icon limitations — two ways the tray UX can silently fail on Linux, both gated at startup + in the prefs panel so users don't enable an option that does nothing.

    • No libayatana-appindicator3 at compile time — wxTaskBarIcon falls back to the legacy GtkStatusIcon backend that GNOME dropped in 3.26; force UseTrayIcon=false, grey the checkbox, tooltip points at the rebuild dep.
    • Wayland session — force MinToTray=false, grey the checkbox, tooltip cites the protocol limitation and points at GDK_BACKEND=x11 as a workaround. SNI tray menu also shows two deterministic entries ("Show aMule" / "Hide aMule") on Wayland instead of the single label-flipping entry, since we can't reliably know which state the window is actually in. Detection via WAYLAND_DISPLAY / XDG_SESSION_TYPE env vars; honours GDK_BACKEND=x11 as an explicit X11 opt-out (the documented XWayland workaround).
  7. muuli_wdr: place 'Enable Tray Icon' master before its dependents — pure layout shuffle in the prefs panel so the master switch shows above the options that gate-depend on it.

Verification

Tested end-to-end on:

  • macOS (wxOSX Cocoa 3.3.2) — close button hides + tray restores, Cmd+Q quits, Dock right-click Quit quits (via OnQueryEndSession hook), tray Exit quits, confirm-exit prompt fires on all quit paths, yellow minimize button still goes to Dock (intentional Mac UX).

  • Windows 11 ARM64 (CLANGARM64) — close button hides + tray icon click restores, minimize button hides to tray, single-left tray click toggles, tray Exit quits cleanly.

  • Ubuntu 26.04 GNOME on Wayland (wxGTK3 3.2.9 with libayatana-appindicator3) — close button hides + tray Show restores, MinToTray correctly disabled with tooltip explaining why, deterministic two-entry tray menu avoids the iconize-detection ambiguity, prefs panel reorder makes the master/dependent relationship visible at a glance.

Known limitations (Wayland)

Two limitations on Wayland sessions, both due to xdg-shell protocol gaps we cannot work around from inside an app. Both are documented inline in the prefs tooltip and consistent with how every other Linux tray-using app (qBittorrent #17265, Telegram #2123, KeePassXC #6502, Slack, Element, Spotify) lives with them.

  • System minimize button cannot trigger tray-hide. xdg-shell intentionally doesn't deliver iconified-state notifications to clients (GTK maintainers confirmed no signal exists; xdg_toplevel.suspended in wayland-protocols MR !201 is neither universally supported nor a clean minimize proxy). So the OS minimize button goes to the Dock as a normal native minimize. The HideOnClose path (X button) still works reliably as the tray-hide gesture. Workaround for users who want minimize-to-tray on Wayland: launch with GDK_BACKEND=x11 amule to force XWayland — the same workaround Discord users adopt — and our detection honours that env var.

  • Dock right-click → Quit respects HideOnClose (i.e., hides instead of quitting when HideOnClose is on). Dock-Quit on Linux sends the same xdg_toplevel.close / WM_DELETE_WINDOW as the X button — they're indistinguishable from inside OnClose. macOS doesn't have this problem because Dock-Quit there fires wxEVT_QUERY_END_SESSION separately, which we hook. For unconditional quit on Linux, users have two working paths: the tray-icon Exit menu, and the Ctrl+Q accelerator (wired to wxID_EXITClose(true)).

got3nks added 7 commits May 10, 2026 22:34
…on tray-icon (amule-project#334)

The "Hide application window when close button is pressed" preference
was gated to macOS only — on Linux/Windows the IDC_MACHIDEONCLOSE
checkbox was force-hidden and the runtime pref forced to false, so
users on those platforms had no way to opt in to the close-button-
hides-window behaviour even though the underlying logic in
CamuleDlg::OnClose has always been cross-platform.

Lift the gate. To keep the option meaningful, also gate it on the
tray icon being enabled: hidden-via-close-button needs a recovery
surface, and the tray icon is the only one available on every
platform. The gate updates live when the user toggles the tray-icon
master switch in the prefs panel.

Default is unchanged (off), so existing users see no behavioural
change unless they explicitly opt in.
…mule-project#334)

When DoMinToTray is enabled and the user clicks the yellow minimize
button on macOS, the previous behaviour left a Dock thumbnail of the
window even though the tray icon was meant to be the recovery
surface. Plain Show(false) cannot remove the Dock thumbnail (the
miniaturize animation completes regardless) and Iconize(false)
re-shows the window via Mac's deminiaturize-then-orderFront sequence.

Toggle NSApp's activation policy instead. While the main window is
hidden via the tray flow, set NSApplicationActivationPolicyAccessory
— that removes the Dock icon entirely (and any in-flight Dock-tile
target), leaving the NSStatusItem as the only surface. When the user
un-hides via the tray click or menu, restore
NSApplicationActivationPolicyRegular and call activateIgnoringOtherApps:
so the window comes to the foreground above other apps.

Implementation:

* `src/MacAppHelper.{h,mm}` — minimal Obj-C++ helper exposing
  `mac_set_accessory_mode(bool)` via `extern "C"`. AppKit is pulled
  in transitively by wxWidgets so no extra `-framework` line is
  needed.
* `cmake/source-vars.cmake` — appends `MacAppHelper.mm` to
  `GUI_SOURCES` only when `APPLE`.
* `src/amuleDlg.cpp::OnMinimize` — drop the Linux-only "evil hack"
  filter on macOS (`wxFindWindowAtPoint` returns NULL during the
  yellow-button transition there, silently skipping the hide-to-tray
  branch) and call `mac_set_accessory_mode(true)` before `Show(false)`
  on the iconize path.
* `src/MuleTrayIcon.cpp::DoShowHide` — call
  `mac_set_accessory_mode(true)` when hiding via the tray gesture,
  and `mac_set_accessory_mode(false)` before `Show(true)` when
  un-hiding.

Linux and Windows code paths unchanged.
…mule-project#334)

HideOnClose used to consume every path that reached CamuleDlg::OnClose
with a vetoable wxCloseEvent — including the tray-icon Exit menu,
Cmd+Q on macOS, and the Dock right-click → Quit. Explicit quit
requests should always quit; HideOnClose should govern only the red
close-button gesture.

Three signals now bypass the hide-on-close veto branch:

* `Close(true)` (force=true ⇒ CanVeto()==false) — already used by the
  wxID_EXIT handler (Cmd+Q / menu Quit).
* `CMuleTrayIcon::DoExit` was passing `force=false`; now sets a new
  `IsQuitting()` flag and calls Close() so the confirm-exit prompt
  can still run if enabled.
* `CamuleGuiApp::OnQueryEndSession` sets `IsQuitting()` before
  forwarding to wx's default session-end handling. Captures the Dock
  right-click → Quit path on macOS, which otherwise looks identical
  to a regular close to the OnClose handler.

The IsQuitting flag lives on CamuleAppCommon so both the monolithic
CamuleApp and the remote-GUI CamuleRemoteGuiApp expose the same
accessors. CamuleDlg::OnClose's hide branch is now
`hideOnClose && evt.CanVeto() && !theApp->IsQuitting()`, and a
confirm-exit veto resets the flag so a later close-button click
respects HideOnClose again.

In the same spirit, the prefs panel no longer greys out the
"Show confirmation on exit" checkbox when HideOnClose is on — they're
orthogonal now that the clean exit paths (tray Exit, Dock Quit) can
still ask for confirmation via the OnClose veto.
…howGUI (amule-project#334)

Cross-platform polish around the tray-icon code paths:

* **Iconize-aware menu and DoShowHide** — `theApp->amuledlg->IsShown()`
  is true even when the window is iconized to Dock/taskbar, which let
  the menu offer "Hide aMule" in those states; clicking would then
  Show(false) the already-hidden window, destroying the only recovery
  surface. Track iconize separately via a `m_iconized_logical` flag
  set from `wxIconizeEvent::IsIconized()` (reliable cross-platform —
  unlike `wxFrame::IsIconized()` which can lag on wxGTK), and require
  `IsShown() && !IsTrayLogicallyIconized()` for the menu label and
  the DoShowHide branch.

* **Iconize(false) before Show(true)** on every platform when
  un-hiding via the tray — without this a previously-iconized window
  would come back as a taskbar-iconized entry, not a real restored
  frame.

* **Single-left tray click toggles the window on Windows** —
  EVT_TASKBAR_LEFT_UP wired to SwitchShow. Matches the convention
  of Discord/Slack/Telegram/qBittorrent. macOS NSStatusItem opens
  its menu on single-click via its own default.

* **`GuiEvents::ShowGUI` brings the window back from any hidden
  state** — running instance picks up the RAISE_DIALOG signal from
  duplicate-launch (pinned-shortcut click etc.) via ED2KLinks polling
  and lands here. Previous implementation only did Iconize(false);
  add Show(true) + Raise() + mac_set_accessory_mode(false) so a
  window hidden via HideOnClose / minimize-to-tray / accessory-mode
  also restores.

* **SNI tray menu refresh on every show/hide** — SNI menus are built
  once and held; expose RebuildMenu() publicly and call it from a new
  EVT_SHOW handler so the "Show aMule"/"Hide aMule" entry stays in
  sync after HideOnClose / programmatic Show(false). Also call it
  from OnMinimize so iconize state changes refresh the label.

The macOS Dock-toggle path from the previous commit still applies on
top — `mac_set_accessory_mode(true)` runs before Show(false), and
`mac_set_accessory_mode(false)` runs before Show(true).
…ct#334)

The Network tab's splitter had `SetSashGravity(0.5f)` set, which made
it the only one in aMule that proportionally re-laid-out children
when the parent window resized. On both Mac and Windows that
proportional recalculation also fired during the minimize/restore
reflow with transient parent dimensions, producing
`wxEVT_SPLITTER_SASH_POS_CHANGED` events with positions outside the
visible range — `OnSashPositionChanged` then stomped the saved
position with garbage that collapsed the log pane the next time the
window came back.

Two-part fix at the root cause:

* Drop `SetSashGravity(0.5f)` in `CServerWnd`'s constructor. The
  other amule splitters (Shared/Transfer/Messages) all use the
  default gravity (0.0) and don't suffer the layout-recalc storm —
  the Network tab now behaves the same way. UX implication: when the
  user resizes the main window, the server list keeps its height
  and the log pane absorbs the extra space (or shrinks), instead of
  the two sharing proportionally. Net positive — the server list is
  a finite enumeration that doesn't benefit from extra room, while
  the log pane does.

* Add a CHANGING/CHANGED protocol around sash persistence:
  `wxEVT_SPLITTER_SASH_POS_CHANGING` only fires while the user is
  actually dragging, so set a flag there; only persist the new
  position in `OnSashPositionChanged` when the flag is set, then
  clear it. Defensive — even if a future wx/GTK quirk fires CHANGED
  from a layout reflow again, we won't stomp the saved position.
…le-project#334)

Linux has two separate ways the tray-icon UX can silently fail; both
are gated at startup + in the prefs panel so users don't enable an
option that does nothing.

**No libayatana-appindicator3 at compile time.** When aMule is built
without the AppIndicator dep, wxTaskBarIcon falls back to the legacy
GtkStatusIcon backend that GNOME Shell dropped in 3.26 (Ubuntu's
default since 2017) and wlroots-based compositors never implemented
— the icon is silently invisible. CamuleApp::OnInit force-clears
UseTrayIcon at startup; PrefsUnifiedDlg disables the IDC_ENABLETRAYICON
checkbox with a tooltip pointing at libayatana-appindicator3 as the
rebuild dependency.

**Wayland session.** xdg-shell intentionally doesn't deliver
iconified-state notifications to clients (no signal exists per GTK
maintainers; wayland-protocols MR !201's xdg_toplevel.suspended is
neither universally supported nor a clean minimize proxy), so the
system minimize button cannot trigger our Show(false) hide-to-tray
path. Same gap is documented across qBittorrent #17265, Telegram
#2123, KeePassXC #6502, Slack/Element/Spotify — none have a fix
either. Force MinToTray off when running under a Wayland session
and disable the IDC_MINTRAY checkbox with a tooltip citing the
protocol limitation. The HideOnClose path still works fine on
Wayland (X button → Show(false) is reliable), so users who want
tray-only behaviour can still get it via that route.

Detection is via standard runtime env vars: WAYLAND_DISPLAY or
XDG_SESSION_TYPE=wayland marks the session as Wayland. Users who
want minimize-to-tray under Wayland can force XWayland by launching
`GDK_BACKEND=x11 amule` — the same workaround Discord users have
adopted — and our detection honours the env var by treating it as
X11. The prefs tooltip mentions the workaround so users discover
it without reading source.
…le-project#334)

The General prefs panel previously had:

    [ ] Prompt on exit
    [ ] Hide application window when close button is pressed
    [ ] Enable Tray Icon
    [ ] Minimize to Tray Icon

Hide-on-close and minimize-to-tray both depend on the tray-icon
master being enabled (the runtime gate force-clears them otherwise
and the prefs-panel live-gating greys both checkboxes when tray is
off). Showing the master switch after its dependents made the
relationship harder to see; move it above so the visual hierarchy
matches the gate hierarchy:

    [ ] Prompt on exit
    [ ] Enable Tray Icon
    [ ] Hide application window when close button is pressed
    [ ] Minimize to Tray Icon

Pure layout shuffle — no behavioural change, no string changes.
@mrjimenez
mrjimenez merged commit 64b83e4 into amule-project:master May 11, 2026
12 checks passed
@got3nks
got3nks deleted the fix/close-to-tray-clean branch May 11, 2026 08:01
got3nks added a commit to got3nks/amule that referenced this pull request Jul 24, 2026
…mule-project#574)

CLoggerAccess tails the daemon logfile so EC clients (amuleGUI, amuleweb)
receive log lines emitted after they connect. The amule-project#215
fix cleared stdio's sticky EOF by re-seeking to the current position, but
wx 3.3 short-circuits a seek to the current offset (no fseek is issued),
so EOF is never cleared: the client gets the backlog captured at connect
time and then no further lines.

Reopen the stream instead -- a fresh wxFFileInputStream has no EOF set --
and seek back to where we left off. This resumes at the first newly
appended byte, independent of seek-clears-EOF semantics. The seek is an
fseek to an absolute offset (O(1)); cost is one open()+fseek() per poll,
flat regardless of logfile size.
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.

Close to tray for the close button

2 participants