Repository navigation
gui: Correct a read-back mismatch in fixed boxes #109
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: bails-v1-pin
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,10 +1,10 @@ | ||
| """The one field that interprets codex32 keystrokes.""" | ||
| """The fields that interpret codex32 keystrokes.""" | ||
|
|
||
| from __future__ import annotations | ||
|
|
||
| from dataclasses import replace | ||
|
|
||
| from gi.repository import GLib, Gtk | ||
| from gi.repository import Gdk, GLib, Gtk | ||
|
|
||
| from codex32.profiles.ms32 import TEXT_LENGTHS | ||
| from codex32_gui.reading import ( | ||
|
|
@@ -21,7 +21,39 @@ | |
| ) | ||
|
|
||
|
|
||
| class Codex32Entry(Gtk.Entry): | ||
| class PrivateEntry(Gtk.Entry): | ||
| """An entry whose selected text does not stay on the primary clipboard.""" | ||
|
|
||
| def __init__(self, **properties: object) -> None: | ||
| super().__init__(**properties) | ||
| self._watching = 0 | ||
| self.connect("realize", lambda _entry: self._watch(True)) | ||
| self.connect("unrealize", lambda _entry: self._watch(False)) | ||
|
|
||
| def _watch(self, start: bool) -> None: | ||
| # Selecting text hands it to the primary selection, where a clipboard | ||
| # manager would copy the card into a history file on disk. GTK publishes | ||
| # it again on focus and on any selection, not only when the selection | ||
| # moves, so the clipboard itself is watched while this entry is on screen | ||
| # and the copy is taken back once GTK has finished. The selection still works. | ||
| primary = self.get_primary_clipboard() | ||
| if start: | ||
| self._watching = primary.connect("changed", self._unpublish) | ||
| elif self._watching: | ||
| primary.disconnect(self._watching) | ||
| self._watching = 0 | ||
|
|
||
| def _unpublish(self, primary: Gdk.Clipboard) -> None: | ||
| if primary.is_local() and primary.get_content() is not None and self.get_selection_bounds(): | ||
| GLib.idle_add(self._drop, primary) | ||
|
|
||
| @staticmethod | ||
| def _drop(primary: Gdk.Clipboard) -> bool: | ||
| primary.set_content(None) | ||
| return False | ||
|
|
||
|
|
||
| class Codex32Entry(PrivateEntry): | ||
| """A Gtk.Entry that can only ever hold grouped uppercase codex32 text.""" | ||
|
|
||
| __gtype_name__ = "Codex32Entry" | ||
|
|
@@ -30,14 +62,12 @@ def __init__(self, *, accepted: tuple[str, ...] = (), length: int | None = None) | |
| super().__init__() | ||
| self._accepted, self._length, self._pending = accepted, length, 0 | ||
| self._limit = (length or TEXT_LENGTHS[-1]) + SLACK | ||
| self._dropped, self._rewriting, self._unpublishing = "", False, 0 | ||
| self._dropped, self._rewriting = "", False | ||
| self.set_hexpand(True) | ||
| self.add_css_class("card-entry") | ||
| self.set_input_hints(Gtk.InputHints.NO_SPELLCHECK | Gtk.InputHints.NO_EMOJI) | ||
| self.set_text(PREFIX) | ||
| self.connect("changed", self._schedule) | ||
| self.connect("notify::selection-bound", self._unpublish) | ||
| self.connect("notify::cursor-position", self._unpublish) | ||
|
|
||
| def prefill(self, text: str) -> None: | ||
| """Offer a known header that the operator may still overtype.""" | ||
|
|
@@ -56,21 +86,6 @@ def clear(self) -> None: | |
| self._dropped = "" | ||
| self.set_text(PREFIX) | ||
|
|
||
| def _unpublish(self, *_arguments: object) -> None: | ||
| # Selecting text in a Gtk.Entry hands it to the primary selection, where a | ||
| # clipboard manager would copy the card into a history file on disk. The | ||
| # selection still works; only the copy that leaves the program is taken | ||
| # back, once GTK has finished publishing it. | ||
| if self.get_selection_bounds() and not self._unpublishing: | ||
| self._unpublishing = GLib.idle_add(self._drop_primary) | ||
|
|
||
| def _drop_primary(self) -> bool: | ||
| self._unpublishing = 0 | ||
| display = self.get_display() | ||
| if display is not None: | ||
| display.get_primary_clipboard().set_content(None) | ||
| return False | ||
|
|
||
| def _schedule(self, _entry: Gtk.Entry) -> None: | ||
| # One edit reaches the buffer as a deletion and then an insertion. Reformat | ||
| # once the whole edit has settled, so no intermediate state is rewritten, | ||
|
|
@@ -94,3 +109,38 @@ def _reformat(self) -> bool: | |
| self._rewriting = False | ||
| self.set_position(kept + max(kept - 1, 0) // GROUP) | ||
| return False | ||
|
|
||
|
|
||
| class GroupEntry(PrivateEntry): | ||
| """One group of a card being corrected in place, shown in capitals.""" | ||
|
|
||
| __gtype_name__ = "GroupEntry" | ||
|
|
||
| def __init__(self, text: str) -> None: | ||
| super().__init__( | ||
| text=text, width_chars=GROUP + 1, max_width_chars=GROUP + 1, max_length=GROUP + SLACK | ||
| ) | ||
| self.add_css_class("card-entry") | ||
| self.set_input_hints(Gtk.InputHints.NO_SPELLCHECK | Gtk.InputHints.NO_EMOJI) | ||
| refuse_paste(self) | ||
| self.connect("changed", lambda _entry: GLib.idle_add(self._capitals)) | ||
|
Comment on lines
+123
to
+126
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
When the operator selects text in a highlighted mismatch box—for example, before replacing the group— AGENTS.md reference: AGENTS.md:L78-L82 Useful? React with 👍 / 👎. |
||
|
|
||
| def _capitals(self) -> bool: | ||
| position, text = self.get_position(), self.get_text() | ||
| if text != text.upper(): | ||
| self.set_text(text.upper()) | ||
| self.set_position(position) | ||
| return False | ||
|
|
||
|
|
||
| def refuse_paste(entry: Gtk.Entry) -> None: | ||
| """Take only keystrokes, so a card is read back from the paper and not pasted from elsewhere.""" | ||
| text = entry.get_delegate() | ||
| text.connect("paste-clipboard", lambda text: text.stop_emission_by_name("paste-clipboard")) | ||
| middle = Gtk.GestureClick(button=2, propagation_phase=Gtk.PropagationPhase.CAPTURE) # Primary paste | ||
| middle.connect("pressed", lambda gesture, *_: gesture.set_state(Gtk.EventSequenceState.CLAIMED)) | ||
| entry.add_controller(middle) | ||
| controllers = text.observe_controllers() | ||
| for controller in [controllers.get_item(i) for i in range(controllers.get_n_items())]: | ||
| if isinstance(controller, Gtk.DropTarget): # Dragged-in text | ||
| text.remove_controller(controller) | ||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This note records the read-back rework, but the review map immediately above still lists
reading.pyandentry.pyat 131 and 73 logical lines; using the test's counting rule they are now 154 and 111. It also still says only the first three modules make decisions even thoughentry.pynow owns the primary-selection and paste-refusal security behavior, so reviewers following the documented order can under-review the newly added boundary logic. Update the table, totals, and review guidance alongside the budget rationale.Useful? React with 👍 / 👎.