From ccf38015457574590f5c082ea1d9fafdc160ed83 Mon Sep 17 00:00:00 2001 From: mARTin-B78 Date: Wed, 10 Jun 2026 00:07:41 +0200 Subject: [PATCH] feat: icons on all settings tabs and section headers (v2.03.12) GTK symbolic icons on every tab label and every section/card header. Also fixes the resize grip landing in the tab bar instead of the bottom-right corner. Co-Authored-By: Claude Sonnet 4.6 --- linux/CHANGELOG.md | 11 ++++ linux/blitztext/__init__.py | 2 +- linux/blitztext/gtksettings.py | 104 +++++++++++++++++++-------------- 3 files changed, 72 insertions(+), 45 deletions(-) diff --git a/linux/CHANGELOG.md b/linux/CHANGELOG.md index 4249a28..d18e04d 100644 --- a/linux/CHANGELOG.md +++ b/linux/CHANGELOG.md @@ -9,6 +9,17 @@ The version is defined in [`blitztext/__init__.py`](blitztext/__init__.py). ## [Unreleased] +## [2.03.12] - 2026-06-10 + +### Added +- **Icons in settings.** All tab labels (Presets, Engines, Input, General, + Benchmark, Log, Manual, About) and every section header inside each tab now + show a small GTK symbolic icon, making the layout easier to scan. + +### Fixed +- **Resize grip position.** The grip indicator now appears correctly at the + bottom-right corner below the notebook, not misplaced in the tab bar. + ## [2.03.11] - 2026-06-09 ### Added diff --git a/linux/blitztext/__init__.py b/linux/blitztext/__init__.py index bdc986d..e62f831 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__ = "2.03.11" +__version__ = "2.03.12" diff --git a/linux/blitztext/gtksettings.py b/linux/blitztext/gtksettings.py index ae499c7..b011cbd 100644 --- a/linux/blitztext/gtksettings.py +++ b/linux/blitztext/gtksettings.py @@ -302,19 +302,27 @@ def _infobox(parent: Gtk.Box, text: str) -> Gtk.Box: return box -def _section_title(parent: Gtk.Box, text: str, margin_top: int = 16) -> None: - """A small bold uppercase section header above a group of settings.""" +def _section_title(parent: Gtk.Box, text: str, margin_top: int = 16, + icon: str = "") -> None: + """A small bold uppercase section header, with optional symbolic icon.""" + row = Gtk.Box(spacing=5) + row.set_margin_top(margin_top); row.set_margin_bottom(3) + row.get_style_context().add_class("bt-section") + if icon: + img = Gtk.Image.new_from_icon_name(icon, Gtk.IconSize.MENU) + img.get_style_context().add_class("bt-section") + row.pack_start(img, False, False, 0) lbl = Gtk.Label(xalign=0.0) lbl.set_markup(f"{GLib.markup_escape_text(text.upper())}") - lbl.set_margin_top(margin_top); lbl.set_margin_bottom(3) - lbl.get_style_context().add_class("bt-section") - parent.pack_start(lbl, False, False, 0) + row.pack_start(lbl, False, False, 0) + parent.pack_start(row, False, False, 0) -def _card_section(parent: Gtk.Box, title: str = "", margin_top: int = 16) -> Gtk.ListBox: +def _card_section(parent: Gtk.Box, title: str = "", margin_top: int = 16, + icon: str = "") -> Gtk.ListBox: """A titled card group. Returns a ListBox styled as a card for packing rows into.""" if title: - _section_title(parent, title, margin_top) + _section_title(parent, title, margin_top, icon=icon) lb = Gtk.ListBox() lb.set_selection_mode(Gtk.SelectionMode.NONE) lb.get_style_context().add_class("boxed-list") @@ -517,9 +525,17 @@ def _url_field_lb(lb: Gtk.ListBox, label: str, placeholder: str, on_reload, return row_box.get_children()[-2] # fallback: second-to-last (before refresh btn) -def _page(nb: Gtk.Notebook, title: str) -> Gtk.Box: +def _page(nb: Gtk.Notebook, title: str, icon: str = "") -> Gtk.Box: outer = Gtk.Box(orientation=Gtk.Orientation.VERTICAL) - nb.append_page(outer, Gtk.Label(label=title)) + if icon: + tab_box = Gtk.Box(spacing=5) + tab_box.pack_start( + Gtk.Image.new_from_icon_name(icon, Gtk.IconSize.MENU), False, False, 0) + tab_box.pack_start(Gtk.Label(label=title), False, False, 0) + tab_box.show_all() + nb.append_page(outer, tab_box) + else: + nb.append_page(outer, Gtk.Label(label=title)) sw = Gtk.ScrolledWindow() sw.set_policy(Gtk.PolicyType.NEVER, Gtk.PolicyType.ALWAYS) sw.set_overlay_scrolling(False) # always-visible scrollbar, not the overlay kind @@ -787,35 +803,35 @@ notebook.bt-nb tab:checked label { nb = Gtk.Notebook() nb.get_style_context().add_class("bt-nb") + self.dlg.get_content_area().pack_start(nb, True, True, 0) - # Resize-grip overlay — draws the classic dotted SE grip so users know - # the window is resizable. - _overlay = Gtk.Overlay() - _overlay.add(nb) + # Resize-grip strip — sits below the notebook, above the button row, so + # users notice the window is resizable. + _grip_strip = Gtk.Box() _grip = Gtk.DrawingArea() - _grip.set_size_request(18, 18) + _grip.set_size_request(18, 10) _grip.set_halign(Gtk.Align.END) - _grip.set_valign(Gtk.Align.END) _grip.set_can_focus(False) _grip.connect("draw", self._draw_resize_grip) - _overlay.add_overlay(_grip) - _overlay.set_overlay_pass_through(_grip, True) - self.dlg.get_content_area().pack_start(_overlay, True, True, 0) + _grip_strip.pack_end(_grip, False, False, 0) + self.dlg.get_content_area().pack_end(_grip_strip, False, False, 0) + # 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), - ("Manual", self._build_manual), - ("About", self._build_about)): - self._pending_tabs[_page(nb, title)] = builder + for title, icon, builder in ( + ("Presets", "document-properties-symbolic", self._build_presets), + ("Engines", "network-server-symbolic", self._build_engines), + ("Input", "audio-input-microphone-symbolic", self._build_input), + ("General", "preferences-system-symbolic", self._build_general), + ("Benchmark", "utilities-system-monitor-symbolic",self._build_benchmark), + ("Log", "text-x-generic-symbolic", self._build_log), + ("Manual", "help-contents-symbolic", self._build_manual), + ("About", "help-about-symbolic", self._build_about)): + self._pending_tabs[_page(nb, title, icon)] = 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)) @@ -862,7 +878,7 @@ notebook.bt-nb tab:checked label { page.pack_start(bar, False, False, 0) # ── Identity card ───────────────────────────────────────────────────── - id_card = _card_section(page, "Identity", margin_top=4) + id_card = _card_section(page, "Identity", margin_top=4, icon="emblem-system-symbolic") self.wf_name = _labeled(id_card, "Name", _entry(placeholder="Preset name"), tooltip="A short name for this action, shown in the main panel.") self.wf_icon = self._icon_field_lb(id_card, "Icon (emoji)") @@ -870,13 +886,13 @@ notebook.bt-nb tab:checked label { tooltip="One line explaining what this preset does.") # ── Trigger card ────────────────────────────────────────────────────── - trig_card = _card_section(page, "Trigger") + trig_card = _card_section(page, "Trigger", icon="input-keyboard-symbolic") self.wf_keywords = _labeled(trig_card, "Keywords", _entry(placeholder="nicer email, bessere email"), tooltip="Spoken trigger words, comma-separated. Say one at the start or end of your speech to activate this preset.") self.wf_hotkey = self._key_field_lb(trig_card, "Hotkey (optional)", "", placeholder="click Set, or e.g. ++e") # ── Behaviour card ──────────────────────────────────────────────────── - beh_card = _card_section(page, "Behaviour") + beh_card = _card_section(page, "Behaviour", icon="system-run-symbolic") self.wf_mode = _labeled(beh_card, "Mode", _combo(["transcribe", "rewrite", "stream"]), tooltip="’transcribe’ types your words as-is. ‘rewrite’ sends them to the language model first. ‘stream’ shows live text from a realtime engine.") # Engine dropdown — "(active engine)" + one entry per configured LLM engine. @@ -890,7 +906,7 @@ notebook.bt-nb tab:checked label { tooltip="Creativity of the rewrite, 0–1. Lower is more predictable. Blank uses the engine default.") # ── Prompt ──────────────────────────────────────────────────────────── - _section_title(page, "Prompt sent to the LLM (rewrite mode)", margin_top=14) + _section_title(page, "Prompt sent to the LLM (rewrite mode)", margin_top=14, icon="applications-science-symbolic") frame = Gtk.Frame(); frame.set_shadow_type(Gtk.ShadowType.IN) sw = Gtk.ScrolledWindow(); sw.set_min_content_height(140) self.wf_prompt = Gtk.TextView(); self.wf_prompt.set_wrap_mode(Gtk.WrapMode.WORD) @@ -1149,9 +1165,9 @@ notebook.bt-nb tab:checked label { _infobox(page, "Engines do the work. The speech-to-text engine turns your voice into " "text; the language model rewrites it. Each can run locally or on " "a server you enter. A green dot means it is reachable, red means offline.") - _section_title(page, "Speech-to-text engine", margin_top=4) + _section_title(page, "Speech-to-text engine", margin_top=4, icon="audio-input-microphone-symbolic") page.pack_start(self._stt_section(), False, False, 0) - _section_title(page, "Language model (rewrite)") + _section_title(page, "Language model (rewrite)", icon="applications-science-symbolic") page.pack_start(self._llm_section(), False, False, 0) self._refresh_status() @@ -1215,7 +1231,7 @@ notebook.bt-nb tab:checked label { self.stt_type.connect("changed", self._stt_type_changed) # ── Device & precision card (local engines only) ───────────────────── - self.stt_dev_card = _card_section(box, "Internal engine — device & precision", margin_top=4) + self.stt_dev_card = _card_section(box, "Internal engine — device & precision", margin_top=4, icon="computer-symbolic") self.stt_device = _labeled(self.stt_dev_card, "Device", _type_combo(_DEVICE_OPTIONS, self.cfg.device), tooltip="Which processor runs the speech model. " "Auto tries your GPU first. GPU (CUDA) requires NVIDIA — much faster than CPU.") @@ -1645,7 +1661,7 @@ notebook.bt-nb tab:checked label { LW = 170 # ── Input mode card ─────────────────────────────────────────────────── - mode_card = _card_section(page, "Input mode & keys", margin_top=4) + mode_card = _card_section(page, "Input mode & keys", margin_top=4, icon="input-keyboard-symbolic") self.in_mode = _labeled(mode_card, "Input mode", _combo(["modifiers", "hotkeys"], self.cfg.input_mode), width=LW, tooltip="’modifiers’: hold/press the keys below — easiest for most people. " @@ -1660,7 +1676,7 @@ notebook.bt-nb tab:checked label { self.in_cancel = self._key_field_lb(mode_card, "Cancel", self.cfg.key_cancel, width=LW) # ── Quality gate card ───────────────────────────────────────────────── - q_card = _card_section(page, "Quality gate") + q_card = _card_section(page, "Quality gate", icon="security-high-symbolic") self.q_min = _labeled(q_card, "Min seconds", _entry(str(self.cfg.min_speech_seconds)), width=LW, tooltip="Minimum audio length. Shorter clips are silently ignored (avoids pasting nothing).") self.q_rms = _labeled(q_card, "Silence RMS", _entry(str(self.cfg.silence_rms)), width=LW, @@ -1673,7 +1689,7 @@ notebook.bt-nb tab:checked label { description="Remove ending punctuation from pasted text — handy for code insertion.") # ── Wakeword card ───────────────────────────────────────────────────── - ww_card = _card_section(page, "Hands-free wakeword") + ww_card = _card_section(page, "Hands-free wakeword", icon="audio-input-microphone-symbolic") self.ww_enabled = Gtk.Switch(); self.ww_enabled.set_active(self.cfg.wakeword_enabled) _switch_row(ww_card, "Enable wakeword", self.ww_enabled, width=LW, description="Start dictation with a spoken keyword via an external openWakeWord server.") @@ -1724,14 +1740,14 @@ notebook.bt-nb tab:checked label { tooltip="Say one of these to type AND press Enter (spoken ‘submit’). Use a distinctive multi-word phrase. Empty = off.") # ── Wakeword sound cues ─────────────────────────────────────────────── - ww_snd_card = _card_section(page, "Wakeword sound cues (hands-free only)") + ww_snd_card = _card_section(page, "Wakeword sound cues (hands-free only)", icon="audio-speakers-symbolic") self.ww_snd_detected = self._sound_field(ww_snd_card, "Wakeword detected", self.cfg.wakeword_sound_detected, "Plays the instant the wake word fires — your ‘speak now’ cue. Leave empty for no sound.", width=LW) self.ww_snd_done = self._sound_field(ww_snd_card, "Speech captured", self.cfg.wakeword_sound_done, "Plays when your spoken command is captured and recording stops.", width=LW) # ── Manual dictation sounds card ────────────────────────────────────── - snd_card = _card_section(page, "Audio cues (keyboard / hotkey dictation)") + snd_card = _card_section(page, "Audio cues (keyboard / hotkey dictation)", icon="audio-speakers-symbolic") self.snd_enabled = Gtk.Switch(); self.snd_enabled.set_active(self.cfg.sounds_enabled) _switch_row(snd_card, "Play audio cues", self.snd_enabled, width=LW, description="On/off for the manual start/stop chimes below. Does not affect wakeword sounds.") @@ -1788,7 +1804,7 @@ notebook.bt-nb tab:checked label { "and whether Blitztext starts automatically when you log in.") # ── Microphone card ─────────────────────────────────────────────────── - mic_card = _card_section(page, "Microphone", margin_top=4) + mic_card = _card_section(page, "Microphone", margin_top=4, icon="audio-input-microphone-symbolic") self._mics = audio.list_mics() names = [label for _, label in self._mics] cur = next((lbl for nm, lbl in self._mics if nm == self.cfg.mic), names[0] if names else "") @@ -1801,7 +1817,7 @@ notebook.bt-nb tab:checked label { self.gen_mic.connect("changed", lambda _c: self._restart_meter()) # ── Output card ─────────────────────────────────────────────────────── - out_card = _card_section(page, "Text output & language") + out_card = _card_section(page, "Text output & language", icon="insert-text-symbolic") self.gen_output = _labeled(out_card, "Output mode", _combo(["type", "paste"], self.cfg.output), tooltip="’type’ types the text key by key via xdotool. " "’paste’ copies it to the clipboard and presses Ctrl+V (faster for long text).") @@ -1809,7 +1825,7 @@ notebook.bt-nb tab:checked label { tooltip="Spoken language code (de, en, …). Leave blank to auto-detect.") # ── Notifications card ──────────────────────────────────────────────── - notif_card = _card_section(page, "Notifications & overlay") + notif_card = _card_section(page, "Notifications & overlay", icon="dialog-information-symbolic") self.gen_notify = Gtk.Switch(); self.gen_notify.set_active(self.cfg.notify) _switch_row(notif_card, "Notifications", self.gen_notify, "Desktop pop-ups for recording, transcription, and errors (manual dictation).") @@ -1821,7 +1837,7 @@ notebook.bt-nb tab:checked label { "Show a microphone, live waveform, and recognised text in a bubble at the cursor while you dictate.") # ── Startup card ────────────────────────────────────────────────────── - start_card = _card_section(page, "Startup") + start_card = _card_section(page, "Startup", icon="system-reboot-symbolic") self.gen_boot = Gtk.Switch(); self.gen_boot.set_active(autostart.is_enabled()) _switch_row(start_card, "Launch on login", self.gen_boot, "Start Blitztext automatically when you log in.") @@ -1975,7 +1991,7 @@ notebook.bt-nb tab:checked label { reff.connect("file-set", _on_ref_set) # ── Engine / model selector ─────────────────────────────────────────── - _section_title(page, "Engines to benchmark", margin_top=10) + _section_title(page, "Engines to benchmark", margin_top=10, icon="utilities-system-monitor-symbolic") sel_hdr = Gtk.Box(spacing=6); sel_hdr.set_margin_bottom(4) self._bench_filter = Gtk.SearchEntry()