From f67220721b166b0eed39e239c28f74970292ba31 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yasin=20O=CC=88zmen?= Date: Sun, 23 Aug 2026 18:37:32 +0300 Subject: [PATCH 1/3] fix: expand editable QComboBoxes to show full model names in settings When using long model names (e.g. OpenRouter models like 'openai/whisper-large-v3-turbo'), the combo boxes in Settings were too narrow to display them, showing truncated text like 'open...ribe'. Apply QSizePolicy.Expanding (horizontal) to all editable QComboBoxes so they stretch to fill the available width of the form layout, just as non-editable selects already do. Affected boxes: transcribe_model, cleanup_model, cleanup_claude_model, cleanup_codex_model, assistant_model, assistant_codex_model, assistant_openrouter_model, meeting_model, paste_shortcut, repo, and all shortcut picker boxes. --- dikte/settings_ui.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/dikte/settings_ui.py b/dikte/settings_ui.py index d3a97b7..d42ac7e 100644 --- a/dikte/settings_ui.py +++ b/dikte/settings_ui.py @@ -10,7 +10,7 @@ from PyQt6.QtWidgets import ( QAbstractItemView, QAbstractSpinBox, QCheckBox, QComboBox, QDialog, QDialogButtonBox, QFileDialog, QFormLayout, QGroupBox, QHBoxLayout, QLabel, QLineEdit, QListWidget, QListWidgetItem, QMenu, QMessageBox, QPlainTextEdit, - QPushButton, QScrollArea, QSpinBox, QTabWidget, QVBoxLayout, QWidget, + QPushButton, QScrollArea, QSpinBox, QTabWidget, QVBoxLayout, QWidget, QSizePolicy, ) from . import __version__ @@ -224,6 +224,7 @@ class LocalModelBox(QGroupBox): if self._repos is not None: self.repo = QComboBox() self.repo.setEditable(True) + self.repo.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed) self.repo.setToolTip(t("A Hugging Face repository of GGUF files. The " "list is fetched; any other one can be typed in.")) self.repo.currentTextChanged.connect(self._repo_changed) @@ -663,6 +664,7 @@ class SettingsWindow(QDialog): # them: a stored one this desktop does not offer is kept as it is # rather than quietly replaced by the first item on the list. self.paste_shortcut.setEditable(True) + self.paste_shortcut.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed) self.paste_shortcut.addItems(paste.desktop().shortcuts) self.paste_shortcut.setToolTip(t( "macOS asks for Accessibility permission the first time this is sent." @@ -761,6 +763,7 @@ class SettingsWindow(QDialog): self.stt_form = stt_form self.transcribe_model = QComboBox() self.transcribe_model.setEditable(True) + self.transcribe_model.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed) self.refresh_transcribe_models = QPushButton(t("Fetch model list")) self.refresh_transcribe_models.clicked.connect(self._load_transcribe_models) self.transcribe_model_row = self._row(self.transcribe_model, @@ -829,6 +832,7 @@ class SettingsWindow(QDialog): self.cleanup_model = QComboBox() self.cleanup_model.setEditable(True) + self.cleanup_model.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed) self.cleanup_model.addItems(CLEANUP_MODELS) self.refresh_models = QPushButton(t("Fetch model list")) self.refresh_models.clicked.connect(self._load_models) @@ -840,11 +844,13 @@ class SettingsWindow(QDialog): # field, and only the row of whoever is chosen is on screen. self.cleanup_claude_model = QComboBox() self.cleanup_claude_model.setEditable(True) + self.cleanup_claude_model.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed) self.cleanup_claude_model.addItems(CLEANUP_CLAUDE_MODELS) orr_form.addRow(t("Model"), self.cleanup_claude_model) self.cleanup_codex_model = QComboBox() self.cleanup_codex_model.setEditable(True) + self.cleanup_codex_model.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed) self.cleanup_codex_model.addItems([t("Codex's own default")] + CODEX_MODELS) orr_form.addRow(t("Model"), self.cleanup_codex_model) @@ -1000,6 +1006,7 @@ class SettingsWindow(QDialog): claude_form = QFormLayout(self.claude_box) self.assistant_model = QComboBox() self.assistant_model.setEditable(True) + self.assistant_model.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed) self.assistant_model.addItems(ASSISTANT_MODELS) self.assistant_model.setToolTip(t( "A name like “sonnet” always means the newest model of that line. " @@ -1017,6 +1024,7 @@ class SettingsWindow(QDialog): codex_form = QFormLayout(self.codex_box) self.assistant_codex_model = QComboBox() self.assistant_codex_model.setEditable(True) + self.assistant_codex_model.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed) self.assistant_codex_model.addItem(t("Codex's own default"), "") for name in CODEX_MODELS: self.assistant_codex_model.addItem(name, name) @@ -1031,6 +1039,7 @@ class SettingsWindow(QDialog): or_form = QFormLayout(self.openrouter_box) self.assistant_openrouter_model = QComboBox() self.assistant_openrouter_model.setEditable(True) + self.assistant_openrouter_model.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed) self.assistant_openrouter_model.addItems(ASSISTANT_OR_MODELS) or_form.addRow(t("Model"), self.assistant_openrouter_model) or_note = QLabel(t( @@ -1178,6 +1187,7 @@ class SettingsWindow(QDialog): models_form = QFormLayout(models) self.meeting_model = QComboBox() self.meeting_model.setEditable(True) + self.meeting_model.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed) self.meeting_model.addItems(MEETING_MODELS) models_form.addRow(t("Model"), self.meeting_model) self.meeting_reasoning = QComboBox() @@ -1508,6 +1518,7 @@ class SettingsWindow(QDialog): """The field a global shortcut is typed or picked in.""" box = QComboBox() box.setEditable(True) + box.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed) box.addItems(MAC_SHORTCUTS if hotkey.backend() == hotkey.MACOS else SHORTCUTS) box.setCurrentText("") From 2be50cd72d5321f973d142fa20450dd8113c5e69 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yasin=20O=CC=88zmen?= Date: Sun, 23 Aug 2026 18:38:16 +0300 Subject: [PATCH 2/3] fix: follow the mouse to the active screen during recording The recording indicator (overlay) was shown in a fixed corner of the screen it first appeared on. Moving the mouse to another monitor while a dictation was in progress caused the indicator to disappear from view, even though the recording was still running. Poll the cursor position every ~333 ms inside the animation tick and call _reposition() when the cursor has moved to a different screen. The window then jumps to the same corner on whichever monitor the user is looking at, making it clear that recording is still active. The check is throttled to every ten ticks rather than every frame (30 Hz) because screenAt() is a trivial coordinate lookup but there is no value in calling it more often than the human hand can move between monitors. --- dikte/overlay.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/dikte/overlay.py b/dikte/overlay.py index 4f4d3f1..746c3ad 100644 --- a/dikte/overlay.py +++ b/dikte/overlay.py @@ -264,6 +264,17 @@ class Overlay(QWidget): # the corner when it does rather than leaving a gap where it was. if self.below is not None and self.below.showing != self._stacked: self._reposition() + elif self.state in LIVE: + # Follow the screen the mouse is currently on. Checked every ten + # ticks (~333 ms) rather than every frame: screenAt() is cheap but + # the user's hand moves slowly, so there is no value in sampling it + # at 30 Hz. + self._screen_check_ticks = getattr(self, "_screen_check_ticks", 0) + 1 + if self._screen_check_ticks >= 10: + self._screen_check_ticks = 0 + current_screen = QApplication.screenAt(QCursor.pos()) + if current_screen is not None and current_screen != self.screen(): + self._reposition() if self.state in LIVE and not self.paused: # keep the ribbon moving even through a pause in speech self.levels = self.levels[1:] + [self.levels[-1] * 0.72] From 9b03da4175ca91121a5dfba9ceeb65f094e4480e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Yasin=20O=CC=88zmen?= Date: Thu, 27 Aug 2026 15:57:52 +0300 Subject: [PATCH 3/3] fix: address settings width and overlay review --- dikte/overlay.py | 13 +++-------- dikte/settings_ui.py | 31 ++++++++++++++++---------- tests/test_ui.py | 53 +++++++++++++++++++++++++++++++++++++++++++- 3 files changed, 74 insertions(+), 23 deletions(-) diff --git a/dikte/overlay.py b/dikte/overlay.py index 228b8bb..11f8b31 100644 --- a/dikte/overlay.py +++ b/dikte/overlay.py @@ -272,16 +272,9 @@ class Overlay(QWidget): if self.below is not None and self.below.showing != self._stacked: self._reposition() elif self.state in LIVE: - # Follow the screen the mouse is currently on. Checked every ten - # ticks (~333 ms) rather than every frame: screenAt() is cheap but - # the user's hand moves slowly, so there is no value in sampling it - # at 30 Hz. - self._screen_check_ticks = getattr(self, "_screen_check_ticks", 0) + 1 - if self._screen_check_ticks >= 10: - self._screen_check_ticks = 0 - current_screen = QApplication.screenAt(QCursor.pos()) - if current_screen is not None and current_screen != self.screen(): - self._reposition() + current_screen = QApplication.screenAt(QCursor.pos()) + if current_screen is not None and current_screen != self.screen(): + self._reposition() if self.state in LIVE and not self.paused: # keep the ribbon moving even through a pause in speech self.levels = self.levels[1:] + [self.levels[-1] * 0.72] diff --git a/dikte/settings_ui.py b/dikte/settings_ui.py index 7b4708c..c7fc52c 100644 --- a/dikte/settings_ui.py +++ b/dikte/settings_ui.py @@ -12,7 +12,7 @@ from PyQt6.QtWidgets import ( QAbstractItemView, QAbstractSpinBox, QCheckBox, QComboBox, QDialog, QDialogButtonBox, QFileDialog, QFormLayout, QGroupBox, QHBoxLayout, QLabel, QLineEdit, QListWidget, QListWidgetItem, QMenu, QMessageBox, QPlainTextEdit, - QPushButton, QScrollArea, QSpinBox, QTabWidget, QVBoxLayout, QWidget, QSizePolicy, + QPushButton, QScrollArea, QSpinBox, QTabWidget, QVBoxLayout, QWidget, ) from . import __version__ @@ -229,6 +229,8 @@ class LocalModelBox(QGroupBox): self._wanted = "" # the model to select once a list arrives form = QFormLayout(self) + form.setFieldGrowthPolicy( + QFormLayout.FieldGrowthPolicy.AllNonFixedFieldsGrow) self.program_label = WrappedLabel() self.install_button = QPushButton(t("Download")) @@ -239,7 +241,6 @@ class LocalModelBox(QGroupBox): if self._repos is not None: self.repo = QComboBox() self.repo.setEditable(True) - self.repo.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed) self.repo.setToolTip(t("A Hugging Face repository of GGUF files. The " "list is fetched; any other one can be typed in.")) self.repo.currentTextChanged.connect(self._repo_changed) @@ -679,6 +680,8 @@ class SettingsWindow(QDialog): def _general_tab(self): page = QWidget() form = QFormLayout(page) + form.setFieldGrowthPolicy( + QFormLayout.FieldGrowthPolicy.AllNonFixedFieldsGrow) self.ui_language = QComboBox() for label, code in UI_LANGUAGES: @@ -704,7 +707,6 @@ class SettingsWindow(QDialog): # them: a stored one this desktop does not offer is kept as it is # rather than quietly replaced by the first item on the list. self.paste_shortcut.setEditable(True) - self.paste_shortcut.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed) self.paste_shortcut.addItems(paste.desktop().shortcuts) self.paste_shortcut.setToolTip(t( "macOS asks for Accessibility permission the first time this is sent." @@ -792,6 +794,8 @@ class SettingsWindow(QDialog): stt = QGroupBox(t("Speech to text")) stt_form = QFormLayout(stt) + stt_form.setFieldGrowthPolicy( + QFormLayout.FieldGrowthPolicy.AllNonFixedFieldsGrow) self.transcribe_provider = QComboBox() for label, value in TRANSCRIBE_PROVIDERS: self.transcribe_provider.addItem(t(label), value) @@ -803,7 +807,6 @@ class SettingsWindow(QDialog): self.stt_form = stt_form self.transcribe_model = QComboBox() self.transcribe_model.setEditable(True) - self.transcribe_model.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed) self.refresh_transcribe_models = QPushButton(t("Fetch model list")) self.refresh_transcribe_models.clicked.connect(self._load_transcribe_models) self.transcribe_model_row = self._row(self.transcribe_model, @@ -854,6 +857,8 @@ class SettingsWindow(QDialog): orr = QGroupBox(t("Transcript cleanup")) orr_form = self.cleanup_form = QFormLayout(orr) + orr_form.setFieldGrowthPolicy( + QFormLayout.FieldGrowthPolicy.AllNonFixedFieldsGrow) self.cleanup_enabled = QCheckBox(t("Clean the transcript with a model")) orr_form.addRow("", self.cleanup_enabled) @@ -872,7 +877,6 @@ class SettingsWindow(QDialog): self.cleanup_model = QComboBox() self.cleanup_model.setEditable(True) - self.cleanup_model.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed) self.cleanup_model.addItems(CLEANUP_MODELS) self.refresh_models = QPushButton(t("Fetch model list")) self.refresh_models.clicked.connect(self._load_models) @@ -884,14 +888,12 @@ class SettingsWindow(QDialog): # field, and only the row of whoever is chosen is on screen. self.cleanup_claude_model = QComboBox() self.cleanup_claude_model.setEditable(True) - self.cleanup_claude_model.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed) self.cleanup_claude_model.addItems(CLEANUP_CLAUDE_MODELS) self.cleanup_claude_model.setToolTip(_typed_model_note("Claude Code")) orr_form.addRow(t("Model"), self.cleanup_claude_model) self.cleanup_codex_model = QComboBox() self.cleanup_codex_model.setEditable(True) - self.cleanup_codex_model.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed) self.cleanup_codex_model.addItems([t("Codex's own default")] + CODEX_MODELS) self.cleanup_codex_model.setToolTip(_typed_model_note("Codex")) orr_form.addRow(t("Model"), self.cleanup_codex_model) @@ -1046,9 +1048,10 @@ class SettingsWindow(QDialog): # be worse than none. self.claude_box = QGroupBox(t("Claude Code")) claude_form = QFormLayout(self.claude_box) + claude_form.setFieldGrowthPolicy( + QFormLayout.FieldGrowthPolicy.AllNonFixedFieldsGrow) self.assistant_model = QComboBox() self.assistant_model.setEditable(True) - self.assistant_model.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed) self.assistant_model.addItems(ASSISTANT_MODELS) self.assistant_model.setToolTip(t( "A name like “sonnet” always means the newest model of that line. " @@ -1064,9 +1067,10 @@ class SettingsWindow(QDialog): self.codex_box = QGroupBox(t("Codex")) codex_form = QFormLayout(self.codex_box) + codex_form.setFieldGrowthPolicy( + QFormLayout.FieldGrowthPolicy.AllNonFixedFieldsGrow) self.assistant_codex_model = QComboBox() self.assistant_codex_model.setEditable(True) - self.assistant_codex_model.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed) self.assistant_codex_model.addItem(t("Codex's own default"), "") for name in CODEX_MODELS: self.assistant_codex_model.addItem(name, name) @@ -1080,9 +1084,10 @@ class SettingsWindow(QDialog): self.openrouter_box = QGroupBox("OpenRouter") or_form = QFormLayout(self.openrouter_box) + or_form.setFieldGrowthPolicy( + QFormLayout.FieldGrowthPolicy.AllNonFixedFieldsGrow) self.assistant_openrouter_model = QComboBox() self.assistant_openrouter_model.setEditable(True) - self.assistant_openrouter_model.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed) self.assistant_openrouter_model.addItems(ASSISTANT_OR_MODELS) or_form.addRow(t("Model"), self.assistant_openrouter_model) or_note = QLabel(t( @@ -1228,9 +1233,10 @@ class SettingsWindow(QDialog): models = QGroupBox(t("Minutes")) models_form = QFormLayout(models) + models_form.setFieldGrowthPolicy( + QFormLayout.FieldGrowthPolicy.AllNonFixedFieldsGrow) self.meeting_model = QComboBox() self.meeting_model.setEditable(True) - self.meeting_model.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed) self.meeting_model.addItems(MEETING_MODELS) models_form.addRow(t("Model"), self.meeting_model) self.meeting_reasoning = QComboBox() @@ -1411,6 +1417,8 @@ class SettingsWindow(QDialog): # and two combination boxes starting at different places read as two # unrelated settings rather than the pair they are. form = QFormLayout() + form.setFieldGrowthPolicy( + QFormLayout.FieldGrowthPolicy.AllNonFixedFieldsGrow) self._shortcut_row( form, "toggle", t("Start and stop"), t("No global shortcut installed."), placeholder="Ctrl+Space", @@ -1561,7 +1569,6 @@ class SettingsWindow(QDialog): """The field a global shortcut is typed or picked in.""" box = QComboBox() box.setEditable(True) - box.setSizePolicy(QSizePolicy.Policy.Expanding, QSizePolicy.Policy.Fixed) box.addItems(MAC_SHORTCUTS if hotkey.backend() == hotkey.MACOS else SHORTCUTS) box.setCurrentText("") diff --git a/tests/test_ui.py b/tests/test_ui.py index 8c7e0cd..84c2a26 100644 --- a/tests/test_ui.py +++ b/tests/test_ui.py @@ -12,7 +12,7 @@ import unittest from typing import ClassVar from unittest import mock -from PyQt6.QtCore import QPoint, QPointF, Qt +from PyQt6.QtCore import QPoint, QPointF, QRect, Qt from PyQt6.QtGui import QWheelEvent from PyQt6.QtWidgets import QApplication, QMessageBox @@ -271,6 +271,46 @@ class Settings(DikteTest): self.assertEqual(shown, [provider]) self.assertFalse(box.isHidden()) + def test_editable_boxes_live_in_forms_that_grow_the_field_column(self): + window = self.window(cfg.Config()) + + def contains(layout, target): + for index in range(layout.count()): + item = layout.itemAt(index) + widget = item.widget() + if widget is target or (widget is not None and + widget.isAncestorOf(target)): + return True + child = item.layout() + if child is not None and contains(child, target): + return True + return False + + forms = window.findChildren(settings_ui.QFormLayout) + boxes = [ + window.paste_shortcut, + window.transcribe_model, + window.cleanup_model, + window.cleanup_claude_model, + window.cleanup_codex_model, + window.assistant_model, + window.assistant_codex_model, + window.assistant_openrouter_model, + window.meeting_model, + window.local_llm.repo, + *(box for box, _status, _missing in + window._shortcut_rows.values()), + ] + for box in boxes: + form = next((candidate for candidate in forms + if contains(candidate, box)), None) + with self.subTest(box=box.objectName() or box.currentText()): + self.assertIsNotNone(form) + self.assertEqual( + form.fieldGrowthPolicy(), + settings_ui.QFormLayout.FieldGrowthPolicy.AllNonFixedFieldsGrow, + ) + def test_codex_answering_refills_both_of_its_boxes(self): """The list Codex gave replaces the built-in one, in both places, and neither loses what was already picked.""" @@ -941,6 +981,17 @@ class Overlay(DikteTest): widget.show_recording() widget._reposition() + def test_a_live_indicator_follows_the_cursor_to_another_screen(self): + widget = self.overlay(corner="top-left") + widget.show_recording() + other_screen = mock.Mock() + other_screen.availableGeometry.return_value = QRect(1000, 200, 800, 600) + with mock.patch.object(QApplication, "screenAt", + return_value=other_screen): + widget._tick() + self.assertEqual(widget.pos(), QPoint(1000 + overlay_module.MARGIN, + 200 + overlay_module.MARGIN)) + def test_a_warning_and_an_error_both_show(self): widget = self.overlay() widget.show_warning("cleanup failed")