mirror of
https://github.com/kuhyx/wake-alarm.git
synced 2026-11-11 14:27:32 +01:00
Split _alarm_view.py: the fan-out proxies move to _view_proxies.py
329 -> 207. The proxies are pure plumbing over already-built widgets — nothing in them constructs Tk, which is what makes them separable from the builders that do. One trap the split surfaced: `tk` had to stay a RUNTIME import in the new module, not a TYPE_CHECKING one. `tk.TclError` is caught in a suppress() there, so deferring the import raised NameError the first time a widget call failed — i.e. exactly the path these proxies exist to swallow. 48 tests caught it, loudly. 465 passed at 100% coverage. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EckWmyfetYq3Mi8MAnUudh
This commit is contained in:
parent
7f24ed4fa6
commit
64ee81f077
@ -2,7 +2,6 @@
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import contextlib
|
||||
from dataclasses import dataclass
|
||||
import tkinter as tk
|
||||
from typing import TYPE_CHECKING
|
||||
@ -10,6 +9,11 @@ from typing import TYPE_CHECKING
|
||||
from gatelock import bind_activate
|
||||
|
||||
from wake_alarm._constants import DISMISS_ROUNDS_REQUIRED
|
||||
from wake_alarm._view_proxies import _EntryProxy, _LabelProxy, _ViewGroup
|
||||
|
||||
# Re-imported so existing references keep resolving; __all__ stops ruff
|
||||
# deleting them as unused.
|
||||
__all__ = ["_EntryProxy", "_LabelProxy", "_ViewGroup"]
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from collections.abc import Callable
|
||||
@ -55,132 +59,6 @@ class _AlarmView:
|
||||
output_name: str = ""
|
||||
|
||||
|
||||
class _LabelProxy:
|
||||
"""Fans one ``configure`` call out to the same label on every monitor."""
|
||||
|
||||
def __init__(self, widgets: list[tk.Label]) -> None:
|
||||
"""Wrap the per-monitor copies of one label."""
|
||||
self._widgets = widgets
|
||||
|
||||
def configure(self, **kwargs: str) -> None:
|
||||
"""Apply the same configuration to every copy."""
|
||||
for widget in self._widgets:
|
||||
with contextlib.suppress(tk.TclError):
|
||||
widget.configure(**kwargs)
|
||||
|
||||
|
||||
class _EntryProxy:
|
||||
"""The dismiss-code entry, as seen across every monitor.
|
||||
|
||||
All copies share one root-mastered ``StringVar``, so typing on whichever
|
||||
screen the user is actually looking at is the same text everywhere -- and
|
||||
reading it needs no idea of which monitor that was.
|
||||
"""
|
||||
|
||||
def __init__(self, widgets: list[tk.Entry], var: tk.StringVar) -> None:
|
||||
"""Wrap the per-monitor entries and the variable behind them."""
|
||||
self._widgets = widgets
|
||||
self._var = var
|
||||
|
||||
def get(self) -> str:
|
||||
"""Return the shared text, whichever monitor it was typed on."""
|
||||
return self._var.get()
|
||||
|
||||
def delete(self, _first: object, _last: object = None) -> None:
|
||||
"""Clear the shared text on every monitor."""
|
||||
self._var.set("")
|
||||
|
||||
def focus_force(self) -> None:
|
||||
"""Focus the first entry that still exists."""
|
||||
for widget in self._widgets:
|
||||
with contextlib.suppress(tk.TclError):
|
||||
widget.focus_force()
|
||||
return
|
||||
|
||||
|
||||
class _ViewGroup:
|
||||
"""Presents N per-monitor views as if they were one.
|
||||
|
||||
Every label update fans out to all monitors, and the challenge can be
|
||||
answered on any of them -- solving it anywhere dismisses the alarm
|
||||
everywhere, because every entry is the same variable and every submit
|
||||
button is the same callable.
|
||||
"""
|
||||
|
||||
def __init__(self, config: LockConfig, entry_var: tk.StringVar) -> None:
|
||||
"""Start empty; surfaces are added as outputs come up."""
|
||||
self.config = config
|
||||
self._entry_var = entry_var
|
||||
self._views: list[_AlarmView] = []
|
||||
|
||||
def add(self, view: _AlarmView) -> None:
|
||||
"""Register a newly built per-monitor view."""
|
||||
self._views.append(view)
|
||||
|
||||
def discard(self, output_name: str) -> None:
|
||||
"""Forget the view for an output that went dark."""
|
||||
self._views = [v for v in self._views if v.output_name != output_name]
|
||||
|
||||
@property
|
||||
def surfaces(self) -> list[_AlarmView]:
|
||||
"""Every per-monitor view, for callers needing more than one widget."""
|
||||
return list(self._views)
|
||||
|
||||
@property
|
||||
def containers(self) -> list[tk.Frame]:
|
||||
"""Every per-monitor container frame."""
|
||||
return [view.container for view in self._views]
|
||||
|
||||
@property
|
||||
def entry_var(self) -> tk.StringVar:
|
||||
"""The one variable every monitor's entry is bound to."""
|
||||
return self._entry_var
|
||||
|
||||
@property
|
||||
def entry(self) -> _EntryProxy:
|
||||
"""The dismiss-code entry across all monitors."""
|
||||
return _EntryProxy([v.entry for v in self._views], self._entry_var)
|
||||
|
||||
def _label(self, name: str) -> _LabelProxy:
|
||||
"""Build a proxy over one named label on every monitor."""
|
||||
return _LabelProxy([getattr(view.labels, name) for view in self._views])
|
||||
|
||||
@property
|
||||
def title_label(self) -> _LabelProxy:
|
||||
"""The "WAKE UP!" heading on every monitor."""
|
||||
return self._label("title")
|
||||
|
||||
@property
|
||||
def round_label(self) -> _LabelProxy:
|
||||
"""The round counter on every monitor."""
|
||||
return self._label("round_counter")
|
||||
|
||||
@property
|
||||
def info_label(self) -> _LabelProxy:
|
||||
"""The challenge hint on every monitor."""
|
||||
return self._label("info")
|
||||
|
||||
@property
|
||||
def code_label(self) -> _LabelProxy:
|
||||
"""The challenge display on every monitor."""
|
||||
return self._label("code")
|
||||
|
||||
@property
|
||||
def status_label(self) -> _LabelProxy:
|
||||
"""The status line on every monitor."""
|
||||
return self._label("status")
|
||||
|
||||
@property
|
||||
def timer_label(self) -> _LabelProxy:
|
||||
"""The countdown on every monitor."""
|
||||
return self._label("timer")
|
||||
|
||||
@property
|
||||
def escape_buttons(self) -> list[tk.Button]:
|
||||
"""The escape-hatch button on every monitor."""
|
||||
return [view.escape_button for view in self._views]
|
||||
|
||||
|
||||
def _build_challenge(
|
||||
parent: tk.Misc,
|
||||
challenge: _Challenge,
|
||||
|
||||
147
wake_alarm/_view_proxies.py
Normal file
147
wake_alarm/_view_proxies.py
Normal file
@ -0,0 +1,147 @@
|
||||
"""Fan-out proxies — one call, applied to the same widget on every monitor.
|
||||
|
||||
Split out of ``_alarm_view.py`` for the 250-line cap. These are pure plumbing
|
||||
over already-built widgets: nothing here constructs Tk, which is what makes
|
||||
them separable from the builders that do.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import contextlib
|
||||
|
||||
# Runtime import, not TYPE_CHECKING: tk.TclError is caught in a suppress()
|
||||
# below, so a deferred import would raise NameError the first time a widget
|
||||
# call fails — which is exactly the path these proxies exist to swallow.
|
||||
import tkinter as tk
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from gatelock import LockConfig
|
||||
|
||||
from wake_alarm._alarm_view import _AlarmView
|
||||
|
||||
|
||||
class _LabelProxy:
|
||||
"""Fans one ``configure`` call out to the same label on every monitor."""
|
||||
|
||||
def __init__(self, widgets: list[tk.Label]) -> None:
|
||||
"""Wrap the per-monitor copies of one label."""
|
||||
self._widgets = widgets
|
||||
|
||||
def configure(self, **kwargs: str) -> None:
|
||||
"""Apply the same configuration to every copy."""
|
||||
for widget in self._widgets:
|
||||
with contextlib.suppress(tk.TclError):
|
||||
widget.configure(**kwargs)
|
||||
|
||||
|
||||
class _EntryProxy:
|
||||
"""The dismiss-code entry, as seen across every monitor.
|
||||
|
||||
All copies share one root-mastered ``StringVar``, so typing on whichever
|
||||
screen the user is actually looking at is the same text everywhere -- and
|
||||
reading it needs no idea of which monitor that was.
|
||||
"""
|
||||
|
||||
def __init__(self, widgets: list[tk.Entry], var: tk.StringVar) -> None:
|
||||
"""Wrap the per-monitor entries and the variable behind them."""
|
||||
self._widgets = widgets
|
||||
self._var = var
|
||||
|
||||
def get(self) -> str:
|
||||
"""Return the shared text, whichever monitor it was typed on."""
|
||||
return self._var.get()
|
||||
|
||||
def delete(self, _first: object, _last: object = None) -> None:
|
||||
"""Clear the shared text on every monitor."""
|
||||
self._var.set("")
|
||||
|
||||
def focus_force(self) -> None:
|
||||
"""Focus the first entry that still exists."""
|
||||
for widget in self._widgets:
|
||||
with contextlib.suppress(tk.TclError):
|
||||
widget.focus_force()
|
||||
return
|
||||
|
||||
|
||||
class _ViewGroup:
|
||||
"""Presents N per-monitor views as if they were one.
|
||||
|
||||
Every label update fans out to all monitors, and the challenge can be
|
||||
answered on any of them -- solving it anywhere dismisses the alarm
|
||||
everywhere, because every entry is the same variable and every submit
|
||||
button is the same callable.
|
||||
"""
|
||||
|
||||
def __init__(self, config: LockConfig, entry_var: tk.StringVar) -> None:
|
||||
"""Start empty; surfaces are added as outputs come up."""
|
||||
self.config = config
|
||||
self._entry_var = entry_var
|
||||
self._views: list[_AlarmView] = []
|
||||
|
||||
def add(self, view: _AlarmView) -> None:
|
||||
"""Register a newly built per-monitor view."""
|
||||
self._views.append(view)
|
||||
|
||||
def discard(self, output_name: str) -> None:
|
||||
"""Forget the view for an output that went dark."""
|
||||
self._views = [v for v in self._views if v.output_name != output_name]
|
||||
|
||||
@property
|
||||
def surfaces(self) -> list[_AlarmView]:
|
||||
"""Every per-monitor view, for callers needing more than one widget."""
|
||||
return list(self._views)
|
||||
|
||||
@property
|
||||
def containers(self) -> list[tk.Frame]:
|
||||
"""Every per-monitor container frame."""
|
||||
return [view.container for view in self._views]
|
||||
|
||||
@property
|
||||
def entry_var(self) -> tk.StringVar:
|
||||
"""The one variable every monitor's entry is bound to."""
|
||||
return self._entry_var
|
||||
|
||||
@property
|
||||
def entry(self) -> _EntryProxy:
|
||||
"""The dismiss-code entry across all monitors."""
|
||||
return _EntryProxy([v.entry for v in self._views], self._entry_var)
|
||||
|
||||
def _label(self, name: str) -> _LabelProxy:
|
||||
"""Build a proxy over one named label on every monitor."""
|
||||
return _LabelProxy([getattr(view.labels, name) for view in self._views])
|
||||
|
||||
@property
|
||||
def title_label(self) -> _LabelProxy:
|
||||
"""The "WAKE UP!" heading on every monitor."""
|
||||
return self._label("title")
|
||||
|
||||
@property
|
||||
def round_label(self) -> _LabelProxy:
|
||||
"""The round counter on every monitor."""
|
||||
return self._label("round_counter")
|
||||
|
||||
@property
|
||||
def info_label(self) -> _LabelProxy:
|
||||
"""The challenge hint on every monitor."""
|
||||
return self._label("info")
|
||||
|
||||
@property
|
||||
def code_label(self) -> _LabelProxy:
|
||||
"""The challenge display on every monitor."""
|
||||
return self._label("code")
|
||||
|
||||
@property
|
||||
def status_label(self) -> _LabelProxy:
|
||||
"""The status line on every monitor."""
|
||||
return self._label("status")
|
||||
|
||||
@property
|
||||
def timer_label(self) -> _LabelProxy:
|
||||
"""The countdown on every monitor."""
|
||||
return self._label("timer")
|
||||
|
||||
@property
|
||||
def escape_buttons(self) -> list[tk.Button]:
|
||||
"""The escape-hatch button on every monitor."""
|
||||
return [view.escape_button for view in self._views]
|
||||
Loading…
Reference in New Issue
Block a user