Merge pull request #60 from AydoganCan60/feature/display-selection

Add display selection for recording indicator
This commit is contained in:
Yusuf İpek
2026-08-27 15:54:30 +03:00
committed by GitHub
6 changed files with 67 additions and 11 deletions
+6 -2
View File
@@ -163,11 +163,13 @@ class Dikte:
self.front_before = None self.front_before = None
self._front_watch = 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 # 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. # up, and drops into the corner when it is alone there.
self.ask_overlay = Overlay(self.conf["overlay_corner"], below=self.overlay, 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.recorder = audio.Recorder()
self.pipeline = Pipeline(self.conf) self.pipeline = Pipeline(self.conf)
self.ask_pipeline = Pipeline(self.conf) self.ask_pipeline = Pipeline(self.conf)
@@ -1296,7 +1298,9 @@ class Dikte:
def _apply_settings(self): def _apply_settings(self):
self.overlay.corner = self.conf["overlay_corner"] 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.corner = self.conf["overlay_corner"]
self.ask_overlay.screen_name = self.conf["overlay_screen"]
self._apply_local() self._apply_local()
self._build_tray() self._build_tray()
self._refresh_tray() self._refresh_tray()
+1
View File
@@ -457,6 +457,7 @@ DEFAULTS = {
"pause_shortcut": "", "pause_shortcut": "",
"evdev_hotkey": False, "evdev_hotkey": False,
"overlay_corner": "bottom-left", "overlay_corner": "bottom-left",
"overlay_screen": "",
"keep_audio": False, "keep_audio": False,
"history_limit": 200, "history_limit": 200,
# A look at the releases page once a day, and nothing more than a look: # 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 ------------------------------------ # --- settings: tabs and general ------------------------------------
"Dikte Settings": "Dikte Ayarları", "Dikte Settings": "Dikte Ayarları",
"General": "Genel", "General": "Genel",
"Display": "Ekran",
"API and models": "API ve modeller", "API and models": "API ve modeller",
"Cleanup rules": "Temizleme kuralları", "Cleanup rules": "Temizleme kuralları",
"Audio file": "Ses dosyası", "Audio file": "Ses dosyası",
@@ -179,6 +180,9 @@ TR = {
"macOS bu ilk gönderildiğinde Erişilebilirlik izni ister.", "macOS bu ilk gönderildiğinde Erişilebilirlik izni ister.",
"Restore the previous clipboard after pasting": "Restore the previous clipboard after pasting":
"Yapıştırdıktan sonra eski pano içeriğini geri koy", "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", "Indicator corner": "Gösterge köşesi",
"bottom-left": "sol-alt", "bottom-left": "sol-alt",
"bottom-right": "sağ-alt", "bottom-right": "sağ-alt",
+12 -3
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 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.""" 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) super().__init__(None)
self.corner = corner self.corner = corner
self.screen_name = screen_name
self.below = below self.below = below
# A job that can run for ten minutes should not have to be watched for # 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 # ten minutes. Clicking such an indicator puts the progress away; the
@@ -251,8 +253,15 @@ class Overlay(QWidget):
self.resize(width, HEIGHT) self.resize(width, HEIGHT)
def _reposition(self): def _reposition(self):
# On a multi-monitor setup, show up where the user actually is. # The screen the settings name, or, when none is named or it is not
screen = QApplication.screenAt(QCursor.pos()) or QApplication.primaryScreen() # 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,
)
screen = screen or QApplication.screenAt(QCursor.pos()) or QApplication.primaryScreen()
area = screen.availableGeometry() area = screen.availableGeometry()
left = "left" in self.corner left = "left" in self.corner
top = "top" in self.corner top = "top" in self.corner
+31 -5
View File
@@ -594,6 +594,7 @@ class SettingsWindow(QDialog):
tabs = self.tabs = QTabWidget(self) tabs = self.tabs = QTabWidget(self)
tabs.addTab(self._scrolled(self._general_tab()), t("General")) 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.api_tab_index = tabs.addTab(
self._scrolled(self._api_tab()), t("API and models")) self._scrolled(self._api_tab()), t("API and models"))
tabs.addTab(self._scrolled(self._prompt_tab()), t("Cleanup rules")) tabs.addTab(self._scrolled(self._prompt_tab()), t("Cleanup rules"))
@@ -714,11 +715,6 @@ class SettingsWindow(QDialog):
self.restore_clipboard = QCheckBox(t("Restore the previous clipboard after pasting")) self.restore_clipboard = QCheckBox(t("Restore the previous clipboard after pasting"))
form.addRow("", self.restore_clipboard) 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 = QSpinBox()
self.max_seconds.setRange(10, 3600) self.max_seconds.setRange(10, 3600)
self.max_seconds.setSuffix(t(" s")) self.max_seconds.setSuffix(t(" s"))
@@ -769,6 +765,31 @@ class SettingsWindow(QDialog):
self.update_now)) self.update_now))
return page 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): def _api_tab(self):
page = QWidget() page = QWidget()
outer = QVBoxLayout(page) outer = QVBoxLayout(page)
@@ -1634,6 +1655,10 @@ class SettingsWindow(QDialog):
self.auto_paste.setChecked(conf["auto_paste"]) self.auto_paste.setChecked(conf["auto_paste"])
self.paste_shortcut.setCurrentText(conf["paste_shortcut"]) self.paste_shortcut.setCurrentText(conf["paste_shortcut"])
self.restore_clipboard.setChecked(conf["restore_clipboard"]) 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._select_data(self.corner, conf["overlay_corner"])
self.max_seconds.setValue(conf["max_seconds"]) self.max_seconds.setValue(conf["max_seconds"])
self.skip_silent.setChecked(conf["skip_silent"]) self.skip_silent.setChecked(conf["skip_silent"])
@@ -1741,6 +1766,7 @@ class SettingsWindow(QDialog):
conf["auto_paste"] = self.auto_paste.isChecked() conf["auto_paste"] = self.auto_paste.isChecked()
conf["paste_shortcut"] = self.paste_shortcut.currentText().strip() conf["paste_shortcut"] = self.paste_shortcut.currentText().strip()
conf["restore_clipboard"] = self.restore_clipboard.isChecked() 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["overlay_corner"] = self.corner.currentData() or "bottom-left"
conf["max_seconds"] = self.max_seconds.value() conf["max_seconds"] = self.max_seconds.value()
conf["skip_silent"] = self.skip_silent.isChecked() conf["skip_silent"] = self.skip_silent.isChecked()
+13 -1
View File
@@ -42,6 +42,7 @@ CHANGED = {
"paste_shortcut": "ctrl+shift+v", "paste_shortcut": "ctrl+shift+v",
"restore_clipboard": True, "restore_clipboard": True,
"overlay_corner": "top-right", "overlay_corner": "top-right",
"overlay_screen": "DP-1",
"max_seconds": 120, "max_seconds": 120,
"skip_silent": False, "skip_silent": False,
"silence_db": -42.0, "silence_db": -42.0,
@@ -161,7 +162,7 @@ class Settings(DikteTest):
def test_the_window_opens_with_every_tab_on_it(self): def test_the_window_opens_with_every_tab_on_it(self):
window = self.window(cfg.Config()) window = self.window(cfg.Config())
tabs = window.findChildren(settings_ui.QTabWidget)[0] tabs = window.findChildren(settings_ui.QTabWidget)[0]
self.assertEqual(tabs.count(), 9) self.assertEqual(tabs.count(), 10)
self.assertEqual(window.windowTitle(), "Dikte Settings") self.assertEqual(window.windowTitle(), "Dikte Settings")
def test_no_tab_can_stretch_the_window_past_a_small_screen(self): def test_no_tab_can_stretch_the_window_past_a_small_screen(self):
@@ -941,6 +942,17 @@ class Overlay(DikteTest):
widget.show_recording() widget.show_recording()
widget._reposition() widget._reposition()
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): def test_a_warning_and_an_error_both_show(self):
widget = self.overlay() widget = self.overlay()
widget.show_warning("cleanup failed") widget.show_warning("cleanup failed")