Add clear-board button and clipboard auto-watch toggle

Board.clear() drops the Nest position, spotters, reference points, and
targets; wired to a header button behind a confirm dialog since it's
not reversible.

The clipboard-watch toggle connects to Gdk.Clipboard's 'changed'
signal and re-runs the merge-all OCR flow automatically whenever the
clipboard gains new image content, skipping non-image changes (e.g.
text copied elsewhere) so it doesn't spam toasts. Off by default.
Disconnected on window destroy.
This commit is contained in:
Dominik Moritz Roth 2026-08-08 23:15:40 +02:00
parent 139601b107
commit 419674b5af
2 changed files with 77 additions and 0 deletions

View File

@ -152,6 +152,7 @@ class MainWindow(Adw.ApplicationWindow):
_install_css()
self.board = Board()
self._clipboard_watch_handler = None
self.toast_overlay = Adw.ToastOverlay()
self.set_content(self.toast_overlay)
@ -178,6 +179,13 @@ class MainWindow(Adw.ApplicationWindow):
clip_btn.connect("clicked", lambda _b: self._fetch_clipboard())
header.pack_start(clip_btn)
self._watch_btn = Gtk.ToggleButton(icon_name="media-playback-start-symbolic")
self._watch_btn.set_tooltip_text(
"Auto-watch clipboard: apply new screenshots as soon as they're copied"
)
self._watch_btn.connect("toggled", self._on_toggle_clipboard_watch)
header.pack_start(self._watch_btn)
header.pack_start(Gtk.Separator(orientation=Gtk.Orientation.VERTICAL))
for label, popover_builder in (
@ -193,6 +201,11 @@ class MainWindow(Adw.ApplicationWindow):
strike_btn.connect("clicked", lambda _b: self._add_strike())
header.pack_start(strike_btn)
clear_btn = Gtk.Button(icon_name="edit-clear-all-symbolic")
clear_btn.set_tooltip_text("Clear board (drop everything)")
clear_btn.connect("clicked", lambda _b: self._clear_board())
header.pack_start(clear_btn)
firing_btn = Gtk.Button(icon_name="sidebar-show-right-symbolic")
firing_btn.set_tooltip_text("Firing commands")
firing_btn.connect("clicked", lambda _b: self._toggle_firing_panel())
@ -235,6 +248,7 @@ class MainWindow(Adw.ApplicationWindow):
)
)
self.add_controller(controller)
self.connect("destroy", self._on_window_destroy)
# -- generic helpers -----------------------------------------------------
def _make_menu_button(self, label: str, build_popover) -> Gtk.MenuButton:
@ -334,6 +348,58 @@ class MainWindow(Adw.ApplicationWindow):
on_parsed(info)
def _on_toggle_clipboard_watch(self, btn: Gtk.ToggleButton) -> None:
"""Auto-watch toggle: while on, every clipboard change that looks
like an image (not e.g. text copied elsewhere) is OCR'd and merged
automatically, no manual fetch needed between screenshots."""
clipboard = Gdk.Display.get_default().get_clipboard()
if btn.get_active():
self._clipboard_watch_handler = clipboard.connect("changed", self._on_clipboard_changed)
btn.set_icon_name("media-playback-stop-symbolic")
btn.set_tooltip_text("Auto-watch clipboard: on, click to stop")
self.toast("Watching clipboard, new screenshots apply automatically.")
else:
if self._clipboard_watch_handler is not None:
clipboard.disconnect(self._clipboard_watch_handler)
self._clipboard_watch_handler = None
btn.set_icon_name("media-playback-start-symbolic")
btn.set_tooltip_text("Auto-watch clipboard: apply new screenshots as soon as they're copied")
def _on_clipboard_changed(self, clipboard: Gdk.Clipboard) -> None:
formats = clipboard.get_formats()
if formats is None or not formats.contain_gtype(Gdk.Texture):
return # not an image (e.g. text copied elsewhere), ignore quietly
self._run_ocr_from_clipboard(self._merge_all)
def _on_window_destroy(self, *_a) -> None:
if self._clipboard_watch_handler is not None:
Gdk.Display.get_default().get_clipboard().disconnect(self._clipboard_watch_handler)
self._clipboard_watch_handler = None
def _clear_board(self) -> None:
board = self.board
if board.nest.coord is None and not board.spotters and not board.reference_points and not board.targets:
return # nothing to clear
dialog = Adw.AlertDialog(
heading="Clear board?",
body="Drops the Nest position and every spotter, reference point, and target. This can't be undone.",
)
dialog.add_response("cancel", "Cancel")
dialog.add_response("clear", "Clear")
dialog.set_response_appearance("clear", Adw.ResponseAppearance.DESTRUCTIVE)
dialog.set_default_response("cancel")
dialog.set_close_response("cancel")
dialog.connect("response", self._on_clear_board_response)
dialog.present(self)
def _on_clear_board_response(self, _dialog, response: str) -> None:
if response != "clear":
return
self.board.clear()
self._set_selection(None)
self._refresh()
self.toast("Board cleared.")
def _merge_all(self, info: "ocr.ParsedInfo") -> None:
changed = []
if info.nest_coord is not None:

View File

@ -384,6 +384,17 @@ class Board:
def remove_target(self, target: Target) -> None:
self.targets.remove(target)
# -- reset ----------------------------------------------------------
def clear(self) -> None:
"""Drop everything: Nest position, spotters, reference points,
targets. Used by the "clear board" action for a fresh start
without restarting the app."""
self.nest = Nest()
self.spotters.clear()
self.reference_points.clear()
self.targets.clear()
self._spotter_seq = 0
def reorder_target(self, target: Target, new_index: int) -> None:
"""Manual drag-order: `self.targets`' list order is itself the
persisted order (saved/loaded as a plain JSON array), and is what