Merge remote-tracking branch 'origin/master' into pr/fix-combobox-and-overlay-multiscreen

# Conflicts:
#	tests/test_ui.py
This commit is contained in:
Yasin Özmen
2026-08-27 16:00:12 +03:00
6 changed files with 68 additions and 12 deletions
+6 -2
View File
@@ -163,11 +163,13 @@ class Dikte:
self.front_before = None
self._front_watch = None
self.overlay = Overlay(self.conf["overlay_corner"])
self.overlay = Overlay(self.conf["overlay_corner"],
screen_name=self.conf["overlay_screen"])
# The agent's indicator sits on top of the dictation one when both are
# up, and drops into the corner when it is alone there.
self.ask_overlay = Overlay(self.conf["overlay_corner"], below=self.overlay,
dismissable=True)
dismissable=True,
screen_name=self.conf["overlay_screen"])
self.recorder = audio.Recorder()
self.pipeline = Pipeline(self.conf)
self.ask_pipeline = Pipeline(self.conf)
@@ -1296,7 +1298,9 @@ class Dikte:
def _apply_settings(self):
self.overlay.corner = self.conf["overlay_corner"]
self.overlay.screen_name = self.conf["overlay_screen"]
self.ask_overlay.corner = self.conf["overlay_corner"]
self.ask_overlay.screen_name = self.conf["overlay_screen"]
self._apply_local()
self._build_tray()
self._refresh_tray()
+1
View File
@@ -457,6 +457,7 @@ DEFAULTS = {
"pause_shortcut": "",
"evdev_hotkey": False,
"overlay_corner": "bottom-left",
"overlay_screen": "",
"keep_audio": False,
"history_limit": 200,
# A look at the releases page once a day, and nothing more than a look:
+4
View File
@@ -151,6 +151,7 @@ TR = {
# --- settings: tabs and general ------------------------------------
"Dikte Settings": "Dikte Ayarları",
"General": "Genel",
"Display": "Ekran",
"API and models": "API ve modeller",
"Cleanup rules": "Temizleme kuralları",
"Audio file": "Ses dosyası",
@@ -179,6 +180,9 @@ TR = {
"macOS bu ilk gönderildiğinde Erişilebilirlik izni ister.",
"Restore the previous clipboard after pasting":
"Yapıştırdıktan sonra eski pano içeriğini geri koy",
"Indicator screen": "Gösterge ekranı",
"Follow the mouse pointer": "Fare imlecini takip et",
"{name} (not connected)": "{name} (bağlı değil)",
"Indicator corner": "Gösterge köşesi",
"bottom-left": "sol-alt",
"bottom-right": "sağ-alt",
+13 -4
View File
@@ -42,9 +42,11 @@ class Overlay(QWidget):
of covering it, which is what lets a dictation and a command to the agent be
under way at the same time and still both be visible."""
def __init__(self, corner="bottom-left", below=None, dismissable=False):
def __init__(self, corner="bottom-left", below=None, dismissable=False,
screen_name=""):
super().__init__(None)
self.corner = corner
self.screen_name = screen_name
self.below = below
# A job that can run for ten minutes should not have to be watched for
# ten minutes. Clicking such an indicator puts the progress away; the
@@ -251,8 +253,15 @@ class Overlay(QWidget):
self.resize(width, HEIGHT)
def _reposition(self):
# On a multi-monitor setup, show up where the user actually is.
screen = QApplication.screenAt(QCursor.pos()) or QApplication.primaryScreen()
# The screen the settings name, or, when none is named or it is not
# plugged in right now, where the user actually is. Names are connector
# names on X11 and model names on macOS, where two identical monitors
# can share one; the first then wins.
screen = next(
(item for item in QApplication.screens() if item.name() == self.screen_name),
None,
) if self.screen_name else None
screen = screen or QApplication.screenAt(QCursor.pos()) or QApplication.primaryScreen()
area = screen.availableGeometry()
left = "left" in self.corner
top = "top" in self.corner
@@ -271,7 +280,7 @@ 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:
elif self.state in LIVE and not self.screen_name:
current_screen = QApplication.screenAt(QCursor.pos())
if current_screen is not None and current_screen != self.screen():
self._reposition()
+31 -5
View File
@@ -596,6 +596,7 @@ class SettingsWindow(QDialog):
tabs = self.tabs = QTabWidget(self)
tabs.addTab(self._scrolled(self._general_tab()), t("General"))
tabs.addTab(self._scrolled(self._display_tab()), t("Display"))
self.api_tab_index = tabs.addTab(
self._scrolled(self._api_tab()), t("API and models"))
tabs.addTab(self._scrolled(self._prompt_tab()), t("Cleanup rules"))
@@ -718,11 +719,6 @@ class SettingsWindow(QDialog):
self.restore_clipboard = QCheckBox(t("Restore the previous clipboard after pasting"))
form.addRow("", self.restore_clipboard)
self.corner = QComboBox()
for value in CORNERS:
self.corner.addItem(t(value), value)
form.addRow(t("Indicator corner"), self.corner)
self.max_seconds = QSpinBox()
self.max_seconds.setRange(10, 3600)
self.max_seconds.setSuffix(t(" s"))
@@ -773,6 +769,31 @@ class SettingsWindow(QDialog):
self.update_now))
return page
def _display_tab(self):
page = QWidget()
form = QFormLayout(page)
self.indicator_screen = QComboBox()
self.indicator_screen.addItem(t("Follow the mouse pointer"), "")
for screen in QGuiApplication.screens():
# The native resolution, so that a scaled 4K screen reads
# 3840 × 2160 and not the 1920 × 1080 Qt sees through the scale.
area = screen.geometry()
ratio = screen.devicePixelRatio()
self.indicator_screen.addItem(
t("{name} ({width} × {height})", name=screen.name(),
width=round(area.width() * ratio),
height=round(area.height() * ratio)),
screen.name(),
)
form.addRow(t("Indicator screen"), self.indicator_screen)
self.corner = QComboBox()
for value in CORNERS:
self.corner.addItem(t(value), value)
form.addRow(t("Indicator corner"), self.corner)
return page
def _api_tab(self):
page = QWidget()
outer = QVBoxLayout(page)
@@ -1652,6 +1673,10 @@ class SettingsWindow(QDialog):
self.auto_paste.setChecked(conf["auto_paste"])
self.paste_shortcut.setCurrentText(conf["paste_shortcut"])
self.restore_clipboard.setChecked(conf["restore_clipboard"])
screen_name = conf["overlay_screen"]
if screen_name and self.indicator_screen.findData(screen_name) < 0:
self.indicator_screen.addItem(t("{name} (not connected)", name=screen_name), screen_name)
self._select_data(self.indicator_screen, screen_name)
self._select_data(self.corner, conf["overlay_corner"])
self.max_seconds.setValue(conf["max_seconds"])
self.skip_silent.setChecked(conf["skip_silent"])
@@ -1759,6 +1784,7 @@ class SettingsWindow(QDialog):
conf["auto_paste"] = self.auto_paste.isChecked()
conf["paste_shortcut"] = self.paste_shortcut.currentText().strip()
conf["restore_clipboard"] = self.restore_clipboard.isChecked()
conf["overlay_screen"] = self.indicator_screen.currentData() or ""
conf["overlay_corner"] = self.corner.currentData() or "bottom-left"
conf["max_seconds"] = self.max_seconds.value()
conf["skip_silent"] = self.skip_silent.isChecked()
+13 -1
View File
@@ -42,6 +42,7 @@ CHANGED = {
"paste_shortcut": "ctrl+shift+v",
"restore_clipboard": True,
"overlay_corner": "top-right",
"overlay_screen": "DP-1",
"max_seconds": 120,
"skip_silent": False,
"silence_db": -42.0,
@@ -161,7 +162,7 @@ class Settings(DikteTest):
def test_the_window_opens_with_every_tab_on_it(self):
window = self.window(cfg.Config())
tabs = window.findChildren(settings_ui.QTabWidget)[0]
self.assertEqual(tabs.count(), 9)
self.assertEqual(tabs.count(), 10)
self.assertEqual(window.windowTitle(), "Dikte Settings")
def test_no_tab_can_stretch_the_window_past_a_small_screen(self):
@@ -992,6 +993,17 @@ class Overlay(DikteTest):
self.assertEqual(widget.pos(), QPoint(1000 + overlay_module.MARGIN,
200 + overlay_module.MARGIN))
def test_a_named_screen_is_used_instead_of_the_pointer_screen(self):
screen = mock.Mock()
screen.name.return_value = "DP-1"
screen.availableGeometry.return_value = settings_ui.QRect(1920, 0, 1920, 1080)
widget = self.overlay(screen_name="DP-1")
with mock.patch.object(QApplication, "screens", return_value=[screen]), \
mock.patch.object(QApplication, "screenAt") as screen_at:
widget._reposition()
screen_at.assert_not_called()
self.assertEqual(widget.pos(), QPoint(1948, 995))
def test_a_warning_and_an_error_both_show(self):
widget = self.overlay()
widget.show_warning("cleanup failed")