# -*- coding: utf-8 -*- """ AzaSettingsMixin – Einstellungsfenster (KG-Modell, Templates, Autotext, Add-ons, etc.). """ import tkinter as tk from tkinter import ttk, messagebox from tkinter.scrolledtext import ScrolledText from aza_audit_log import log_event as _audit_log from aza_persistence import ( load_settings_geometry, save_settings_geometry, load_templates_text, save_templates_text, save_autotext, save_model, _clamp_geometry_str, load_signature_name, save_signature_name, load_user_profile, ) from aza_ui_helpers import add_resize_grip, add_font_scale_control from aza_config import MODEL_LABELS, ALLOWED_SUMMARY_MODELS class AzaSettingsMixin: """Mixin für das Einstellungsfenster.""" def _open_settings(self): SETTINGS_MIN_W, SETTINGS_MIN_H = 680, 520 win = tk.Toplevel(self) win.title("Einstellungen") win.transient(self) win.minsize(SETTINGS_MIN_W, SETTINGS_MIN_H) win.attributes("-topmost", True) if hasattr(self, "_aza_windows"): self._aza_windows.add(win) self._register_window(win) saved_geom = load_settings_geometry() if saved_geom: try: win.geometry(_clamp_geometry_str(saved_geom, SETTINGS_MIN_W, SETTINGS_MIN_H)) except Exception: win.geometry(f"{SETTINGS_MIN_W}x{SETTINGS_MIN_H}") if not saved_geom: win.geometry(f"{SETTINGS_MIN_W}x{SETTINGS_MIN_H}") win.update_idletasks() sw = win.winfo_screenwidth() sh = win.winfo_screenheight() w, h = SETTINGS_MIN_W, SETTINGS_MIN_H x = max(0, (sw - w) // 2) y = max(0, (sh - h) // 2) win.geometry(f"{w}x{h}+{x}+{y}") add_resize_grip(win, SETTINGS_MIN_W, SETTINGS_MIN_H) add_font_scale_control(win) f = ttk.Frame(win, padding=16) f.pack(fill="both", expand=True) ttk.Label(f, text="KG-Modell:").grid(row=0, column=0, sticky="w", pady=(0, 8)) display_values = [MODEL_LABELS[m] for m in ALLOWED_SUMMARY_MODELS] current = MODEL_LABELS.get(self.model_var.get(), display_values[0]) model_var_dialog = tk.StringVar(value=current) model_box = ttk.Combobox( f, textvariable=model_var_dialog, values=display_values, state="readonly", width=42 ) model_box.grid(row=0, column=1, sticky="ew", padx=(12, 0), pady=(0, 8)) f.columnconfigure(1, weight=1) def open_templates(): tw = tk.Toplevel(win) tw.title("Templates") tw.transient(win) tw.geometry("620x370") tw.configure(bg="#B9ECFA") tw.minsize(450, 280) tw.attributes("-topmost", True) self._register_window(tw) add_resize_grip(tw, 450, 280) add_font_scale_control(tw) tf = ttk.Frame(tw, padding=12) tf.pack(fill="both", expand=True) ttk.Label(tf, text="Kontext für die KI (z. B. „Ich bin ein Dermatologe und schreibe dermatologische Berichte.“). Wird bei der KG-Erstellung berücksichtigt:").pack(anchor="w") ttxt = ScrolledText(tf, wrap="word", font=self._text_font, bg="#F5FCFF", height=8) ttxt.pack(fill="both", expand=True, pady=(4, 8)) ttxt.insert("1.0", load_templates_text()) self._bind_autotext(ttxt) btn_f = ttk.Frame(tf) btn_f.pack(fill="x") def save_and_close(): save_templates_text(ttxt.get("1.0", "end").strip()) tw.destroy() ttk.Button(btn_f, text="OK", command=save_and_close).pack(side="left", padx=(0, 8)) ttk.Button(btn_f, text="Abbrechen", command=tw.destroy).pack(side="left") def do_reset(): save_templates_text("") messagebox.showinfo("Reset", "Template-Text wurde zurückgesetzt und ist jetzt leer.") ttk.Button(f, text="Templates", command=open_templates).grid(row=1, column=0, pady=(8, 4), sticky="w") ttk.Button(f, text="Reset", command=do_reset).grid(row=1, column=1, pady=(8, 4), sticky="w", padx=(12, 0)) start_frame = ttk.LabelFrame(f, text="Startverhalten / Fenster", padding=(10, 5)) start_frame.grid(row=2, column=0, columnspan=2, sticky="ew", pady=(8, 4)) diktat_auto_var = tk.BooleanVar(value=self._autotext_data.get("diktat_auto_start", True)) ttk.Checkbutton(start_frame, text="Diktat startet sofort (wenn aus: Aufnahme manuell starten)", variable=diktat_auto_var).pack(anchor="w", pady=2) notizen_open_on_start_var = tk.BooleanVar(value=self._autotext_data.get("notizen_open_on_start", True)) ttk.Checkbutton(start_frame, text="Audionotiz beim Programmstart automatisch öffnen", variable=notizen_open_on_start_var).pack(anchor="w", pady=2) kommentare_auto_var = tk.BooleanVar(value=self._autotext_data.get("kommentare_auto_open", False)) ttk.Checkbutton(start_frame, text="Kommentare-Fenster beim Programmstart automatisch öffnen", variable=kommentare_auto_var).pack(anchor="w", pady=2) def _live_textbloecke_visible(*_args): vis = bool(textbloecke_visible_var.get()) self._autotext_data["textbloecke_visible"] = vis try: if vis: self._textbloecke_container.pack(fill="x", before=self._textbloecke_anchor) else: self._textbloecke_container.pack_forget() self.update_idletasks() except Exception: pass textbloecke_visible_var = tk.BooleanVar(value=self._autotext_data.get("textbloecke_visible", True)) cb_textbloecke = ttk.Checkbutton(f, text="Textblöcke anzeigen (Inhalt bleibt gespeichert, wenn ausgeblendet)", variable=textbloecke_visible_var, command=_live_textbloecke_visible) cb_textbloecke.grid(row=3, column=0, columnspan=2, sticky="w", pady=(4, 2)) def _live_addon_visible(*_args): vis = bool(addon_visible_var.get()) self._autotext_data["addon_visible"] = vis try: if vis: self._addon_container.pack(fill="x", before=self._addon_anchor) self._update_addon_buttons_visibility() else: self._addon_container.pack_forget() self.update_idletasks() except Exception: pass addon_visible_var = tk.BooleanVar(value=self._autotext_data.get("addon_visible", True)) cb_addon = ttk.Checkbutton(f, text="Add-ons anzeigen", variable=addon_visible_var, command=_live_addon_visible) cb_addon.grid(row=4, column=0, columnspan=2, sticky="w", pady=(4, 2)) def _live_logo_visible(*_args): vis = bool(logo_visible_var.get()) self._autotext_data["logo_visible"] = vis try: if vis: self._logo_frame.place(relx=0.01, rely=0.97, anchor="sw") else: self._logo_frame.place_forget() self.update_idletasks() except Exception: pass logo_visible_var = tk.BooleanVar(value=self._autotext_data.get("logo_visible", True)) cb_logo = ttk.Checkbutton(f, text="Logo anzeigen (Klick auf Logo startet/stoppt Aufnahme)", variable=logo_visible_var, command=_live_logo_visible) cb_logo.grid(row=4, column=1, sticky="w", pady=(4, 2), padx=(12, 0)) # Unterkategorie: Welche Add-on-Buttons sollen angezeigt werden? addon_buttons_frame = ttk.LabelFrame(f, text="Welche Add-on-Buttons anzeigen?", padding=(10, 5)) addon_buttons_frame.grid(row=5, column=0, columnspan=2, sticky="ew", pady=(8, 4)) addon_buttons = self._autotext_data.get("addon_buttons", {}) addon_button_vars = {} addon_button_options = [ ("uebersetzer", "Übersetzer (provisorisch)"), ("email", "E-Mail"), ("autotext", "Autotext"), ("whatsapp", "WhatsApp"), ("docapp", "MedWork"), ("todo", "To-do"), ("macro", "Makro starten"), ("kongresse", "Kongresse"), ("news", "News"), ("empfang", "An Empfang senden"), ] todo_auto_open_var = tk.BooleanVar( value=self._autotext_data.get("todo_auto_open", True)) def _live_addon_toggle(*_args): self._autotext_data["addon_buttons"] = { bid: bool(v.get()) for bid, v in addon_button_vars.items() } try: self._update_addon_buttons_visibility() except Exception: pass grid_row = 0 for button_id, label in addon_button_options: var = tk.BooleanVar(value=addon_buttons.get(button_id, True)) addon_button_vars[button_id] = var cb = ttk.Checkbutton(addon_buttons_frame, text=label, variable=var, command=_live_addon_toggle) cb.grid(row=grid_row, column=0, sticky="w", padx=10, pady=2) grid_row += 1 if button_id == "todo": cb_auto = ttk.Checkbutton( addon_buttons_frame, text=" ↳ To-do beim Start automatisch öffnen", variable=todo_auto_open_var) cb_auto.grid(row=grid_row, column=0, sticky="w", padx=10, pady=(0, 2)) grid_row += 1 kg_auto_delete_var = tk.BooleanVar(value=self._autotext_data.get("kg_auto_delete_old", False)) cb_kg_auto = ttk.Checkbutton(f, text="KG-Einträge älter als 2 Wochen automatisch löschen (Speicher schonen)", variable=kg_auto_delete_var) cb_kg_auto.grid(row=6, column=0, columnspan=2, sticky="w", pady=(4, 2)) # Statusanzeige-Farbe status_color_frame = ttk.LabelFrame(f, text="Statusanzeige", padding=(10, 5)) status_color_frame.grid(row=7, column=0, columnspan=2, sticky="ew", pady=(8, 4)) _status_color_options = {"Standard (Orange)": "#BD4500", "Blau": "#1a4d6d", "Ausblenden": "hidden"} _current_sc = self._autotext_data.get("status_color", "#BD4500") _sc_label = "Standard (Orange)" for _lbl, _val in _status_color_options.items(): if _val == _current_sc: _sc_label = _lbl break status_color_var = tk.StringVar(value=_sc_label) def _live_status_color(*_args): sc_sel = status_color_var.get() sc_v = _status_color_options.get(sc_sel, "#BD4500") self._autotext_data["status_color"] = sc_v try: self._apply_status_color() except Exception: pass for sc_col, (sc_label, sc_val) in enumerate(_status_color_options.items()): ttk.Radiobutton(status_color_frame, text=sc_label, variable=status_color_var, value=sc_label, command=_live_status_color).grid(row=0, column=sc_col, padx=8, pady=2, sticky="w") autotext_var = tk.BooleanVar(value=self._autotext_data.get("enabled", True)) cb_autotext = ttk.Checkbutton(f, text="Autotext (Abkürzungen z. B. „mfg“ → „mit freundlichen Grüßen“)", variable=autotext_var) cb_autotext.grid(row=8, column=0, columnspan=2, sticky="w", pady=(4, 2)) def open_autotext_manage(): self._open_autotext_dialog(win) ttk.Button(f, text="Autotext verwalten", command=open_autotext_manage).grid(row=9, column=0, pady=(2, 4), sticky="w") autocopy_var = tk.BooleanVar( value=self._autotext_data.get("autocopy_after_diktat", True) ) cb_autocopy = ttk.Checkbutton( f, text="Autocopy: Nach Diktat/Transkription automatisch in Zwischenablage kopieren", variable=autocopy_var, ) cb_autocopy.grid(row=10, column=0, columnspan=2, sticky="w", pady=(4, 2)) if not hasattr(self, "_rclick_paste_var"): self._rclick_paste_var = tk.BooleanVar( value=bool(self._autotext_data.get("global_right_click_paste", True))) cb_global_right_click = ttk.Checkbutton( f, text="Global: Rechtsklick fügt direkt ein (ohne Kontextmenü, nur externe Apps)", variable=self._rclick_paste_var, command=self._toggle_rclick_paste, ) cb_global_right_click.grid(row=11, column=0, columnspan=2, sticky="w", pady=(4, 2)) sig_frame = ttk.LabelFrame(f, text="Unterschrift / Signatur", padding=(10, 5)) sig_frame.grid(row=12, column=0, columnspan=2, sticky="ew", pady=(8, 4)) sig_frame.columnconfigure(1, weight=1) profile_name = self._user_profile.get("name", "") current_sig = load_signature_name(fallback_to_profile=False) use_profile = not bool(current_sig) sig_auto_var = tk.BooleanVar(value=use_profile) sig_name_var = tk.StringVar(value=current_sig if current_sig else profile_name) cb_sig_auto = ttk.Checkbutton(sig_frame, text=f"Profilname verwenden: {profile_name}" if profile_name else "Profilname verwenden", variable=sig_auto_var) cb_sig_auto.grid(row=0, column=0, columnspan=2, sticky="w", pady=(0, 4)) ttk.Label(sig_frame, text="Abweichender Name:").grid(row=1, column=0, sticky="w", padx=(0, 8)) ent_sig = ttk.Entry(sig_frame, textvariable=sig_name_var, width=36) ent_sig.grid(row=1, column=1, sticky="ew", pady=(0, 2)) def _update_sig_entry(*_): if sig_auto_var.get(): ent_sig.configure(state="disabled") sig_name_var.set(profile_name) else: ent_sig.configure(state="normal") sig_auto_var.trace_add("write", _update_sig_entry) _update_sig_entry() self._sig_auto_var = sig_auto_var self._sig_name_var = sig_name_var # --- Audio-Test --- audio_frame = ttk.LabelFrame(f, text="Audio / Mikrofon", padding=(10, 5)) audio_frame.grid(row=13, column=0, columnspan=2, sticky="ew", pady=(8, 4)) audio_status_var = tk.StringVar(value="") def _run_audio_test(): audio_status_var.set("Test läuft …") win.update_idletasks() try: from aza_audio import test_audio_device result = test_audio_device(duration_sec=1.5) if result["ok"]: audio_status_var.set("✓ " + result["message"]) else: audio_status_var.set("✗ " + result["message"]) except Exception as exc: audio_status_var.set(f"✗ Fehler: {exc}") ttk.Button(audio_frame, text="Audio-Test starten", command=_run_audio_test).pack(side="left", padx=(0, 12)) tk.Label(audio_frame, textvariable=audio_status_var, font=("Segoe UI", 9), fg="#333", bg="#F0F0F0", wraplength=400, justify="left").pack(side="left", fill="x", expand=True) legal_frame = ttk.LabelFrame(f, text="Datenschutz & Recht", padding=(10, 5)) legal_frame.grid(row=14, column=0, columnspan=2, sticky="ew", pady=(8, 4)) ttk.Button(legal_frame, text="Datenschutzerklärung anzeigen", command=lambda: self._show_legal_text(win, "Datenschutzerklärung", "privacy_policy.md") ).grid(row=0, column=0, padx=(0, 8), pady=2, sticky="w") ttk.Button(legal_frame, text="KI-Einwilligung anzeigen", command=lambda: self._show_legal_text(win, "KI-Einwilligung", "ai_consent.md") ).grid(row=0, column=1, padx=0, pady=2, sticky="w") from aza_consent import get_consent_status, record_revoke, has_valid_consent, record_consent, export_consent_log uid = self._user_profile.get("name", "default") consent_ok = has_valid_consent(uid) consent_status_var = tk.StringVar( value=f"KI-Einwilligung: {'Erteilt' if consent_ok else 'Nicht erteilt / widerrufen'}") ttk.Label(legal_frame, textvariable=consent_status_var).grid( row=1, column=0, columnspan=2, sticky="w", pady=(6, 2)) def toggle_consent(): nonlocal consent_ok _uid = self._user_profile.get("name", "default") if has_valid_consent(_uid): if messagebox.askyesno("Einwilligung widerrufen", "Möchten Sie Ihre KI-Einwilligung widerrufen?\n\n" "KI-Funktionen (Transkription, KG-Erstellung,\n" "Interaktionsprüfung) werden danach gesperrt.", parent=win): record_revoke(_uid, source="ui") _audit_log("CONSENT_REVOKE", _uid) consent_ok = False consent_status_var.set("KI-Einwilligung: Widerrufen") btn_consent.configure(text="KI-Einwilligung erteilen") messagebox.showinfo("Widerruf", "Ihre KI-Einwilligung wurde widerrufen und protokolliert.", parent=win) else: if self._check_ai_consent(): consent_ok = True consent_status_var.set("KI-Einwilligung: Erteilt") btn_consent.configure(text="KI-Einwilligung widerrufen") btn_consent = ttk.Button(legal_frame, text="KI-Einwilligung widerrufen" if consent_ok else "KI-Einwilligung erteilen", command=toggle_consent) btn_consent.grid(row=2, column=0, padx=(0, 8), pady=2, sticky="w") def do_export(): from aza_audit_log import export_audit_log try: path_consent = export_consent_log() path_audit = export_audit_log() _audit_log("EXPORT", uid, detail="consent+audit log") messagebox.showinfo("Export", f"Consent-Log exportiert:\n{path_consent}\n\n" f"Audit-Log exportiert:\n{path_audit}", parent=win) except Exception as e: messagebox.showerror("Export-Fehler", str(e), parent=win) ttk.Button(legal_frame, text="Logs exportieren (Audit)", command=do_export).grid(row=2, column=1, padx=0, pady=2, sticky="w") def save_and_close(): try: save_settings_geometry(win.geometry()) except Exception: pass if hasattr(self, "_aza_windows"): self._aza_windows.discard(win) win.destroy() def on_ok(): selected_label = model_var_dialog.get().strip() for model_id, label in MODEL_LABELS.items(): if label == selected_label: self.model_var.set(model_id) save_model(model_id) break self._autotext_data["enabled"] = bool(autotext_var.get()) self._autotext_data["diktat_auto_start"] = bool(diktat_auto_var.get()) self._autotext_data["notizen_open_on_start"] = bool(notizen_open_on_start_var.get()) self._autotext_data["textbloecke_visible"] = bool(textbloecke_visible_var.get()) self._autotext_data["addon_visible"] = bool(addon_visible_var.get()) # Speichere die einzelnen Button-Einstellungen self._autotext_data["addon_buttons"] = { button_id: bool(var.get()) for button_id, var in addon_button_vars.items() } self._autotext_data["kg_auto_delete_old"] = bool(kg_auto_delete_var.get()) self._autotext_data["todo_auto_open"] = bool(todo_auto_open_var.get()) self._autotext_data["autocopy_after_diktat"] = bool(autocopy_var.get()) self._autotext_data["global_right_click_paste"] = bool(self._rclick_paste_var.get()) self._autotext_data["kommentare_auto_open"] = bool(kommentare_auto_var.get()) self._autotext_data["logo_visible"] = bool(logo_visible_var.get()) if self._sig_auto_var.get(): save_signature_name("") else: save_signature_name(self._sig_name_var.get().strip()) # Statusanzeige-Farbe speichern sc_selected = status_color_var.get() sc_value = _status_color_options.get(sc_selected, "#BD4500") self._autotext_data["status_color"] = sc_value save_autotext(self._autotext_data) save_and_close() # UI-Updates nach Schließen des Einstellungsfensters (verhindert Hang) def _apply_ui(): try: if self._autotext_data["textbloecke_visible"]: self._textbloecke_container.pack(fill="x", before=self._textbloecke_anchor) else: self._textbloecke_container.pack_forget() if self._autotext_data["addon_visible"]: self._addon_container.pack(fill="x", before=self._addon_anchor) self._update_addon_buttons_visibility() self.update_idletasks() h = self.winfo_height() if h < 500: self.geometry(f"{self.winfo_width()}x500") else: self._addon_container.pack_forget() except Exception: pass try: self._apply_status_color() except Exception: pass self.update_idletasks() self.after(50, _apply_ui) win.protocol("WM_DELETE_WINDOW", save_and_close) ttk.Button(f, text="OK", command=on_ok).grid(row=15, column=0, columnspan=2, pady=(12, 0)) win.focus_set() def _show_legal_text(self, parent, title: str, filename: str): """Zeigt einen Rechtstext (Markdown) in einem Lesefenster an.""" import os legal_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "legal") filepath = os.path.join(legal_dir, filename) try: with open(filepath, "r", encoding="utf-8") as fh: content = fh.read() except FileNotFoundError: messagebox.showerror("Fehler", f"Datei nicht gefunden:\n{filepath}", parent=parent) return except OSError as e: messagebox.showerror("Fehler", f"Datei konnte nicht gelesen werden:\n{e}", parent=parent) return tw = tk.Toplevel(parent) tw.title(title) tw.transient(parent) tw.geometry("720x600") tw.minsize(500, 400) tw.attributes("-topmost", True) self._register_window(tw) from aza_ui_helpers import add_resize_grip, add_font_scale_control add_resize_grip(tw, 500, 400) add_font_scale_control(tw) frame = ttk.Frame(tw, padding=12) frame.pack(fill="both", expand=True) txt = ScrolledText(frame, wrap="word", font=("Segoe UI", 10), bg="#FAFAFA") txt.pack(fill="both", expand=True, pady=(0, 8)) txt.insert("1.0", content) txt.configure(state="disabled") ttk.Button(frame, text="Schliessen", command=tw.destroy).pack(anchor="e")