From 419674b5af7a910c469a1bee787bdb1e6a32d7b0 Mon Sep 17 00:00:00 2001 From: Dominik Roth Date: Sat, 8 Aug 2026 23:15:40 +0200 Subject: [PATCH] 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. --- src/fenigma/app.py | 66 +++++++++++++++++++++++++++++++++++++++++++ src/fenigma/models.py | 11 ++++++++ 2 files changed, 77 insertions(+) diff --git a/src/fenigma/app.py b/src/fenigma/app.py index 03e9495..d6698cc 100644 --- a/src/fenigma/app.py +++ b/src/fenigma/app.py @@ -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: diff --git a/src/fenigma/models.py b/src/fenigma/models.py index a3175cb..e8b9178 100644 --- a/src/fenigma/models.py +++ b/src/fenigma/models.py @@ -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