Skip to content

Commit e37d5c9

Browse files
committed
Minimise to system tray on close
Closing the window now hides the app to the system tray (like Teams) and it keeps running in the background. The tray icon restores the window on click and offers Open / Quit. Quit fully exits and stops the Bluetooth service. Falls back to normal close if no tray is available. https://claude.ai/code/session_014KrpftzUgVFGFXVUU3DUTT
1 parent d3b83b5 commit e37d5c9

1 file changed

Lines changed: 72 additions & 0 deletions

File tree

src/nothing_ear/ui/main_window.py

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,23 @@
22

33
from __future__ import annotations
44

5+
from pathlib import Path
6+
57
from PySide6.QtCore import Qt, QObject, Signal
8+
from PySide6.QtGui import QAction, QIcon
69
from 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,
@@ -24,6 +30,15 @@
2430
from .eq_tab import EqTab
2531
from .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

2843
class _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

Comments
 (0)