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 <noreply@anthropic.com>
This commit is contained in:
95
src/testium/main_win/desktop_integration.py
Normal file
95
src/testium/main_win/desktop_integration.py
Normal file
@@ -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
|
||||||
@@ -678,6 +678,24 @@ def MainWin(
|
|||||||
debug=False,
|
debug=False,
|
||||||
):
|
):
|
||||||
app = QApplication(sys.argv)
|
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(
|
ui = MainWindow(
|
||||||
test_file,
|
test_file,
|
||||||
config_files,
|
config_files,
|
||||||
|
|||||||
Reference in New Issue
Block a user