mirror of
https://github.com/kuhyx/screen-locker.git
synced 2026-11-10 22:47:13 +01:00
The grab axes are grab=GrabPolicy(kind=..., disable_vt=..., preempt_weaker_holder=...) and every colour read goes through config.palette; nothing about the lock's behaviour changed. Also carries the pylint hook on 4.0.8 with the source-only scope config; the gate stays at 8.0 until the remaining findings are cleared. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01HHCbkJWXHjtTUaqvnHRAqA
139 lines
5.0 KiB
Python
139 lines
5.0 KiB
Python
"""Labelled form fields and initial focus, shared by every lock surface.
|
|
|
|
Split out of :mod:`screen_locker._ui_widgets` to keep every file under the
|
|
250-line cap. Composed back into ``UIWidgetsMixin`` there, so every host
|
|
class keeps the same surface.
|
|
|
|
Each helper builds its widget on *every* monitor (the lock spans them all),
|
|
which is why they return groups rather than single widgets.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import contextlib
|
|
import tkinter as tk
|
|
from typing import TYPE_CHECKING
|
|
|
|
from gatelock import escape_text_tab_trap
|
|
|
|
if TYPE_CHECKING:
|
|
from screen_locker._surface_group import FrameGroup, WidgetGroup
|
|
|
|
|
|
def disable_paste(widget: tk.Widget) -> None:
|
|
"""Disable paste in a Tk Entry/Text widget.
|
|
|
|
Friction-only: a determined user can still bypass via xdotool, but the
|
|
point is removing the trivial Ctrl+V shortcut so the user must
|
|
actually type their input.
|
|
"""
|
|
for sequence in ("<<Paste>>", "<Control-v>", "<Control-V>", "<Button-2>"):
|
|
with contextlib.suppress(tk.TclError, AttributeError):
|
|
widget.bind(sequence, lambda _e: "break")
|
|
|
|
|
|
class UIFormFieldsMixin:
|
|
"""Labelled entry/text fields and initial focus for the lock surfaces."""
|
|
|
|
def _add_label_entry(
|
|
self,
|
|
parent: FrameGroup,
|
|
*,
|
|
label: str,
|
|
variable: tk.StringVar,
|
|
focus: bool = False,
|
|
) -> None:
|
|
"""Add a label + single-line entry pair on every monitor.
|
|
|
|
All copies share one ``variable``, so typing on any screen is the
|
|
same text everywhere and reading it back needs no idea which monitor
|
|
the user actually used.
|
|
|
|
``focus`` gives the primary surface's copy the keyboard focus. Exactly
|
|
one field per screen should ask: the lock takes a global grab, so with
|
|
nothing focused the user's typing goes nowhere and there is no other
|
|
window on screen to make that obvious.
|
|
"""
|
|
row = parent.child_frame(bg=self._colors.palette.bg)
|
|
row.pack(pady=self._colors.space("sm"), fill="x")
|
|
row.child_widgets(
|
|
tk.Label,
|
|
text=label,
|
|
font=self._colors.font("label"),
|
|
fg=self._colors.palette.fg,
|
|
bg=self._colors.palette.bg,
|
|
anchor="w",
|
|
).pack(side="top", anchor="w")
|
|
entries = row.child_widgets(
|
|
tk.Entry,
|
|
textvariable=variable,
|
|
width=50,
|
|
font=self._colors.font("label"),
|
|
bg=self._colors.palette.field_bg,
|
|
fg=self._colors.palette.fg,
|
|
insertbackground=self._colors.palette.fg,
|
|
**self._colors.focus_kwargs(),
|
|
)
|
|
entries.pack(side="top", anchor="w", pady=self._colors.space("xs"))
|
|
for entry in entries:
|
|
disable_paste(entry)
|
|
if focus:
|
|
entries.first.focus_set()
|
|
|
|
def _add_label_text(
|
|
self,
|
|
parent: FrameGroup,
|
|
*,
|
|
label: str,
|
|
height: int = 4,
|
|
) -> WidgetGroup:
|
|
"""Add a label + multi-line ``Text`` pair on every monitor.
|
|
|
|
Use this instead of building a bare ``tk.Text``: it un-traps Tab.
|
|
Tk makes ``<Tab>`` insert a literal tab and refocus the widget, and
|
|
binds ``<Shift-Tab>`` to nothing, so an untreated multi-line field is a
|
|
keyboard dead end whose only exits (``Ctrl+Tab``/``Ctrl+Shift+Tab``) are
|
|
not advertised anywhere. On a lock surface that means a user who tabs
|
|
into it can never reach the submit button.
|
|
"""
|
|
row = parent.child_frame(bg=self._colors.palette.bg)
|
|
row.pack(pady=self._colors.space("sm"), fill="x")
|
|
row.child_widgets(
|
|
tk.Label,
|
|
text=label,
|
|
font=self._colors.font("label"),
|
|
fg=self._colors.palette.fg,
|
|
bg=self._colors.palette.bg,
|
|
anchor="w",
|
|
).pack(side="top", anchor="w")
|
|
boxes = row.child_widgets(
|
|
tk.Text,
|
|
width=60,
|
|
height=height,
|
|
wrap="word",
|
|
font=self._colors.font("label"),
|
|
bg=self._colors.palette.field_bg,
|
|
fg=self._colors.palette.fg,
|
|
insertbackground=self._colors.palette.fg,
|
|
**self._colors.focus_kwargs(),
|
|
)
|
|
boxes.pack(side="top", anchor="w", pady=self._colors.space("xs"))
|
|
for box in boxes:
|
|
disable_paste(box)
|
|
escape_text_tab_trap(box)
|
|
return boxes
|
|
|
|
def _focus_first_button(self, row: FrameGroup) -> None:
|
|
"""Give the primary surface's first button keyboard focus.
|
|
|
|
The lock takes a global grab and there is no other window on screen, so
|
|
a surface that starts with *nothing* focused leaves the user pressing
|
|
keys into the void with no visible ring to hint where focus went. The
|
|
retry screen (TRY AGAIN / I'm sick / Log Manual Workout) had exactly
|
|
that problem, because ``on_focus_ready`` is a documented no-op.
|
|
"""
|
|
for child in row.first.winfo_children():
|
|
if child.winfo_class() == "Button":
|
|
child.focus_set()
|
|
return
|