diet-guard/diet_guard/_gatelock_calendar_schedule.py
Krzysztof kuhy Rudnicki c0564c05a2 Let the PC gate set the eating window and meal count
The second edit surface, so the schedule is editable from either device the
way the budget already is. A row on the History tab beside the budget row,
with the same Edit/Save toggle: first hour, last hour, meals per day, and the
derived checkpoint times underneath so the effect of a change is visible
before it is saved.

Validated rather than clamped. MealSchedule.normalized() would happily turn
"20 to 8" into a two-meal day ending at 09:00, which is a sensible defence
against a peer's corrupt sync data and a terrible response to a typo at the
keyboard. Each rejection says what is wrong and leaves the row in edit mode so
the value can be corrected instead of silently becoming a different schedule.
The narrow-window case has its own message, because that one is genuinely
non-obvious: six meals between 08:00 and 11:00 would round two checkpoints
onto the same hour, and a duplicate slot silently drops one.

Three structural notes:

- The row's widgets are their own module. _gatelock_calendar_widgets.py was at
  216 of the 250-line cap and is now 233; adding the row inline would have
  breached it.
- The five new StringVars are grouped in a ScheduleVars dataclass rather than
  flattened onto CalendarVars, which pylint flagged at 11 attributes. Grouping
  two unrelated concerns separately is the better shape anyway.
- _gate_fixtures.py's _GATE_TK_MODULES is a hand-listed patch target, so both
  new modules are registered there. Left out, they build real tk widgets under
  a fake parent -- which is how it failed the first time, loudly, rather than
  silently opening a window.

785 tests, 100% branch coverage.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_0159b4E5hh512jYUeaJVNxfM
2026-08-16 15:02:35 +02:00

110 lines
3.2 KiB
Python

"""The History tab's meal-schedule row widgets.
Split out of :mod:`._gatelock_calendar_widgets` (216 lines against the repo's
250-line cap) rather than added to it. The behaviour behind these widgets
lives in :mod:`._gatelock_scheduleedit`, mirroring how the budget row's
widgets and its edit/save logic are split.
"""
from __future__ import annotations
import tkinter as tk
from typing import TYPE_CHECKING
from gatelock import make_button
from diet_guard._gatelock_calendar_types import _COLORS, _FIELD_BG
from diet_guard._gatelock_spacing import SM, XS
from diet_guard._gatelock_typography import BODY, LABEL
from diet_guard._gatelock_ui import BG, FG
if TYPE_CHECKING:
from collections.abc import Callable
from diet_guard._gatelock_calendar_types import CalendarVars
__all__ = ["build_schedule_row"]
_ENTRY_WIDTH = 4
def _spin_entry(row: tk.Frame, variable: tk.StringVar) -> tk.Entry:
"""Return one read-only-by-default numeric entry in the schedule row."""
entry = tk.Entry(
row,
textvariable=variable,
font=(_COLORS.font_family, BODY),
width=_ENTRY_WIDTH,
bg=_FIELD_BG,
fg=FG,
insertbackground=FG,
justify="center",
state="readonly",
readonlybackground=_FIELD_BG,
)
entry.pack(side="left", padx=(XS, SM), ipady=XS)
return entry
def build_schedule_row(
parent: tk.Frame,
vars_: CalendarVars,
on_edit_or_save_schedule: Callable[[], None],
) -> tuple[tk.Entry, tk.Entry, tk.Entry, tk.Button, tk.Label]:
"""Build the meal-schedule row.
Returns the three hour/count entries, the edit button, and the status
label, in that order. Like the budget row the entries start read-only:
the schedule is displayed but not directly typeable until "Edit".
Returns:
``(first_entry, last_entry, count_entry, edit_button, status_label)``.
"""
row = tk.Frame(parent, bg=BG)
row.pack(pady=(SM, XS))
tk.Label(
row,
text="Meals:",
font=(_COLORS.font_family, LABEL),
bg=BG,
fg=FG,
).pack(side="left")
first_entry = _spin_entry(row, vars_.schedule.first)
tk.Label(
row,
text="to",
font=(_COLORS.font_family, LABEL),
bg=BG,
fg=FG,
).pack(side="left")
last_entry = _spin_entry(row, vars_.schedule.last)
tk.Label(
row,
text="x",
font=(_COLORS.font_family, LABEL),
bg=BG,
fg=FG,
).pack(side="left")
count_entry = _spin_entry(row, vars_.schedule.count)
edit_button = make_button(row, _COLORS, "Edit", on_edit_or_save_schedule)
edit_button.pack(side="left")
# The derived checkpoint times, so the effect of a change is visible
# before it is saved rather than only after the next lock.
tk.Label(
parent,
textvariable=vars_.schedule.times,
font=(_COLORS.font_family, LABEL),
bg=BG,
fg=FG,
).pack(pady=(0, XS))
status_label = tk.Label(
parent,
textvariable=vars_.schedule.status,
font=(_COLORS.font_family, LABEL),
bg=BG,
fg=FG,
)
status_label.pack(pady=(0, XS))
return first_entry, last_entry, count_entry, edit_button, status_label