From 0376b7749447fab129533a07599a1d78a0f7d5c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Fran=C3=A7ois?= Date: Sun, 31 May 2026 16:14:04 +0200 Subject: [PATCH] fix(gui): show the testium icon in the GNOME task bar (Wayland) Set the app id via setDesktopFileName so the window stops inheriting the launcher's class ("python3" under the AppImage), which is what GNOME was keying the wrong icon off. On native Wayland the task-bar icon comes from an installed desktop file matched to the app id (setWindowIcon is ignored there), so on Linux drop an idempotent desktop entry + 256px icon under ~/.local/share. Flatpak keeps its own id/desktop; Windows / macOS use the window icon. No-op off Linux. Co-Authored-By: Claude Opus 4.8 --- src/testium/main_win/desktop_integration.py | 95 +++++++++++++++++++++ src/testium/main_win/testium_win.py | 18 ++++ 2 files changed, 113 insertions(+) create mode 100644 src/testium/main_win/desktop_integration.py diff --git a/src/testium/main_win/desktop_integration.py b/src/testium/main_win/desktop_integration.py new file mode 100644 index 0000000..c60c2fc --- /dev/null +++ b/src/testium/main_win/desktop_integration.py @@ -0,0 +1,95 @@ +"""Install a desktop entry + icon under the user's data dir so desktop shells +show the testium icon in the task bar / dock. + +On a native Wayland session GNOME takes a window's task-bar icon from the +``.desktop`` file whose name (or ``StartupWMClass``) matches the window +``app_id`` — ``QGuiApplication.setWindowIcon`` is ignored there. The portable +channels (source checkout, PyInstaller binary, AppImage) install no system +desktop file, so we drop an idempotent one in ``~/.local/share``. The window +``app_id`` is set to ``testium`` (see ``QApplication.setDesktopFileName`` in +``testium_win``), which is exactly this file's base name. + +Flatpak ships its own ``org.testium.Testium.desktop`` and keeps its own app id, +so the caller skips this integration there. +""" +import os +import sys + +from PySide6.QtCore import Qt +from PySide6.QtGui import QPixmap + +# Must match QApplication.setDesktopFileName(...) for the GUI, and is used as +# both the desktop-file base name and the StartupWMClass. +APP_ID = "testium" + + +def _launch_command(): + """Best-effort Exec= for the menu entry. Not needed for icon matching, but + makes the entry actually launchable when possible.""" + appimage = os.environ.get("APPIMAGE") + if appimage: + return f'"{appimage}"' + if getattr(sys, "frozen", False): + return f'"{os.path.abspath(sys.executable)}"' + argv0 = os.path.abspath(sys.argv[0]) if sys.argv and sys.argv[0] else "" + if argv0 and os.path.exists(argv0): + return f'"{os.path.abspath(sys.executable)}" "{argv0}"' + return f'"{os.path.abspath(sys.executable)}" -m testium' + + +def ensure_desktop_entry(): + """Create (or refresh) ~/.local/share icon + desktop entry. Best-effort: + any failure is swallowed so it can never take the GUI down. + + Freedesktop-only: a no-op off Linux (Windows / macOS use the window icon).""" + if not sys.platform.startswith("linux"): + return + try: + data_home = os.environ.get("XDG_DATA_HOME") or os.path.join( + os.path.expanduser("~"), ".local", "share" + ) + icon_dir = os.path.join(data_home, "icons", "hicolor", "256x256", "apps") + app_dir = os.path.join(data_home, "applications") + icon_path = os.path.join(icon_dir, f"{APP_ID}.png") + desktop_path = os.path.join(app_dir, f"{APP_ID}.desktop") + + os.makedirs(icon_dir, exist_ok=True) + os.makedirs(app_dir, exist_ok=True) + + # Icon: render the bundled Qt resource to a PNG once. Requires a live + # QGuiApplication (the caller creates it before calling us). + if not os.path.isfile(icon_path): + pixmap = QPixmap(u":/black/testium_logo.png") + if not pixmap.isNull(): + pixmap = pixmap.scaled( + 256, 256, + Qt.AspectRatioMode.KeepAspectRatio, + Qt.TransformationMode.SmoothTransformation, + ) + pixmap.save(icon_path, "PNG") + + # Absolute Icon= path so the shell resolves it without an icon-cache + # refresh; StartupWMClass lets X11 / XWayland match too. + desktop = ( + "[Desktop Entry]\n" + "Type=Application\n" + "Name=Testium\n" + "Comment=Test sequencer\n" + f"Icon={icon_path}\n" + f"Exec={_launch_command()} %f\n" + "Terminal=false\n" + f"StartupWMClass={APP_ID}\n" + "Categories=Utility;Development;\n" + ) + + # Write only when missing or changed, to avoid needless menu churn. + current = None + if os.path.isfile(desktop_path): + with open(desktop_path, "r") as fh: + current = fh.read() + if current != desktop: + with open(desktop_path, "w") as fh: + fh.write(desktop) + except Exception: + # Desktop integration is a nicety, never a hard requirement. + pass diff --git a/src/testium/main_win/testium_win.py b/src/testium/main_win/testium_win.py index be6a3b1..23267f0 100755 --- a/src/testium/main_win/testium_win.py +++ b/src/testium/main_win/testium_win.py @@ -678,6 +678,24 @@ def MainWin( debug=False, ): app = QApplication(sys.argv) + # Application identity so desktop shells (GNOME, ...) show the testium + # icon in the task bar / dock instead of a generic one. On Wayland this + # sets the surface app_id; on X11/XWayland it sets WM_CLASS, so the window + # stops inheriting the launcher's class (e.g. "python3" under the AppImage, + # which is what GNOME was keying the wrong icon off) and the window icon + # below is used as the fallback. In Flatpak the id must be the Flatpak app + # id so it matches the installed desktop file. + app.setApplicationName("Testium") + app.setApplicationDisplayName("Testium") + app.setDesktopFileName(os.environ.get("FLATPAK_ID", "testium")) + app.setWindowIcon(QIcon(u":/black/testium_logo.png")) + # On native Wayland the task-bar icon comes from an installed desktop file + # matched to the app_id, not from setWindowIcon(). Flatpak ships its own; + # for the other Linux channels drop an idempotent one under ~/.local/share. + # Windows / macOS use the window icon set above, so this is Linux-only. + if sys.platform.startswith("linux") and not os.environ.get("FLATPAK_ID"): + from main_win.desktop_integration import ensure_desktop_entry + ensure_desktop_entry() ui = MainWindow( test_file, config_files,