diff --git a/dikte/app.py b/dikte/app.py index d82cc7c..5bf335b 100644 --- a/dikte/app.py +++ b/dikte/app.py @@ -1103,15 +1103,20 @@ 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.language_changed.connect(self._reopen_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)) @@ -1135,11 +1140,19 @@ class Dikte: # would drop the reference to the new window a moment after it is made. old.finished.disconnect(self._settings_closed) old.close() - old.deleteLater() + # 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.open_settings() - self.settings_window.tabs.setCurrentIndex(tab) + 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/settings_ui.py b/dikte/settings_ui.py index 276efb0..f166140 100644 --- a/dikte/settings_ui.py +++ b/dikte/settings_ui.py @@ -1804,9 +1804,22 @@ class SettingsWindow(QDialog): # 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: + 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): index = combo.findData(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())