# -*- 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 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" 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_dark_pref() -> bool: try: with open(_prefs_path(), encoding="utf-8") as fh: data = json.load(fh) return bool(data.get("dark_mode", False)) except Exception: return False def _save_dark_pref(dark: bool) -> None: try: path = _prefs_path() data = {} try: with open(path, encoding="utf-8") as fh: data = json.load(fh) except Exception: pass data["dark_mode"] = dark 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 Erscheinungsbild nicht speichern: {exc}") 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 = True self._sec_ersch_open: bool = True 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._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] = [] # ── Öffentlich ──────────────────────────────────────────────────── def install(self): app = self.app 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() 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: 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 # ── 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) # ── Sektion: Arbeitsoptionen ───────────────────────────── head_arb = tk.Frame(bar, bg=acc, cursor="hand2") head_arb.pack(fill="x", padx=10, pady=(14, 4)) 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(bar, 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) if self._sec_arb_open: self._sec_arb_body.pack(fill="x") tk.Frame(bar, bg=acc, height=10).pack() # ── Sektion: Erscheinungsbild ──────────────────────────── head_ersch = tk.Frame(bar, bg=acc, cursor="hand2") head_ersch.pack(fill="x", padx=10, pady=(2, 4)) 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(bar, 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, 12)) link.bind("", lambda e: _safe_call(app, "_open_settings")) if self._sec_ersch_open: self._sec_ersch_body.pack(fill="x") 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", before=self._sec_ersch_arrow.master if self._sec_ersch_arrow else None, ) else: self._sec_arb_body.pack_forget() except Exception: pass 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: self._sec_ersch_body.pack(fill="x") else: self._sec_ersch_body.pack_forget() except Exception: pass 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="▶", 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 = True 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 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="▼", 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"]) body.pack(fill="both", expand=True, pady=(6, 0)) 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 = False 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 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) soap_lbl = tk.Label(wrap, text="SOAP", bg=p["BG"], fg=p["TEXT"], font=FONT_SECTION) soap_lbl.pack(anchor="w") self._shell_labels.append(soap_lbl) sec_row = tk.Frame(wrap, 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(wrap, 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 _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) doc_lbl = tk.Label(wrap, text="Dokumente", bg=p["BG"], fg=p["TEXT"], font=FONT_SECTION) doc_lbl.pack(anchor="w") self._shell_labels.append(doc_lbl) grid = tk.Frame(wrap, 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 _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 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"]