Files
aza/AzA march 2026/aza_office_shell_v1.py
2026-05-06 22:43:22 +02:00

2007 lines
68 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# -*- coding: utf-8 -*-
"""
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("<Configure>", lambda e: self._draw())
self.bind("<Enter>", self._on_enter)
self.bind("<Leave>", self._on_leave)
self.bind("<ButtonPress-1>", self._on_press)
self.bind("<ButtonRelease-1>", 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("<Button-1>", self._on_click)
self.bind("<Configure>", 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(
"<Button-1>",
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("<Button-1>", 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("<Button-1>", 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(
"<Button-1>",
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("<Button-1>", 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(
"<Button-1>",
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(
"<Button-1>",
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("<Button-1>", 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(
"<Button-1>",
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("<Button-1>", 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("<Button-1>", 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("<Button-1>", 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(
"<Button-1>",
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("<Button-1>", _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("<Button-1>", _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("<Button-1>",
lambda e: _safe_call(app, "_reset_all_soap_sections"))
reset_lbl.bind("<Enter>", lambda e: reset_lbl.configure(fg=p["ACCENT"]))
reset_lbl.bind("<Leave>", 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("<Button-1>", _soap_toggle)
self._soap_arrow.bind("<Button-1>", _soap_toggle)
soap_title.bind("<Button-1>", _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("<Button-1>", _doc_toggle)
self._documents_arrow.bind("<Button-1>", _doc_toggle)
ttl.bind("<Button-1>", _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"]