Upload
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,133 @@
|
||||
"""System tray icon — the pet window is frameless with no taskbar entry, so
|
||||
this menu is the only always-available way to control or exit it: talk now,
|
||||
mute, wander, click-through, nap, history, wake-word tuning, quit.
|
||||
|
||||
Every entry is a plain callback passed in by ui/app.py; this file knows
|
||||
nothing about the controller or the pet window.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Callable, Optional
|
||||
|
||||
from PySide6.QtCore import Qt
|
||||
from PySide6.QtGui import QAction, QColor, QIcon, QPainter, QPixmap
|
||||
from PySide6.QtWidgets import QMenu, QSystemTrayIcon
|
||||
|
||||
|
||||
def _make_icon(muted: bool, napping: bool = False) -> QIcon:
|
||||
pixmap = QPixmap(32, 32)
|
||||
pixmap.fill(Qt.transparent)
|
||||
painter = QPainter(pixmap)
|
||||
painter.setRenderHint(QPainter.Antialiasing)
|
||||
if muted:
|
||||
color = QColor(200, 60, 60)
|
||||
elif napping:
|
||||
color = QColor(120, 120, 140)
|
||||
else:
|
||||
color = QColor(120, 170, 240)
|
||||
painter.setBrush(color)
|
||||
painter.setPen(Qt.NoPen)
|
||||
painter.drawEllipse(2, 2, 28, 28)
|
||||
painter.end()
|
||||
return QIcon(pixmap)
|
||||
|
||||
|
||||
class PetTray(QSystemTrayIcon):
|
||||
def __init__(
|
||||
self,
|
||||
on_talk_now: Callable[[], None],
|
||||
on_toggle_mute: Callable[[], bool],
|
||||
on_quit: Callable[[], None],
|
||||
on_set_wander: Optional[Callable[[bool], None]] = None,
|
||||
wander_enabled: bool = True,
|
||||
on_set_click_through: Optional[Callable[[bool], None]] = None,
|
||||
click_through_enabled: bool = False,
|
||||
on_set_nap: Optional[Callable[[bool], None]] = None,
|
||||
on_show_history: Optional[Callable[[], None]] = None,
|
||||
on_show_wake_tuner: Optional[Callable[[], None]] = None,
|
||||
parent=None,
|
||||
):
|
||||
super().__init__(_make_icon(muted=False), parent)
|
||||
self._on_toggle_mute = on_toggle_mute
|
||||
self._muted = False
|
||||
self._napping = False
|
||||
self.setToolTip("Bolt")
|
||||
|
||||
menu = QMenu()
|
||||
self._talk_action = QAction("Talk now", menu)
|
||||
self._talk_action.triggered.connect(on_talk_now)
|
||||
menu.addAction(self._talk_action)
|
||||
|
||||
self._mute_action = QAction("Mute mic", menu)
|
||||
self._mute_action.setCheckable(True)
|
||||
self._mute_action.triggered.connect(self._handle_toggle_mute)
|
||||
menu.addAction(self._mute_action)
|
||||
|
||||
self._nap_action = None
|
||||
if on_set_nap is not None:
|
||||
self._nap_action = QAction("Nap (no proactive noise)", menu)
|
||||
self._nap_action.setCheckable(True)
|
||||
self._nap_action.triggered.connect(lambda checked: on_set_nap(checked))
|
||||
menu.addAction(self._nap_action)
|
||||
|
||||
menu.addSeparator()
|
||||
|
||||
if on_set_wander is not None:
|
||||
self._wander_action = QAction("Wander around", menu)
|
||||
self._wander_action.setCheckable(True)
|
||||
self._wander_action.setChecked(wander_enabled)
|
||||
self._wander_action.triggered.connect(lambda checked: on_set_wander(checked))
|
||||
menu.addAction(self._wander_action)
|
||||
|
||||
if on_set_click_through is not None:
|
||||
self._click_through_action = QAction("Click through the pet", menu)
|
||||
self._click_through_action.setCheckable(True)
|
||||
self._click_through_action.setChecked(click_through_enabled)
|
||||
self._click_through_action.setToolTip(
|
||||
"Ignore the mouse entirely — control it from this menu instead."
|
||||
)
|
||||
self._click_through_action.triggered.connect(lambda checked: on_set_click_through(checked))
|
||||
menu.addAction(self._click_through_action)
|
||||
|
||||
menu.addSeparator()
|
||||
|
||||
if on_show_history is not None:
|
||||
history_action = QAction("History…", menu)
|
||||
history_action.triggered.connect(on_show_history)
|
||||
menu.addAction(history_action)
|
||||
|
||||
if on_show_wake_tuner is not None:
|
||||
tuner_action = QAction("Wake word tuning…", menu)
|
||||
tuner_action.triggered.connect(on_show_wake_tuner)
|
||||
menu.addAction(tuner_action)
|
||||
|
||||
menu.addSeparator()
|
||||
quit_action = QAction("Quit", menu)
|
||||
quit_action.triggered.connect(on_quit)
|
||||
menu.addAction(quit_action)
|
||||
|
||||
self.setContextMenu(menu)
|
||||
self.show()
|
||||
|
||||
def _handle_toggle_mute(self) -> None:
|
||||
self._muted = self._on_toggle_mute()
|
||||
self._mute_action.setChecked(self._muted)
|
||||
self._refresh_icon()
|
||||
|
||||
def set_napping(self, napping: bool) -> None:
|
||||
"""Reflect a nap the *controller* decided on (quiet hours, fullscreen,
|
||||
or a petctl command) — not just ones clicked here."""
|
||||
self._napping = napping
|
||||
if self._nap_action is not None:
|
||||
self._nap_action.setChecked(napping)
|
||||
self._refresh_icon()
|
||||
|
||||
def _refresh_icon(self) -> None:
|
||||
self.setIcon(_make_icon(self._muted, self._napping))
|
||||
if self._muted:
|
||||
self.setToolTip("Bolt (muted)")
|
||||
elif self._napping:
|
||||
self.setToolTip("Bolt (napping)")
|
||||
else:
|
||||
self.setToolTip("Bolt")
|
||||
Reference in New Issue
Block a user