22
33from __future__ import annotations
44
5+ from pathlib import Path
6+
57from PySide6 .QtCore import Qt , QObject , Signal
8+ from PySide6 .QtGui import QAction , QIcon
69from PySide6 .QtWidgets import (
10+ QApplication ,
711 QFrame ,
812 QHBoxLayout ,
913 QLabel ,
1014 QListWidget ,
1115 QListWidgetItem ,
1216 QMainWindow ,
17+ QMenu ,
1318 QProgressBar ,
1419 QPushButton ,
1520 QSizePolicy ,
21+ QSystemTrayIcon ,
1622 QTabWidget ,
1723 QVBoxLayout ,
1824 QWidget ,
2430from .eq_tab import EqTab
2531from .style import STYLESHEET
2632
33+ _ICON_PATH = Path (__file__ ).resolve ().parent .parent / "assets" / "icon.svg"
34+
35+
36+ class _Bridge (QObject ):
37+ """Marshals background-thread updates onto the Qt event loop."""
38+
39+ devicesChanged = Signal (list )
40+ errorOccurred = Signal (str )
41+
2742
2843class _Bridge (QObject ):
2944 """Marshals background-thread updates onto the Qt event loop."""
@@ -43,6 +58,13 @@ def __init__(self, mock: bool = False):
4358 self ._devices : list [BtDevice ] = []
4459 self ._primary_path : str | None = None
4560 self ._last_signature : tuple | None = None
61+ self ._force_quit = False
62+ self ._tray : QSystemTrayIcon | None = None
63+ self ._tray_notified = False
64+
65+ self ._app_icon = QIcon (str (_ICON_PATH )) if _ICON_PATH .exists () else QIcon ()
66+ if not self ._app_icon .isNull ():
67+ self .setWindowIcon (self ._app_icon )
4668
4769 self ._bridge = _Bridge ()
4870 self ._bridge .devicesChanged .connect (self ._on_devices )
@@ -64,8 +86,46 @@ def on_update(devices, error):
6486 self ._service = BlueZService (on_update )
6587
6688 self ._build_ui ()
89+ self ._setup_tray ()
6790 self ._service .start ()
6891
92+ def _setup_tray (self ) -> None :
93+ if not QSystemTrayIcon .isSystemTrayAvailable ():
94+ return
95+ tray = QSystemTrayIcon (self ._app_icon , self )
96+ tray .setToolTip ("NothingX Desktop" )
97+
98+ menu = QMenu ()
99+ open_action = QAction ("Open" , self )
100+ open_action .triggered .connect (self ._show_window )
101+ quit_action = QAction ("Quit" , self )
102+ quit_action .triggered .connect (self ._quit )
103+ menu .addAction (open_action )
104+ menu .addSeparator ()
105+ menu .addAction (quit_action )
106+
107+ tray .setContextMenu (menu )
108+ tray .activated .connect (self ._on_tray_activated )
109+ tray .show ()
110+ self ._tray = tray
111+ QApplication .instance ().setQuitOnLastWindowClosed (False )
112+
113+ def _on_tray_activated (self , reason ) -> None :
114+ if reason in (QSystemTrayIcon .Trigger , QSystemTrayIcon .DoubleClick ):
115+ self ._show_window ()
116+
117+ def _show_window (self ) -> None :
118+ self .showNormal ()
119+ self .raise_ ()
120+ self .activateWindow ()
121+
122+ def _quit (self ) -> None :
123+ self ._force_quit = True
124+ self ._service .stop ()
125+ if self ._tray is not None :
126+ self ._tray .hide ()
127+ QApplication .instance ().quit ()
128+
69129 def _build_ui (self ) -> None :
70130 root = QWidget ()
71131 layout = QVBoxLayout (root )
@@ -251,5 +311,17 @@ def _on_error(self, message: str) -> None:
251311 self ._status .setText (f"⚠ { message } " )
252312
253313 def closeEvent (self , event ) -> None :
314+ if self ._tray is not None and not self ._force_quit :
315+ event .ignore ()
316+ self .hide ()
317+ if not self ._tray_notified :
318+ self ._tray .showMessage (
319+ "NothingX Desktop" ,
320+ "Still running in the tray. Right-click the icon to quit." ,
321+ self ._app_icon ,
322+ 3000 ,
323+ )
324+ self ._tray_notified = True
325+ return
254326 self ._service .stop ()
255327 super ().closeEvent (event )
0 commit comments