trackerkeeper 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- trackerkeeper/__init__.py +55 -0
- trackerkeeper/__main__.py +3 -0
- trackerkeeper/_version.py +24 -0
- trackerkeeper/app.py +445 -0
- trackerkeeper/assets/__init__.py +2 -0
- trackerkeeper/assets/trackerkeeper.svg +12 -0
- trackerkeeper/async_io.py +299 -0
- trackerkeeper/autostart/__init__.py +52 -0
- trackerkeeper/autostart/_linux.py +129 -0
- trackerkeeper/autostart/_macos.py +163 -0
- trackerkeeper/autostart/_msix.py +181 -0
- trackerkeeper/autostart/_unsupported.py +22 -0
- trackerkeeper/autostart/_windows.py +96 -0
- trackerkeeper/bake.py +217 -0
- trackerkeeper/blur/__init__.py +212 -0
- trackerkeeper/blur/_dwm.py +348 -0
- trackerkeeper/blur/_faux_frost.py +143 -0
- trackerkeeper/blur/_kwin.py +478 -0
- trackerkeeper/blur/_macos.py +356 -0
- trackerkeeper/blur/_unsupported.py +39 -0
- trackerkeeper/boot_timing.py +75 -0
- trackerkeeper/breadboard.py +1554 -0
- trackerkeeper/bus.py +110 -0
- trackerkeeper/catalog.py +176 -0
- trackerkeeper/color_tokens.py +697 -0
- trackerkeeper/credentials.py +471 -0
- trackerkeeper/custom_tooltip.py +254 -0
- trackerkeeper/dashboard.py +704 -0
- trackerkeeper/deliver.py +442 -0
- trackerkeeper/design_tokens.py +415 -0
- trackerkeeper/diagnostics.py +168 -0
- trackerkeeper/drag_repaint/__init__.py +68 -0
- trackerkeeper/drag_repaint/_kwin.py +192 -0
- trackerkeeper/drag_repaint/_unsupported.py +41 -0
- trackerkeeper/drag_repaint/effect/dragrepaint/contents/code/main.js +86 -0
- trackerkeeper/drag_repaint/effect/dragrepaint/metadata.json +17 -0
- trackerkeeper/frosted_dialog.py +377 -0
- trackerkeeper/i18n/__init__.py +101 -0
- trackerkeeper/i18n/fmt.py +137 -0
- trackerkeeper/icon_button.py +137 -0
- trackerkeeper/icons.py +532 -0
- trackerkeeper/identity.py +135 -0
- trackerkeeper/item_dialog.py +185 -0
- trackerkeeper/kde_titlebar.py +196 -0
- trackerkeeper/keyboard_focus.py +353 -0
- trackerkeeper/log.py +112 -0
- trackerkeeper/macos_menubar.py +282 -0
- trackerkeeper/macos_window.py +172 -0
- trackerkeeper/metadata.py +121 -0
- trackerkeeper/noborder/__init__.py +61 -0
- trackerkeeper/noborder/_kwin.py +247 -0
- trackerkeeper/noborder/_unsupported.py +42 -0
- trackerkeeper/notifications/__init__.py +84 -0
- trackerkeeper/notifications/_linux.py +68 -0
- trackerkeeper/notifications/_macos.py +159 -0
- trackerkeeper/notifications/_unsupported.py +22 -0
- trackerkeeper/notifications/_windows.py +115 -0
- trackerkeeper/platform_compat.py +149 -0
- trackerkeeper/power/__init__.py +84 -0
- trackerkeeper/power/_linux.py +79 -0
- trackerkeeper/power/_unsupported.py +18 -0
- trackerkeeper/power/_windows.py +52 -0
- trackerkeeper/rig.py +338 -0
- trackerkeeper/scaffold.py +310 -0
- trackerkeeper/selector.py +529 -0
- trackerkeeper/settings.py +226 -0
- trackerkeeper/settings_dialog.py +307 -0
- trackerkeeper/settings_migration.py +101 -0
- trackerkeeper/single_instance.py +330 -0
- trackerkeeper/smooth_scroll.py +155 -0
- trackerkeeper/sources.py +318 -0
- trackerkeeper/system_accent.py +363 -0
- trackerkeeper/terminal.py +423 -0
- trackerkeeper/test_bridge.py +514 -0
- trackerkeeper/theme.py +725 -0
- trackerkeeper/top_bar.py +154 -0
- trackerkeeper/tray.py +184 -0
- trackerkeeper/ui_helpers.py +2294 -0
- trackerkeeper/update_chip.py +119 -0
- trackerkeeper/updates.py +216 -0
- trackerkeeper/win_frameless.py +364 -0
- trackerkeeper/window.py +609 -0
- trackerkeeper/windows_shortcut.py +355 -0
- trackerkeeper-0.1.0.dist-info/METADATA +144 -0
- trackerkeeper-0.1.0.dist-info/RECORD +89 -0
- trackerkeeper-0.1.0.dist-info/WHEEL +5 -0
- trackerkeeper-0.1.0.dist-info/entry_points.txt +8 -0
- trackerkeeper-0.1.0.dist-info/licenses/LICENSE +338 -0
- trackerkeeper-0.1.0.dist-info/top_level.txt +1 -0
trackerkeeper/top_bar.py
ADDED
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
"""TopBar — the app's top bar, which doubles as the titlebar in borderless mode.
|
|
2
|
+
|
|
3
|
+
A compact strip modelled on the dough shell: a hamburger menu + settings on the
|
|
4
|
+
LEFT, then the app title and a status badge; on the right an app-fillable action
|
|
5
|
+
slot, the update chip, and (when trackerkeeper owns the chrome) the minimize /
|
|
6
|
+
maximize / close window controls. Dragging the bar moves the window
|
|
7
|
+
(``startSystemMove``); double-click toggles maximize.
|
|
8
|
+
|
|
9
|
+
Apps fold their OWN header controls onto this one line instead of a second row:
|
|
10
|
+
``insert_title_widget()`` drops a badge beside the title, ``add_action()`` adds a
|
|
11
|
+
right-side control (a button, a status label), and ``add_menu_action()`` extends
|
|
12
|
+
the hamburger menu. See :class:`~trackerkeeper.dashboard.Dashboard`.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
from __future__ import annotations
|
|
16
|
+
|
|
17
|
+
from PySide6.QtCore import Qt
|
|
18
|
+
from PySide6.QtWidgets import QHBoxLayout, QLabel
|
|
19
|
+
|
|
20
|
+
from trackerkeeper import ui_helpers
|
|
21
|
+
from trackerkeeper.bus import AppBus
|
|
22
|
+
from trackerkeeper.design_tokens import TYPE_SUBHEAD, type_qss
|
|
23
|
+
from trackerkeeper.icon_button import IconButton
|
|
24
|
+
from trackerkeeper.icons import icon
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class TopBar(ui_helpers.CenteredBar):
|
|
28
|
+
HEIGHT = 44
|
|
29
|
+
# The chrome-button footprint. Exported so app controls folded onto this bar
|
|
30
|
+
# (see Dashboard) can match it instead of guessing.
|
|
31
|
+
BUTTON_SIZE = (30, 26)
|
|
32
|
+
|
|
33
|
+
def __init__(self, window, *, titlebar_mode: bool, title: str = "trackerkeeper"):
|
|
34
|
+
super().__init__(window)
|
|
35
|
+
self._window = window
|
|
36
|
+
self._titlebar_mode = titlebar_mode
|
|
37
|
+
self._menu_actions: list = []
|
|
38
|
+
self.setFixedHeight(self.HEIGHT)
|
|
39
|
+
self.setStyleSheet("background: transparent;")
|
|
40
|
+
|
|
41
|
+
lay = QHBoxLayout(self)
|
|
42
|
+
lay.setContentsMargins(8, 5, 8, 5)
|
|
43
|
+
lay.setSpacing(5)
|
|
44
|
+
|
|
45
|
+
# ── left: hamburger menu + settings ──
|
|
46
|
+
self.menu_btn = self._chrome_button("menu", "Menu")
|
|
47
|
+
self.menu_btn.clicked.connect(self._open_menu)
|
|
48
|
+
lay.addWidget(self.menu_btn)
|
|
49
|
+
self.settings_btn = self._chrome_button("settings", "Settings")
|
|
50
|
+
self.settings_btn.clicked.connect(lambda: AppBus.get().show_settings.emit())
|
|
51
|
+
lay.addWidget(self.settings_btn)
|
|
52
|
+
|
|
53
|
+
# ── title, then a slot right after it for an app badge (update count) ──
|
|
54
|
+
self.title = QLabel(title)
|
|
55
|
+
lay.addWidget(self.title)
|
|
56
|
+
self._title_slot = QHBoxLayout()
|
|
57
|
+
self._title_slot.setContentsMargins(4, 0, 0, 0)
|
|
58
|
+
self._title_slot.setSpacing(6)
|
|
59
|
+
lay.addLayout(self._title_slot)
|
|
60
|
+
|
|
61
|
+
lay.addStretch(1)
|
|
62
|
+
|
|
63
|
+
# ── right: an app action slot (buttons / a status label) ──
|
|
64
|
+
self._action_slot = QHBoxLayout()
|
|
65
|
+
self._action_slot.setContentsMargins(0, 0, 0, 0)
|
|
66
|
+
self._action_slot.setSpacing(6)
|
|
67
|
+
lay.addLayout(self._action_slot)
|
|
68
|
+
|
|
69
|
+
# Update-available chip — invisible until trackerkeeper.updates finds a newer
|
|
70
|
+
# release (AppBus.update_available); Download / What's-new / Dismiss.
|
|
71
|
+
from trackerkeeper.update_chip import UpdateChip
|
|
72
|
+
|
|
73
|
+
self.update_chip = UpdateChip(self)
|
|
74
|
+
lay.addWidget(self.update_chip)
|
|
75
|
+
|
|
76
|
+
# Window controls only when we own the titlebar (borderless / frameless).
|
|
77
|
+
if titlebar_mode:
|
|
78
|
+
self.min_btn = self._chrome_button("win_minimize", "Minimize")
|
|
79
|
+
self.min_btn.clicked.connect(window.showMinimized)
|
|
80
|
+
self.max_btn = self._chrome_button("win_maximize", "Maximize")
|
|
81
|
+
self.max_btn.clicked.connect(self._toggle_max)
|
|
82
|
+
self.close_btn = self._chrome_button("win_close", "Close")
|
|
83
|
+
self.close_btn.clicked.connect(window.close)
|
|
84
|
+
for b in (self.min_btn, self.max_btn, self.close_btn):
|
|
85
|
+
lay.addWidget(b)
|
|
86
|
+
|
|
87
|
+
self.restyle()
|
|
88
|
+
|
|
89
|
+
# ── the app-fills-the-bar API ──────────────────────────────────────────
|
|
90
|
+
def insert_title_widget(self, widget) -> None:
|
|
91
|
+
"""Add a widget immediately after the title (e.g. an update-count badge)."""
|
|
92
|
+
self._title_slot.addWidget(widget)
|
|
93
|
+
|
|
94
|
+
def add_action(self, widget) -> None:
|
|
95
|
+
"""Append an app control to the right-side slot — left of the update chip
|
|
96
|
+
and the window buttons."""
|
|
97
|
+
self._action_slot.addWidget(widget)
|
|
98
|
+
|
|
99
|
+
def add_menu_action(self, label: str, callback) -> None:
|
|
100
|
+
"""Add an entry to the hamburger menu (above the always-present Settings)."""
|
|
101
|
+
self._menu_actions.append((label, callback))
|
|
102
|
+
|
|
103
|
+
def _open_menu(self) -> None:
|
|
104
|
+
menu = ui_helpers.opaque_menu(self, blur_corner_radius=8)
|
|
105
|
+
for label, cb in self._menu_actions:
|
|
106
|
+
menu.addAction(label).triggered.connect(lambda _=False, c=cb: c())
|
|
107
|
+
if self._menu_actions:
|
|
108
|
+
menu.addSeparator()
|
|
109
|
+
menu.addAction("Settings…").triggered.connect(
|
|
110
|
+
lambda _=False: AppBus.get().show_settings.emit())
|
|
111
|
+
menu.exec(self.menu_btn.mapToGlobal(self.menu_btn.rect().bottomLeft()))
|
|
112
|
+
|
|
113
|
+
# ── chrome ─────────────────────────────────────────────────────────────
|
|
114
|
+
def _chrome_button(self, icon_name: str, tip: str) -> IconButton:
|
|
115
|
+
# accessible_name passed explicitly (the preferred pattern) even though
|
|
116
|
+
# the tooltip would fall back to the same string.
|
|
117
|
+
b = IconButton(accessible_name=tip)
|
|
118
|
+
b.setIcon(icon(icon_name))
|
|
119
|
+
b.setToolTip(tip)
|
|
120
|
+
b.setFixedSize(*self.BUTTON_SIZE) # compact, dough-matched (was 36×32)
|
|
121
|
+
return b
|
|
122
|
+
|
|
123
|
+
def restyle(self) -> None:
|
|
124
|
+
"""Re-read theme colors (call on AppBus.theme_changed) + refresh icons."""
|
|
125
|
+
self.title.setStyleSheet(f"color: {ui_helpers.TEXT}; {type_qss(TYPE_SUBHEAD)}")
|
|
126
|
+
for name, attr in (
|
|
127
|
+
("menu", "menu_btn"),
|
|
128
|
+
("settings", "settings_btn"),
|
|
129
|
+
("win_minimize", "min_btn"),
|
|
130
|
+
("win_maximize", "max_btn"),
|
|
131
|
+
("win_close", "close_btn"),
|
|
132
|
+
):
|
|
133
|
+
btn = getattr(self, attr, None)
|
|
134
|
+
if btn is not None:
|
|
135
|
+
btn.setIcon(icon(name))
|
|
136
|
+
|
|
137
|
+
def _toggle_max(self) -> None:
|
|
138
|
+
w = self._window
|
|
139
|
+
w.showNormal() if w.isMaximized() else w.showMaximized()
|
|
140
|
+
|
|
141
|
+
# ── Drag-to-move / double-click maximize (titlebar) ────────────────────
|
|
142
|
+
def mousePressEvent(self, e):
|
|
143
|
+
if self._titlebar_mode and e.button() == Qt.MouseButton.LeftButton:
|
|
144
|
+
handle = self._window.windowHandle()
|
|
145
|
+
if handle is not None:
|
|
146
|
+
handle.startSystemMove()
|
|
147
|
+
return
|
|
148
|
+
super().mousePressEvent(e)
|
|
149
|
+
|
|
150
|
+
def mouseDoubleClickEvent(self, e):
|
|
151
|
+
if self._titlebar_mode and e.button() == Qt.MouseButton.LeftButton:
|
|
152
|
+
self._toggle_max()
|
|
153
|
+
return
|
|
154
|
+
super().mouseDoubleClickEvent(e)
|
trackerkeeper/tray.py
ADDED
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
"""The system-tray presence — tracker keeper's resting state.
|
|
2
|
+
|
|
3
|
+
A watchtower is only useful if it's *watching*, which means living in the tray
|
|
4
|
+
rather than in a window you have to keep open. :class:`AppTray` puts the app
|
|
5
|
+
there: an icon whose tooltip carries the current update count, a menu (Show /
|
|
6
|
+
Check for updates / Settings / Quit), left-click to toggle the window, and an
|
|
7
|
+
optional close-to-tray so the X button hides instead of quitting.
|
|
8
|
+
|
|
9
|
+
Everything is best-effort and self-disabling: if the desktop has no tray
|
|
10
|
+
(``isSystemTrayAvailable()`` False — headless, some wlroots sessions), nothing
|
|
11
|
+
is created, ``available`` stays False, and the app behaves exactly as before —
|
|
12
|
+
the close button never traps the window in an invisible process.
|
|
13
|
+
|
|
14
|
+
Preferences live under the documented extension path (``get_settings()._s``), so
|
|
15
|
+
no base settings file is touched:
|
|
16
|
+
``app/show_tray_icon`` and ``app/close_to_tray``.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import logging
|
|
22
|
+
|
|
23
|
+
from PySide6.QtCore import QEvent, QObject
|
|
24
|
+
from PySide6.QtGui import QIcon
|
|
25
|
+
from PySide6.QtWidgets import QApplication, QSystemTrayIcon
|
|
26
|
+
|
|
27
|
+
from trackerkeeper.bus import AppBus
|
|
28
|
+
from trackerkeeper.settings import get_settings
|
|
29
|
+
|
|
30
|
+
logger = logging.getLogger(__name__)
|
|
31
|
+
|
|
32
|
+
_KEY_SHOW = "app/show_tray_icon"
|
|
33
|
+
_KEY_CLOSE = "app/close_to_tray"
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _as_bool(val, default: bool) -> bool:
|
|
37
|
+
if val is None:
|
|
38
|
+
return default
|
|
39
|
+
if isinstance(val, bool):
|
|
40
|
+
return val
|
|
41
|
+
return str(val).strip().lower() in ("1", "true", "yes", "on")
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def show_tray_icon() -> bool:
|
|
45
|
+
return _as_bool(get_settings()._s.value(_KEY_SHOW), True)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def set_show_tray_icon(v: bool) -> None:
|
|
49
|
+
get_settings()._s.setValue(_KEY_SHOW, bool(v))
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def close_to_tray() -> bool:
|
|
53
|
+
"""Close hides to the tray instead of quitting. On by default for a
|
|
54
|
+
watchtower — but only ever honoured while a tray icon is actually live."""
|
|
55
|
+
return _as_bool(get_settings()._s.value(_KEY_CLOSE), True)
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def set_close_to_tray(v: bool) -> None:
|
|
59
|
+
get_settings()._s.setValue(_KEY_CLOSE, bool(v))
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def tooltip_text(app_name: str, n_updates: int) -> str:
|
|
63
|
+
"""The hover text: the fleet's headline, no window needed."""
|
|
64
|
+
if n_updates <= 0:
|
|
65
|
+
return f"{app_name} — all current"
|
|
66
|
+
return f"{app_name} — {n_updates} update{'s' if n_updates != 1 else ''} available"
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class AppTray(QObject):
|
|
70
|
+
"""The tray icon + its menu. Construct with the main window; call
|
|
71
|
+
:meth:`set_update_count` whenever the fleet's update total changes."""
|
|
72
|
+
|
|
73
|
+
def __init__(self, window, *, app_name: str = "tracker keeper",
|
|
74
|
+
on_refresh=None, parent=None):
|
|
75
|
+
super().__init__(parent or window)
|
|
76
|
+
self._window = window
|
|
77
|
+
self._app_name = app_name
|
|
78
|
+
self._on_refresh = on_refresh
|
|
79
|
+
self._quitting = False
|
|
80
|
+
self._icon: QSystemTrayIcon | None = None
|
|
81
|
+
|
|
82
|
+
if not (show_tray_icon() and QSystemTrayIcon.isSystemTrayAvailable()):
|
|
83
|
+
return
|
|
84
|
+
|
|
85
|
+
from trackerkeeper.ui_helpers import make_app_icon, opaque_menu
|
|
86
|
+
|
|
87
|
+
self._icon = QSystemTrayIcon(QIcon(make_app_icon(64)), self)
|
|
88
|
+
self._icon.setToolTip(tooltip_text(app_name, 0))
|
|
89
|
+
|
|
90
|
+
menu = opaque_menu(window, blur_corner_radius=8)
|
|
91
|
+
self._show_action = menu.addAction("Show tracker keeper")
|
|
92
|
+
self._show_action.triggered.connect(lambda _=False: self._toggle_window())
|
|
93
|
+
if on_refresh is not None:
|
|
94
|
+
menu.addAction("Check for updates").triggered.connect(
|
|
95
|
+
lambda _=False: on_refresh())
|
|
96
|
+
menu.addSeparator()
|
|
97
|
+
menu.addAction("Settings…").triggered.connect(
|
|
98
|
+
lambda _=False: AppBus.get().show_settings.emit())
|
|
99
|
+
menu.addAction("Quit").triggered.connect(lambda _=False: self.quit())
|
|
100
|
+
self._menu = menu
|
|
101
|
+
self._icon.setContextMenu(menu)
|
|
102
|
+
self._icon.activated.connect(self._on_activated)
|
|
103
|
+
self._icon.show()
|
|
104
|
+
|
|
105
|
+
# Hiding the last window must NOT end the process once we live in the
|
|
106
|
+
# tray — the app's whole point is to keep watching.
|
|
107
|
+
app = QApplication.instance()
|
|
108
|
+
if app is not None:
|
|
109
|
+
app.setQuitOnLastWindowClosed(False)
|
|
110
|
+
window.installEventFilter(self)
|
|
111
|
+
|
|
112
|
+
# ── state ──────────────────────────────────────────────────────────────
|
|
113
|
+
@property
|
|
114
|
+
def available(self) -> bool:
|
|
115
|
+
"""True when a tray icon is actually live (desktop supports it and the
|
|
116
|
+
user hasn't turned it off). Everything else no-ops when False."""
|
|
117
|
+
return self._icon is not None
|
|
118
|
+
|
|
119
|
+
def set_update_count(self, n: int) -> None:
|
|
120
|
+
"""Reflect the fleet's update total in the tooltip."""
|
|
121
|
+
if self._icon is not None:
|
|
122
|
+
self._icon.setToolTip(tooltip_text(self._app_name, n))
|
|
123
|
+
|
|
124
|
+
def notify(self, title: str, body: str) -> None:
|
|
125
|
+
"""A tray balloon — the fallback when the OS notification backend
|
|
126
|
+
isn't available."""
|
|
127
|
+
if self._icon is not None:
|
|
128
|
+
self._icon.showMessage(title, body, QSystemTrayIcon.MessageIcon.Information)
|
|
129
|
+
|
|
130
|
+
# ── actions ────────────────────────────────────────────────────────────
|
|
131
|
+
def _on_activated(self, reason) -> None:
|
|
132
|
+
if reason in (QSystemTrayIcon.ActivationReason.Trigger,
|
|
133
|
+
QSystemTrayIcon.ActivationReason.DoubleClick):
|
|
134
|
+
self._toggle_window()
|
|
135
|
+
|
|
136
|
+
def _toggle_window(self) -> None:
|
|
137
|
+
w = self._window
|
|
138
|
+
if w.isVisible() and not w.isMinimized():
|
|
139
|
+
self._hide_window()
|
|
140
|
+
else:
|
|
141
|
+
from trackerkeeper.single_instance import force_foreground
|
|
142
|
+
|
|
143
|
+
w.showNormal()
|
|
144
|
+
force_foreground(w)
|
|
145
|
+
|
|
146
|
+
def _hide_window(self) -> None:
|
|
147
|
+
# Geometry is normally persisted by the window's closeEvent — hiding
|
|
148
|
+
# skips that, so save it here or a tray-only session forgets its size.
|
|
149
|
+
try:
|
|
150
|
+
get_settings().save_geometry(self._window)
|
|
151
|
+
except Exception:
|
|
152
|
+
logger.debug("save_geometry on hide-to-tray failed", exc_info=True)
|
|
153
|
+
self._window.hide()
|
|
154
|
+
|
|
155
|
+
def quit(self) -> None:
|
|
156
|
+
"""Really exit (the tray's Quit) — bypasses close-to-tray."""
|
|
157
|
+
self._quitting = True
|
|
158
|
+
try:
|
|
159
|
+
get_settings().save_geometry(self._window)
|
|
160
|
+
except Exception:
|
|
161
|
+
logger.debug("save_geometry on quit failed", exc_info=True)
|
|
162
|
+
if self._icon is not None:
|
|
163
|
+
self._icon.hide()
|
|
164
|
+
app = QApplication.instance()
|
|
165
|
+
if app is not None:
|
|
166
|
+
app.quit()
|
|
167
|
+
|
|
168
|
+
# ── close-to-tray ──────────────────────────────────────────────────────
|
|
169
|
+
def eventFilter(self, obj, event): # noqa: N802 (Qt override)
|
|
170
|
+
# Guard order matters: bail out on the no-tray / quitting cases BEFORE
|
|
171
|
+
# touching the event, so the filter is inert (and safe) when there's no
|
|
172
|
+
# tray to hide into.
|
|
173
|
+
if (
|
|
174
|
+
self.available
|
|
175
|
+
and not self._quitting
|
|
176
|
+
and obj is self._window
|
|
177
|
+
and close_to_tray()
|
|
178
|
+
and event is not None
|
|
179
|
+
and event.type() == QEvent.Type.Close
|
|
180
|
+
):
|
|
181
|
+
event.ignore()
|
|
182
|
+
self._hide_window()
|
|
183
|
+
return True
|
|
184
|
+
return False
|