From 2c764f35c7bdaaed187b311d5ee88ebb96ded065 Mon Sep 17 00:00:00 2001 From: gummiflip Date: Thu, 18 Jun 2026 19:11:44 +0200 Subject: [PATCH 1/2] =?UTF-8?q?fix:=20Layout=20des=20KI-Workflows-Tabs=20a?= =?UTF-8?q?ufr=C3=A4umen?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Der KI-Workflows-Tab (und konsistent alle Settings-Tabs) wirkte unfertig: gequetschte, kontrastarme Hilfetexte, ein dominierendes Prompt-Feld und nicht bündig ausgerichtete Felder. - Hilfetexte über die volle Breite spannen statt in die schmale Feld-Spalte zu quetschen (addRow("", ...) -> addRow(...)) - create_help_label: Theme-Farbe APP_TEXT_DIM statt gray, 11px, padding-bottom für klare Gruppentrennung - FieldGrowthPolicy.AllNonFixedFieldsGrow für bündige Eingabefelder - Dampf-Prompt-Feld auf 72-120px und Begriffsliste auf max 120px begrenzt, damit sie die Seite nicht dominieren - Tabs in scrollbare QScrollArea gewrappt, damit der inhaltsreiche LLM-Tab nicht überläuft; Initialgröße 550x480 -> 580x560 --- app/blitztext_linux.py | 66 +++++++++++++++++++++++++++--------------- 1 file changed, 43 insertions(+), 23 deletions(-) diff --git a/app/blitztext_linux.py b/app/blitztext_linux.py index 026eb18..bbd3107 100644 --- a/app/blitztext_linux.py +++ b/app/blitztext_linux.py @@ -22,7 +22,7 @@ QApplication, QDialog, QVBoxLayout, QHBoxLayout, QTabWidget, QWidget, QFormLayout, QComboBox, QLineEdit, QCheckBox, QPlainTextEdit, QPushButton, QDialogButtonBox, QLabel, QMessageBox, QMenu, QSystemTrayIcon, QStyle, - QListWidget, + QListWidget, QScrollArea, QFrame, ) # Make project importable when running directly @@ -119,8 +119,12 @@ def _require_display_environment() -> None: def create_help_label(text: str) -> QLabel: """Create a styled small help label for config fields.""" + from app import theme + label = QLabel(text) - label.setStyleSheet("color: gray; font-size: 10px;") + label.setStyleSheet( + f"color: {theme.APP_TEXT_DIM}; font-size: 11px; padding: 1px 0 8px 2px;" + ) label.setWordWrap(True) return label @@ -132,9 +136,19 @@ def __init__(self, config: Config, parent: Optional[QWidget] = None) -> None: super().__init__(parent) self.config = config self.setWindowTitle("Blitztext Einstellungen") - self.resize(550, 480) + self.resize(580, 560) self.init_ui() + @staticmethod + def _scrollable(content: QWidget) -> QScrollArea: + """Wrap a tab page so long forms scroll instead of overflowing the dialog.""" + scroll = QScrollArea() + scroll.setWidgetResizable(True) + scroll.setFrameShape(QFrame.Shape.NoFrame) + scroll.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff) + scroll.setWidget(content) + return scroll + def init_ui(self) -> None: layout = QVBoxLayout(self) layout.setContentsMargins(15, 15, 15, 15) @@ -147,6 +161,7 @@ def init_ui(self) -> None: tab_whisper = QWidget() form_whisper = QFormLayout(tab_whisper) form_whisper.setSpacing(10) + form_whisper.setFieldGrowthPolicy(QFormLayout.FieldGrowthPolicy.AllNonFixedFieldsGrow) self.combo_model = QComboBox() self.combo_model.addItems(["tiny", "base", "small", "medium", "large", "large-v2", "large-v3", "large-v3-turbo"]) @@ -173,29 +188,30 @@ def init_ui(self) -> None: self.combo_transcription_key.setCurrentText(self.config.transcription_hotkey) form_whisper.addRow("Whisper-Modell:", self.combo_model) - form_whisper.addRow("", create_help_label("Wählen Sie die Modellgröße. Größere Modelle sind genauer, benötigen aber mehr Ressourcen.")) + form_whisper.addRow(create_help_label("Wählen Sie die Modellgröße. Größere Modelle sind genauer, benötigen aber mehr Ressourcen.")) form_whisper.addRow("Transkription-Backend:", self.combo_backend) - form_whisper.addRow("", create_help_label("faster-whisper ist deutlich schneller und ressourcenschonender.")) + form_whisper.addRow(create_help_label("faster-whisper ist deutlich schneller und ressourcenschonender.")) form_whisper.addRow("Sprache:", self.edit_language) - form_whisper.addRow("", create_help_label("Zweistelliger Ländercode (z. B. 'de', 'en') oder 'auto' für automatische Erkennung.")) + form_whisper.addRow(create_help_label("Zweistelliger Ländercode (z. B. 'de', 'en') oder 'auto' für automatische Erkennung.")) form_whisper.addRow("Audio-Eingabegerät:", self.edit_audio_device) - form_whisper.addRow("", create_help_label("'@DEFAULT_SOURCE@' nutzt das Standardmikrofon von PulseAudio/PipeWire.")) + form_whisper.addRow(create_help_label("'@DEFAULT_SOURCE@' nutzt das Standardmikrofon von PulseAudio/PipeWire.")) form_whisper.addRow("Hotkey-Modus:", self.combo_hotkey_mode) - form_whisper.addRow("", create_help_label("toggle: Einmal drücken zum Starten, erneut drücken zum Stoppen.\nhold: Gedrückt halten zum Aufnehmen, Loslassen zum Stoppen.")) + form_whisper.addRow(create_help_label("toggle: Einmal drücken zum Starten, erneut drücken zum Stoppen.\nhold: Gedrückt halten zum Aufnehmen, Loslassen zum Stoppen.")) form_whisper.addRow("Aufnahme-Taste:", self.combo_transcription_key) - form_whisper.addRow("", create_help_label("Einzelne Taste ohne Modifier (z. B. KEY_LEFTALT). Änderung wird sofort übernommen.")) + form_whisper.addRow(create_help_label("Einzelne Taste ohne Modifier (z. B. KEY_LEFTALT). Änderung wird sofort übernommen.")) - self.tabs.addTab(tab_whisper, "Spracherkennung") + self.tabs.addTab(self._scrollable(tab_whisper), "Spracherkennung") # Tab 2: LLM (KI) tab_llm = QWidget() form_llm = QFormLayout(tab_llm) form_llm.setSpacing(10) + form_llm.setFieldGrowthPolicy(QFormLayout.FieldGrowthPolicy.AllNonFixedFieldsGrow) self.edit_api_key_env = QLineEdit() self.edit_api_key_env.setText(self.config.openai_api_key_env) @@ -253,6 +269,8 @@ def init_ui(self) -> None: self.edit_dampf_prompt = QPlainTextEdit() self.edit_dampf_prompt.setPlainText(self.config.dampf_system_prompt) self.edit_dampf_prompt.setPlaceholderText("Standard-Systemprompt verwenden...") + self.edit_dampf_prompt.setMinimumHeight(72) + self.edit_dampf_prompt.setMaximumHeight(120) self.edit_custom_term = QLineEdit() self.edit_custom_term.setPlaceholderText("z. B. Blitztext") @@ -260,6 +278,7 @@ def init_ui(self) -> None: self.list_custom_terms = QListWidget() self.list_custom_terms.addItems(self.config.custom_terms) self.list_custom_terms.setSelectionMode(QListWidget.SelectionMode.ExtendedSelection) + self.list_custom_terms.setMaximumHeight(120) self.btn_add_custom_term = QPushButton("Hinzufügen") self.btn_add_custom_term.clicked.connect(self._add_custom_term) self.btn_remove_custom_term = QPushButton("Ausgewählte entfernen") @@ -278,53 +297,54 @@ def init_ui(self) -> None: custom_terms_widget.setLayout(custom_terms_layout) form_llm.addRow("API-Key-Umgebung:", api_key_layout) - form_llm.addRow("", create_help_label("Nur der Name der Umgebungsvariable wird gespeichert. Der Schlüssel selbst wird aus os.environ gelesen (secrets.env). Für OpenRouter z. B. OPENROUTER_API_KEY.")) + form_llm.addRow(create_help_label("Nur der Name der Umgebungsvariable wird gespeichert. Der Schlüssel selbst wird aus os.environ gelesen (secrets.env). Für OpenRouter z. B. OPENROUTER_API_KEY.")) form_llm.addRow("LLM-Anbieter:", self.combo_llm_provider) - form_llm.addRow("", create_help_label("OpenAI = Standard. OpenRouter und 'Eigener Endpunkt' nutzen das OpenAI-kompatible API über eine eigene Basis-URL und ein eigenes Modell.")) + form_llm.addRow(create_help_label("OpenAI = Standard. OpenRouter und 'Eigener Endpunkt' nutzen das OpenAI-kompatible API über eine eigene Basis-URL und ein eigenes Modell.")) form_llm.addRow("Basis-URL (base_url):", self.edit_base_url) - form_llm.addRow("", create_help_label("Leer = OpenAI-Standard. Für OpenRouter: https://openrouter.ai/api/v1. Muss mit http:// oder https:// beginnen.")) + form_llm.addRow(create_help_label("Leer = OpenAI-Standard. Für OpenRouter: https://openrouter.ai/api/v1. Muss mit http:// oder https:// beginnen.")) form_llm.addRow("LLM-Modell:", self.edit_llm_model) - form_llm.addRow("", create_help_label("Modellname beim Anbieter, z. B. 'gpt-4o-mini' (OpenAI) oder 'openai/gpt-4o' (OpenRouter).")) + form_llm.addRow(create_help_label("Modellname beim Anbieter, z. B. 'gpt-4o-mini' (OpenAI) oder 'openai/gpt-4o' (OpenRouter).")) form_llm.addRow("Text-Verbesserer Tonfall:", self.combo_tone) form_llm.addRow("Schreibstil-Vorlage:", self.combo_writing_preset) - form_llm.addRow("", create_help_label("Vorlage für den Text-Verbesserer (z. B. E-Mail formell, Stichpunkte). Bei 'Standard' greift der Tonfall oben; jede andere Vorlage bestimmt den Schreibstil selbst und ersetzt den Tonfall.")) + form_llm.addRow(create_help_label("Vorlage für den Text-Verbesserer (z. B. E-Mail formell, Stichpunkte). Bei 'Standard' greift der Tonfall oben; jede andere Vorlage bestimmt den Schreibstil selbst und ersetzt den Tonfall.")) form_llm.addRow("Emoji-Dichte:", self.combo_emoji) form_llm.addRow("Dampf-Umschreiber Prompt:", self.edit_dampf_prompt) - form_llm.addRow("", create_help_label("Eigener System-Prompt, um wütende Aussagen in eine professionelle Form umzuschreiben.")) + form_llm.addRow(create_help_label("Eigener System-Prompt, um wütende Aussagen in eine professionelle Form umzuschreiben.")) form_llm.addRow("Eigennamen / Begriffe:", custom_terms_widget) - form_llm.addRow("", create_help_label("Wörter, Namen und Fachbegriffe, die bei Transkription und KI-Umschreibung exakt beibehalten werden sollen.")) + form_llm.addRow(create_help_label("Wörter, Namen und Fachbegriffe, die bei Transkription und KI-Umschreibung exakt beibehalten werden sollen.")) - self.tabs.addTab(tab_llm, "KI-Workflows") + self.tabs.addTab(self._scrollable(tab_llm), "KI-Workflows") # Tab 3: Allgemein tab_general = QWidget() form_general = QFormLayout(tab_general) form_general.setSpacing(10) + form_general.setFieldGrowthPolicy(QFormLayout.FieldGrowthPolicy.AllNonFixedFieldsGrow) self.check_autopaste = QCheckBox("Text automatisch einfügen (Auto-Paste)") self.check_autopaste.setChecked(self.config.autopaste) form_general.addRow(self.check_autopaste) - form_general.addRow("", create_help_label("Simuliert Strg+V nach Abschluss der Aufnahme. Benötigt das Tool 'ydotool'.")) + form_general.addRow(create_help_label("Simuliert Strg+V nach Abschluss der Aufnahme. Benötigt das Tool 'ydotool'.")) self.edit_notes_folder = QLineEdit() self.edit_notes_folder.setText(self.config.notes_folder) self.edit_notes_folder.setPlaceholderText(str(Path.home() / "Blitztext-Notizen")) form_general.addRow("Diktat-Notizordner:", self.edit_notes_folder) - form_general.addRow("", create_help_label("Ordner für Diktat-Notizen (muss innerhalb von ~ liegen). Leer = Speichern deaktiviert.")) + form_general.addRow(create_help_label("Ordner für Diktat-Notizen (muss innerhalb von ~ liegen). Leer = Speichern deaktiviert.")) self.spin_history_size = QComboBox() self.spin_history_size.addItems(["10", "25", "50", "75", "100"]) self.spin_history_size.setCurrentText(str(self.config.history_size)) form_general.addRow("Verlauf-Größe:", self.spin_history_size) - form_general.addRow("", create_help_label("Maximale Anzahl der im Verlauf gespeicherten Einträge.")) + form_general.addRow(create_help_label("Maximale Anzahl der im Verlauf gespeicherten Einträge.")) self.btn_open_config = QPushButton("📄 Konfigurationsdatei öffnen") self.btn_open_config.clicked.connect(self._open_config_file) form_general.addRow(self.btn_open_config) - form_general.addRow("", create_help_label( + form_general.addRow(create_help_label( "Öffnet config.json im Standard-Editor – für erweiterte Prompt- und " "Workflow-Anpassungen, die über die Felder oben hinausgehen.")) @@ -334,7 +354,7 @@ def init_ui(self) -> None: version_label.setAlignment(Qt.AlignmentFlag.AlignRight) form_general.addRow(version_label) - self.tabs.addTab(tab_general, "Allgemein") + self.tabs.addTab(self._scrollable(tab_general), "Allgemein") layout.addWidget(self.tabs) From 72478b24bcc1c13237fdb749166a49154b7a142b Mon Sep 17 00:00:00 2001 From: gummiflip Date: Thu, 18 Jun 2026 19:21:40 +0200 Subject: [PATCH 2/2] =?UTF-8?q?fix:=20Stauchen=20der=20Settings-Tabs=20bei?= =?UTF-8?q?=20mittlerer=20Fensterh=C3=B6he=20verhindern?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit setWidgetResizable(True) staucht das innere Formular auf jede Höhe zwischen minimumSizeHint und sizeHint. Da die Hilfe-Labels Wortumbruch nutzen, ist ihre Minimalhöhe weit kleiner als die tatsächlich benötigte Höhe -- dadurch überlappten die Zeilen, sobald das Fenster knapp zu klein für die volle Höhe war (Standardgröße), ohne dass eine Scrollbar erschien. FormScrollArea erzwingt im resizeEvent als Mindesthöhe des Inhalts stets die für die aktuelle Breite nötige Höhe (heightForWidth). Dadurch wird bei Platzmangel sauber gescrollt statt gequetscht -- bei jeder Fenstergröße. --- app/blitztext_linux.py | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/app/blitztext_linux.py b/app/blitztext_linux.py index bbd3107..6295181 100644 --- a/app/blitztext_linux.py +++ b/app/blitztext_linux.py @@ -117,6 +117,27 @@ def _require_display_environment() -> None: sys.exit(1) +class FormScrollArea(QScrollArea): + """Scroll area for settings tabs. + + Mit ``setWidgetResizable(True)`` würde Qt das innere Formular auf jede + Höhe zwischen ``minimumSizeHint`` und ``sizeHint`` stauchen. Da die + Hilfe-Labels Wortumbruch nutzen, ist ihre Minimalhöhe viel kleiner als + die tatsächlich benötigte Höhe – dadurch überlappen die Zeilen, sobald + das Fenster knapp zu klein für die volle Höhe ist. Wir erzwingen daher + als Mindesthöhe stets die für die aktuelle Breite nötige Höhe, sodass + bei Platzmangel sauber gescrollt statt gequetscht wird. + """ + + def resizeEvent(self, event) -> None: # type: ignore[override] + super().resizeEvent(event) + content = self.widget() + if content is not None: + needed = content.heightForWidth(self.viewport().width()) + if needed > 0 and content.minimumHeight() != needed: + content.setMinimumHeight(needed) + + def create_help_label(text: str) -> QLabel: """Create a styled small help label for config fields.""" from app import theme @@ -142,7 +163,7 @@ def __init__(self, config: Config, parent: Optional[QWidget] = None) -> None: @staticmethod def _scrollable(content: QWidget) -> QScrollArea: """Wrap a tab page so long forms scroll instead of overflowing the dialog.""" - scroll = QScrollArea() + scroll = FormScrollArea() scroll.setWidgetResizable(True) scroll.setFrameShape(QFrame.Shape.NoFrame) scroll.setHorizontalScrollBarPolicy(Qt.ScrollBarPolicy.ScrollBarAlwaysOff)