Files
aza/AzA march 2026/aza_office_shell_v1.py

2007 lines
68 KiB
Python
Raw Normal View History

2026-05-04 21:34:19 +02:00
# -*- 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``)
2026-05-04 23:43:34 +02:00
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.
2026-05-04 21:34:19 +02:00
* 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
2026-05-06 22:43:22 +02:00
from tkinter import messagebox
2026-05-04 21:34:19 +02:00
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"
2026-05-06 22:43:22 +02:00
# Sektions-Toggles (Sidebar + Hauptbereich); Schema-Hochzählung nur bei echten Strukturänderungen.
SECTION_PREFS_SCHEMA = 2
2026-05-04 21:34:19 +02:00
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)
2026-05-06 22:43:22 +02:00
def _load_office_prefs() -> dict:
2026-05-04 21:34:19 +02:00
try:
with open(_prefs_path(), encoding="utf-8") as fh:
2026-05-06 22:43:22 +02:00
d = json.load(fh)
return d if isinstance(d, dict) else {}
2026-05-04 21:34:19 +02:00
except Exception:
2026-05-06 22:43:22 +02:00
return {}
2026-05-04 21:34:19 +02:00
2026-05-06 22:43:22 +02:00
def _save_office_prefs(data: dict) -> None:
2026-05-04 21:34:19 +02:00
try:
path = _prefs_path()
2026-05-06 22:43:22 +02:00
root = os.path.dirname(path)
if root:
os.makedirs(root, exist_ok=True)
2026-05-04 21:34:19 +02:00
with open(path, "w", encoding="utf-8") as fh:
json.dump(data, fh, indent=2, ensure_ascii=False)
except Exception as exc:
2026-05-06 22:43:22 +02:00
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)
2026-05-04 21:34:19 +02:00
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
2026-05-06 22:43:22 +02:00
self._sec_arb_open: bool = False
self._sec_ersch_open: bool = False
2026-05-04 21:34:19 +02:00
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
2026-05-06 22:43:22 +02:00
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
2026-05-04 21:34:19 +02:00
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] = []
2026-05-06 22:43:22 +02:00
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
2026-05-04 21:34:19 +02:00
# ── Öffentlich ────────────────────────────────────────────────────
def install(self):
app = self.app
2026-05-06 22:43:22 +02:00
self._hydrate_shell_section_prefs()
2026-05-04 21:34:19 +02:00
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()
2026-05-06 22:43:22 +02:00
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
2026-05-04 21:34:19 +02:00
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
2026-05-06 22:43:22 +02:00
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}")
2026-05-04 21:34:19 +02:00
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
2026-05-06 22:43:22 +02:00
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)
2026-05-04 21:34:19 +02:00
# ── 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",
2026-05-04 23:43:34 +02:00
selectcolor="#1a4d6d", highlightthickness=0,
2026-05-04 21:34:19 +02:00
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
2026-05-04 23:43:34 +02:00
self._theme_switch_pop = None
2026-05-04 21:34:19 +02:00
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",
2026-05-04 23:43:34 +02:00
selectcolor="#1a4d6d", highlightthickness=0,
2026-05-04 21:34:19 +02:00
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)
2026-05-06 22:43:22 +02:00
stack = tk.Frame(bar, bg=acc)
stack.pack(fill="both", expand=True)
2026-05-04 21:34:19 +02:00
# ── Sektion: Arbeitsoptionen ─────────────────────────────
2026-05-06 22:43:22 +02:00
head_arb = tk.Frame(stack, bg=acc, cursor="hand2")
2026-05-04 21:34:19 +02:00
head_arb.pack(fill="x", padx=10, pady=(14, 4))
2026-05-06 22:43:22 +02:00
self._sidebar_head_arb = head_arb
2026-05-04 21:34:19 +02:00
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())
2026-05-06 22:43:22 +02:00
self._sec_arb_body = tk.Frame(stack, bg=acc)
2026-05-04 21:34:19 +02:00
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)
2026-05-05 23:36:13 +02:00
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"),
)
2026-05-06 22:43:22 +02:00
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(),
)
2026-05-04 21:34:19 +02:00
if self._sec_arb_open:
2026-05-06 22:43:22 +02:00
self._sec_arb_body.pack(fill="x", after=self._sidebar_head_arb)
2026-05-04 21:34:19 +02:00
2026-05-06 22:43:22 +02:00
self._build_textbloecke_sidebar_section(stack, acc)
2026-05-04 21:34:19 +02:00
2026-05-06 22:43:22 +02:00
# ── 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
2026-05-04 21:34:19 +02:00
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())
2026-05-06 22:43:22 +02:00
self._sec_ersch_body = tk.Frame(stack, bg=acc)
2026-05-04 21:34:19 +02:00
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",
)
2026-05-06 22:43:22 +02:00
link.pack(anchor="w", padx=14, pady=(2, 6))
2026-05-04 21:34:19 +02:00
link.bind("<Button-1>", lambda e: _safe_call(app, "_open_settings"))
if self._sec_ersch_open:
2026-05-06 22:43:22 +02:00
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}")
2026-05-04 21:34:19 +02:00
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",
2026-05-06 22:43:22 +02:00
after=self._sidebar_head_arb,
2026-05-04 21:34:19 +02:00
)
else:
self._sec_arb_body.pack_forget()
except Exception:
pass
2026-05-06 22:43:22 +02:00
self._persist_shell_sections()
2026-05-04 21:34:19 +02:00
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:
2026-05-06 22:43:22 +02:00
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")
2026-05-04 21:34:19 +02:00
else:
self._sec_ersch_body.pack_forget()
except Exception:
pass
2026-05-06 22:43:22 +02:00
self._persist_shell_sections()
2026-05-04 21:34:19 +02:00
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(
2026-05-06 22:43:22 +02:00
head,
text=("" if self._transcript_open else ""),
bg=p["BG"], fg=p["TEXT"], font=FONT_SECTION,
2026-05-04 21:34:19 +02:00
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
2026-05-06 22:43:22 +02:00
app._transcript_collapsed = not bool(self._transcript_open)
if self._transcript_open:
body.pack(fill="x")
2026-05-04 21:34:19 +02:00
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
2026-05-06 22:43:22 +02:00
self._persist_shell_sections()
2026-05-04 21:34:19 +02:00
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(
2026-05-06 22:43:22 +02:00
toggle_box,
text=("" if self._kg_open else ""),
bg=p["BG"], fg=p["TEXT"],
2026-05-04 21:34:19 +02:00
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
2026-05-06 22:43:22 +02:00
app._kg_collapsed = not bool(self._kg_open)
if self._kg_open:
body.pack(fill="both", expand=True, pady=(6, 0))
2026-05-04 21:34:19 +02:00
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
2026-05-06 22:43:22 +02:00
self._persist_shell_sections()
2026-05-04 21:34:19 +02:00
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)
2026-05-06 22:43:22 +02:00
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)
2026-05-04 21:34:19 +02:00
2026-05-06 22:43:22 +02:00
sec_row = tk.Frame(self._soap_fold_body, bg=p["BG"])
2026-05-04 21:34:19 +02:00
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}")
2026-05-06 22:43:22 +02:00
act = tk.Frame(self._soap_fold_body, bg=p["BG"])
2026-05-04 21:34:19 +02:00
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
2026-05-06 22:43:22 +02:00
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)
2026-05-04 21:34:19 +02:00
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)
2026-05-06 22:43:22 +02:00
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)
2026-05-04 21:34:19 +02:00
2026-05-06 22:43:22 +02:00
grid = tk.Frame(self._documents_fold_body, bg=p["BG"])
2026-05-04 21:34:19 +02:00
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")
2026-05-06 22:43:22 +02:00
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)
2026-05-04 21:34:19 +02:00
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
2026-05-06 22:43:22 +02:00
app._suppress_inline_soap_reset_icon = True
2026-05-04 21:34:19 +02:00
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"]