# -*- coding: utf-8 -*- """Verwaltungsfenster für Autotext und Textblöcke — ruhige Office-Hülle, ohne alte Hauptfenster-Optik.""" from __future__ import annotations import threading import tkinter as tk from tkinter import messagebox from tkinter.scrolledtext import ScrolledText from typing import Dict FF = "Segoe UI" _AUTOTEXT_ATTR = "_aza_workspace_autotext_toplevel" _TB_WIN_DICT_ATTR = "_aza_workspace_tb_toplevels" def _palette_for_app(app) -> Dict[str, str]: sh = getattr(app, "_aza_office_v1", None) p = getattr(sh, "_palette", None) if sh is not None else None if isinstance(p, dict) and p: return dict(p) try: from aza_office_shell_v1 import PALETTE_LIGHT return dict(PALETTE_LIGHT) except Exception: return { "BG": "#EAF2F7", "SURFACE": "#FFFFFF", "BORDER": "#D6E2EB", "TEXT": "#1A4D6D", "SUBTLE": "#5C7A8E", "ACCENT": "#5B8DB3", "TEXT_AREA_BG": "#FFFFFF", "TEXT_AREA_FG": "#1A4D6D", "ACCENT_SOFT": "#E2EEF6", "TEXT_STRONG": "#0F3850", } def _header_bar(parent, palette: Dict[str, str], title: str) -> tk.Frame: bar = tk.Frame(parent, bg=palette["ACCENT"], bd=0, highlightthickness=0) bar.pack(fill="x") tk.Label( bar, text=title, bg=palette["ACCENT"], fg="white", font=(FF, 10, "bold"), padx=12, pady=8, anchor="w", ).pack(side="left") return bar def _register_if_any(app, win: tk.Toplevel) -> None: reg = getattr(app, "_register_window", None) if callable(reg): try: reg(win) except Exception: pass def _lift_above_main(win: tk.Toplevel, app: tk.Misc) -> None: """Hält den Dialog über dem Hauptfenster (Windows: transient + lift + topmost).""" try: win.transient(app) except Exception: pass try: win.lift(app) win.focus_force() except Exception: pass try: win.attributes("-topmost", True) except Exception: pass def open_workspace_autotext_manager(app) -> None: """Genau ein Autotext-Fenster; Kürzel wie bisher über basis14 / Persistenz.""" from aza_ui_helpers import ( center_window, load_toplevel_geometry, save_toplevel_geometry, ) from aza_persistence import load_autotext, save_autotext from aza_workspace_sync import ( prune_autotext_meta, schedule_workspace_cloud_push, touch_autotext_entry_meta, ) exist = getattr(app, _AUTOTEXT_ATTR, None) if exist is not None: try: if exist.winfo_exists(): rf = getattr(exist, "_aza_reload_autotext_disk", None) if callable(rf): rf() exist.deiconify() _lift_above_main(exist, app) return except tk.TclError: pass p = _palette_for_app(app) root = tk.Toplevel(app) setattr(app, _AUTOTEXT_ATTR, root) root.title("Autotext") root.configure(bg=p["SURFACE"]) root.minsize(560, 540) root.transient(app) _register_if_any(app, root) geom_name = "workspace_autotext" saved = load_toplevel_geometry(geom_name) if saved: try: root.geometry(saved) except tk.TclError: root.geometry("720x640") center_window(root, 720, 640) else: root.geometry("720x640") center_window(root, 720, 640) dik_state = {"active": False} def _clear_ref_and_close() -> None: if dik_state["active"]: dik_state["active"] = False try: rec = getattr(app, "recorder", None) if rec is not None: try: rec.stop_and_save_wav() except Exception: pass except Exception: pass try: if getattr(app, _AUTOTEXT_ATTR, None) is root: setattr(app, _AUTOTEXT_ATTR, None) except Exception: pass try: save_toplevel_geometry(geom_name, root.geometry()) except Exception: pass try: root.attributes("-topmost", False) except Exception: pass try: root.destroy() except Exception: pass root.protocol("WM_DELETE_WINDOW", _clear_ref_and_close) _lift_above_main(root, app) outer = tk.Frame(root, bg=p["SURFACE"]) outer.pack(fill="both", expand=True) _header_bar(outer, p, "Autotext verwalten") body = tk.Frame(outer, bg=p["SURFACE"]) body.pack(fill="both", expand=True, padx=12, pady=(0, 12)) btn_bar = tk.Frame(body, bg=p["SURFACE"]) btn_bar.pack(side="bottom", fill="x", pady=(10, 0)) top_part = tk.Frame(body, bg=p["SURFACE"]) top_part.pack(fill="both", expand=True) tk.Label( top_part, text="Kürzel tippen und mit Leerzeichen, Tab oder Satzzeichen abschließen — " "Ersetzung funktioniert in AzA und (wenn aktiv) global in anderen Programmen.", bg=p["SURFACE"], fg=p["SUBTLE"], font=(FF, 9), wraplength=640, justify="left", ).pack(anchor="w", pady=(10, 8)) data_ref = getattr(app, "_autotext_data", None) if not isinstance(data_ref, dict): messagebox.showerror("Autotext", "Interne Datenbasis fehlt.", parent=root) _clear_ref_and_close() return def pull_disk_into_data_ref() -> None: fresh = load_autotext() if not isinstance(fresh, dict): return data_ref["enabled"] = bool(fresh.get("enabled", True)) fe = fresh.get("entries") if isinstance(fresh.get("entries"), dict) else {} te = data_ref.setdefault("entries", {}) if isinstance(te, dict): te.clear() te.update(fe) else: data_ref["entries"] = dict(fe) fm = fresh.get("entry_meta") if isinstance(fm, dict): tm = data_ref.setdefault("entry_meta", {}) if isinstance(tm, dict): tm.clear() tm.update(fm) fts = fresh.get("workspace_backup_ts") if fts is not None: data_ref["workspace_backup_ts"] = fts pull_disk_into_data_ref() entries = data_ref.setdefault("entries", {}) enabled_var = tk.BooleanVar(value=bool(data_ref.get("enabled", True))) tk.Checkbutton( top_part, text="Autotext aktiv (In-App und global)", variable=enabled_var, command=lambda: _persist_enabled(), bg=p["SURFACE"], fg=p["TEXT"], font=(FF, 9), activebackground=p["SURFACE"], anchor="w", highlightthickness=0, ).pack(fill="x", pady=(0, 10)) def _persist_enabled() -> None: if not isinstance(data_ref, dict): return data_ref["enabled"] = bool(enabled_var.get()) save_autotext(data_ref) schedule_workspace_cloud_push() edit_grid = tk.Frame(top_part, bg=p["SURFACE"]) edit_grid.pack(fill="x", pady=(0, 6)) tk.Label(edit_grid, text="Kürzel", bg=p["SURFACE"], fg=p["TEXT"], font=(FF, 9, "bold")).grid(row=0, column=0, sticky="w") tk.Label(edit_grid, text="Ersetzungstext (mehrzeilig)", bg=p["SURFACE"], fg=p["TEXT"], font=(FF, 9, "bold")).grid(row=0, column=1, sticky="w") abbrev_var = tk.StringVar() ae = tk.Entry(edit_grid, textvariable=abbrev_var, width=18, relief="solid", bd=1, highlightthickness=1, highlightbackground=p["BORDER"], fg=p["TEXT_AREA_FG"], insertbackground=p["TEXT_AREA_FG"], bg=p["TEXT_AREA_BG"], font=(FF, 10)) ae.grid(row=1, column=0, sticky="nw", padx=(0, 12), pady=4) repl_box = tk.Text( edit_grid, wrap="word", height=5, width=52, relief="flat", bd=0, highlightthickness=1, highlightbackground=p["BORDER"], bg=p["TEXT_AREA_BG"], fg=p["TEXT_AREA_FG"], font=(FF, 10), padx=6, pady=6, ) repl_box.grid(row=1, column=1, sticky="ew", pady=4) edit_grid.columnconfigure(1, weight=1) lst_fr = tk.Frame(top_part, bg=p["SURFACE"]) lst_fr.pack(fill="both", expand=True, pady=(6, 0)) tk.Label(lst_fr, text="Gespeicherte Kürzel", bg=p["SURFACE"], fg=p["TEXT"], font=(FF, 9, "bold")).pack(anchor="w") lb_wrap = tk.Frame(lst_fr, bg=p["SURFACE"], highlightthickness=1, highlightbackground=p["BORDER"]) lb_wrap.pack(fill="both", expand=False, pady=4) scrollbar = tk.Scrollbar(lb_wrap) scrollbar.pack(side="right", fill="y") list_height = 5 listbox = tk.Listbox( lb_wrap, height=list_height, font=(FF, 10), activestyle="dotbox", yscrollcommand=scrollbar.set, bg=p["TEXT_AREA_BG"], fg=p["TEXT_AREA_FG"], selectbackground=p.get("ACCENT_SOFT", "#E2EEF6"), selectforeground=p.get("TEXT_STRONG", p["TEXT"]), borderwidth=0, highlightthickness=0, ) scrollbar.config(command=listbox.yview) listbox.pack(side="left", fill="both", expand=False) def refresh_list() -> None: listbox.delete(0, "end") ks = sorted(entries.keys(), key=lambda s: (s.lower(), s)) for k in ks: val = entries.get(k, "") or "" short = val.replace("\n", " ") suffix = (" …" + short[-28:]) if len(short) > 48 else "" preview = short[:48] + suffix if len(short) > 48 else short listbox.insert("end", f"«{k}» {preview}") def persist() -> None: ent = data_ref.setdefault("entries", {}) prune_autotext_meta(data_ref, set(ent.keys())) save_autotext(data_ref) schedule_workspace_cloud_push() refresh_list() refresh_list() def on_pick(_evt=None) -> None: sel = listbox.curselection() if not sel: return keys_sorted = sorted(entries.keys(), key=lambda s: (s.lower(), s)) i = sel[0] if i >= len(keys_sorted): return k = keys_sorted[i] abbrev_var.set(k) repl_box.delete("1.0", "end") repl_box.insert("1.0", entries.get(k, "")) listbox.bind("<>", on_pick) def reload_ui_full() -> None: pull_disk_into_data_ref() try: enabled_var.set(bool(data_ref.get("enabled", True))) except tk.TclError: pass refresh_list() abbrev_var.set("") try: repl_box.delete("1.0", "end") except tk.TclError: pass root._aza_reload_autotext_disk = reload_ui_full def do_save_entry() -> None: ab = abbrev_var.get().strip() if not ab: messagebox.showinfo("Autotext", "Bitte ein Kürzel eingeben.", parent=root) return entries[ab] = repl_box.get("1.0", "end").rstrip("\n") touch_autotext_entry_meta(data_ref, ab) persist() getattr(app, "set_status", lambda *_: None)("Autotext gespeichert.") abbrev_var.set("") repl_box.delete("1.0", "end") def do_delete_entry() -> None: sel = listbox.curselection() if not sel: messagebox.showinfo("Autotext", "Bitte ein Kürzel in der Liste wählen.", parent=root) return keys_sorted = sorted(entries.keys(), key=lambda s: (s.lower(), s)) i = sel[0] if i >= len(keys_sorted): return k_del = keys_sorted[i] if not messagebox.askyesno("Autotext", f"Kürzel «{k_del}» wirklich löschen?", parent=root): return entries.pop(k_del, None) em = data_ref.get("entry_meta") if isinstance(em, dict): em.pop(k_del, None) persist() abbrev_var.set("") repl_box.delete("1.0", "end") def _diktat_toggle() -> None: if not dik_state["active"]: if not callable(getattr(app, "ensure_ready", None)): return if not app.ensure_ready(): messagebox.showinfo( "Diktat", "KI-Verbindung noch nicht bereit.", parent=root, ) return if callable(getattr(app, "_check_ai_consent", None)): try: if not app._check_ai_consent(): return except Exception: pass mic_fn = getattr(app, "_ensure_microphone_ready", None) recorder = getattr(app, "recorder", None) transcribe_fn = getattr(app, "transcribe_wav", None) if recorder is None or transcribe_fn is None or not callable(mic_fn): messagebox.showinfo("Mikrofon", "Recorder nicht verfügbar.", parent=root) return if not mic_fn(): return try: recorder.start() except Exception as exc: messagebox.showwarning("Aufnahme", str(exc), parent=root) return dik_state["active"] = True try: btn_diktat.configure(text="Stoppen") except tk.TclError: pass return dik_state["active"] = False try: btn_diktat.configure(text="Diktat") except tk.TclError: pass wav_path = None try: rec = getattr(app, "recorder", None) if rec is None: return wav_path = rec.stop_and_save_wav() except Exception: return def worker(): try: from aza_audio import persist_audio_safe transcribe_fn = getattr(app, "transcribe_wav", None) if wav_path is None or transcribe_fn is None: return safe_path = None try: safe_path = persist_audio_safe(wav_path) except Exception: safe_path = wav_path text_raw = transcribe_fn(safe_path) raw = text_raw if isinstance(text_raw, str) else str(text_raw or "") txt = raw.strip() dik_apply = getattr(app, "_diktat_apply_punctuation", None) if callable(dik_apply): try: txt = dik_apply(txt) except Exception: pass if safe_path and safe_path != wav_path: try: import os os.remove(wav_path) except Exception: pass def ins() -> None: try: if root.winfo_exists(): chunk = (txt or "").strip() if chunk: cur = repl_box.get("1.0", "end").rstrip() if not cur: repl_box.insert("1.0", chunk) else: if not cur.endswith("\n"): repl_box.insert(tk.END, "\n") repl_box.insert(tk.END, chunk) repl_box.see(tk.INSERT) except tk.TclError: pass app.after(0, ins) except Exception as exc_inner: def err() -> None: messagebox.showerror( "Fehler bei Transkription", str(exc_inner), parent=root, ) app.after(0, err) threading.Thread(target=worker, daemon=True).start() tk.Button(btn_bar, text="Speichern / Aktualisieren", command=do_save_entry, bg=p["ACCENT"], fg="white", font=(FF, 9), relief="flat", padx=12, pady=6, cursor="hand2", activebackground="#4A7A9E", ).pack(side="left", padx=(0, 6)) btn_diktat = tk.Button( btn_bar, text="Diktat", command=_diktat_toggle, bg=p["SURFACE"], fg=p["TEXT"], font=(FF, 9), relief="solid", bd=1, padx=10, pady=5, cursor="hand2", ) btn_diktat.pack(side="left", padx=(0, 6)) tk.Button(btn_bar, text="Löschen", command=do_delete_entry, bg=p["SURFACE"], fg=p["TEXT"], font=(FF, 9), relief="solid", bd=1, padx=10, pady=5, cursor="hand2", ).pack(side="left", padx=(0, 6)) tk.Button(btn_bar, text="Schließen", command=_clear_ref_and_close, bg=p["SURFACE"], fg=p["SUBTLE"], font=(FF, 9), relief="solid", bd=1, padx=10, pady=5, cursor="hand2", ).pack(side="right") def _tb_windows_map(app): d = getattr(app, _TB_WIN_DICT_ATTR, None) if not isinstance(d, dict): d = {} setattr(app, _TB_WIN_DICT_ATTR, d) return d def open_workspace_textblock_editor(app, slot_key: str) -> None: from aza_ui_helpers import ( center_window, load_toplevel_geometry, save_toplevel_geometry, ) from aza_persistence import load_textbloecke, save_textbloecke from aza_workspace_sync import schedule_workspace_cloud_push, utc_now_iso p = _palette_for_app(app) key = str(slot_key).strip() if not key.isdigit(): return wins = _tb_windows_map(app) prev = wins.get(key) if prev is not None: try: if prev.winfo_exists(): prev.deiconify() _lift_above_main(prev, app) return except tk.TclError: pass data = load_textbloecke() slot = dict(data.get(key) or {}) root = tk.Toplevel(app) wins[key] = root root.title(slot.get("name") or f"Textblock {key}") root.configure(bg=p["SURFACE"]) root.minsize(500, 360) root.transient(app) _register_if_any(app, root) geom_name = f"workspace_tb_{key}" saved = load_toplevel_geometry(geom_name) if saved: try: root.geometry(saved) except tk.TclError: root.geometry("660x520") center_window(root, 660, 520) else: root.geometry("660x520") center_window(root, 660, 520) def _close_tb() -> None: wm = _tb_windows_map(app) if wm.get(key) is root: wm.pop(key, None) try: save_toplevel_geometry(geom_name, root.geometry()) except Exception: pass try: root.attributes("-topmost", False) except Exception: pass try: root.destroy() except Exception: pass root.protocol("WM_DELETE_WINDOW", _close_tb) _lift_above_main(root, app) outer = tk.Frame(root, bg=p["SURFACE"]) outer.pack(fill="both", expand=True) ttl = slot.get("name") or f"Textblock {key}" _header_bar(outer, p, ttl) body = tk.Frame(outer, bg=p["SURFACE"]) body.pack(fill="both", expand=True, padx=14, pady=12) tk.Label(body, text="Anzeigename", bg=p["SURFACE"], fg=p["TEXT"], font=(FF, 9, "bold")).pack(anchor="w") name_var = tk.StringVar(value=str(slot.get("name") or ttl)) tk.Entry(body, textvariable=name_var, width=40, relief="solid", bd=1, bg=p["TEXT_AREA_BG"], fg=p["TEXT_AREA_FG"], insertbackground=p["TEXT_AREA_FG"], highlightthickness=1, highlightbackground=p["BORDER"], font=(FF, 10)).pack(fill="x", pady=(2, 10)) tk.Label(body, text="Inhalt", bg=p["SURFACE"], fg=p["TEXT"], font=(FF, 9, "bold")).pack(anchor="w") txt = ScrolledText( body, wrap="word", height=16, font=(FF, 10), bg=p["TEXT_AREA_BG"], fg=p["TEXT_AREA_FG"], insertbackground=p["TEXT_AREA_FG"], relief="flat", bd=0, highlightthickness=1, highlightbackground=p["BORDER"], padx=8, pady=8, ) txt.pack(fill="both", expand=True, pady=(4, 10)) txt.insert("1.0", slot.get("content") or "") def do_save() -> None: fresh = load_textbloecke() nm = name_var.get().strip() or f"Textblock {key}" fresh[key] = { "name": nm, "content": txt.get("1.0", "end").rstrip("\n"), "updated_at": utc_now_iso(), } save_textbloecke(fresh) schedule_workspace_cloud_push() shell = getattr(app, "_aza_office_v1", None) if shell is not None and hasattr(shell, "refresh_sidebar_textbloecke_section"): try: shell.refresh_sidebar_textbloecke_section() except Exception: pass getattr(app, "set_status", lambda *_: None)(f"Textblock {key} gespeichert.") try: root.title(nm) except tk.TclError: pass bar_fr = tk.Frame(body, bg=p["SURFACE"]) bar_fr.pack(fill="x") tk.Button(bar_fr, text="Speichern", command=do_save, bg=p["ACCENT"], fg="white", font=(FF, 9), relief="flat", padx=14, pady=6, cursor="hand2", ).pack(side="left", padx=(0, 8)) tk.Button(bar_fr, text="Schließen", command=_close_tb, bg=p["SURFACE"], fg=p["SUBTLE"], font=(FF, 9), relief="solid", bd=1, padx=12, pady=5, cursor="hand2", ).pack(side="left") __all__ = [ "open_workspace_autotext_manager", "open_workspace_textblock_editor", ]