# -*- coding: utf-8 -*- """ AzA Office Hülle V1.2 ====================== Aufbauend auf V1.1: * Keine feste linke Arbeitsoptionen-Leiste mehr — mehr Platz für den Inhalt. * **Zahnrad** oben rechts öffnet/schließt ein kompaktes Einstellungs-Popup (gleiche Akzentfarbe wie Start/Korrigieren/Diktat). * Popup: Rechtsklick-Einfügen, Kommentare anzeigen, **Chat-Empfang** (Auto-Option + einmaliges Öffnen/Anheben des Empfang-Fensters über ``_send_to_empfang``). * **Erscheinungsbild**: dieselbe **Transparenz-Logik** wie in der klassischen Hauptfenster-Kopfzeile (``_opacity_var_main``, ``MIN_OPACITY``, ``save_opacity``) sowie Zugriff auf **alle Einstellungen** (``_open_settings``). Die Farbpalette der Office-Hülle folgt der beim Start geladenen Hell/Dunkel-Präferenz; Umschalten nur noch über das klassische Einstellungsfenster, falls dort angeboten. * Footer-Branding und Logo ca. **30 % größer** als in V1.1. Technische Strategie -------------------- Nach ``_build_ui`` blendet die Hülle die alten Hauptfenster-Kinder aus und baut die Office-Oberfläche neu auf. Bestehende App-Methoden und Variablen aus ``KGDesktopApp`` werden verdrahtet. """ from __future__ import annotations import json import os import sys import threading import tkinter as tk from tkinter import messagebox from tkinter import ttk from tkinter.scrolledtext import ScrolledText from typing import Callable, Dict, List, Optional try: from PIL import Image, ImageTk _HAS_PIL = True except Exception: _HAS_PIL = False try: from aza_config import MIN_OPACITY except Exception: MIN_OPACITY = 0.4 try: from aza_persistence import load_opacity, save_opacity except Exception: def load_opacity() -> float: return 1.0 def save_opacity(_v: float) -> None: pass PREFS_FILENAME = "aza_office_shell_v11_prefs.json" # Sektions-Toggles (Sidebar + Hauptbereich); Schema-Hochzählung nur bei echten Strukturänderungen. SECTION_PREFS_SCHEMA = 2 FF = "Segoe UI" FONT_DEFAULT = (FF, 9) FONT_BOLD = (FF, 9, "bold") FONT_SECTION = (FF, 10, "bold") FONT_BRAND = (FF, 18, "bold") # V1.1: 14 → +~29 % FONT_BRAND_SUB = (FF, 12) # V1.1: 9 → +33 % BTN_W_HEADER = 110 BTN_W_ACTION = 130 BTN_W_DOC = 150 BTN_W_SOAP = 158 BTN_H = 32 LOGO_PX = 57 # V1.1: 44 → ×1.295 ≈ +30 % # Popover = gleiche Farbe wie Primärbuttons (ACCENT aus Palette) POPOVER_TRACK = "#3D6F8D" POPOVER_KNOB = "#FFFFFF" POPOVER_KNOB_RING = "#E2EEF6" PALETTE_LIGHT: Dict[str, str] = { "BG": "#EAF2F7", "SURFACE": "#FFFFFF", "SURFACE_ALT": "#F4F8FB", "BORDER": "#D6E2EB", "TEXT": "#1A4D6D", "TEXT_STRONG": "#0F3850", "SUBTLE": "#5C7A8E", "ACCENT": "#5B8DB3", "ACCENT_HOVER": "#4A7A9E", "ACCENT_PRESSED": "#3A6884", "ACCENT_SOFT": "#E2EEF6", "WARN": "#C2840F", "RECORD": "#C0392B", "RECORD_HOVER": "#A6291C", "TEXT_AREA_BG": "#FFFFFF", "TEXT_AREA_FG": "#1A4D6D", } PALETTE_DARK: Dict[str, str] = { "BG": "#1B2E36", "SURFACE": "#243B45", "SURFACE_ALT": "#2D4652", "BORDER": "#3D5866", "TEXT": "#E8F2F7", "TEXT_STRONG": "#FFFFFF", "SUBTLE": "#9BB8C9", "ACCENT": "#6BA3C4", "ACCENT_HOVER": "#5A92B5", "ACCENT_PRESSED": "#4A82A6", "ACCENT_SOFT": "#2F4F5E", "WARN": "#E0B456", "RECORD": "#E74C3C", "RECORD_HOVER": "#C0392B", "TEXT_AREA_BG": "#1E323B", "TEXT_AREA_FG": "#EAF4F8", } def _prefs_path() -> str: try: from aza_config import get_writable_data_dir base = get_writable_data_dir() except Exception: base = os.path.dirname(os.path.abspath(__file__)) return os.path.join(base, PREFS_FILENAME) def _load_office_prefs() -> dict: try: with open(_prefs_path(), encoding="utf-8") as fh: d = json.load(fh) return d if isinstance(d, dict) else {} except Exception: return {} def _save_office_prefs(data: dict) -> None: try: path = _prefs_path() root = os.path.dirname(path) if root: os.makedirs(root, exist_ok=True) with open(path, "w", encoding="utf-8") as fh: json.dump(data, fh, indent=2, ensure_ascii=False) except Exception as exc: print(f"[OfficeV1.2] Konnte Office-Prefs nicht speichern: {exc}") def _has_autotext_config_file_on_disk() -> bool: try: from aza_persistence import _autotext_config_path return os.path.isfile(_autotext_config_path()) except Exception: return False def _shell_sections_strict_new_defaults() -> dict: """Neuinstallation / kein vorhandenes autotext.json: alles zu außer KG.""" return { "schema": SECTION_PREFS_SCHEMA, "arb_open": False, "ersch_open": False, "tb_open": False, "transcript_open": False, "kg_open": True, "soap_open": False, "documents_open": False, } def _shell_sections_upgrade_defaults(app) -> dict: """Bestehende Installation ohne Schema: früheres Layout (SOAP/Dokumente sichtbar).""" ad = getattr(app, "_autotext_data", None) tb_open = True if isinstance(ad, dict): tb_open = bool(ad.get("office_sidebar_textbloecke_open", True)) return { "schema": SECTION_PREFS_SCHEMA, "arb_open": True, "ersch_open": True, "tb_open": tb_open, "transcript_open": False, "kg_open": True, "soap_open": True, "documents_open": True, } def _load_dark_pref() -> bool: return bool(_load_office_prefs().get("dark_mode", False)) def _sidebar_nav_icon_char(kind: str) -> tuple[str, str]: """Kleine Navigations-Icons (MDL2 mit Fallback, keine Emojis).""" if kind == "chat": return ("\uE8F2", "C") if kind == "addon": return ("\uE71D", "A") if kind == "autotext": return ("\uE70F", "T") if kind == "folder": return ("\uE8B7", "O") if kind == "library": return ("\uE82D", "B") return ("", "") class DocTypePicker(tk.Frame): """AzA-konformer Dokumenttyp-Wähler: gewählter Typ als Überschrift mit ▼.""" def __init__(self, parent, app, palette: dict, doc_types: list, *, initial_key: str = "kg"): super().__init__(parent, bg=palette["BG"]) self.app = app self._palette = palette self._doc_types = list(doc_types) self._labels = {k: v for k, v in doc_types} self._key = initial_key if initial_key in self._labels else "kg" self._display = tk.Label( self, text=self._display_text(), bg=palette["ACCENT_SOFT"], fg=palette["TEXT_STRONG"], font=FONT_SECTION, padx=10, pady=3, cursor="hand2", highlightthickness=1, highlightbackground=palette["ACCENT"], highlightcolor=palette["ACCENT"], ) self._display.pack(side="left") self._display.bind("", self._show_menu) def _display_text(self) -> str: label = self._labels.get(self._key, "Krankengeschichte") return f"{label} \u25BC" def _show_menu(self, event=None): p = self._palette menu = tk.Menu( self, tearoff=0, font=FONT_DEFAULT, bg=p["ACCENT"], fg="#FFFFFF", activebackground=p["ACCENT_HOVER"], activeforeground="#FFFFFF", borderwidth=0, relief="flat", ) for key, label in self._doc_types: menu.add_command( label=label, command=lambda k=key: self._select(k), ) try: x = self._display.winfo_rootx() y = self._display.winfo_rooty() + self._display.winfo_height() menu.tk_popup(x, y) finally: try: menu.grab_release() except Exception: pass def _select(self, key: str): if key not in self._labels: key = "kg" self._key = key self._display.configure(text=self._display_text()) if hasattr(self.app, "_set_main_doc_type"): self.app._set_main_doc_type(key) elif hasattr(self.app, "_on_doc_type_changed"): self.app._current_doc_type = key self.app._update_doc_type_buttons() def set_type_key(self, key: str): if key not in self._labels: key = "kg" self._key = key self._display.configure(text=self._display_text()) def get_type_key(self) -> str: return self._key def _build_sidebar_link(parent, icon_kind: str, text: str, command, *, bg: str, fg: str, indent: int = 28): """Sidebar-Zeile mit kleinem Icon links (Chat / Add-on Beta).""" mdl2, fallback = _sidebar_nav_icon_char(icon_kind) row = tk.Frame(parent, bg=bg, cursor="hand2") def _icon_font(): for ff in ("Segoe MDL2 Assets", "Segoe UI Symbol", "Segoe UI"): yield ff, mdl2 if ff == "Segoe MDL2 Assets" else fallback icon_lbl = None for ff, glyph in _icon_font(): try: icon_lbl = tk.Label(row, text=glyph, font=(ff, 11), bg=bg, fg="#B8D4E8", width=2) break except Exception: continue if icon_lbl is None: icon_lbl = tk.Label(row, text=fallback, font=(FF, 9, "bold"), bg=bg, fg="#B8D4E8", width=2) icon_lbl.pack(side="left", padx=(indent, 4)) txt_lbl = tk.Label(row, text=text, bg=bg, fg=fg, font=FONT_DEFAULT, anchor="w") txt_lbl.pack(side="left", fill="x") def _activate(_e=None): try: command() except Exception: pass for w in (row, icon_lbl, txt_lbl): w.bind("", _activate) row.pack(fill="x", padx=(0, 12), pady=(2, 8 if icon_kind == "chat" else 10)) return row def _save_dark_pref(dark: bool) -> None: data = _load_office_prefs() data["dark_mode"] = dark _save_office_prefs(data) class PillButton(tk.Canvas): """Pill-Button; Farben aus Palette-Dict.""" def __init__( self, parent, text: str, command: Optional[Callable] = None, *, kind: str = "default", width: int = BTN_W_ACTION, height: int = BTN_H, weight: str = "normal", tooltip: Optional[str] = None, palette: Dict[str, str], ): bg = parent.cget("bg") if hasattr(parent, "cget") else PALETTE_LIGHT["BG"] super().__init__(parent, width=width, height=height, bg=bg, highlightthickness=0, bd=0, cursor="hand2") self._text = text self._command = command self._kind = kind self._btn_w = width self._btn_h = height self._weight = weight self._palette = palette self._hover = False self._press = False self.bind("", lambda e: self._draw()) self.bind("", self._on_enter) self.bind("", self._on_leave) self.bind("", self._on_press) self.bind("", self._on_release) if tooltip: try: from aza_ui_helpers import add_tooltip add_tooltip(self, tooltip) except Exception: pass self._draw() def _p(self) -> Dict[str, str]: return self._palette def _colors(self): p = self._p() if self._kind == "primary": fill = (p["ACCENT_PRESSED"] if self._press else p["ACCENT_HOVER"] if self._hover else p["ACCENT"]) return fill, "white", fill if self._kind == "danger": fill = p["RECORD_HOVER"] if (self._hover or self._press) else p["RECORD"] return fill, "white", fill if self._kind == "update": fill = "#D4882A" if (self._hover or self._press) else "#E8A04A" return fill, "white", fill if self._kind == "ghost": fill = p["ACCENT_SOFT"] if (self._hover or self._press) else p["SURFACE"] border = p["ACCENT"] if (self._hover or self._press) else p["BORDER"] return fill, p["TEXT"], border fill = p["ACCENT_SOFT"] if (self._hover or self._press) else p["SURFACE_ALT"] border = p["ACCENT"] if (self._hover or self._press) else p["BORDER"] return fill, p["TEXT"], border def _draw(self): try: self.delete("all") w = max(self._btn_w, int(self.winfo_width() or 0)) h = max(self._btn_h, int(self.winfo_height() or 0)) fill, fg, border = self._colors() r = max(2, min(8, h // 2)) self._round_rect(0, 0, w, h, r, fill=fill, outline=border) self.create_text(w // 2, h // 2, text=self._text, fill=fg, font=(FF, 9, self._weight)) except Exception: pass def _round_rect(self, x1, y1, x2, y2, r, **kw): pts = [ x1 + r, y1, x2 - r, y1, x2, y1, x2, y1 + r, x2, y2 - r, x2, y2, x2 - r, y2, x1 + r, y2, x1, y2, x1, y2 - r, x1, y1 + r, x1, y1, ] return self.create_polygon(pts, smooth=True, **kw) def _on_enter(self, _e=None): self._hover = True self._draw() def _on_leave(self, _e=None): self._hover = False self._press = False self._draw() def _on_press(self, _e=None): self._press = True self._draw() def _on_release(self, _e=None): was = self._press self._press = False self._draw() if was and self._command: try: self._command() except Exception as exc: print(f"[OfficeV1.2] Aktion '{self._text}' fehlgeschlagen: {exc}") def configure(self, **kw): if "command" in kw: self._command = kw.pop("command") if "text" in kw: self._text = str(kw.pop("text")) self._draw() if kw: try: super().configure(**kw) except Exception: pass config = configure def set_text(self, t: str): self.configure(text=t) def set_palette_ref(self, palette: Dict[str, str]): self._palette = palette self._draw() def set_font_size_scale(self, _s: float): return None def set_button_size_scale(self, _s: float): return None def set_font_scale(self, _s: float): return None class PopoverThemeSwitch(tk.Canvas): """Hell/Dunkel-Schalter für Office-Hülle (auf Akzent-Hintergrund).""" def __init__( self, parent, *, width: int = 52, height: int = 26, is_dark: bool, command: Callable[[], None], bg_accent: str, ): super().__init__( parent, width=width, height=height, bg=bg_accent, highlightthickness=0, bd=0, cursor="hand2", ) self._is_dark = is_dark self._command = command self._bg_accent = bg_accent self._track_w = width self._track_h = height self.bind("", self._on_click) self.bind("", lambda e: self._draw()) self._draw() def _on_click(self, _e=None): try: self._command() except Exception as exc: print(f"[OfficeV1.2] Theme-Toggle: {exc}") def set_dark(self, dark: bool): self._is_dark = dark self._draw() def _draw(self): try: self.delete("all") w = max(self._track_w, int(self.winfo_width() or 0)) h = max(self._track_h, int(self.winfo_height() or 0)) pad = 3 x1, y1, x2, y2 = pad, h // 2 - 8, w - pad, h // 2 + 8 r = 10 pts = [ x1 + r, y1, x2 - r, y1, x2, y1, x2, y1 + r, x2, y2 - r, x2, y2, x2 - r, y2, x1 + r, y2, x1, y2, x1, y2 - r, x1, y1 + r, x1, y1, ] self.create_polygon(pts, smooth=True, fill=POPOVER_TRACK, outline="") knob_r = 9 cx = (w - pad - knob_r - 4) if self._is_dark else (pad + knob_r + 4) cy = h // 2 self.create_oval( cx - knob_r, cy - knob_r, cx + knob_r, cy + knob_r, fill=POPOVER_KNOB, outline=POPOVER_KNOB_RING, width=1, ) except Exception: pass def _load_logo(size: int): if not _HAS_PIL: return None cands: list[str] = [os.path.dirname(os.path.abspath(__file__))] if getattr(sys, "frozen", False): try: cands.append(os.path.dirname(os.path.abspath(sys.executable))) except Exception: pass m = getattr(sys, "_MEIPASS", "") if m: cands.append(m) for d in cands: p = os.path.join(d, "logo.png") if not os.path.isfile(p): continue try: img = Image.open(p) if img.mode not in ("RGB", "RGBA"): img = img.convert("RGBA") resample = (Image.Resampling.LANCZOS if hasattr(Image, "Resampling") else Image.LANCZOS) img = img.resize((size, size), resample) return ImageTk.PhotoImage(img) except Exception: continue return None def _safe_call(obj, attr: str): fn = getattr(obj, attr, None) if not callable(fn): print(f"[OfficeV1.2] Methode '{attr}' nicht verfügbar.") return try: fn() except Exception as exc: print(f"[OfficeV1.2] Aufruf '{attr}' fehlgeschlagen: {exc}") class _OfficeShellV12: def __init__(self, app): self.app = app self._logo_img = None self._record_btn: Optional[PillButton] = None self._korrigieren_btn: Optional[PillButton] = None self._diktat_btn: Optional[PillButton] = None self._license_lbl: Optional[tk.Label] = None self._theme_switch_pop: Optional[PopoverThemeSwitch] = None self._sidebar: Optional[tk.Frame] = None self._sec_arb_open: bool = False self._sec_ersch_open: bool = False self._sec_arb_arrow: Optional[tk.Label] = None self._sec_ersch_arrow: Optional[tk.Label] = None self._sec_arb_body: Optional[tk.Frame] = None self._sec_ersch_body: Optional[tk.Frame] = None self._transcript_open: bool = False self._kg_open: bool = True self._soap_open: bool = False self._documents_open: bool = False self._soap_arrow: Optional[tk.Label] = None self._soap_fold_body: Optional[tk.Frame] = None self._documents_arrow: Optional[tk.Label] = None self._documents_fold_body: Optional[tk.Frame] = None self._main_fill: Optional[tk.Frame] = None self._content: Optional[tk.Frame] = None self._header_inner: Optional[tk.Frame] = None self._header_bar: Optional[tk.Frame] = None self._footer_bar: Optional[tk.Frame] = None self._sep_top: Optional[tk.Frame] = None self._sep_bottom: Optional[tk.Frame] = None self._status_row_fr: Optional[tk.Frame] = None self._palette: Dict[str, str] = {} self._dark_mode: bool = False self._pills: List[PillButton] = [] self._footer_tb: Optional[tk.Frame] = None self._footer_logo_lbl: Optional[tk.Label] = None self._footer_brand_title: Optional[tk.Label] = None self._footer_brand_sub: Optional[tk.Label] = None self._shell_labels: List[tk.Label] = [] self._settings_win = None # Legacy-Popup self._gear_btn: Optional[tk.Misc] = None self._head_tb: Optional[tk.Frame] = None self._sidebar_head_arb: Optional[tk.Frame] = None self._sec_tb_open: bool = False self._sec_tb_arrow: Optional[tk.Label] = None self._sec_tb_body: Optional[tk.Frame] = None self._sidebar_head_ersch: Optional[tk.Frame] = None # ── Öffentlich ──────────────────────────────────────────────────── def install(self): app = self.app self._hydrate_shell_section_prefs() self._dark_mode = _load_dark_pref() self._palette = ( PALETTE_DARK.copy() if self._dark_mode else PALETTE_LIGHT.copy() ) try: from aza_version import APP_VERSION app.title(f"AzA Office (v{APP_VERSION})") except Exception: app.title("AzA Office") try: app.configure(bg=self._palette["BG"]) except Exception: pass self._hide_legacy_children() self._apply_ttk_theme() self._build_footer() self._build_header() self._build_status_row() self._build_main_fill() try: btp = getattr(app, "_bind_textblock_pending", None) if callable(btp): for attr in ("txt_transcript", "txt_output"): wid = getattr(app, attr, None) if wid is not None: btp(wid) except Exception: pass if self._record_btn is not None: app.btn_record = self._record_btn if self._korrigieren_btn is not None: app.btn_record_append = self._korrigieren_btn if self._diktat_btn is not None: app._btn_diktat_top = self._diktat_btn try: app.after(250, self._enforce_default_fonts) except Exception: pass try: ud = getattr(app, "_update_kg_detail_display", None) if callable(ud): ud() except Exception: pass self._update_license_label() try: self.bind_ai_budget_display() except Exception: pass try: app.after(2500, self._periodic_license_refresh) except Exception: pass try: app.after(3000, self._schedule_update_button_poll) except Exception: pass try: from aza_workspace_license import ensure_workspace_license_dialog_then_start_hybrid_sync ensure_workspace_license_dialog_then_start_hybrid_sync(app) except Exception as shell_exc: print(f"[OfficeV1.2] Workspace-Hybrid-Sync konnte nicht gestartet werden: {shell_exc}") try: existing = getattr(app, "_dev_status_window", None) if existing is not None and existing.winfo_exists(): existing.destroy() except Exception: pass app._dev_status_window = None def _hydrate_shell_section_prefs(self) -> None: app = self.app prefs = _load_office_prefs() ss = prefs.get("shell_sections") if isinstance(prefs.get("shell_sections"), dict) else {} valid = ss.get("schema") == SECTION_PREFS_SCHEMA if not valid: if _has_autotext_config_file_on_disk(): new_ss = _shell_sections_upgrade_defaults(app) else: new_ss = _shell_sections_strict_new_defaults() prefs["shell_sections"] = new_ss _save_office_prefs(prefs) self._apply_shell_section_prefs_from_dict(new_ss) sync_tb = getattr(app, "_autotext_data", None) if isinstance(sync_tb, dict): sync_tb["office_sidebar_textbloecke_open"] = bool(new_ss["tb_open"]) try: from aza_persistence import save_autotext save_autotext(sync_tb) except Exception: pass return self._apply_shell_section_prefs_from_dict(ss) def _apply_shell_section_prefs_from_dict(self, ss: dict) -> None: app = self.app self._sec_arb_open = bool(ss.get("arb_open", False)) self._sec_ersch_open = bool(ss.get("ersch_open", False)) self._sec_tb_open = bool(ss.get("tb_open", False)) self._transcript_open = bool(ss.get("transcript_open", False)) self._kg_open = bool(ss.get("kg_open", True)) self._soap_open = bool(ss.get("soap_open", False)) self._documents_open = bool(ss.get("documents_open", False)) setattr(app, "_transcript_collapsed", not self._transcript_open) setattr(app, "_kg_collapsed", not self._kg_open) def _persist_shell_sections(self) -> None: d = _load_office_prefs() d["shell_sections"] = { "schema": SECTION_PREFS_SCHEMA, "arb_open": self._sec_arb_open, "ersch_open": self._sec_ersch_open, "tb_open": self._sec_tb_open, "transcript_open": self._transcript_open, "kg_open": self._kg_open, "soap_open": self._soap_open, "documents_open": self._documents_open, } _save_office_prefs(d) # ── Hilfen ──────────────────────────────────────────────────────── def _register_pill(self, b: PillButton) -> PillButton: self._pills.append(b) return b def _hide_legacy_children(self): for child in list(self.app.winfo_children()): for forget in ("pack_forget", "place_forget", "grid_forget"): try: getattr(child, forget)() except Exception: pass def _apply_opacity_percent_str(self, val: str) -> None: """Wie ``on_opacity_main`` in ``basis14._build_ui`` (Transparenz).""" app = self.app try: alpha = float(val) / 100.0 alpha = max(float(MIN_OPACITY), min(1.0, alpha)) app.attributes("-alpha", alpha) save_opacity(alpha) ov = getattr(app, "_opacity_var_main", None) if ov is not None: ov.set(round(alpha * 100)) sc = getattr(app, "_opacity_scale_main", None) if sc is not None: try: sc.set(round(alpha * 100)) except Exception: pass except Exception: pass def _apply_ttk_theme(self): p = self._palette try: style = ttk.Style(self.app) try: style.theme_use("clam") except tk.TclError: pass style.configure("TFrame", background=p["BG"]) style.configure("TLabel", background=p["BG"], foreground=p["TEXT"], font=FONT_DEFAULT) style.configure( "TButton", background=p["ACCENT"], foreground="white", padding=(10, 6), borderwidth=0, font=FONT_DEFAULT, ) style.map("TButton", background=[ ("active", p["ACCENT_HOVER"]), ("pressed", p["ACCENT_PRESSED"]), ]) style.configure( "OfficePop.Horizontal.TScale", troughcolor="#E2EEF6", background=p["ACCENT"], ) except Exception: pass def _apply_main_theme(self): p = self._palette app = self.app try: app.configure(bg=p["BG"]) except Exception: pass for fr in (self._header_inner, self._header_bar): if fr is not None: try: fr.configure(bg=p["SURFACE"]) except Exception: pass if self._header_inner is not None: try: for ch in self._header_inner.winfo_children(): try: if ch is self._gear_btn: continue ch.configure(bg=p["SURFACE"]) except Exception: pass except Exception: pass for fr in (self._main_fill, self._content, self._status_row_fr, self._footer_bar): if fr is not None: try: fr.configure(bg=p["BG"]) except Exception: pass if self._sep_top is not None: try: self._sep_top.configure(bg=p["BORDER"]) except Exception: pass if self._sep_bottom is not None: try: self._sep_bottom.configure(bg=p["BORDER"]) except Exception: pass try: app.lbl_status.configure(bg=p["BG"], fg=p["SUBTLE"]) except Exception: pass if getattr(self, "_ai_budget_col", None) is not None: try: self._ai_budget_col.configure(bg=p["BG"]) except Exception: pass if getattr(self, "_ai_budget_lbl", None) is not None: try: fg = getattr(app, "_ai_budget_fg", None) or p["SUBTLE"] self._ai_budget_lbl.configure(bg=p["BG"], fg=fg) except Exception: pass if getattr(self, "_ai_budget_renewal_lbl", None) is not None: try: self._ai_budget_renewal_lbl.configure(bg=p["BG"], fg="#8A9AA8") except Exception: pass if self._license_lbl is not None: try: mode = getattr(app, "license_mode", "demo") self._license_lbl.configure( bg=p["SURFACE"], fg=p["ACCENT"] if mode == "active" else p["WARN"], ) except Exception: pass self._apply_section_backgrounds(p["BG"]) self._apply_shell_labels(p) self._apply_text_widgets(p) if self._footer_tb is not None: try: self._footer_tb.configure(bg=p["BG"]) except Exception: pass if self._footer_logo_lbl is not None: try: self._footer_logo_lbl.configure(bg=p["BG"]) except Exception: pass if self._footer_brand_title is not None: try: self._footer_brand_title.configure(bg=p["BG"], fg=p["TEXT_STRONG"]) except Exception: pass if self._footer_brand_sub is not None: try: self._footer_brand_sub.configure(bg=p["BG"], fg=p["SUBTLE"]) except Exception: pass for pill in self._pills: try: pill.configure(bg=p["SURFACE"]) except Exception: pass pill.set_palette_ref(p) app._soap_bg = p["BG"] try: app._rebuild_soap_section_controls() except Exception: pass self._apply_ttk_theme() if self._sidebar is not None: try: self._sidebar.configure(bg=p["ACCENT"]) except Exception: pass try: self._build_sidebar_content() except Exception: pass def _toggle_theme_main(self): self._dark_mode = not self._dark_mode _save_dark_pref(self._dark_mode) self._palette = ( PALETTE_DARK.copy() if self._dark_mode else PALETTE_LIGHT.copy() ) self._apply_main_theme() def _apply_shell_labels(self, p: Dict[str, str]): for w in self._shell_labels: try: w.configure(bg=p["BG"], fg=p["TEXT"]) except Exception: pass def _apply_section_backgrounds(self, bg: str): sh = getattr(self, "_shell_section_frames", None) if not sh: return for fr in sh: try: fr.configure(bg=bg) except Exception: pass def _apply_text_widgets(self, p: Dict[str, str]): for attr in ("txt_output", "txt_transcript"): w = getattr(self.app, attr, None) if w is None: continue try: w.configure( bg=p["TEXT_AREA_BG"], fg=p["TEXT_AREA_FG"], highlightbackground=p["BORDER"], insertbackground=p["TEXT_AREA_FG"], ) except Exception: pass def _on_chat_empfang_toggle(self): _safe_call(self.app, "_toggle_empfang_auto") try: self.app.after(80, lambda: _safe_call(self.app, "_send_to_empfang")) except Exception: pass # ── (Legacy Popover-Code, in V1.2.1 nicht mehr verwendet) ───────── def _destroy_settings_pop(self): if self._settings_win is not None: try: self._settings_win.destroy() except Exception: pass self._settings_win = None self._theme_switch_pop = None def _toggle_settings_pop(self): if self._settings_win is not None: try: if self._settings_win.winfo_exists(): self._destroy_settings_pop() return except tk.TclError: pass self._show_settings_pop() def _show_settings_pop(self): self._destroy_settings_pop() app = self.app acc = self._palette["ACCENT"] pop = tk.Toplevel(app) pop.withdraw() pop.configure(bg=acc) try: pop.transient(app) except Exception: pass self._settings_win = pop pad = dict( bg=acc, fg="white", font=FONT_DEFAULT, activebackground=acc, activeforeground="white", selectcolor="#1a4d6d", highlightthickness=0, bd=0, anchor="w", ) outer = tk.Frame(pop, bg=acc, padx=14, pady=12) outer.pack(fill="both", expand=True) tk.Label( outer, text="Arbeitsoptionen", bg=acc, fg="white", font=(FF, 10, "bold"), ).pack(anchor="w", pady=(0, 8)) if getattr(app, "_rclick_paste_var", None) is None: app._rclick_paste_var = tk.BooleanVar(master=app, value=True) if getattr(app, "_kommentare_auto_var", None) is None: app._kommentare_auto_var = tk.BooleanVar(master=app, value=False) if getattr(app, "_empfang_auto_var", None) is None: app._empfang_auto_var = tk.BooleanVar(master=app, value=False) if getattr(app, "_diagnose_grouping_var", None) is None: try: _dg_default = bool(getattr(app, "_autotext_data", {}).get( "diagnose_grouping_enabled", True )) except Exception: _dg_default = True app._diagnose_grouping_var = tk.BooleanVar(master=app, value=_dg_default) tk.Checkbutton( outer, text="Rechtsklick = Einfügen", variable=app._rclick_paste_var, command=lambda: _safe_call(app, "_toggle_rclick_paste"), **pad, ).pack(fill="x", pady=3) tk.Checkbutton( outer, text="Kommentare anzeigen", variable=app._kommentare_auto_var, command=lambda: _safe_call(app, "_toggle_kommentare_auto"), **pad, ).pack(fill="x", pady=3) tk.Checkbutton( outer, text="Diagnosen gliedern", variable=app._diagnose_grouping_var, command=lambda: _safe_call(app, "_toggle_diagnose_grouping"), **pad, ).pack(fill="x", pady=3) tk.Checkbutton( outer, text="Chat-Empfang", variable=app._empfang_auto_var, command=self._on_chat_empfang_toggle, **pad, ).pack(fill="x", pady=3) tk.Frame(outer, bg=acc, height=8).pack() tk.Label( outer, text="Erscheinungsbild", bg=acc, fg="white", font=(FF, 10, "bold"), ).pack(anchor="w", pady=(4, 6)) tk.Label( outer, text="Fenster-Transparenz (wie Hauptfenster)", bg=acc, fg="#E2EEF6", font=FONT_DEFAULT, ).pack(anchor="w") ov = getattr(app, "_opacity_var_main", None) if ov is None: ov = tk.DoubleVar(master=app, value=round(load_opacity() * 100)) app._opacity_var_main = ov row_op = tk.Frame(outer, bg=acc) row_op.pack(fill="x", pady=(4, 2)) lbl_half = tk.Label( row_op, text="◐", font=("Segoe UI Symbol", 14), bg=acc, fg="white", cursor="hand2", ) lbl_half.pack(side="left", padx=(0, 4)) lbl_half.bind( "", lambda e: self._apply_opacity_percent_str(str(int(MIN_OPACITY * 100))), ) sc = ttk.Scale( row_op, from_=40, to=100, variable=ov, orient="horizontal", length=140, command=self._apply_opacity_percent_str, style="OfficePop.Horizontal.TScale", ) sc.pack(side="left", fill="x", expand=True, padx=(0, 4)) lbl_full = tk.Label( row_op, text="☀", font=("Segoe UI Symbol", 14), bg=acc, fg="white", cursor="hand2", ) lbl_full.pack(side="left") lbl_full.bind("", lambda e: self._apply_opacity_percent_str("100")) tk.Label( outer, text="Office-Hülle hell / dunkel", bg=acc, fg="#E2EEF6", font=FONT_DEFAULT, ).pack(anchor="w", pady=(8, 4)) row_th = tk.Frame(outer, bg=acc) row_th.pack(fill="x") tk.Label(row_th, text="Hell", bg=acc, fg="white", font=FONT_DEFAULT).pack(side="left", padx=(0, 6)) def _flip(): self._toggle_theme_main() if self._theme_switch_pop: self._theme_switch_pop.set_dark(self._dark_mode) self._theme_switch_pop = PopoverThemeSwitch( row_th, is_dark=self._dark_mode, command=_flip, bg_accent=acc, ) self._theme_switch_pop.pack(side="left") tk.Label(row_th, text="Dunkel", bg=acc, fg="white", font=FONT_DEFAULT).pack(side="left", padx=(6, 0)) tk.Frame(outer, bg=acc, height=6).pack() link = tk.Label( outer, text="Weitere Einstellungen …", bg=acc, fg="white", font=(FF, 9, "underline"), cursor="hand2", ) link.pack(anchor="w", pady=(4, 0)) link.bind("", lambda e: (_safe_call(app, "_open_settings"), self._destroy_settings_pop())) pop.update_idletasks() w_req = max(outer.winfo_reqwidth() + 28, 280) h_req = outer.winfo_reqheight() + 24 if self._gear_btn is not None: self._gear_btn.update_idletasks() gx = self._gear_btn.winfo_rootx() gy = self._gear_btn.winfo_rooty() gh = self._gear_btn.winfo_height() gw = self._gear_btn.winfo_width() sw = pop.winfo_screenwidth() sh = pop.winfo_screenheight() px = min(max(8, gx + gw - w_req), sw - w_req - 8) py = gy + gh + 6 if py + h_req > sh - 8: py = max(8, gy - h_req - 6) else: px = app.winfo_rootx() + app.winfo_width() - w_req - 24 py = app.winfo_rooty() + 72 pop.geometry(f"{w_req}x{h_req}+{px}+{py}") try: pop.overrideredirect(True) except Exception: pass try: pop.deiconify() pop.lift() pop.attributes("-topmost", True) pop.after(120, lambda: pop.attributes("-topmost", False)) except Exception: pass # ── Header / Body ───────────────────────────────────────────────── def _build_header(self): app = self.app p = self._palette self._header_bar = tk.Frame(app, bg=p["SURFACE"], bd=0, highlightthickness=0) self._header_bar.pack(side="top", fill="x") self._sep_top = tk.Frame(app, bg=p["BORDER"], height=1) self._sep_top.pack(side="top", fill="x") self._header_inner = tk.Frame(self._header_bar, bg=p["SURFACE"]) self._header_inner.pack(fill="x", padx=18, pady=10) left = tk.Frame(self._header_inner, bg=p["SURFACE"]) left.pack(side="left") self._record_btn = self._register_pill(PillButton( left, "⏺ Start", command=lambda: _safe_call(app, "toggle_record"), kind="primary", width=BTN_W_ACTION, weight="bold", tooltip="Aufnahme starten / stoppen (Transkription)", palette=p, )) self._record_btn.pack(side="left", padx=(0, 8)) self._korrigieren_btn = self._register_pill(PillButton( left, "⏺ Korrigieren", command=lambda: _safe_call(app, "_toggle_record_append"), kind="primary", width=BTN_W_ACTION, weight="bold", tooltip="Korrektur-/Append-Aufnahme", palette=p, )) self._korrigieren_btn.pack(side="left", padx=(0, 8)) self._diktat_btn = self._register_pill(PillButton( left, "Diktat", command=lambda: _safe_call(app, "open_diktat_window"), kind="primary", width=BTN_W_ACTION, weight="bold", tooltip="Diktatfenster öffnen", palette=p, )) self._diktat_btn.pack(side="left") # ── Rechts: Pin + Lizenz + Profil + Aktivierung ────────────────── # right muss VOR center gepackt werden, damit das center-Frame # den echten Mittelbereich einnimmt. right = tk.Frame(self._header_inner, bg=p["SURFACE"]) right.pack(side="right") # Pin-Nadel fuer das Hauptfenster (Always-on-top toggeln). # Standard: angepinnt (rot). Ein Klick togglet und speichert. try: _initial_pinned = bool(getattr(app, "_main_pinned", True)) except Exception: _initial_pinned = True self._main_pin_btn = tk.Label( right, text=("📌" if _initial_pinned else "📍"), font=("Segoe UI Emoji", 11), bg=p["SURFACE"], fg=("#1A6FB5" if _initial_pinned else "#90A4B8"), cursor="hand2", padx=8, pady=4, ) self._main_pin_btn.pack(side="left", padx=(0, 6)) self._main_pin_btn.bind( "", lambda e: _safe_call(app, "_toggle_main_pin"), ) try: app._main_pin_btn = self._main_pin_btn except Exception: pass self._license_lbl = tk.Label( right, text="Lizenz prüfen …", font=FONT_DEFAULT, bg=p["SURFACE"], fg=p["SUBTLE"], cursor="hand2", padx=8, pady=4, ) self._license_lbl.pack(side="left", padx=(0, 12)) self._license_lbl.bind( "", lambda e: _safe_call(app, "_show_activation_dialog"), ) self._register_pill(PillButton( right, "Profil", command=lambda: _safe_call(app, "_show_profile_editor"), kind="ghost", width=BTN_W_HEADER, palette=p, tooltip="Profil bearbeiten", )).pack(side="left", padx=(0, 6)) self._register_pill(PillButton( right, "Aktivierung", command=lambda: _safe_call(app, "_show_activation_dialog"), kind="ghost", width=BTN_W_HEADER, palette=p, tooltip="Aktivierungsdialog öffnen", )).pack(side="left") self._update_btn = self._register_pill(PillButton( right, "Update", command=lambda: _safe_call(app, "_manual_update_check"), kind="update", width=78, height=28, palette=p, tooltip="Neue Version verfuegbar", )) self._update_btn.pack(side="left", padx=(8, 0)) self._update_btn.pack_forget() center = tk.Frame(self._header_inner, bg=p["SURFACE"]) center.pack(side="left", expand=True, fill="x") def _build_status_row(self): app = self.app p = self._palette self._status_row_fr = tk.Frame(app, bg=p["BG"]) self._status_row_fr.pack(side="top", fill="x", padx=18, pady=(8, 4)) var = getattr(app, "status_var", None) if var is None: var = tk.StringVar(master=app, value="Bereit.") app.status_var = var budget_var = getattr(app, "_ai_budget_var", None) if budget_var is None: budget_var = tk.StringVar(master=app, value="KI-Guthaben: prüft…") app._ai_budget_var = budget_var renewal_var = getattr(app, "_ai_budget_renewal_var", None) if renewal_var is None: renewal_var = tk.StringVar(master=app, value="") app._ai_budget_renewal_var = renewal_var self._ai_budget_col = tk.Frame(self._status_row_fr, bg=p["BG"]) self._ai_budget_col.pack(side="right", padx=(8, 0), anchor="ne") self._ai_budget_col.grid_columnconfigure(0, weight=1) self._ai_budget_lbl = tk.Label( self._ai_budget_col, textvariable=budget_var, bg=p["BG"], fg=p["SUBTLE"], font=("Segoe UI", 7), anchor="e", justify="right", cursor="hand2", ) self._ai_budget_lbl.grid(row=0, column=0, sticky="e") self._ai_budget_renewal_lbl = tk.Label( self._ai_budget_col, textvariable=renewal_var, bg=p["BG"], fg="#8A9AA8", font=("Segoe UI", 7), anchor="e", justify="right", ) self._ai_budget_renewal_lbl.grid(row=1, column=0, sticky="e", pady=(1, 0)) lbl = tk.Label( self._status_row_fr, textvariable=var, bg=p["BG"], fg=p["SUBTLE"], font=("Segoe UI", 8), anchor="w", ) lbl.pack(side="left", fill="x", expand=True) app.lbl_status = lbl self._ai_budget_lbl.bind( "", lambda e: _safe_call(app, "_refresh_remote_ai_budget_async"), ) app._ai_budget_status_lbl = self._ai_budget_lbl app._ai_budget_renewal_lbl = self._ai_budget_renewal_lbl app._ai_budget_header_lbl = None try: app._log_ai_budget_ui("UI_PACKED", where="office_status_row") except Exception: pass def _build_main_fill(self): app = self.app p = self._palette self._main_fill = tk.Frame(app, bg=p["BG"]) self._main_fill.pack(side="top", fill="both", expand=True) self._sidebar = tk.Frame(self._main_fill, bg=p["ACCENT"], width=246) self._sidebar.pack(side="left", fill="y") self._sidebar.pack_propagate(False) self._build_sidebar_content() self._content = tk.Frame(self._main_fill, bg=p["BG"]) self._content.pack(side="left", fill="both", expand=True) self._shell_section_frames = [] self._build_transcript_section() self._build_kg_section() self._build_soap_section() self._build_documents_section() def _build_sidebar_content(self): app = self.app acc = self._palette["ACCENT"] bar = self._sidebar self._theme_switch_pop = None for w in list(bar.winfo_children()): try: w.destroy() except Exception: pass cb_pad = dict( bg=acc, fg="white", font=FONT_DEFAULT, activebackground=acc, activeforeground="white", selectcolor="#1a4d6d", highlightthickness=0, bd=0, anchor="w", ) if getattr(app, "_rclick_paste_var", None) is None: app._rclick_paste_var = tk.BooleanVar(master=app, value=True) if getattr(app, "_kommentare_auto_var", None) is None: app._kommentare_auto_var = tk.BooleanVar(master=app, value=False) if getattr(app, "_empfang_auto_var", None) is None: app._empfang_auto_var = tk.BooleanVar(master=app, value=False) if getattr(app, "_diagnose_grouping_var", None) is None: try: _dg_default = bool(getattr(app, "_autotext_data", {}).get( "diagnose_grouping_enabled", True )) except Exception: _dg_default = True app._diagnose_grouping_var = tk.BooleanVar(master=app, value=_dg_default) stack = tk.Frame(bar, bg=acc) stack.pack(fill="both", expand=True) # ── Sektion: Arbeitsoptionen ───────────────────────────── head_arb = tk.Frame(stack, bg=acc, cursor="hand2") head_arb.pack(fill="x", padx=10, pady=(14, 4)) self._sidebar_head_arb = head_arb self._sec_arb_arrow = tk.Label( head_arb, text=("▼" if self._sec_arb_open else "▶"), bg=acc, fg="white", font=(FF, 9, "bold"), cursor="hand2", ) self._sec_arb_arrow.pack(side="left", padx=(0, 6)) ttl_arb = tk.Label( head_arb, text="Arbeitsoptionen", bg=acc, fg="white", font=(FF, 9, "bold"), cursor="hand2", ) ttl_arb.pack(side="left") for _w in (head_arb, self._sec_arb_arrow, ttl_arb): _w.bind("", lambda e: self._toggle_section_arb()) self._sec_arb_body = tk.Frame(stack, bg=acc) tk.Checkbutton( self._sec_arb_body, text="Rechtsklick = Einfügen", variable=app._rclick_paste_var, command=lambda: _safe_call(app, "_toggle_rclick_paste"), **cb_pad, ).pack(fill="x", padx=12, pady=3) tk.Checkbutton( self._sec_arb_body, text="Kommentare anzeigen", variable=app._kommentare_auto_var, command=lambda: _safe_call(app, "_toggle_kommentare_auto"), **cb_pad, ).pack(fill="x", padx=12, pady=3) tk.Checkbutton( self._sec_arb_body, text="Diagnosen gliedern", variable=app._diagnose_grouping_var, command=lambda: _safe_call(app, "_toggle_diagnose_grouping"), **cb_pad, ).pack(fill="x", padx=12, pady=3) tk.Checkbutton( self._sec_arb_body, text="Chat-Empfang", variable=app._empfang_auto_var, command=self._on_chat_empfang_toggle, **cb_pad, ).pack(fill="x", padx=12, pady=3) _build_sidebar_link( self._sec_arb_body, "chat", "Chat", lambda: _safe_call(app, "_send_to_empfang"), bg=acc, fg="white", indent=20, ) _build_sidebar_link( self._sec_arb_body, "autotext", "Autotext verwalten …", self._open_workspace_autotext, bg=acc, fg="#E2EEF6", indent=20, ) _build_sidebar_link( self._sec_arb_body, "folder", "Ordner", lambda: _safe_call(self.app, "open_ordner_window"), bg=acc, fg="#E2EEF6", indent=20, ) _build_sidebar_link( self._sec_arb_body, "library", "Bibliothek", lambda: _safe_call(self.app, "_open_bibliothek_window"), bg=acc, fg="#E2EEF6", indent=20, ) _build_sidebar_link( self._sec_arb_body, "addon", "Add-on Beta", lambda: _safe_call(self.app, "_open_addon_shell"), bg=acc, fg="#E2EEF6", indent=20, ) if self._sec_arb_open: self._sec_arb_body.pack(fill="x", after=self._sidebar_head_arb) self._build_textbloecke_sidebar_section(stack, acc) # ── Sektion: Erscheinungsbild (nach Textblöcken) ─ head_ersch = tk.Frame(stack, bg=acc, cursor="hand2") head_ersch.pack(fill="x", padx=10, pady=(10, 4)) self._sidebar_head_ersch = head_ersch self._sec_ersch_arrow = tk.Label( head_ersch, text=("▼" if self._sec_ersch_open else "▶"), bg=acc, fg="white", font=(FF, 9, "bold"), cursor="hand2", ) self._sec_ersch_arrow.pack(side="left", padx=(0, 6)) ttl_ersch = tk.Label( head_ersch, text="Erscheinungsbild", bg=acc, fg="white", font=(FF, 9, "bold"), cursor="hand2", ) ttl_ersch.pack(side="left") for _w in (head_ersch, self._sec_ersch_arrow, ttl_ersch): _w.bind("", lambda e: self._toggle_section_ersch()) self._sec_ersch_body = tk.Frame(stack, bg=acc) tk.Label( self._sec_ersch_body, text="Fenster-Transparenz", bg=acc, fg="#E2EEF6", font=FONT_DEFAULT, ).pack(anchor="w", padx=14) ov = getattr(app, "_opacity_var_main", None) if ov is None: ov = tk.DoubleVar(master=app, value=round(load_opacity() * 100)) app._opacity_var_main = ov row_op = tk.Frame(self._sec_ersch_body, bg=acc) row_op.pack(fill="x", padx=12, pady=(2, 4)) lbl_half = tk.Label( row_op, text="◐", font=("Segoe UI Symbol", 14), bg=acc, fg="white", cursor="hand2", ) lbl_half.pack(side="left", padx=(0, 4)) lbl_half.bind( "", lambda e: self._apply_opacity_percent_str(str(int(MIN_OPACITY * 100))), ) sc = ttk.Scale( row_op, from_=40, to=100, variable=ov, orient="horizontal", length=120, command=self._apply_opacity_percent_str, style="OfficePop.Horizontal.TScale", ) sc.pack(side="left", fill="x", expand=True, padx=(0, 4)) lbl_full = tk.Label( row_op, text="☀", font=("Segoe UI Symbol", 14), bg=acc, fg="white", cursor="hand2", ) lbl_full.pack(side="left") lbl_full.bind("", lambda e: self._apply_opacity_percent_str("100")) tk.Frame(self._sec_ersch_body, bg=acc, height=8).pack() link = tk.Label( self._sec_ersch_body, text="Weitere Einstellungen …", bg=acc, fg="white", font=(FF, 9, "underline"), cursor="hand2", ) link.pack(anchor="w", padx=14, pady=(2, 6)) link.bind("", lambda e: _safe_call(app, "_open_settings")) if self._sec_ersch_open: self._sec_ersch_body.pack(fill="x", after=self._sidebar_head_ersch) close_fr = tk.Frame(bar, bg=acc) close_fr.pack(side="bottom", fill="x", padx=12, pady=(8, 12)) self._sidebar_close_btn = tk.Button( close_fr, text="AzA schliessen", font=(FF, 9), bg="#E8F4FA", fg="#8B3A3A", activebackground="#D8E8F0", activeforeground="#7A2E2E", relief="flat", bd=1, highlightbackground="#C8A8A8", highlightthickness=1, padx=14, pady=7, cursor="hand2", anchor="center", command=lambda: _safe_call(app, "_on_close"), ) self._sidebar_close_btn.pack(fill="x") try: from aza_ui_helpers import add_tooltip add_tooltip( self._sidebar_close_btn, "AzA und alle zugehoerigen Fenster schliessen", ) except Exception: pass def _persist_tb_sidebar_open_flag(self) -> None: app = self.app data = getattr(app, "_autotext_data", None) if not isinstance(data, dict): return data["office_sidebar_textbloecke_open"] = self._sec_tb_open try: from aza_persistence import save_autotext save_autotext(data) except Exception: pass def _toggle_section_tb(self) -> None: self._sec_tb_open = not self._sec_tb_open self._persist_tb_sidebar_open_flag() if self._sec_tb_arrow is not None: try: self._sec_tb_arrow.configure( text=("▼" if self._sec_tb_open else "▶"), ) except tk.TclError: pass if self._sec_tb_body is not None: try: if self._sec_tb_open: self._sec_tb_body.pack(fill="x", after=self._head_tb) else: self._sec_tb_body.pack_forget() except tk.TclError: pass self._persist_shell_sections() def _open_workspace_autotext(self) -> None: try: from aza_office_workspace_ui import open_workspace_autotext_manager open_workspace_autotext_manager(self.app) except Exception as exc: print(f"[OfficeV1.2] Autotext-Fenster: {exc}") def _build_textbloecke_sidebar_section( self, parent: tk.Frame, acc: str, ) -> None: self._head_tb = tk.Frame(parent, bg=acc, cursor="hand2") self._head_tb.pack(fill="x", padx=10, pady=(10, 2)) self._sec_tb_arrow = tk.Label( self._head_tb, text=("▼" if self._sec_tb_open else "▶"), bg=acc, fg="white", font=(FF, 9, "bold"), cursor="hand2", ) self._sec_tb_arrow.pack(side="left", padx=(0, 6)) ttl = tk.Label( self._head_tb, text="Textblöcke", bg=acc, fg="white", font=(FF, 9, "bold"), cursor="hand2", ) ttl.pack(side="left") minus = tk.Button( self._head_tb, text="−", command=self._office_tb_remove, bg=acc, fg="white", activebackground=acc, activeforeground="white", font=(FF, 11, "bold"), relief="flat", bd=0, highlightthickness=0, cursor="hand2", width=2, padx=0, pady=0, ) minus.pack(side="right") plus = tk.Button( self._head_tb, text="+", command=self._office_tb_add, bg=acc, fg="white", activebackground=acc, activeforeground="white", font=(FF, 11, "bold"), relief="flat", bd=0, highlightthickness=0, cursor="hand2", width=2, padx=0, pady=0, ) plus.pack(side="right", padx=(0, 4)) for _w in (self._sec_tb_arrow, ttl): _w.bind("", lambda e: self._toggle_section_tb()) self._sec_tb_body = tk.Frame(parent, bg=acc) from aza_persistence import load_textbloecke tb = load_textbloecke() for sk in sorted(tb.keys(), key=int): row = tk.Frame(self._sec_tb_body, bg=acc) row.pack(fill="x", padx=(18, 10), pady=2) label = (tb.get(sk) or {}).get("name") or f"Textblock {sk}" lb = tk.Label( row, text=f" · {label}", bg=acc, fg="white", font=FONT_DEFAULT, cursor="hand2", anchor="w", ) lb.pack(fill="x") lb.bind( "", lambda e, k=sk: self._on_sidebar_textblock_click(e, str(k)), ) if self._sec_tb_open: self._sec_tb_body.pack(fill="x", after=self._head_tb) def _on_sidebar_textblock_click(self, event: tk.Event, slot_key: str) -> None: """Einzelklick = Einfügen; Shift+Klick öffnet den Editor.""" st = int(getattr(event, "state", 0) or 0) if st & 0x0001: self._open_workspace_textblock(str(slot_key)) return ins = getattr(self.app, "_office_sidebar_insert_textblock", None) if callable(ins): try: ins(str(slot_key)) except Exception as exc: print(f"[OfficeV1.2] Textblock-Einfügen: {exc}") def _open_workspace_textblock(self, slot_key: str) -> None: try: from aza_office_workspace_ui import ( open_workspace_textblock_editor, ) open_workspace_textblock_editor(self.app, slot_key) except Exception as exc: print(f"[OfficeV1.2] Textblock-Editor: {exc}") def _office_tb_add(self) -> None: try: from aza_persistence import load_textbloecke, save_textbloecke from aza_workspace_sync import schedule_workspace_cloud_push, utc_now_iso tb = load_textbloecke() keys = sorted(tb.keys(), key=int) new_key = str(int(keys[-1]) + 1) tb[new_key] = { "name": f"Textblock {new_key}", "content": "", "updated_at": utc_now_iso(), } save_textbloecke(tb) schedule_workspace_cloud_push() self.refresh_sidebar_textbloecke_section() except Exception as exc: try: messagebox.showerror("Textblöcke", str(exc), parent=self.app) except Exception: print(f"[OfficeV1.2] Textblock +: {exc}") def _office_tb_remove(self) -> None: try: from aza_persistence import load_textbloecke, save_textbloecke from aza_workspace_sync import schedule_workspace_cloud_push tb = load_textbloecke() keys = sorted(tb.keys(), key=int) if len(keys) <= 2: messagebox.showinfo( "Textblöcke", "Es bleiben mindestens zwei Textblöcke erhalten.", parent=self.app, ) return last_k = str(keys[-1]) name = (tb.get(last_k) or {}).get("name") or f"Textblock {last_k}" if not messagebox.askyesno( "Textblöcke", f"„{name}“ wirklich löschen?", parent=self.app, ): return del tb[last_k] save_textbloecke(tb) schedule_workspace_cloud_push() self.refresh_sidebar_textbloecke_section() except Exception as exc: try: messagebox.showerror("Textblöcke", str(exc), parent=self.app) except Exception: print(f"[OfficeV1.2] Textblock −: {exc}") def refresh_sidebar_textbloecke_section(self) -> None: try: self._build_sidebar_content() except Exception as exc: print(f"[OfficeV1.2] Sidebar-Refresh: {exc}") def _toggle_section_arb(self): self._sec_arb_open = not self._sec_arb_open if self._sec_arb_arrow is not None: try: self._sec_arb_arrow.configure( text=("▼" if self._sec_arb_open else "▶"), ) except Exception: pass if self._sec_arb_body is not None: try: if self._sec_arb_open: self._sec_arb_body.pack( fill="x", after=self._sidebar_head_arb, ) else: self._sec_arb_body.pack_forget() except Exception: pass self._persist_shell_sections() def _toggle_section_ersch(self): self._sec_ersch_open = not self._sec_ersch_open if self._sec_ersch_arrow is not None: try: self._sec_ersch_arrow.configure( text=("▼" if self._sec_ersch_open else "▶"), ) except Exception: pass if self._sec_ersch_body is not None: try: if self._sec_ersch_open: hd = getattr(self, "_sidebar_head_ersch", None) if hd is not None: self._sec_ersch_body.pack(fill="x", after=hd) else: self._sec_ersch_body.pack(fill="x") else: self._sec_ersch_body.pack_forget() except Exception: pass self._persist_shell_sections() def _build_transcript_section(self): app = self.app p = self._palette parent = self._content wrap = tk.Frame(parent, bg=p["BG"]) wrap.pack(side="top", fill="x", padx=18, pady=(4, 4)) self._shell_section_frames.append(wrap) head = tk.Frame(wrap, bg=p["BG"], cursor="hand2") head.pack(fill="x") self._shell_section_frames.extend([head]) arrow = tk.Label( head, text=("▼" if self._transcript_open else "▶"), bg=p["BG"], fg=p["TEXT"], font=FONT_SECTION, padx=2, cursor="hand2", ) arrow.pack(side="left") title = tk.Label(head, text=" Transkript", bg=p["BG"], fg=p["TEXT"], font=FONT_SECTION, cursor="hand2") title.pack(side="left") self._shell_labels.extend([arrow, title]) body = tk.Frame(wrap, bg=p["BG"]) self._shell_section_frames.append(body) txt = ScrolledText( body, wrap="word", font=FONT_DEFAULT, bg=p["TEXT_AREA_BG"], fg=p["TEXT_AREA_FG"], relief="flat", bd=0, height=8, highlightthickness=1, highlightbackground=p["BORDER"], padx=8, pady=6, insertbackground=p["TEXT_AREA_FG"], ) txt.pack(fill="x", pady=(6, 0)) app.txt_transcript = txt app._transcript_frame = body app._transcript_collapsed = not bool(self._transcript_open) if self._transcript_open: body.pack(fill="x") def _toggle(_e=None): self._transcript_open = not self._transcript_open if self._transcript_open: body.pack(fill="x") arrow.configure(text="▼") app._transcript_collapsed = False else: body.pack_forget() arrow.configure(text="▶") app._transcript_collapsed = True self._persist_shell_sections() for w in (head, arrow, title): w.bind("", _toggle) def _build_kg_section(self): app = self.app p = self._palette parent = self._content wrap = tk.Frame(parent, bg=p["BG"]) wrap.pack(side="top", fill="both", expand=True, padx=18, pady=(8, 4)) self._shell_section_frames.append(wrap) head = tk.Frame(wrap, bg=p["BG"]) head.pack(fill="x") self._shell_section_frames.append(head) toggle_box = tk.Frame(head, bg=p["BG"], cursor="hand2") toggle_box.pack(side="left") self._shell_section_frames.append(toggle_box) kg_arrow = tk.Label( toggle_box, text=("▼" if self._kg_open else "▶"), bg=p["BG"], fg=p["TEXT"], font=FONT_SECTION, padx=2, cursor="hand2", ) kg_arrow.pack(side="left") self._shell_labels.append(kg_arrow) initial_key = getattr(app, "_current_doc_type", None) or "kg" try: from aza_doku_vorlagen import DOC_TYPES, DOC_TYPE_CREATE_LABELS, DOC_TYPE_COPY_LABELS from aza_persistence import load_main_doc_type app._doc_type_labels = {k: v for k, v in DOC_TYPES} app._doc_type_create_labels = DOC_TYPE_CREATE_LABELS app._doc_type_copy_labels = DOC_TYPE_COPY_LABELS if not getattr(app, "_current_doc_type", None): app._current_doc_type = load_main_doc_type() initial_key = getattr(app, "_current_doc_type", None) or "kg" app._doc_type_picker = DocTypePicker( head, app, p, DOC_TYPES, initial_key=initial_key, ) app._doc_type_picker.pack(side="left", padx=(4, 8)) try: from aza_ui_helpers import add_tooltip add_tooltip( app._doc_type_picker._display, "Dokumenttyp für Erstellung aus dem Transkript", ) except Exception: pass except Exception as exc: print(f"[OfficeV1.2] Dokumenttyp-Picker nicht geladen: {exc}") actions = tk.Frame(head, bg=p["BG"]) actions.pack(side="right") self._shell_section_frames.append(actions) btn_make = self._register_pill(PillButton( actions, "KG erstellen", command=lambda: _safe_call(app, "make_kg_from_text"), kind="primary", width=BTN_W_ACTION, weight="bold", tooltip="Dokument aus Transkript erstellen", palette=p, )) btn_make.pack(side="left", padx=(0, 6)) app.btn_make_kg = btn_make btn_copy = self._register_pill(PillButton( actions, "KG kopieren", command=lambda: _safe_call(app, "copy_output"), kind="ghost", width=BTN_W_ACTION, palette=p, tooltip="Dokument in Zwischenablage kopieren", )) btn_copy.pack(side="left", padx=(0, 6)) app.btn_copy = btn_copy btn_kom = self._register_pill(PillButton( actions, "Doku-Prompt", command=lambda: _safe_call(app, "_open_doku_vorlage_fenster"), kind="ghost", width=BTN_W_ACTION, palette=p, tooltip="Doku-Prompts für medizinische Dokumente verwalten", )) btn_kom.pack(side="left") app._btn_doku_prompt = btn_kom if hasattr(app, "_set_main_doc_type"): app._set_main_doc_type(initial_key, persist=False) elif hasattr(app, "_update_doc_type_buttons"): app._update_doc_type_buttons() body = tk.Frame(wrap, bg=p["BG"]) self._shell_section_frames.append(body) txt = ScrolledText( body, wrap="word", font=FONT_DEFAULT, bg=p["TEXT_AREA_BG"], fg=p["TEXT_AREA_FG"], relief="flat", bd=0, height=15, highlightthickness=1, highlightbackground=p["BORDER"], padx=8, pady=6, insertbackground=p["TEXT_AREA_FG"], ) txt.pack(fill="both", expand=True) app.txt_output = txt app._kg_frame = body app._kg_collapsed = not bool(self._kg_open) if self._kg_open: body.pack(fill="both", expand=True, pady=(6, 0)) def _toggle(_e=None): self._kg_open = not self._kg_open if self._kg_open: body.pack(fill="both", expand=True, pady=(6, 0)) kg_arrow.configure(text="▼") app._kg_collapsed = False else: body.pack_forget() kg_arrow.configure(text="▶") app._kg_collapsed = True self._persist_shell_sections() for w in (toggle_box, kg_arrow): w.bind("", _toggle) def _build_soap_section(self): app = self.app p = self._palette parent = self._content wrap = tk.Frame(parent, bg=p["BG"]) wrap.pack(side="top", fill="x", padx=18, pady=(8, 4)) self._shell_section_frames.append(wrap) head_soap = tk.Frame(wrap, bg=p["BG"], cursor="hand2") head_soap.pack(fill="x") self._shell_section_frames.append(head_soap) self._soap_arrow = tk.Label( head_soap, text=("▼" if self._soap_open else "▶"), bg=p["BG"], fg=p["TEXT"], font=FONT_SECTION, padx=2, cursor="hand2", ) self._soap_arrow.pack(side="left") soap_title = tk.Label( head_soap, text=" SOAP", bg=p["BG"], fg=p["TEXT"], font=FONT_SECTION, cursor="hand2", ) soap_title.pack(side="left") self._shell_labels.extend([self._soap_arrow, soap_title]) reset_frm = tk.Frame(head_soap, bg=p["BG"], cursor="hand2") reset_frm.pack(side="right") reset_lbl = tk.Label( reset_frm, text="\u21BB", font=("Segoe UI", 13), bg=p["BG"], fg=p["SUBTLE"], cursor="hand2", padx=4, ) reset_lbl.pack(side="right") reset_lbl.bind("", lambda e: _safe_call(app, "_reset_all_soap_sections")) reset_lbl.bind("", lambda e: reset_lbl.configure(fg=p["ACCENT"])) reset_lbl.bind("", lambda e: reset_lbl.configure(fg=p["SUBTLE"])) self._shell_labels.append(reset_lbl) self._soap_fold_body = tk.Frame(wrap, bg=p["BG"]) self._shell_section_frames.append(self._soap_fold_body) sec_row = tk.Frame(self._soap_fold_body, bg=p["BG"]) sec_row.pack(fill="x", pady=(6, 0)) self._shell_section_frames.append(sec_row) soap_inner = tk.Frame(sec_row, bg=p["BG"]) soap_inner.pack(side="left") self._shell_section_frames.append(soap_inner) app._soap_inner = soap_inner app._soap_bg = p["BG"] if not hasattr(app, "_soap_section_labels"): app._soap_section_labels = {} try: app._rebuild_soap_section_controls() except Exception as exc: print(f"[OfficeV1.2] SOAP-Sektionen: {exc}") act = tk.Frame(self._soap_fold_body, bg=p["BG"]) act.pack(fill="x", pady=(8, 0)) self._shell_section_frames.append(act) btn_kuerz = self._register_pill(PillButton( act, "Kürzer", command=lambda: _safe_call(app, "_kg_kuerzer"), kind="default", width=BTN_W_SOAP, tooltip="Krankengeschichte kürzer fassen", palette=p, )) btn_kuerz.pack(side="left", padx=(0, 8)) app.btn_kg_kuerzer = btn_kuerz btn_ausf = self._register_pill(PillButton( act, "Ausführlicher", command=lambda: _safe_call(app, "_kg_ausfuehrlicher"), kind="default", width=BTN_W_SOAP, tooltip="Krankengeschichte ausführlicher gestalten", palette=p, )) btn_ausf.pack(side="left", padx=(0, 8)) app.btn_kg_ausfuehrlicher = btn_ausf btn_vor = self._register_pill(PillButton( act, "Vorlage", command=lambda: _safe_call(app, "_open_kg_vorlage"), kind="default", width=BTN_W_SOAP, tooltip="Vorlage für KG-Erstellung bearbeiten", palette=p, )) btn_vor.pack(side="left") app.btn_kg_vorlage = btn_vor def _soap_toggle(_e=None): self._soap_open = not self._soap_open arr = getattr(self, "_soap_arrow", None) fold = getattr(self, "_soap_fold_body", None) if isinstance(arr, tk.Label): try: arr.configure(text=("▼" if self._soap_open else "▶")) except tk.TclError: pass try: if self._soap_open: fold.pack(fill="x", after=head_soap) elif fold is not None: fold.pack_forget() except tk.TclError: pass self._persist_shell_sections() head_soap.bind("", _soap_toggle) self._soap_arrow.bind("", _soap_toggle) soap_title.bind("", _soap_toggle) if self._soap_open: self._soap_fold_body.pack(fill="x", after=head_soap) def _build_documents_section(self): app = self.app p = self._palette parent = self._content wrap = tk.Frame(parent, bg=p["BG"]) wrap.pack(side="top", fill="x", padx=18, pady=(12, 4)) self._shell_section_frames.append(wrap) head_doc = tk.Frame(wrap, bg=p["BG"], cursor="hand2") head_doc.pack(fill="x") self._shell_section_frames.append(head_doc) self._documents_arrow = tk.Label( head_doc, text=("▼" if self._documents_open else "▶"), bg=p["BG"], fg=p["TEXT"], font=FONT_SECTION, padx=2, cursor="hand2", ) self._documents_arrow.pack(side="left") ttl = tk.Label( head_doc, text=" Dokumente", bg=p["BG"], fg=p["TEXT"], font=FONT_SECTION, cursor="hand2", ) ttl.pack(side="left") self._shell_labels.extend([self._documents_arrow, ttl]) self._documents_fold_body = tk.Frame(wrap, bg=p["BG"]) self._shell_section_frames.append(self._documents_fold_body) grid = tk.Frame(self._documents_fold_body, bg=p["BG"]) grid.pack(fill="x", pady=(6, 0)) self._shell_section_frames.append(grid) items = [ ("Brief", "open_brief_window"), ("Rezept", "open_rezept_window"), ("OP-Bericht", "open_op_bericht_window"), ("KOGU", "open_kogu_window"), ("Diskussion mit KI", "open_diskussion_window"), ("Arztzeugnis", "_open_arztzeugnis"), ("KI-Kontrolle", "open_ki_pruefen"), ("Korrektur", "open_pruefen_window"), ] cols = 4 for i, (label, method) in enumerate(items): r, c = divmod(i, cols) b = self._register_pill(PillButton( grid, label, command=(lambda m=method: _safe_call(app, m)), kind="default", width=BTN_W_DOC, palette=p, tooltip=f"{label} öffnen", )) b.grid(row=r, column=c, padx=(0 if c == 0 else 8), pady=4, sticky="w") def _doc_toggle(_e=None): self._documents_open = not self._documents_open arr = getattr(self, "_documents_arrow", None) fold = getattr(self, "_documents_fold_body", None) if isinstance(arr, tk.Label): try: arr.configure(text=("▼" if self._documents_open else "▶")) except tk.TclError: pass try: if self._documents_open: fold.pack(fill="x", after=head_doc) elif fold is not None: fold.pack_forget() except tk.TclError: pass self._persist_shell_sections() head_doc.bind("", _doc_toggle) self._documents_arrow.bind("", _doc_toggle) ttl.bind("", _doc_toggle) if self._documents_open: self._documents_fold_body.pack(fill="x", after=head_doc) def _build_footer(self): app = self.app p = self._palette self._sep_bottom = tk.Frame(app, bg=p["BORDER"], height=1) self._sep_bottom.pack(side="bottom", fill="x") self._footer_bar = tk.Frame(app, bg=p["BG"]) self._footer_bar.pack(side="bottom", fill="x", padx=18, pady=12) self._logo_img = _load_logo(LOGO_PX) self._footer_logo_lbl = None if self._logo_img is not None: self._footer_logo_lbl = tk.Label( self._footer_bar, image=self._logo_img, bg=p["BG"], bd=0, highlightthickness=0, ) self._footer_logo_lbl.image = self._logo_img self._footer_logo_lbl.pack(side="left", padx=(0, 12)) tb = tk.Frame(self._footer_bar, bg=p["BG"]) tb.pack(side="left", anchor="w") self._footer_tb = tb self._footer_brand_title = tk.Label( tb, text="AzA von Arzt zu Arzt", bg=p["BG"], fg=p["TEXT_STRONG"], font=FONT_BRAND, ) self._footer_brand_title.pack(anchor="w") self._footer_brand_sub = tk.Label( tb, text="Informatik zu fairen Preisen", bg=p["BG"], fg=p["SUBTLE"], font=FONT_BRAND_SUB, ) self._footer_brand_sub.pack(anchor="w") try: from aza_client_watermark import get_client_watermark_text wm = get_client_watermark_text() except Exception: wm = "v?" self._build_watermark_lbl = tk.Label( self._footer_bar, text=wm, bg=p["BG"], fg=p["SUBTLE"], font=("Segoe UI", 8), anchor="e", ) self._build_watermark_lbl.pack(side="right", anchor="se", padx=(8, 0)) def bind_ai_budget_display(self) -> None: """Bindet KI-Guthaben-Label in der Statuszeile (idempotent).""" app = self.app if getattr(app, "_ai_budget_var", None) is None: app._ai_budget_var = tk.StringVar(master=app, value="KI-Guthaben: prüft…") lbl = getattr(self, "_ai_budget_lbl", None) col = getattr(self, "_ai_budget_col", None) if lbl is not None: app._ai_budget_status_lbl = lbl if col is not None: try: if not col.winfo_ismapped(): col.pack(side="right", padx=(8, 0), anchor="ne") except Exception: try: col.pack(side="right", padx=(8, 0), anchor="ne") except Exception: pass app._ai_budget_header_lbl = None try: app.update_token_display() except Exception: pass def _set_update_button_visible(self, visible: bool) -> None: btn = getattr(self, "_update_btn", None) if btn is None: return try: if visible: if not btn.winfo_ismapped(): btn.pack(side="left", padx=(8, 0)) else: btn.pack_forget() except Exception: pass def _poll_update_button_visibility(self) -> None: def _worker(): info = None try: from desktop_update_check import check_for_updates info = check_for_updates() except Exception: info = None show = info is not None def _apply(): try: self._set_update_button_visible(show) except Exception: pass # AzA Office ist EINZIGER Update-Owner: nach Sichtbarwerden des # Hauptfensters genau einmal pro Sitzung das professionelle # Updatefenster zeigen (Parent = Hauptfenster). Kein zweiter # Manifest-Fetch (das geladene info wird wiederverwendet); der # Session-Guard verhindert Wiederholung bei spaeteren Polls. if info is not None: try: from desktop_update_check import maybe_show_startup_update_dialog maybe_show_startup_update_dialog(info, parent=self.app) except Exception: pass try: self.app.after(0, _apply) except Exception: pass threading.Thread(target=_worker, daemon=True).start() def _schedule_update_button_poll(self) -> None: try: self._poll_update_button_visibility() self.app.after(3600000, self._schedule_update_button_poll) except Exception: pass def _periodic_license_refresh(self): try: self._update_license_label() finally: try: self.app.after(15000, self._periodic_license_refresh) except Exception: pass def _update_license_label(self): if not self._license_lbl: return p = self._palette try: mode = getattr(self.app, "license_mode", "demo") self._license_lbl.configure( text=("Lizenz: aktiv" if mode == "active" else "Demo / Testversion"), fg=p["ACCENT"] if mode == "active" else p["WARN"], bg=p["SURFACE"], ) except Exception: pass def _enforce_default_fonts(self): for attr in ("txt_output", "txt_transcript"): w = getattr(self.app, attr, None) if w is None: continue try: w.configure(font=FONT_DEFAULT) except Exception: pass def apply_office_shell_v1(app) -> None: """Wendet die AzA Office Hülle V1.2 auf eine bestehende ``KGDesktopApp`` an. Der Funktionsname bleibt aus Kompatibilität zu ``basis14.py`` erhalten. """ if getattr(app, "_aza_office_v1_installed", False): return app._suppress_inline_soap_reset_icon = True try: shell = _OfficeShellV12(app) shell.install() app._aza_office_v1 = shell app._aza_office_v1_installed = True except Exception as exc: print(f"[OfficeV1.2] Installation fehlgeschlagen: {exc}") import traceback traceback.print_exc() __all__ = ["apply_office_shell_v1"]