Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 17 additions & 1 deletion app/blitztext_linux.py
Original file line number Diff line number Diff line change
Expand Up @@ -869,8 +869,21 @@ def _on_writing_preset_selected(self, key: str) -> None:
self.config.save()
self.llm_service = self._build_llm_service()
self.update_menu_availability()
if self._main_window is not None:
self._main_window.set_preset(key)
logger.info("Writing preset changed via tray: %s", key)

def main_window_preset_changed(self, key: str) -> None:
"""Vom Hauptfenster aufgerufen, wenn die Preset-Combo geändert wird."""
if key == self.config.writing_preset:
return
self.config.writing_preset = key
self.config.save()
self.llm_service = self._build_llm_service()
self.update_menu_availability()
self._refresh_preset_menu()
logger.info("Writing preset changed via main window: %s", key)

def start_hotkey_worker(self) -> None:
self.stop_hotkey_worker()

Expand Down Expand Up @@ -904,8 +917,10 @@ def show_settings_dialog(self) -> None:
self.llm_service = self._build_llm_service()
self._refresh_i18n_texts()
self.update_menu_availability()
# Preset kann im Dialog geändert worden sein -> Häkchen angleichen.
# Preset kann im Dialog geändert worden sein -> Häkchen + Combo angleichen.
self._refresh_preset_menu()
if self._main_window is not None:
self._main_window.set_preset(self.config.writing_preset)

# Restart hotkey listener if mode or key changed
if self.hotkey_worker and (
Expand Down Expand Up @@ -1167,6 +1182,7 @@ def _ensure_main_window(self) -> MainWindow:
if self._history_panel is not None:
window.set_history_count(self._history_panel.entry_count)
window.update_state(self.state, self.current_workflow, self._tray_error_message)
window.set_preset(self.config.writing_preset)
self._main_window = window
return self._main_window

Expand Down
33 changes: 33 additions & 0 deletions app/main_window.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,7 @@
from app.llm_service import WorkflowType, LLM_WORKFLOWS
from app.i18n import t
from app import theme
from app.writing_presets import WRITING_PRESET_KEYS

# Reihenfolge der Workflows in der Auswahl
_WORKFLOW_ORDER = [
Expand Down Expand Up @@ -180,8 +181,18 @@ def _setup_ui(self) -> None:
self._workflow_combo.setMinimumHeight(28)
for wf in _WORKFLOW_ORDER:
self._workflow_combo.addItem(t(f"workflow.{wf.value}.name"), userData=wf)
self._workflow_combo.currentIndexChanged.connect(self._on_workflow_changed)
layout.addWidget(self._workflow_combo)

# Schreibstil-Preset-Auswahl (nur sichtbar bei Blitztext+)
self._preset_combo = QComboBox()
self._preset_combo.setMinimumHeight(28)
for key in WRITING_PRESET_KEYS:
self._preset_combo.addItem(t(f"preset.{key}.name"), userData=key)
self._preset_combo.currentIndexChanged.connect(self._on_preset_changed)
self._preset_combo.setVisible(False)
layout.addWidget(self._preset_combo)

# Hero: runder Record-Shutter
self._btn_toggle = RecordButton()
self._btn_toggle.clicked.connect(self._on_toggle_clicked)
Expand Down Expand Up @@ -264,6 +275,18 @@ def _selected_workflow(self) -> WorkflowType:
wf = self._workflow_combo.currentData()
return wf if isinstance(wf, WorkflowType) else WorkflowType.TRANSCRIPTION

@pyqtSlot()
def _on_workflow_changed(self) -> None:
is_text_improver = self._selected_workflow() == WorkflowType.TEXT_IMPROVER
self._preset_combo.setVisible(is_text_improver)
self.adjustSize()

@pyqtSlot()
def _on_preset_changed(self) -> None:
key = self._preset_combo.currentData()
if key:
self._controller.main_window_preset_changed(key)

@pyqtSlot()
def _on_toggle_clicked(self) -> None:
self._controller.gui_toggle_recording(self._selected_workflow())
Expand Down Expand Up @@ -293,6 +316,7 @@ def update_state(self, state: str, workflow: Optional[WorkflowType], error: Opti
)
self._btn_discard.setEnabled(recording)
self._workflow_combo.setEnabled(state == "IDLE")
self._preset_combo.setEnabled(state == "IDLE")

if error:
self._set_status(t("mainwindow.status.error"), theme.STATE_ERROR)
Expand Down Expand Up @@ -343,6 +367,15 @@ def set_dictation_checked(self, checked: bool) -> None:
self._btn_dictation.setChecked(checked)
self._btn_dictation.blockSignals(False)

def set_preset(self, key: str) -> None:
"""Setzt den Preset-Combo auf ``key`` ohne Signal auszulösen."""
self._preset_combo.blockSignals(True)
for i in range(self._preset_combo.count()):
if self._preset_combo.itemData(i) == key:
self._preset_combo.setCurrentIndex(i)
break
self._preset_combo.blockSignals(False)

def _update_timer_label(self) -> None:
if self._rec_start is None:
return
Expand Down
89 changes: 88 additions & 1 deletion tests/test_tray_preset_menu.py
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
"""Tests für das Tray-Submenu „Schreibstil-Vorlage" (Paket F).
"""Tests für das Tray-Submenu „Schreibstil-Vorlage" (Paket F) und die
bidirektionale Sync Tray ↔ Hauptfenster (Paket K).

Deckt das Zusammenspiel von Preset-Auswahl im Tray, Config-Persistenz und
LLM-Service-Neuaufbau ab:

* Auswahl im Submenu persistiert den Preset und baut den LLMService neu.
* Das Menü spiegelt jederzeit die ``config.writing_preset`` (Häkchen).
* Ein Settings-Save mit geändertem Preset gleicht das Häkchen wieder an.
* Tray-Auswahl spiegelt sich im Hauptfenster-Combo (und umgekehrt).

GUI-gated über ``WHISPER_GUI_TESTS=1``: die echte ``BlitztextApp`` baut Tray +
QActionGroup auf und benötigt eine (Offscreen-)QApplication, analog zu
Expand Down Expand Up @@ -136,3 +138,88 @@ def test_submenu_enabled_when_llm_available(self, tray_app, monkeypatch):
tray_app.update_menu_availability()

assert tray_app.menu_preset.isEnabled() is True


@gui_only
class TestMainWindowPresetSync:
"""Bidirektionale Sync Tray ↔ Hauptfenster-Preset-Combo (Paket K)."""

def _open_window(self, tray_app):
"""Öffnet das Hauptfenster und gibt es zurück."""
tray_app.show_main_window()
return tray_app._main_window

def test_window_init_syncs_config_preset(self, tray_app):
"""Beim Öffnen spiegelt der Combo den gespeicherten Preset."""
target = _other_key(tray_app.config.writing_preset)
tray_app.config.writing_preset = target

window = self._open_window(tray_app)

assert window._preset_combo.currentData() == target

def test_tray_change_updates_main_window(self, tray_app):
"""Tray-Auswahl aktualisiert den Preset-Combo im Hauptfenster."""
window = self._open_window(tray_app)
target = _other_key(tray_app.config.writing_preset)

tray_app._on_writing_preset_selected(target)

assert window._preset_combo.currentData() == target

def test_main_window_change_updates_tray(self, tray_app):
"""Preset-Änderung im Hauptfenster setzt das Tray-Häkchen."""
self._open_window(tray_app)
target = _other_key(tray_app.config.writing_preset)

tray_app.main_window_preset_changed(target)

assert tray_app.preset_actions[target].isChecked() is True
assert tray_app.config.writing_preset == target

def test_main_window_change_noop_on_same_preset(self, tray_app):
"""Erneutes Setzen desselben Presets aus dem Hauptfenster ist No-Op."""
self._open_window(tray_app)
current = tray_app.config.writing_preset
old_service = tray_app.llm_service

tray_app.main_window_preset_changed(current)

assert tray_app.llm_service is old_service
assert not tray_app.config.config_file.is_file()

def test_settings_save_syncs_main_window_combo(self, tray_app, monkeypatch):
"""Settings-Save mit geändertem Preset gleicht auch den Combo an."""
from PyQt6.QtWidgets import QDialog
import app.blitztext_linux as mod

window = self._open_window(tray_app)
target = _other_key(tray_app.config.writing_preset)

class _FakeDialog:
def __init__(self, config):
config.writing_preset = target

def exec(self):
return QDialog.DialogCode.Accepted

monkeypatch.setattr(mod, "SettingsDialog", _FakeDialog)
tray_app.show_settings_dialog()

assert window._preset_combo.currentData() == target

def test_preset_combo_visible_only_for_text_improver(self, tray_app):
"""Preset-Combo im Hauptfenster ist nur bei Blitztext+ sichtbar."""
from app.workflows import WorkflowType

window = self._open_window(tray_app)

# Standard-Workflow ist TRANSCRIPTION → Combo unsichtbar
for i in range(window._workflow_combo.count()):
wf = window._workflow_combo.itemData(i)
window._workflow_combo.setCurrentIndex(i)
expected = wf == WorkflowType.TEXT_IMPROVER
assert window._preset_combo.isVisible() == expected, (
f"Workflow {wf}: Combo sichtbar={window._preset_combo.isVisible()}, "
f"erwartet={expected}"
)