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")