1545 lines
51 KiB
Python
1545 lines
51 KiB
Python
# -*- coding: utf-8 -*-
|
||
"""
|
||
AzA Office Hülle V1.2
|
||
======================
|
||
|
||
Aufbauend auf V1.1:
|
||
|
||
* Keine feste linke Arbeitsoptionen-Leiste mehr — mehr Platz für den Inhalt.
|
||
* **Zahnrad** oben rechts öffnet/schließt ein kompaktes Einstellungs-Popup
|
||
(gleiche Akzentfarbe wie Start/Korrigieren/Diktat).
|
||
* Popup: Rechtsklick-Einfügen, Kommentare anzeigen, **Chat-Empfang**
|
||
(Auto-Option + einmaliges Öffnen/Anheben des Empfang-Fensters über
|
||
``_send_to_empfang``).
|
||
* **Erscheinungsbild**: dieselbe **Transparenz-Logik** wie in der klassischen
|
||
Hauptfenster-Kopfzeile (``_opacity_var_main``, ``MIN_OPACITY``, ``save_opacity``)
|
||
sowie Zugriff auf **alle Einstellungen** (``_open_settings``). Die Farbpalette
|
||
der Office-Hülle folgt der beim Start geladenen Hell/Dunkel-Präferenz;
|
||
Umschalten nur noch über das klassische Einstellungsfenster, falls dort angeboten.
|
||
* Footer-Branding und Logo ca. **30 % größer** als in V1.1.
|
||
|
||
Technische Strategie
|
||
--------------------
|
||
|
||
Nach ``_build_ui`` blendet die Hülle die alten Hauptfenster-Kinder aus und
|
||
baut die Office-Oberfläche neu auf. Bestehende App-Methoden und Variablen
|
||
aus ``KGDesktopApp`` werden verdrahtet.
|
||
"""
|
||
|
||
from __future__ import annotations
|
||
|
||
import json
|
||
import os
|
||
import sys
|
||
import tkinter as tk
|
||
from tkinter import ttk
|
||
from tkinter.scrolledtext import ScrolledText
|
||
from typing import Callable, Dict, List, Optional
|
||
|
||
try:
|
||
from PIL import Image, ImageTk
|
||
_HAS_PIL = True
|
||
except Exception:
|
||
_HAS_PIL = False
|
||
|
||
try:
|
||
from aza_config import MIN_OPACITY
|
||
except Exception:
|
||
MIN_OPACITY = 0.4
|
||
|
||
try:
|
||
from aza_persistence import load_opacity, save_opacity
|
||
except Exception:
|
||
def load_opacity() -> float:
|
||
return 1.0
|
||
|
||
def save_opacity(_v: float) -> None:
|
||
pass
|
||
|
||
|
||
PREFS_FILENAME = "aza_office_shell_v11_prefs.json"
|
||
|
||
FF = "Segoe UI"
|
||
FONT_DEFAULT = (FF, 9)
|
||
FONT_BOLD = (FF, 9, "bold")
|
||
FONT_SECTION = (FF, 10, "bold")
|
||
FONT_BRAND = (FF, 18, "bold") # V1.1: 14 → +~29 %
|
||
FONT_BRAND_SUB = (FF, 12) # V1.1: 9 → +33 %
|
||
|
||
BTN_W_HEADER = 110
|
||
BTN_W_ACTION = 130
|
||
BTN_W_DOC = 150
|
||
BTN_W_SOAP = 158
|
||
BTN_H = 32
|
||
|
||
LOGO_PX = 57 # V1.1: 44 → ×1.295 ≈ +30 %
|
||
|
||
# Popover = gleiche Farbe wie Primärbuttons (ACCENT aus Palette)
|
||
POPOVER_TRACK = "#3D6F8D"
|
||
POPOVER_KNOB = "#FFFFFF"
|
||
POPOVER_KNOB_RING = "#E2EEF6"
|
||
|
||
PALETTE_LIGHT: Dict[str, str] = {
|
||
"BG": "#EAF2F7",
|
||
"SURFACE": "#FFFFFF",
|
||
"SURFACE_ALT": "#F4F8FB",
|
||
"BORDER": "#D6E2EB",
|
||
"TEXT": "#1A4D6D",
|
||
"TEXT_STRONG": "#0F3850",
|
||
"SUBTLE": "#5C7A8E",
|
||
"ACCENT": "#5B8DB3",
|
||
"ACCENT_HOVER": "#4A7A9E",
|
||
"ACCENT_PRESSED": "#3A6884",
|
||
"ACCENT_SOFT": "#E2EEF6",
|
||
"WARN": "#C2840F",
|
||
"RECORD": "#C0392B",
|
||
"RECORD_HOVER": "#A6291C",
|
||
"TEXT_AREA_BG": "#FFFFFF",
|
||
"TEXT_AREA_FG": "#1A4D6D",
|
||
}
|
||
|
||
PALETTE_DARK: Dict[str, str] = {
|
||
"BG": "#1B2E36",
|
||
"SURFACE": "#243B45",
|
||
"SURFACE_ALT": "#2D4652",
|
||
"BORDER": "#3D5866",
|
||
"TEXT": "#E8F2F7",
|
||
"TEXT_STRONG": "#FFFFFF",
|
||
"SUBTLE": "#9BB8C9",
|
||
"ACCENT": "#6BA3C4",
|
||
"ACCENT_HOVER": "#5A92B5",
|
||
"ACCENT_PRESSED": "#4A82A6",
|
||
"ACCENT_SOFT": "#2F4F5E",
|
||
"WARN": "#E0B456",
|
||
"RECORD": "#E74C3C",
|
||
"RECORD_HOVER": "#C0392B",
|
||
"TEXT_AREA_BG": "#1E323B",
|
||
"TEXT_AREA_FG": "#EAF4F8",
|
||
}
|
||
|
||
|
||
def _prefs_path() -> str:
|
||
try:
|
||
from aza_config import get_writable_data_dir
|
||
base = get_writable_data_dir()
|
||
except Exception:
|
||
base = os.path.dirname(os.path.abspath(__file__))
|
||
return os.path.join(base, PREFS_FILENAME)
|
||
|
||
|
||
def _load_dark_pref() -> bool:
|
||
try:
|
||
with open(_prefs_path(), encoding="utf-8") as fh:
|
||
data = json.load(fh)
|
||
return bool(data.get("dark_mode", False))
|
||
except Exception:
|
||
return False
|
||
|
||
|
||
def _save_dark_pref(dark: bool) -> None:
|
||
try:
|
||
path = _prefs_path()
|
||
data = {}
|
||
try:
|
||
with open(path, encoding="utf-8") as fh:
|
||
data = json.load(fh)
|
||
except Exception:
|
||
pass
|
||
data["dark_mode"] = dark
|
||
with open(path, "w", encoding="utf-8") as fh:
|
||
json.dump(data, fh, indent=2, ensure_ascii=False)
|
||
except Exception as exc:
|
||
print(f"[OfficeV1.2] Konnte Erscheinungsbild nicht speichern: {exc}")
|
||
|
||
|
||
class PillButton(tk.Canvas):
|
||
"""Pill-Button; Farben aus Palette-Dict."""
|
||
|
||
def __init__(
|
||
self,
|
||
parent,
|
||
text: str,
|
||
command: Optional[Callable] = None,
|
||
*,
|
||
kind: str = "default",
|
||
width: int = BTN_W_ACTION,
|
||
height: int = BTN_H,
|
||
weight: str = "normal",
|
||
tooltip: Optional[str] = None,
|
||
palette: Dict[str, str],
|
||
):
|
||
bg = parent.cget("bg") if hasattr(parent, "cget") else PALETTE_LIGHT["BG"]
|
||
super().__init__(parent, width=width, height=height, bg=bg,
|
||
highlightthickness=0, bd=0, cursor="hand2")
|
||
self._text = text
|
||
self._command = command
|
||
self._kind = kind
|
||
self._btn_w = width
|
||
self._btn_h = height
|
||
self._weight = weight
|
||
self._palette = palette
|
||
self._hover = False
|
||
self._press = False
|
||
self.bind("<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="#1a4d6d", highlightthickness=0,
|
||
bd=0, anchor="w",
|
||
)
|
||
|
||
outer = tk.Frame(pop, bg=acc, padx=14, pady=12)
|
||
outer.pack(fill="both", expand=True)
|
||
|
||
tk.Label(
|
||
outer, text="Arbeitsoptionen",
|
||
bg=acc, fg="white", font=(FF, 10, "bold"),
|
||
).pack(anchor="w", pady=(0, 8))
|
||
|
||
if getattr(app, "_rclick_paste_var", None) is None:
|
||
app._rclick_paste_var = tk.BooleanVar(master=app, value=True)
|
||
if getattr(app, "_kommentare_auto_var", None) is None:
|
||
app._kommentare_auto_var = tk.BooleanVar(master=app, value=False)
|
||
if getattr(app, "_empfang_auto_var", None) is None:
|
||
app._empfang_auto_var = tk.BooleanVar(master=app, value=False)
|
||
|
||
tk.Checkbutton(
|
||
outer, text="Rechtsklick = Einfügen",
|
||
variable=app._rclick_paste_var,
|
||
command=lambda: _safe_call(app, "_toggle_rclick_paste"),
|
||
**pad,
|
||
).pack(fill="x", pady=3)
|
||
|
||
tk.Checkbutton(
|
||
outer, text="Kommentare anzeigen",
|
||
variable=app._kommentare_auto_var,
|
||
command=lambda: _safe_call(app, "_toggle_kommentare_auto"),
|
||
**pad,
|
||
).pack(fill="x", pady=3)
|
||
|
||
tk.Checkbutton(
|
||
outer, text="Chat-Empfang",
|
||
variable=app._empfang_auto_var,
|
||
command=self._on_chat_empfang_toggle,
|
||
**pad,
|
||
).pack(fill="x", pady=3)
|
||
|
||
tk.Frame(outer, bg=acc, height=8).pack()
|
||
|
||
tk.Label(
|
||
outer, text="Erscheinungsbild",
|
||
bg=acc, fg="white", font=(FF, 10, "bold"),
|
||
).pack(anchor="w", pady=(4, 6))
|
||
|
||
tk.Label(
|
||
outer,
|
||
text="Fenster-Transparenz (wie Hauptfenster)",
|
||
bg=acc, fg="#E2EEF6", font=FONT_DEFAULT,
|
||
).pack(anchor="w")
|
||
|
||
ov = getattr(app, "_opacity_var_main", None)
|
||
if ov is None:
|
||
ov = tk.DoubleVar(master=app, value=round(load_opacity() * 100))
|
||
app._opacity_var_main = ov
|
||
|
||
row_op = tk.Frame(outer, bg=acc)
|
||
row_op.pack(fill="x", pady=(4, 2))
|
||
|
||
lbl_half = tk.Label(
|
||
row_op, text="◐", font=("Segoe UI Symbol", 14),
|
||
bg=acc, fg="white", cursor="hand2",
|
||
)
|
||
lbl_half.pack(side="left", padx=(0, 4))
|
||
lbl_half.bind(
|
||
"<Button-1>",
|
||
lambda e: self._apply_opacity_percent_str(str(int(MIN_OPACITY * 100))),
|
||
)
|
||
|
||
sc = ttk.Scale(
|
||
row_op,
|
||
from_=40,
|
||
to=100,
|
||
variable=ov,
|
||
orient="horizontal",
|
||
length=140,
|
||
command=self._apply_opacity_percent_str,
|
||
style="OfficePop.Horizontal.TScale",
|
||
)
|
||
sc.pack(side="left", fill="x", expand=True, padx=(0, 4))
|
||
|
||
lbl_full = tk.Label(
|
||
row_op, text="☀", font=("Segoe UI Symbol", 14),
|
||
bg=acc, fg="white", cursor="hand2",
|
||
)
|
||
lbl_full.pack(side="left")
|
||
lbl_full.bind("<Button-1>", lambda e: self._apply_opacity_percent_str("100"))
|
||
|
||
tk.Label(
|
||
outer,
|
||
text="Office-Hülle hell / dunkel",
|
||
bg=acc, fg="#E2EEF6", font=FONT_DEFAULT,
|
||
).pack(anchor="w", pady=(8, 4))
|
||
|
||
row_th = tk.Frame(outer, bg=acc)
|
||
row_th.pack(fill="x")
|
||
|
||
tk.Label(row_th, text="Hell", bg=acc, fg="white",
|
||
font=FONT_DEFAULT).pack(side="left", padx=(0, 6))
|
||
|
||
def _flip():
|
||
self._toggle_theme_main()
|
||
if self._theme_switch_pop:
|
||
self._theme_switch_pop.set_dark(self._dark_mode)
|
||
|
||
self._theme_switch_pop = PopoverThemeSwitch(
|
||
row_th,
|
||
is_dark=self._dark_mode,
|
||
command=_flip,
|
||
bg_accent=acc,
|
||
)
|
||
self._theme_switch_pop.pack(side="left")
|
||
|
||
tk.Label(row_th, text="Dunkel", bg=acc, fg="white",
|
||
font=FONT_DEFAULT).pack(side="left", padx=(6, 0))
|
||
|
||
tk.Frame(outer, bg=acc, height=6).pack()
|
||
|
||
link = tk.Label(
|
||
outer,
|
||
text="Weitere Einstellungen …",
|
||
bg=acc, fg="white", font=(FF, 9, "underline"),
|
||
cursor="hand2",
|
||
)
|
||
link.pack(anchor="w", pady=(4, 0))
|
||
link.bind("<Button-1>", lambda e: (_safe_call(app, "_open_settings"), self._destroy_settings_pop()))
|
||
|
||
pop.update_idletasks()
|
||
w_req = max(outer.winfo_reqwidth() + 28, 280)
|
||
h_req = outer.winfo_reqheight() + 24
|
||
|
||
if self._gear_btn is not None:
|
||
self._gear_btn.update_idletasks()
|
||
gx = self._gear_btn.winfo_rootx()
|
||
gy = self._gear_btn.winfo_rooty()
|
||
gh = self._gear_btn.winfo_height()
|
||
gw = self._gear_btn.winfo_width()
|
||
sw = pop.winfo_screenwidth()
|
||
sh = pop.winfo_screenheight()
|
||
px = min(max(8, gx + gw - w_req), sw - w_req - 8)
|
||
py = gy + gh + 6
|
||
if py + h_req > sh - 8:
|
||
py = max(8, gy - h_req - 6)
|
||
else:
|
||
px = app.winfo_rootx() + app.winfo_width() - w_req - 24
|
||
py = app.winfo_rooty() + 72
|
||
|
||
pop.geometry(f"{w_req}x{h_req}+{px}+{py}")
|
||
try:
|
||
pop.overrideredirect(True)
|
||
except Exception:
|
||
pass
|
||
try:
|
||
pop.deiconify()
|
||
pop.lift()
|
||
pop.attributes("-topmost", True)
|
||
pop.after(120, lambda: pop.attributes("-topmost", False))
|
||
except Exception:
|
||
pass
|
||
|
||
# ── Header / Body ─────────────────────────────────────────────────
|
||
|
||
def _build_header(self):
|
||
app = self.app
|
||
p = self._palette
|
||
|
||
self._header_bar = tk.Frame(app, bg=p["SURFACE"], bd=0,
|
||
highlightthickness=0)
|
||
self._header_bar.pack(side="top", fill="x")
|
||
|
||
self._sep_top = tk.Frame(app, bg=p["BORDER"], height=1)
|
||
self._sep_top.pack(side="top", fill="x")
|
||
|
||
self._header_inner = tk.Frame(self._header_bar, bg=p["SURFACE"])
|
||
self._header_inner.pack(fill="x", padx=18, pady=10)
|
||
|
||
left = tk.Frame(self._header_inner, bg=p["SURFACE"])
|
||
left.pack(side="left")
|
||
|
||
self._record_btn = self._register_pill(PillButton(
|
||
left, "⏺ Start",
|
||
command=lambda: _safe_call(app, "toggle_record"),
|
||
kind="primary", width=BTN_W_ACTION, weight="bold",
|
||
tooltip="Aufnahme starten / stoppen (Transkription)",
|
||
palette=p,
|
||
))
|
||
self._record_btn.pack(side="left", padx=(0, 8))
|
||
|
||
self._korrigieren_btn = self._register_pill(PillButton(
|
||
left, "⏺ Korrigieren",
|
||
command=lambda: _safe_call(app, "_toggle_record_append"),
|
||
kind="primary", width=BTN_W_ACTION, weight="bold",
|
||
tooltip="Korrektur-/Append-Aufnahme",
|
||
palette=p,
|
||
))
|
||
self._korrigieren_btn.pack(side="left", padx=(0, 8))
|
||
|
||
self._diktat_btn = self._register_pill(PillButton(
|
||
left, "Diktat",
|
||
command=lambda: _safe_call(app, "open_diktat_window"),
|
||
kind="primary", width=BTN_W_ACTION, weight="bold",
|
||
tooltip="Diktatfenster öffnen",
|
||
palette=p,
|
||
))
|
||
self._diktat_btn.pack(side="left")
|
||
|
||
right = tk.Frame(self._header_inner, bg=p["SURFACE"])
|
||
right.pack(side="right")
|
||
|
||
self._license_lbl = tk.Label(
|
||
right, text="Lizenz prüfen …", font=FONT_DEFAULT,
|
||
bg=p["SURFACE"], fg=p["SUBTLE"], cursor="hand2", padx=8, pady=4,
|
||
)
|
||
self._license_lbl.pack(side="left", padx=(0, 12))
|
||
self._license_lbl.bind(
|
||
"<Button-1>",
|
||
lambda e: _safe_call(app, "_show_activation_dialog"),
|
||
)
|
||
|
||
self._register_pill(PillButton(
|
||
right, "Profil",
|
||
command=lambda: _safe_call(app, "_show_profile_editor"),
|
||
kind="ghost", width=BTN_W_HEADER, palette=p,
|
||
tooltip="Profil bearbeiten",
|
||
)).pack(side="left", padx=(0, 6))
|
||
|
||
self._register_pill(PillButton(
|
||
right, "Aktivierung",
|
||
command=lambda: _safe_call(app, "_show_activation_dialog"),
|
||
kind="ghost", width=BTN_W_HEADER, palette=p,
|
||
tooltip="Aktivierungsdialog öffnen",
|
||
)).pack(side="left")
|
||
|
||
def _build_status_row(self):
|
||
app = self.app
|
||
p = self._palette
|
||
self._status_row_fr = tk.Frame(app, bg=p["BG"])
|
||
self._status_row_fr.pack(side="top", fill="x", padx=18, pady=(8, 4))
|
||
|
||
var = getattr(app, "status_var", None)
|
||
if var is None:
|
||
var = tk.StringVar(master=app, value="Bereit.")
|
||
app.status_var = var
|
||
|
||
lbl = tk.Label(self._status_row_fr, textvariable=var, bg=p["BG"],
|
||
fg=p["SUBTLE"], font=FONT_DEFAULT, anchor="w")
|
||
lbl.pack(side="left")
|
||
app.lbl_status = lbl
|
||
|
||
def _build_main_fill(self):
|
||
app = self.app
|
||
p = self._palette
|
||
self._main_fill = tk.Frame(app, bg=p["BG"])
|
||
self._main_fill.pack(side="top", fill="both", expand=True)
|
||
|
||
self._sidebar = tk.Frame(self._main_fill, bg=p["ACCENT"], width=220)
|
||
self._sidebar.pack(side="left", fill="y")
|
||
self._sidebar.pack_propagate(False)
|
||
|
||
self._build_sidebar_content()
|
||
|
||
self._content = tk.Frame(self._main_fill, bg=p["BG"])
|
||
self._content.pack(side="left", fill="both", expand=True)
|
||
|
||
self._shell_section_frames = []
|
||
|
||
self._build_transcript_section()
|
||
self._build_kg_section()
|
||
self._build_soap_section()
|
||
self._build_documents_section()
|
||
|
||
def _build_sidebar_content(self):
|
||
app = self.app
|
||
acc = self._palette["ACCENT"]
|
||
bar = self._sidebar
|
||
self._theme_switch_pop = None
|
||
for w in list(bar.winfo_children()):
|
||
try:
|
||
w.destroy()
|
||
except Exception:
|
||
pass
|
||
|
||
cb_pad = dict(
|
||
bg=acc, fg="white", font=FONT_DEFAULT,
|
||
activebackground=acc, activeforeground="white",
|
||
selectcolor="#1a4d6d", highlightthickness=0,
|
||
bd=0, anchor="w",
|
||
)
|
||
|
||
if getattr(app, "_rclick_paste_var", None) is None:
|
||
app._rclick_paste_var = tk.BooleanVar(master=app, value=True)
|
||
if getattr(app, "_kommentare_auto_var", None) is None:
|
||
app._kommentare_auto_var = tk.BooleanVar(master=app, value=False)
|
||
if getattr(app, "_empfang_auto_var", None) is None:
|
||
app._empfang_auto_var = tk.BooleanVar(master=app, value=False)
|
||
|
||
# ── 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)
|
||
|
||
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"),
|
||
)
|
||
|
||
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.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"]
|