From a472ce62bae0dbae34fa3b1fa3ff387dce3d5df7 Mon Sep 17 00:00:00 2001 From: mARTin-B78 Date: Mon, 8 Jun 2026 19:30:08 +0200 Subject: [PATCH] settings: lazy-build tabs (instant open); move connection dots beside the field; single-instance Settings dialog; Release 1.9.1 - gtksettings: build each notebook tab on first view instead of all up front, so the dialog opens instantly (was ~1.3s building Input/Benchmark file-choosers); _collect() force-builds unvisited tabs before saving so no field is missed. - gtksettings: connection dot now sits left of the URL entry (like the Engines tab) instead of at the far right. - gtkui: open_settings raises the existing dialog instead of opening a second. Co-Authored-By: Claude Opus 4.8 (1M context) --- linux/CHANGELOG.md | 15 +++++++++++++ linux/blitztext/__init__.py | 2 +- linux/blitztext/gtksettings.py | 41 ++++++++++++++++++++++++++-------- linux/blitztext/gtkui.py | 11 ++++++++- 4 files changed, 58 insertions(+), 11 deletions(-) diff --git a/linux/CHANGELOG.md b/linux/CHANGELOG.md index 5f5c139..f84ad00 100644 --- a/linux/CHANGELOG.md +++ b/linux/CHANGELOG.md @@ -9,6 +9,21 @@ The version is defined in [`blitztext/__init__.py`](blitztext/__init__.py). ## [Unreleased] +## [1.9.1] - 2026-06-08 + +### Changed +- **Settings opens instantly.** Each tab's contents are now built the first time + you view it instead of all up front, so the dialog no longer pauses ~1.3s + constructing the file-choosers in the Input/Benchmark tabs. Saving force-builds + any tab you didn't visit first, so no field is ever missed. +- **Connection dots moved beside their field.** The Wakeword and TTS reachability + dots now sit just left of the URL entry (matching the Engines tab) instead of + at the far right of the row. + +### Fixed +- **Settings could be opened more than once.** Choosing Settings while it's + already open now raises the existing window instead of stacking a second copy. + ## [1.9.0] - 2026-06-08 ### Added diff --git a/linux/blitztext/__init__.py b/linux/blitztext/__init__.py index 412abe7..959baf1 100644 --- a/linux/blitztext/__init__.py +++ b/linux/blitztext/__init__.py @@ -6,4 +6,4 @@ counterpart to the macOS Blitztext menu bar app: it runs natively on the host (not in a container) so it can type into any application via xdotool. """ -__version__ = "1.9.0" +__version__ = "1.9.1" diff --git a/linux/blitztext/gtksettings.py b/linux/blitztext/gtksettings.py index 6eeb05a..c067d11 100644 --- a/linux/blitztext/gtksettings.py +++ b/linux/blitztext/gtksettings.py @@ -256,6 +256,8 @@ def _url_field(parent: Gtk.Box, label: str, placeholder: str, on_reload, row = Gtk.Box(spacing=10); row.set_margin_top(3); row.set_margin_bottom(3) lbl = Gtk.Label(label=label, xalign=0.0); lbl.set_size_request(130, -1) row.pack_start(lbl, False, False, 0) + if dot is not None: + row.pack_start(dot, False, False, 0) # connection dot beside the field (left), like Engines e = Gtk.Entry(); e.set_hexpand(True) if placeholder: e.set_placeholder_text(placeholder) @@ -272,8 +274,6 @@ def _url_field(parent: Gtk.Box, label: str, placeholder: str, on_reload, btn.get_accessible().set_name("Refresh models") btn.connect("clicked", lambda _b: on_reload()) row.pack_start(btn, False, False, 0) - if dot is not None: - row.pack_start(dot, False, False, 0) parent.pack_start(row, False, False, 0) return e @@ -340,13 +340,22 @@ class SettingsDialog: nb = Gtk.Notebook() self.dlg.get_content_area().pack_start(nb, True, True, 0) - self._build_presets(_page(nb, "Presets")) - self._build_engines(_page(nb, "Engines")) - self._build_input(_page(nb, "Input")) - self._build_general(_page(nb, "General")) - self._build_benchmark(_page(nb, "Benchmark")) - self._build_log(_page(nb, "Log")) - self._build_about(_page(nb, "About")) + # Build each tab lazily on first view so the dialog opens instantly. The + # heavy bits are the Gtk.FileChooserButton pickers in Input/Benchmark + # (~0.2s each to construct); deferring them until you visit that tab keeps + # the initial open near-instant. _collect() force-builds any unvisited tab + # before saving, so no field is ever missed. + self._pending_tabs: dict = {} + for title, builder in (("Presets", self._build_presets), + ("Engines", self._build_engines), + ("Input", self._build_input), + ("General", self._build_general), + ("Benchmark", self._build_benchmark), + ("Log", self._build_log), + ("About", self._build_about)): + self._pending_tabs[_page(nb, title)] = builder + self._build_tab(nb.get_nth_page(0)) # the visible tab, eagerly + nb.connect("switch-page", lambda _nb, page, _n: self._build_tab(page)) self._bind_entry = None self._bind_pressed: list[str] = [] @@ -355,6 +364,18 @@ class SettingsDialog: self.dlg.connect("response", self._on_response) self.dlg.connect("destroy", lambda *_: self._cleanup()) + def _build_tab(self, page: Gtk.Box) -> None: + """Build a notebook tab's contents the first time it's shown (lazy).""" + builder = self._pending_tabs.pop(page, None) + if builder is not None: + builder(page) + page.show_all() # the dialog may already be on-screen + + def _force_build_tabs(self) -> None: + """Build any not-yet-visited tabs so _collect() sees every field.""" + for page in list(self._pending_tabs): + self._build_tab(page) + # ===== Presets ========================================================== def _build_presets(self, page: Gtk.Box) -> None: _infobox(page, "Presets are your dictation actions. Pick one to edit it, or add your " @@ -1495,6 +1516,8 @@ class SettingsDialog: d.run(); d.destroy() def _on_response(self, dlg, resp): + if resp in (RESP_SAVE, RESP_SAVE_RESTART): + self._force_build_tabs() # ensure every tab's fields exist if resp == RESP_SAVE: if self._collect(): save(self.cfg) diff --git a/linux/blitztext/gtkui.py b/linux/blitztext/gtkui.py index 6e55d35..16eb4b4 100644 --- a/linux/blitztext/gtkui.py +++ b/linux/blitztext/gtkui.py @@ -366,9 +366,18 @@ class App: # -- panel / settings / lifecycle ----------------------------------------- def open_settings(self) -> None: + # Single instance: if Settings is already open, bring it to the front + # instead of spawning a second dialog (the tray menu bypasses the + # dialog's own modality, so it could otherwise be opened repeatedly). + existing = getattr(self, "_settings", None) + if existing is not None: + existing.dlg.present() + return from .gtksettings import SettingsDialog - SettingsDialog(self.win, self.cfg, daemon=self.daemon).run_dialog() + self._settings = SettingsDialog(self.win, self.cfg, daemon=self.daemon) + self._settings.dlg.connect("destroy", lambda *_: setattr(self, "_settings", None)) + self._settings.run_dialog() def show_panel(self) -> None: self.win.show_all()