Files
aza/AzA march 2026 - Kopie (8)/aza_settings_mixin.py
2026-04-16 13:32:32 +02:00

500 lines
23 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# -*- 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"),
]
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")