diff --git a/dikte/app.py b/dikte/app.py index abede82..5bf335b 100644 --- a/dikte/app.py +++ b/dikte/app.py @@ -1103,18 +1103,57 @@ class Dikte: def open_settings(self): if self.settings_window is None: - self.settings_window = SettingsWindow(self.conf, self.meetings) - self.settings_window.applied.connect(self._apply_settings) - self.settings_window.update_found.connect(self._found_update) - self.settings_window.finished.connect(self._settings_closed) + self._make_settings() self.settings_window.show() self.settings_window.raise_() self.settings_window.activateWindow() + def _make_settings(self): + """Build the window without showing it, so a caller that knows where + it belongs can place it first.""" + self.settings_window = SettingsWindow(self.conf, self.meetings) + self.settings_window.applied.connect(self._apply_settings) + self.settings_window.language_changed.connect(self._reopen_settings) + self.settings_window.update_found.connect(self._found_update) + self.settings_window.finished.connect(self._settings_closed) + def _settings_closed(self, *_): # Don't drop the object while its own signal is still being delivered. QTimer.singleShot(0, lambda: setattr(self, "settings_window", None)) + def _reopen_settings(self): + """Replace the settings window, so a language change reaches it too. + + A save switches the language everywhere strings are made at the moment + they are shown: the tray is rebuilt, the indicator and the message box + translate as they speak. The settings window is the one place written + once, at construction, so the window that took the new language is the + one place still showing the old one. A fresh window comes up where the + old one stood, on the same tab. + """ + old = self.settings_window + if old is None: + return + tab = old.tabs.currentIndex() + geometry = old.geometry() + # Replaced rather than merely closed: left connected, _settings_closed + # would drop the reference to the new window a moment after it is made. + old.finished.disconnect(self._settings_closed) + old.close() + # No deleteLater: a daemon thread of the old window's may still be + # running, and a closure holding self is what keeps the object alive + # until the thread is done. Dropping the reference is how the ordinary + # close path lets a window go, and it is enough here too. + self.settings_window = None + self._make_settings() + # Placed and turned to the old tab before it is shown, so the new + # window does not come up at the default size and jump. + self.settings_window.setGeometry(geometry) + self.settings_window.tabs.setCurrentIndex(tab) + self.settings_window.show() + self.settings_window.raise_() + self.settings_window.activateWindow() + def _apply_local(self): """Pass the local settings on, and hold the models ready if asked to. diff --git a/dikte/i18n.py b/dikte/i18n.py index f7f4910..d8cb759 100644 --- a/dikte/i18n.py +++ b/dikte/i18n.py @@ -162,8 +162,6 @@ TR = { "Automatic (system)": "Otomatik (sistem)", "Turkish": "Türkçe", "English": "İngilizce", - "Restart Dikte for the language change to reach every window.": - "Dil değişikliğinin her pencereye işlemesi için Dikte'yi yeniden başlat.", "Microphone": "Mikrofon", "Default microphone": "Varsayılan mikrofon", "Speech language": "Konuşma dili", diff --git a/dikte/settings_ui.py b/dikte/settings_ui.py index d3a97b7..f166140 100644 --- a/dikte/settings_ui.py +++ b/dikte/settings_ui.py @@ -23,6 +23,7 @@ from . import filetranscribe from . import ggml from . import hotkey from . import hub +from . import i18n from . import ipc from . import meeting from . import paste @@ -515,6 +516,10 @@ class LocalModelBox(QGroupBox): class SettingsWindow(QDialog): applied = pyqtSignal() + # A save changed the interface language. Every label below is translated + # as the window is built, so the change cannot reach this window: the + # owner replaces it with a fresh one instead. + language_changed = pyqtSignal() # A newer release this window's own check found, so that the tray icon # hears about it from here rather than waiting for its own next check. update_found = pyqtSignal(object) @@ -541,6 +546,8 @@ class SettingsWindow(QDialog): self._key_fields = {} self._testers = {} self._shown_provider = "" + # What _save compares against to know a rebuild is due. + self._built_language = i18n.language() # Where "Open the release page" goes: the release itself once a check # has named one, and the page that redirects to the newest until then. self._release_url = update.RELEASES_PAGE @@ -639,9 +646,6 @@ class SettingsWindow(QDialog): self.ui_language = QComboBox() for label, code in UI_LANGUAGES: self.ui_language.addItem(t(label), code) - self.ui_language.setToolTip( - t("Restart Dikte for the language change to reach every window.") - ) form.addRow(t("Interface language"), self.ui_language) self.mic = QComboBox() @@ -1794,7 +1798,27 @@ class SettingsWindow(QDialog): print(f"dikte: could not trim the history ({exc})") self._load_history() # the trim may just have dropped rows from the list self.applied.emit() + # conf.save() has switched the language t() speaks, so the message box + # already answers in the new one; the labels around it were translated + # when the window was built and stay behind. Asking for the rebuild + # waits until the box is dismissed, so the window is not pulled out + # from under a dialog it is holding up. QMessageBox.information(self, t("Dikte Settings"), t("Saved successfully.")) + if i18n.language() != self._built_language and not self._work_in_flight(): + self.language_changed.emit() + + def _work_in_flight(self): + """A daemon thread of this window's is still running. + + Replacing the window now would let it be collected, taking the C++ + side of the model boxes down with it, and the thread's next progress + report would land on a deleted object. The stale labels stand until a + later save finds the window quiet; _built_language keeps the old + language, so that save asks for the rebuild by itself. + """ + return (self.transcriber.busy + or self.local_whisper._downloading + or self.local_llm._downloading) @staticmethod def _select_data(combo, value): diff --git a/tests/test_ui.py b/tests/test_ui.py index 867e040..060686a 100644 --- a/tests/test_ui.py +++ b/tests/test_ui.py @@ -240,6 +240,21 @@ class Settings(DikteTest): with self.subTest(key=key): self.assertEqual(stored[key], value) + def test_only_a_save_that_changed_the_language_says_so(self): + # The owner answers language_changed by replacing the window, so a + # save that left the language alone must keep quiet. + i18n = settings_ui.i18n + self.addCleanup(i18n.set_language, i18n.language()) + window = self.window(cfg.Config()) + heard = [] + window.language_changed.connect(lambda: heard.append(True)) + window._save() + self.assertEqual(heard, []) + other = "en" if i18n.language() == "tr" else "tr" + window._select_data(window.ui_language, other) + window._save() + self.assertEqual(heard, [True]) + def test_the_model_box_on_screen_belongs_to_whoever_cleans_up(self): """An OpenRouter id and a Claude alias are not the same field.""" window = self.window(cfg.Config())