# -*- 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 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 _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 == "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: app.after(2500, self._periodic_license_refresh) 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 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) 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="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") right = tk.Frame(self._header_inner, bg=p["SURFACE"]) right.pack(side="right") 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") 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 lbl = tk.Label(self._status_row_fr, textvariable=var, bg=p["BG"], fg=p["SUBTLE"], font=FONT_DEFAULT, anchor="w") lbl.pack(side="left") app.lbl_status = lbl 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=220) 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) 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="Chat-Empfang", variable=app._empfang_auto_var, command=self._on_chat_empfang_toggle, **cb_pad, ).pack(fill="x", padx=12, pady=3) tk.Label( self._sec_arb_body, text="Chatverlauf", bg=acc, fg="white", font=FONT_DEFAULT, cursor="hand2", anchor="w", ).pack(fill="x", padx=(28, 12), pady=(2, 8)) self._sec_arb_body.winfo_children()[-1].bind( "", lambda e: _safe_call(app, "_open_empfang_chat_history"), ) tk.Label( self._sec_arb_body, text="Autotext verwalten …", bg=acc, fg="#E2EEF6", font=FONT_DEFAULT, cursor="hand2", anchor="w", ).pack(fill="x", padx=(28, 12), pady=(2, 10)) self._sec_arb_body.winfo_children()[-1].bind( "", lambda e: self._open_workspace_autotext(), ) 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) 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") title = tk.Label( toggle_box, text=" Krankengeschichte", bg=p["BG"], fg=p["TEXT"], font=FONT_SECTION, cursor="hand2", ) title.pack(side="left") self._shell_labels.extend([kg_arrow, title]) 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="Krankengeschichte 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="Krankengeschichte in Zwischenablage kopieren", )) btn_copy.pack(side="left", padx=(0, 6)) app.btn_copy = btn_copy btn_kom = self._register_pill(PillButton( actions, "Kommentare", command=lambda: _safe_call(app, "_open_kommentare_fenster"), kind="ghost", width=BTN_W_ACTION, palette=p, tooltip="Medizinische Kurzkommentare zum KG-Inhalt", )) btn_kom.pack(side="left") app._btn_kommentare = btn_kom 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, title): 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") 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"]