Files
aza/AzA march 2026/aza_office_shell_v1.py
2026-05-04 21:34:19 +02:00

1555 lines
52 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``). Zusätzlich
weiterhin **Hell/Dunkel** für die Office-Hülle-Farbpalette (persistiert),
da das klassische ``basis14``-Hauptfenster keine globale Farb-Umschaltung hat — nur
Transparenz und das separate Einstellungsfenster.
* Footer-Branding und Logo ca. **30 % größer** als in V1.1.
Technische Strategie
--------------------
Nach ``_build_ui`` blendet die Hülle die alten Hauptfenster-Kinder aus und
baut die Office-Oberfläche neu auf. Bestehende App-Methoden und Variablen
aus ``KGDesktopApp`` werden verdrahtet.
"""
from __future__ import annotations
import json
import os
import sys
import tkinter as tk
from tkinter import ttk
from tkinter.scrolledtext import ScrolledText
from typing import Callable, Dict, List, Optional
try:
from PIL import Image, ImageTk
_HAS_PIL = True
except Exception:
_HAS_PIL = False
try:
from aza_config import MIN_OPACITY
except Exception:
MIN_OPACITY = 0.4
try:
from aza_persistence import load_opacity, save_opacity
except Exception:
def load_opacity() -> float:
return 1.0
def save_opacity(_v: float) -> None:
pass
PREFS_FILENAME = "aza_office_shell_v11_prefs.json"
FF = "Segoe UI"
FONT_DEFAULT = (FF, 9)
FONT_BOLD = (FF, 9, "bold")
FONT_SECTION = (FF, 10, "bold")
FONT_BRAND = (FF, 18, "bold") # V1.1: 14 → +~29 %
FONT_BRAND_SUB = (FF, 12) # V1.1: 9 → +33 %
BTN_W_HEADER = 110
BTN_W_ACTION = 130
BTN_W_DOC = 150
BTN_W_SOAP = 158
BTN_H = 32
LOGO_PX = 57 # V1.1: 44 → ×1.295 ≈ +30 %
# Popover = gleiche Farbe wie Primärbuttons (ACCENT aus Palette)
POPOVER_TRACK = "#3D6F8D"
POPOVER_KNOB = "#FFFFFF"
POPOVER_KNOB_RING = "#E2EEF6"
PALETTE_LIGHT: Dict[str, str] = {
"BG": "#EAF2F7",
"SURFACE": "#FFFFFF",
"SURFACE_ALT": "#F4F8FB",
"BORDER": "#D6E2EB",
"TEXT": "#1A4D6D",
"TEXT_STRONG": "#0F3850",
"SUBTLE": "#5C7A8E",
"ACCENT": "#5B8DB3",
"ACCENT_HOVER": "#4A7A9E",
"ACCENT_PRESSED": "#3A6884",
"ACCENT_SOFT": "#E2EEF6",
"WARN": "#C2840F",
"RECORD": "#C0392B",
"RECORD_HOVER": "#A6291C",
"TEXT_AREA_BG": "#FFFFFF",
"TEXT_AREA_FG": "#1A4D6D",
}
PALETTE_DARK: Dict[str, str] = {
"BG": "#1B2E36",
"SURFACE": "#243B45",
"SURFACE_ALT": "#2D4652",
"BORDER": "#3D5866",
"TEXT": "#E8F2F7",
"TEXT_STRONG": "#FFFFFF",
"SUBTLE": "#9BB8C9",
"ACCENT": "#6BA3C4",
"ACCENT_HOVER": "#5A92B5",
"ACCENT_PRESSED": "#4A82A6",
"ACCENT_SOFT": "#2F4F5E",
"WARN": "#E0B456",
"RECORD": "#E74C3C",
"RECORD_HOVER": "#C0392B",
"TEXT_AREA_BG": "#1E323B",
"TEXT_AREA_FG": "#EAF4F8",
}
def _prefs_path() -> str:
try:
from aza_config import get_writable_data_dir
base = get_writable_data_dir()
except Exception:
base = os.path.dirname(os.path.abspath(__file__))
return os.path.join(base, PREFS_FILENAME)
def _load_dark_pref() -> bool:
try:
with open(_prefs_path(), encoding="utf-8") as fh:
data = json.load(fh)
return bool(data.get("dark_mode", False))
except Exception:
return False
def _save_dark_pref(dark: bool) -> None:
try:
path = _prefs_path()
data = {}
try:
with open(path, encoding="utf-8") as fh:
data = json.load(fh)
except Exception:
pass
data["dark_mode"] = dark
with open(path, "w", encoding="utf-8") as fh:
json.dump(data, fh, indent=2, ensure_ascii=False)
except Exception as exc:
print(f"[OfficeV1.2] Konnte Erscheinungsbild nicht speichern: {exc}")
class PillButton(tk.Canvas):
"""Pill-Button; Farben aus Palette-Dict."""
def __init__(
self,
parent,
text: str,
command: Optional[Callable] = None,
*,
kind: str = "default",
width: int = BTN_W_ACTION,
height: int = BTN_H,
weight: str = "normal",
tooltip: Optional[str] = None,
palette: Dict[str, str],
):
bg = parent.cget("bg") if hasattr(parent, "cget") else PALETTE_LIGHT["BG"]
super().__init__(parent, width=width, height=height, bg=bg,
highlightthickness=0, bd=0, cursor="hand2")
self._text = text
self._command = command
self._kind = kind
self._btn_w = width
self._btn_h = height
self._weight = weight
self._palette = palette
self._hover = False
self._press = False
self.bind("<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 = True
self._sec_ersch_open: bool = True
self._sec_arb_arrow: Optional[tk.Label] = None
self._sec_ersch_arrow: Optional[tk.Label] = None
self._sec_arb_body: Optional[tk.Frame] = None
self._sec_ersch_body: Optional[tk.Frame] = None
self._transcript_open: bool = False
self._kg_open: bool = True
self._main_fill: Optional[tk.Frame] = None
self._content: Optional[tk.Frame] = None
self._header_inner: Optional[tk.Frame] = None
self._header_bar: Optional[tk.Frame] = None
self._footer_bar: Optional[tk.Frame] = None
self._sep_top: Optional[tk.Frame] = None
self._sep_bottom: Optional[tk.Frame] = None
self._status_row_fr: Optional[tk.Frame] = None
self._palette: Dict[str, str] = {}
self._dark_mode: bool = False
self._pills: List[PillButton] = []
self._footer_tb: Optional[tk.Frame] = None
self._footer_logo_lbl: Optional[tk.Label] = None
self._footer_brand_title: Optional[tk.Label] = None
self._footer_brand_sub: Optional[tk.Label] = None
self._shell_labels: List[tk.Label] = []
# ── Öffentlich ────────────────────────────────────────────────────
def install(self):
app = self.app
self._dark_mode = _load_dark_pref()
self._palette = (
PALETTE_DARK.copy() if self._dark_mode else PALETTE_LIGHT.copy()
)
try:
from aza_version import APP_VERSION
app.title(f"AzA Office (v{APP_VERSION})")
except Exception:
app.title("AzA Office")
try:
app.configure(bg=self._palette["BG"])
except Exception:
pass
self._hide_legacy_children()
self._apply_ttk_theme()
self._build_footer()
self._build_header()
self._build_status_row()
self._build_main_fill()
if self._record_btn is not None:
app.btn_record = self._record_btn
if self._korrigieren_btn is not None:
app.btn_record_append = self._korrigieren_btn
if self._diktat_btn is not None:
app._btn_diktat_top = self._diktat_btn
try:
app.after(250, self._enforce_default_fonts)
except Exception:
pass
try:
ud = getattr(app, "_update_kg_detail_display", None)
if callable(ud):
ud()
except Exception:
pass
self._update_license_label()
try:
app.after(2500, self._periodic_license_refresh)
except Exception:
pass
try:
existing = getattr(app, "_dev_status_window", None)
if existing is not None and existing.winfo_exists():
existing.destroy()
except Exception:
pass
app._dev_status_window = None
# ── Hilfen ────────────────────────────────────────────────────────
def _register_pill(self, b: PillButton) -> PillButton:
self._pills.append(b)
return b
def _hide_legacy_children(self):
for child in list(self.app.winfo_children()):
for forget in ("pack_forget", "place_forget", "grid_forget"):
try:
getattr(child, forget)()
except Exception:
pass
def _apply_opacity_percent_str(self, val: str) -> None:
"""Wie ``on_opacity_main`` in ``basis14._build_ui`` (Transparenz)."""
app = self.app
try:
alpha = float(val) / 100.0
alpha = max(float(MIN_OPACITY), min(1.0, alpha))
app.attributes("-alpha", alpha)
save_opacity(alpha)
ov = getattr(app, "_opacity_var_main", None)
if ov is not None:
ov.set(round(alpha * 100))
sc = getattr(app, "_opacity_scale_main", None)
if sc is not None:
try:
sc.set(round(alpha * 100))
except Exception:
pass
except Exception:
pass
def _apply_ttk_theme(self):
p = self._palette
try:
style = ttk.Style(self.app)
try:
style.theme_use("clam")
except tk.TclError:
pass
style.configure("TFrame", background=p["BG"])
style.configure("TLabel", background=p["BG"], foreground=p["TEXT"],
font=FONT_DEFAULT)
style.configure(
"TButton", background=p["ACCENT"], foreground="white",
padding=(10, 6), borderwidth=0, font=FONT_DEFAULT,
)
style.map("TButton", background=[
("active", p["ACCENT_HOVER"]),
("pressed", p["ACCENT_PRESSED"]),
])
style.configure(
"OfficePop.Horizontal.TScale",
troughcolor="#E2EEF6",
background=p["ACCENT"],
)
except Exception:
pass
def _apply_main_theme(self):
p = self._palette
app = self.app
try:
app.configure(bg=p["BG"])
except Exception:
pass
for fr in (self._header_inner, self._header_bar):
if fr is not None:
try:
fr.configure(bg=p["SURFACE"])
except Exception:
pass
if self._header_inner is not None:
try:
for ch in self._header_inner.winfo_children():
try:
if ch is self._gear_btn:
continue
ch.configure(bg=p["SURFACE"])
except Exception:
pass
except Exception:
pass
for fr in (self._main_fill, self._content, self._status_row_fr,
self._footer_bar):
if fr is not None:
try:
fr.configure(bg=p["BG"])
except Exception:
pass
if self._sep_top is not None:
try:
self._sep_top.configure(bg=p["BORDER"])
except Exception:
pass
if self._sep_bottom is not None:
try:
self._sep_bottom.configure(bg=p["BORDER"])
except Exception:
pass
try:
app.lbl_status.configure(bg=p["BG"], fg=p["SUBTLE"])
except Exception:
pass
if self._license_lbl is not None:
try:
mode = getattr(app, "license_mode", "demo")
self._license_lbl.configure(
bg=p["SURFACE"],
fg=p["ACCENT"] if mode == "active" else p["WARN"],
)
except Exception:
pass
self._apply_section_backgrounds(p["BG"])
self._apply_shell_labels(p)
self._apply_text_widgets(p)
if self._footer_tb is not None:
try:
self._footer_tb.configure(bg=p["BG"])
except Exception:
pass
if self._footer_logo_lbl is not None:
try:
self._footer_logo_lbl.configure(bg=p["BG"])
except Exception:
pass
if self._footer_brand_title is not None:
try:
self._footer_brand_title.configure(bg=p["BG"], fg=p["TEXT_STRONG"])
except Exception:
pass
if self._footer_brand_sub is not None:
try:
self._footer_brand_sub.configure(bg=p["BG"], fg=p["SUBTLE"])
except Exception:
pass
for pill in self._pills:
try:
pill.configure(bg=p["SURFACE"])
except Exception:
pass
pill.set_palette_ref(p)
app._soap_bg = p["BG"]
try:
app._rebuild_soap_section_controls()
except Exception:
pass
self._apply_ttk_theme()
if self._sidebar is not None:
try:
self._sidebar.configure(bg=p["ACCENT"])
except Exception:
pass
try:
self._build_sidebar_content()
except Exception:
pass
def _toggle_theme_main(self):
self._dark_mode = not self._dark_mode
_save_dark_pref(self._dark_mode)
self._palette = (
PALETTE_DARK.copy() if self._dark_mode else PALETTE_LIGHT.copy()
)
self._apply_main_theme()
def _apply_shell_labels(self, p: Dict[str, str]):
for w in self._shell_labels:
try:
w.configure(bg=p["BG"], fg=p["TEXT"])
except Exception:
pass
def _apply_section_backgrounds(self, bg: str):
sh = getattr(self, "_shell_section_frames", None)
if not sh:
return
for fr in sh:
try:
fr.configure(bg=bg)
except Exception:
pass
def _apply_text_widgets(self, p: Dict[str, str]):
for attr in ("txt_output", "txt_transcript"):
w = getattr(self.app, attr, None)
if w is None:
continue
try:
w.configure(
bg=p["TEXT_AREA_BG"], fg=p["TEXT_AREA_FG"],
highlightbackground=p["BORDER"], insertbackground=p["TEXT_AREA_FG"],
)
except Exception:
pass
def _on_chat_empfang_toggle(self):
_safe_call(self.app, "_toggle_empfang_auto")
try:
self.app.after(80, lambda: _safe_call(self.app, "_send_to_empfang"))
except Exception:
pass
# ── (Legacy Popover-Code, in V1.2.1 nicht mehr verwendet) ─────────
def _destroy_settings_pop(self):
if self._settings_win is not None:
try:
self._settings_win.destroy()
except Exception:
pass
self._settings_win = None
self._theme_switch_pop = None
def _toggle_settings_pop(self):
if self._settings_win is not None:
try:
if self._settings_win.winfo_exists():
self._destroy_settings_pop()
return
except tk.TclError:
pass
self._show_settings_pop()
def _show_settings_pop(self):
self._destroy_settings_pop()
app = self.app
acc = self._palette["ACCENT"]
pop = tk.Toplevel(app)
pop.withdraw()
pop.configure(bg=acc)
try:
pop.transient(app)
except Exception:
pass
self._settings_win = pop
pad = dict(
bg=acc, fg="white", font=FONT_DEFAULT,
activebackground=acc, activeforeground="white",
selectcolor="#E2EEF6", 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
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="#E2EEF6", highlightthickness=0,
bd=0, anchor="w",
)
if getattr(app, "_rclick_paste_var", None) is None:
app._rclick_paste_var = tk.BooleanVar(master=app, value=True)
if getattr(app, "_kommentare_auto_var", None) is None:
app._kommentare_auto_var = tk.BooleanVar(master=app, value=False)
if getattr(app, "_empfang_auto_var", None) is None:
app._empfang_auto_var = tk.BooleanVar(master=app, value=False)
# ── Sektion: Arbeitsoptionen ─────────────────────────────
head_arb = tk.Frame(bar, bg=acc, cursor="hand2")
head_arb.pack(fill="x", padx=10, pady=(14, 4))
self._sec_arb_arrow = tk.Label(
head_arb,
text=("" if self._sec_arb_open else ""),
bg=acc, fg="white", font=(FF, 9, "bold"),
cursor="hand2",
)
self._sec_arb_arrow.pack(side="left", padx=(0, 6))
ttl_arb = tk.Label(
head_arb, text="Arbeitsoptionen",
bg=acc, fg="white", font=(FF, 9, "bold"),
cursor="hand2",
)
ttl_arb.pack(side="left")
for _w in (head_arb, self._sec_arb_arrow, ttl_arb):
_w.bind("<Button-1>", lambda e: self._toggle_section_arb())
self._sec_arb_body = tk.Frame(bar, bg=acc)
tk.Checkbutton(
self._sec_arb_body, text="Rechtsklick = Einfügen",
variable=app._rclick_paste_var,
command=lambda: _safe_call(app, "_toggle_rclick_paste"),
**cb_pad,
).pack(fill="x", padx=12, pady=3)
tk.Checkbutton(
self._sec_arb_body, text="Kommentare anzeigen",
variable=app._kommentare_auto_var,
command=lambda: _safe_call(app, "_toggle_kommentare_auto"),
**cb_pad,
).pack(fill="x", padx=12, pady=3)
tk.Checkbutton(
self._sec_arb_body, text="Chat-Empfang",
variable=app._empfang_auto_var,
command=self._on_chat_empfang_toggle,
**cb_pad,
).pack(fill="x", padx=12, pady=3)
if self._sec_arb_open:
self._sec_arb_body.pack(fill="x")
tk.Frame(bar, bg=acc, height=10).pack()
# ── Sektion: Erscheinungsbild ────────────────────────────
head_ersch = tk.Frame(bar, bg=acc, cursor="hand2")
head_ersch.pack(fill="x", padx=10, pady=(2, 4))
self._sec_ersch_arrow = tk.Label(
head_ersch,
text=("" if self._sec_ersch_open else ""),
bg=acc, fg="white", font=(FF, 9, "bold"),
cursor="hand2",
)
self._sec_ersch_arrow.pack(side="left", padx=(0, 6))
ttl_ersch = tk.Label(
head_ersch, text="Erscheinungsbild",
bg=acc, fg="white", font=(FF, 9, "bold"),
cursor="hand2",
)
ttl_ersch.pack(side="left")
for _w in (head_ersch, self._sec_ersch_arrow, ttl_ersch):
_w.bind("<Button-1>", lambda e: self._toggle_section_ersch())
self._sec_ersch_body = tk.Frame(bar, bg=acc)
tk.Label(
self._sec_ersch_body, text="Fenster-Transparenz",
bg=acc, fg="#E2EEF6", font=FONT_DEFAULT,
).pack(anchor="w", padx=14)
ov = getattr(app, "_opacity_var_main", None)
if ov is None:
ov = tk.DoubleVar(master=app, value=round(load_opacity() * 100))
app._opacity_var_main = ov
row_op = tk.Frame(self._sec_ersch_body, bg=acc)
row_op.pack(fill="x", padx=12, pady=(2, 4))
lbl_half = tk.Label(
row_op, text="", font=("Segoe UI Symbol", 14),
bg=acc, fg="white", cursor="hand2",
)
lbl_half.pack(side="left", padx=(0, 4))
lbl_half.bind(
"<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.Label(
self._sec_ersch_body, text="Office-Hülle hell / dunkel",
bg=acc, fg="#E2EEF6", font=FONT_DEFAULT,
).pack(anchor="w", padx=14, pady=(8, 4))
row_th = tk.Frame(self._sec_ersch_body, bg=acc)
row_th.pack(fill="x", padx=12)
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 is not None:
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(self._sec_ersch_body, bg=acc, height=8).pack()
link = tk.Label(
self._sec_ersch_body, text="Weitere Einstellungen …",
bg=acc, fg="white", font=(FF, 9, "underline"),
cursor="hand2",
)
link.pack(anchor="w", padx=14, pady=(2, 12))
link.bind("<Button-1>", lambda e: _safe_call(app, "_open_settings"))
if self._sec_ersch_open:
self._sec_ersch_body.pack(fill="x")
def _toggle_section_arb(self):
self._sec_arb_open = not self._sec_arb_open
if self._sec_arb_arrow is not None:
try:
self._sec_arb_arrow.configure(
text=("" if self._sec_arb_open else ""),
)
except Exception:
pass
if self._sec_arb_body is not None:
try:
if self._sec_arb_open:
self._sec_arb_body.pack(
fill="x",
before=self._sec_ersch_arrow.master if self._sec_ersch_arrow else None,
)
else:
self._sec_arb_body.pack_forget()
except Exception:
pass
def _toggle_section_ersch(self):
self._sec_ersch_open = not self._sec_ersch_open
if self._sec_ersch_arrow is not None:
try:
self._sec_ersch_arrow.configure(
text=("" if self._sec_ersch_open else ""),
)
except Exception:
pass
if self._sec_ersch_body is not None:
try:
if self._sec_ersch_open:
self._sec_ersch_body.pack(fill="x")
else:
self._sec_ersch_body.pack_forget()
except Exception:
pass
def _build_transcript_section(self):
app = self.app
p = self._palette
parent = self._content
wrap = tk.Frame(parent, bg=p["BG"])
wrap.pack(side="top", fill="x", padx=18, pady=(4, 4))
self._shell_section_frames.append(wrap)
head = tk.Frame(wrap, bg=p["BG"], cursor="hand2")
head.pack(fill="x")
self._shell_section_frames.extend([head])
arrow = tk.Label(
head, text="", bg=p["BG"], fg=p["TEXT"], font=FONT_SECTION,
padx=2, cursor="hand2",
)
arrow.pack(side="left")
title = tk.Label(head, text=" Transkript", bg=p["BG"], fg=p["TEXT"],
font=FONT_SECTION, cursor="hand2")
title.pack(side="left")
self._shell_labels.extend([arrow, title])
body = tk.Frame(wrap, bg=p["BG"])
self._shell_section_frames.append(body)
txt = ScrolledText(
body, wrap="word", font=FONT_DEFAULT,
bg=p["TEXT_AREA_BG"], fg=p["TEXT_AREA_FG"], relief="flat", bd=0,
height=8, highlightthickness=1, highlightbackground=p["BORDER"],
padx=8, pady=6, insertbackground=p["TEXT_AREA_FG"],
)
txt.pack(fill="x", pady=(6, 0))
app.txt_transcript = txt
app._transcript_frame = body
app._transcript_collapsed = True
def _toggle(_e=None):
self._transcript_open = not self._transcript_open
if self._transcript_open:
body.pack(fill="x")
arrow.configure(text="")
app._transcript_collapsed = False
else:
body.pack_forget()
arrow.configure(text="")
app._transcript_collapsed = True
for w in (head, arrow, title):
w.bind("<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="", bg=p["BG"], fg=p["TEXT"],
font=FONT_SECTION, padx=2, cursor="hand2",
)
kg_arrow.pack(side="left")
title = tk.Label(
toggle_box, text=" Krankengeschichte", bg=p["BG"], fg=p["TEXT"],
font=FONT_SECTION, cursor="hand2",
)
title.pack(side="left")
self._shell_labels.extend([kg_arrow, title])
actions = tk.Frame(head, bg=p["BG"])
actions.pack(side="right")
self._shell_section_frames.append(actions)
btn_make = self._register_pill(PillButton(
actions, "KG erstellen",
command=lambda: _safe_call(app, "make_kg_from_text"),
kind="primary", width=BTN_W_ACTION, weight="bold",
tooltip="Krankengeschichte aus Transkript erstellen",
palette=p,
))
btn_make.pack(side="left", padx=(0, 6))
app.btn_make_kg = btn_make
btn_copy = self._register_pill(PillButton(
actions, "KG kopieren",
command=lambda: _safe_call(app, "copy_output"),
kind="ghost", width=BTN_W_ACTION, palette=p,
tooltip="Krankengeschichte in Zwischenablage kopieren",
))
btn_copy.pack(side="left", padx=(0, 6))
app.btn_copy = btn_copy
btn_kom = self._register_pill(PillButton(
actions, "Kommentare",
command=lambda: _safe_call(app, "_open_kommentare_fenster"),
kind="ghost", width=BTN_W_ACTION, palette=p,
tooltip="Medizinische Kurzkommentare zum KG-Inhalt",
))
btn_kom.pack(side="left")
app._btn_kommentare = btn_kom
body = tk.Frame(wrap, bg=p["BG"])
body.pack(fill="both", expand=True, pady=(6, 0))
self._shell_section_frames.append(body)
txt = ScrolledText(
body, wrap="word", font=FONT_DEFAULT,
bg=p["TEXT_AREA_BG"], fg=p["TEXT_AREA_FG"], relief="flat", bd=0,
height=15, highlightthickness=1, highlightbackground=p["BORDER"],
padx=8, pady=6, insertbackground=p["TEXT_AREA_FG"],
)
txt.pack(fill="both", expand=True)
app.txt_output = txt
app._kg_frame = body
app._kg_collapsed = False
def _toggle(_e=None):
self._kg_open = not self._kg_open
if self._kg_open:
body.pack(fill="both", expand=True, pady=(6, 0))
kg_arrow.configure(text="")
app._kg_collapsed = False
else:
body.pack_forget()
kg_arrow.configure(text="")
app._kg_collapsed = True
for w in (toggle_box, kg_arrow, title):
w.bind("<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)
soap_lbl = tk.Label(wrap, text="SOAP", bg=p["BG"], fg=p["TEXT"],
font=FONT_SECTION)
soap_lbl.pack(anchor="w")
self._shell_labels.append(soap_lbl)
sec_row = tk.Frame(wrap, bg=p["BG"])
sec_row.pack(fill="x", pady=(6, 0))
self._shell_section_frames.append(sec_row)
soap_inner = tk.Frame(sec_row, bg=p["BG"])
soap_inner.pack(side="left")
self._shell_section_frames.append(soap_inner)
app._soap_inner = soap_inner
app._soap_bg = p["BG"]
if not hasattr(app, "_soap_section_labels"):
app._soap_section_labels = {}
try:
app._rebuild_soap_section_controls()
except Exception as exc:
print(f"[OfficeV1.2] SOAP-Sektionen: {exc}")
act = tk.Frame(wrap, bg=p["BG"])
act.pack(fill="x", pady=(8, 0))
self._shell_section_frames.append(act)
btn_kuerz = self._register_pill(PillButton(
act, "Kürzer",
command=lambda: _safe_call(app, "_kg_kuerzer"),
kind="default", width=BTN_W_SOAP,
tooltip="Krankengeschichte kürzer fassen",
palette=p,
))
btn_kuerz.pack(side="left", padx=(0, 8))
app.btn_kg_kuerzer = btn_kuerz
btn_ausf = self._register_pill(PillButton(
act, "Ausführlicher",
command=lambda: _safe_call(app, "_kg_ausfuehrlicher"),
kind="default", width=BTN_W_SOAP,
tooltip="Krankengeschichte ausführlicher gestalten",
palette=p,
))
btn_ausf.pack(side="left", padx=(0, 8))
app.btn_kg_ausfuehrlicher = btn_ausf
btn_vor = self._register_pill(PillButton(
act, "Vorlage",
command=lambda: _safe_call(app, "_open_kg_vorlage"),
kind="default", width=BTN_W_SOAP,
tooltip="Vorlage für KG-Erstellung bearbeiten",
palette=p,
))
btn_vor.pack(side="left")
app.btn_kg_vorlage = btn_vor
def _build_documents_section(self):
app = self.app
p = self._palette
parent = self._content
wrap = tk.Frame(parent, bg=p["BG"])
wrap.pack(side="top", fill="x", padx=18, pady=(12, 4))
self._shell_section_frames.append(wrap)
doc_lbl = tk.Label(wrap, text="Dokumente", bg=p["BG"], fg=p["TEXT"],
font=FONT_SECTION)
doc_lbl.pack(anchor="w")
self._shell_labels.append(doc_lbl)
grid = tk.Frame(wrap, bg=p["BG"])
grid.pack(fill="x", pady=(6, 0))
self._shell_section_frames.append(grid)
items = [
("Brief", "open_brief_window"),
("Rezept", "open_rezept_window"),
("OP-Bericht", "open_op_bericht_window"),
("KOGU", "open_kogu_window"),
("Diskussion mit KI", "open_diskussion_window"),
("Arztzeugnis", "_open_arztzeugnis"),
("KI-Kontrolle", "open_ki_pruefen"),
("Korrektur", "open_pruefen_window"),
]
cols = 4
for i, (label, method) in enumerate(items):
r, c = divmod(i, cols)
b = self._register_pill(PillButton(
grid, label,
command=(lambda m=method: _safe_call(app, m)),
kind="default", width=BTN_W_DOC, palette=p,
tooltip=f"{label} öffnen",
))
b.grid(row=r, column=c,
padx=(0 if c == 0 else 8), pady=4, sticky="w")
def _build_footer(self):
app = self.app
p = self._palette
self._sep_bottom = tk.Frame(app, bg=p["BORDER"], height=1)
self._sep_bottom.pack(side="bottom", fill="x")
self._footer_bar = tk.Frame(app, bg=p["BG"])
self._footer_bar.pack(side="bottom", fill="x", padx=18, pady=12)
self._logo_img = _load_logo(LOGO_PX)
self._footer_logo_lbl = None
if self._logo_img is not None:
self._footer_logo_lbl = tk.Label(
self._footer_bar, image=self._logo_img, bg=p["BG"],
bd=0, highlightthickness=0,
)
self._footer_logo_lbl.image = self._logo_img
self._footer_logo_lbl.pack(side="left", padx=(0, 12))
tb = tk.Frame(self._footer_bar, bg=p["BG"])
tb.pack(side="left", anchor="w")
self._footer_tb = tb
self._footer_brand_title = tk.Label(
tb, text="AzA von Arzt zu Arzt", bg=p["BG"],
fg=p["TEXT_STRONG"], font=FONT_BRAND,
)
self._footer_brand_title.pack(anchor="w")
self._footer_brand_sub = tk.Label(
tb, text="Informatik zu fairen Preisen", bg=p["BG"],
fg=p["SUBTLE"], font=FONT_BRAND_SUB,
)
self._footer_brand_sub.pack(anchor="w")
def _periodic_license_refresh(self):
try:
self._update_license_label()
finally:
try:
self.app.after(15000, self._periodic_license_refresh)
except Exception:
pass
def _update_license_label(self):
if not self._license_lbl:
return
p = self._palette
try:
mode = getattr(self.app, "license_mode", "demo")
self._license_lbl.configure(
text=("Lizenz: aktiv" if mode == "active" else "Demo / Testversion"),
fg=p["ACCENT"] if mode == "active" else p["WARN"],
bg=p["SURFACE"],
)
except Exception:
pass
def _enforce_default_fonts(self):
for attr in ("txt_output", "txt_transcript"):
w = getattr(self.app, attr, None)
if w is None:
continue
try:
w.configure(font=FONT_DEFAULT)
except Exception:
pass
def apply_office_shell_v1(app) -> None:
"""Wendet die AzA Office Hülle V1.2 auf eine bestehende ``KGDesktopApp`` an.
Der Funktionsname bleibt aus Kompatibilität zu ``basis14.py`` erhalten.
"""
if getattr(app, "_aza_office_v1_installed", False):
return
try:
shell = _OfficeShellV12(app)
shell.install()
app._aza_office_v1 = shell
app._aza_office_v1_installed = True
except Exception as exc:
print(f"[OfficeV1.2] Installation fehlgeschlagen: {exc}")
import traceback
traceback.print_exc()
__all__ = ["apply_office_shell_v1"]