1118 lines
41 KiB
Python
1118 lines
41 KiB
Python
|
|
# -*- coding: utf-8 -*-
|
|||
|
|
"""
|
|||
|
|
AzA Desktop-Hülle
|
|||
|
|
=================
|
|||
|
|
|
|||
|
|
Saubere, professionelle Hauptoberfläche für AzA Office.
|
|||
|
|
|
|||
|
|
Diese Hülle ersetzt die alte, chaotische Toolbar von ``KGDesktopApp``
|
|||
|
|
(``basis14.py``) durch eine ruhige, medizinisch-saubere Layout-Schicht
|
|||
|
|
im Stil der Empfang-Oberfläche.
|
|||
|
|
|
|||
|
|
Was die Hülle macht
|
|||
|
|
-------------------
|
|||
|
|
|
|||
|
|
* Versteckt die alte obere Button-Leiste (``top``-Frame mit dem
|
|||
|
|
Mix aus 👤/🔑/⊞/↺/Token-Anzeige/Slider/Status-Pills usw.).
|
|||
|
|
* Versteckt das doppelte Logo unten links (``_logo_frame``).
|
|||
|
|
* Versteckt den "Ablauf:"-Text unten (``_bottom_frame``).
|
|||
|
|
* Versteckt die alte Addon-/Modul-Spalte mit "An Empfang senden"
|
|||
|
|
– dieser Button wird stattdessen prominent im neuen Header platziert.
|
|||
|
|
* Baut eine neue klare Kopfzeile mit Logo + "AzA von Arzt zu Arzt" +
|
|||
|
|
"Informatik zu fairen Preisen" (oben links).
|
|||
|
|
* Baut eine neue Aktionsleiste mit einheitlichen Pill-Buttons:
|
|||
|
|
Aufnahme, Korrigieren, Audio importieren, Diktat, Notizen, Neu,
|
|||
|
|
An Empfang senden, Profil, Status, Hell/Dunkel, A−/A+, Transkript.
|
|||
|
|
* Bindet die zustandsbehafteten Knöpfe (Start/Stopp-Texte) so um,
|
|||
|
|
dass die bestehende Aufnahme-Logik (``toggle_record`` etc.) ohne
|
|||
|
|
Änderung weiter funktioniert.
|
|||
|
|
* Wendet ein ruhiges hell- und ein dezentes dunkel-Theme an.
|
|||
|
|
* Klappt das Transkript beim Start ein.
|
|||
|
|
* Unterdrückt das Developer-Status-Popup beim Start.
|
|||
|
|
|
|||
|
|
Was die Hülle NICHT macht
|
|||
|
|
-------------------------
|
|||
|
|
|
|||
|
|
* Sie ersetzt KEINE produktive Logik.
|
|||
|
|
* Sie löscht KEINE Methoden, Mixins oder Dialoge.
|
|||
|
|
* Das Profil-Bearbeiten-Fenster (``_show_profile_editor``) bleibt 1:1.
|
|||
|
|
* KG-Editor, Transkript, SOAP, Dokumente, Textblöcke, Backend-Anbindung
|
|||
|
|
und alle Submodule (E-Mail, WhatsApp, Übersetzer, MedWork, ...)
|
|||
|
|
bleiben unverändert nutzbar.
|
|||
|
|
"""
|
|||
|
|
|
|||
|
|
from __future__ import annotations
|
|||
|
|
|
|||
|
|
import os
|
|||
|
|
import sys
|
|||
|
|
import tkinter as tk
|
|||
|
|
from tkinter import ttk
|
|||
|
|
from typing import Optional, Callable
|
|||
|
|
|
|||
|
|
try:
|
|||
|
|
from PIL import Image, ImageTk
|
|||
|
|
_HAS_PIL = True
|
|||
|
|
except Exception:
|
|||
|
|
_HAS_PIL = False
|
|||
|
|
|
|||
|
|
try:
|
|||
|
|
from aza_ui_helpers import RoundedButton, add_tooltip
|
|||
|
|
except Exception:
|
|||
|
|
RoundedButton = None # type: ignore
|
|||
|
|
add_tooltip = lambda *a, **k: None # type: ignore
|
|||
|
|
|
|||
|
|
|
|||
|
|
# ─── Empfang-Farbpalette (medizinisch, ruhig, professionell) ─────────────────
|
|||
|
|
|
|||
|
|
# Hell (default): heller Hintergrund, kräftiges aber dezentes Empfang-Blau.
|
|||
|
|
LIGHT = dict(
|
|||
|
|
bg="#EAF2F7", # Hauptfenster-Hintergrund
|
|||
|
|
surface="#FFFFFF", # Karten / Header
|
|||
|
|
surface_alt="#F4F8FB",
|
|||
|
|
border="#D6E2EB",
|
|||
|
|
divider="#E0EAF0",
|
|||
|
|
text="#1A4D6D", # Primärtext
|
|||
|
|
text_strong="#0F3850",
|
|||
|
|
subtle="#5C7A8E",
|
|||
|
|
accent="#5B8DB3", # Empfang-Blau
|
|||
|
|
accent_hover="#4A7A9E",
|
|||
|
|
accent_pressed="#3A6884",
|
|||
|
|
accent_soft="#E2EEF6",
|
|||
|
|
success="#2E8B57",
|
|||
|
|
warn="#C2840F",
|
|||
|
|
danger="#C0392B",
|
|||
|
|
record="#C0392B",
|
|||
|
|
record_active="#A6291C",
|
|||
|
|
canvas_bg="#EAF2F7",
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
# Dunkel: dezent dunkelblau, augenschonend, nicht reines Schwarz.
|
|||
|
|
DARK = dict(
|
|||
|
|
bg="#1F2A33",
|
|||
|
|
surface="#28333E",
|
|||
|
|
surface_alt="#2F3B47",
|
|||
|
|
border="#3A4A57",
|
|||
|
|
divider="#36454F",
|
|||
|
|
text="#E2ECF2",
|
|||
|
|
text_strong="#FFFFFF",
|
|||
|
|
subtle="#9DB3C2",
|
|||
|
|
accent="#7AB0D4",
|
|||
|
|
accent_hover="#92C2E0",
|
|||
|
|
accent_pressed="#5A92B6",
|
|||
|
|
accent_soft="#33414F",
|
|||
|
|
success="#5DC08C",
|
|||
|
|
warn="#E1AB4F",
|
|||
|
|
danger="#E07060",
|
|||
|
|
record="#E07060",
|
|||
|
|
record_active="#FF8B79",
|
|||
|
|
canvas_bg="#1F2A33",
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
_FF = "Segoe UI"
|
|||
|
|
|
|||
|
|
_FONT_BRAND_TITLE = (_FF, 16, "bold")
|
|||
|
|
_FONT_BRAND_SUB = (_FF, 9)
|
|||
|
|
_FONT_HEADER_LABEL = (_FF, 9)
|
|||
|
|
_FONT_FONTSTEP = (_FF, 9)
|
|||
|
|
_FONT_FONTSTEP_BTN = (_FF, 11, "bold")
|
|||
|
|
|
|||
|
|
_PREFS_FILE = "aza_desktop_shell_prefs.json"
|
|||
|
|
|
|||
|
|
# Einheitliche Standard-Maße der Pill-Buttons in der Aktionsleiste.
|
|||
|
|
_BTN_W_NORMAL = 130
|
|||
|
|
_BTN_W_WIDE = 168
|
|||
|
|
_BTN_W_ICON = 38
|
|||
|
|
_BTN_H = 34
|
|||
|
|
|
|||
|
|
|
|||
|
|
# ─── Persistenz für Theme + Schriftgröße ─────────────────────────────────────
|
|||
|
|
|
|||
|
|
def _prefs_path() -> str:
|
|||
|
|
try:
|
|||
|
|
from aza_persistence import get_writable_data_dir
|
|||
|
|
return os.path.join(get_writable_data_dir(), _PREFS_FILE)
|
|||
|
|
except Exception:
|
|||
|
|
return os.path.join(os.path.dirname(os.path.abspath(__file__)), _PREFS_FILE)
|
|||
|
|
|
|||
|
|
|
|||
|
|
def _load_prefs() -> dict:
|
|||
|
|
import json
|
|||
|
|
try:
|
|||
|
|
p = _prefs_path()
|
|||
|
|
if os.path.isfile(p):
|
|||
|
|
with open(p, "r", encoding="utf-8") as f:
|
|||
|
|
data = json.load(f)
|
|||
|
|
if isinstance(data, dict):
|
|||
|
|
return data
|
|||
|
|
except Exception:
|
|||
|
|
pass
|
|||
|
|
return {}
|
|||
|
|
|
|||
|
|
|
|||
|
|
def _save_prefs(prefs: dict) -> None:
|
|||
|
|
import json
|
|||
|
|
try:
|
|||
|
|
with open(_prefs_path(), "w", encoding="utf-8") as f:
|
|||
|
|
json.dump(prefs, f, indent=2, ensure_ascii=False)
|
|||
|
|
except Exception:
|
|||
|
|
pass
|
|||
|
|
|
|||
|
|
|
|||
|
|
# ─── Logo ────────────────────────────────────────────────────────────────────
|
|||
|
|
|
|||
|
|
def _load_logo(size: int) -> Optional[object]:
|
|||
|
|
if not _HAS_PIL:
|
|||
|
|
return None
|
|||
|
|
candidates: list[str] = []
|
|||
|
|
try:
|
|||
|
|
candidates.append(os.path.dirname(os.path.abspath(__file__)))
|
|||
|
|
except Exception:
|
|||
|
|
pass
|
|||
|
|
if getattr(sys, "frozen", False):
|
|||
|
|
try:
|
|||
|
|
candidates.append(os.path.dirname(os.path.abspath(sys.executable)))
|
|||
|
|
except Exception:
|
|||
|
|
pass
|
|||
|
|
meipass = getattr(sys, "_MEIPASS", "")
|
|||
|
|
if meipass:
|
|||
|
|
candidates.append(meipass)
|
|||
|
|
for d in candidates:
|
|||
|
|
if not d:
|
|||
|
|
continue
|
|||
|
|
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
|
|||
|
|
|
|||
|
|
|
|||
|
|
# ─── ttk-Style ───────────────────────────────────────────────────────────────
|
|||
|
|
|
|||
|
|
def _apply_ttk_theme(root: tk.Misc, t: dict) -> None:
|
|||
|
|
"""Sorgt dafür, dass ttk.Frame/PanedWindow/Label im Hauptfenster sauber
|
|||
|
|
in den neuen Hintergrund integriert sind."""
|
|||
|
|
try:
|
|||
|
|
style = ttk.Style(root)
|
|||
|
|
try:
|
|||
|
|
style.theme_use("clam")
|
|||
|
|
except tk.TclError:
|
|||
|
|
pass
|
|||
|
|
style.configure("TFrame", background=t["bg"])
|
|||
|
|
style.configure("TPanedwindow", background=t["bg"])
|
|||
|
|
try:
|
|||
|
|
style.configure("TPanedwindow.Sash", background=t["divider"], width=8)
|
|||
|
|
except tk.TclError:
|
|||
|
|
pass
|
|||
|
|
style.configure("TLabel", background=t["bg"], foreground=t["text"])
|
|||
|
|
style.configure("TopBar.TFrame", background=t["surface"])
|
|||
|
|
style.configure("StatusBar.TFrame", background=t["bg"])
|
|||
|
|
style.configure("TranscriptBar.TFrame", background=t["bg"])
|
|||
|
|
style.configure(
|
|||
|
|
"TButton",
|
|||
|
|
background=t["accent"], foreground="white",
|
|||
|
|
padding=(10, 6), borderwidth=0,
|
|||
|
|
)
|
|||
|
|
style.map(
|
|||
|
|
"TButton",
|
|||
|
|
background=[("active", t["accent_hover"]),
|
|||
|
|
("pressed", t["accent_pressed"])],
|
|||
|
|
)
|
|||
|
|
except Exception:
|
|||
|
|
pass
|
|||
|
|
|
|||
|
|
|
|||
|
|
# ─── Pill-Button im neuen Stil ───────────────────────────────────────────────
|
|||
|
|
|
|||
|
|
class ShellButton(tk.Canvas):
|
|||
|
|
"""Pill-Button mit einheitlicher Höhe für die neue Hülle.
|
|||
|
|
|
|||
|
|
API ist absichtlich kompatibel zu ``RoundedButton``, damit zustands-
|
|||
|
|
behaftete Knöpfe (z. B. ``self.btn_record``) nahtlos getauscht werden
|
|||
|
|
können: ``configure(text=...)``, ``set_text``, ``set_font_size_scale``,
|
|||
|
|
``set_button_size_scale`` werden unterstützt.
|
|||
|
|
"""
|
|||
|
|
|
|||
|
|
def __init__(self, parent, text: str, command: Optional[Callable] = None,
|
|||
|
|
*, theme: dict, kind: str = "default",
|
|||
|
|
width: int = _BTN_W_NORMAL, height: int = _BTN_H,
|
|||
|
|
radius: int = 8, tooltip: Optional[str] = None,
|
|||
|
|
font_weight: str = "normal"):
|
|||
|
|
bg = parent.cget("bg") if hasattr(parent, "cget") else theme["surface"]
|
|||
|
|
super().__init__(parent, width=width, height=height,
|
|||
|
|
bg=bg, highlightthickness=0, bd=0, cursor="hand2")
|
|||
|
|
self._theme = theme
|
|||
|
|
self._kind = kind
|
|||
|
|
self._radius = radius
|
|||
|
|
self._text = text
|
|||
|
|
self._command = command
|
|||
|
|
self._base_w = width
|
|||
|
|
self._base_h = height
|
|||
|
|
self._scale_w = 1.0
|
|||
|
|
self._scale_f = 1.0
|
|||
|
|
self._font_size_base = 10 if font_weight == "normal" else 10
|
|||
|
|
self._weight = "bold" if font_weight == "bold" else "normal"
|
|||
|
|
self._active = False
|
|||
|
|
self._pressed = 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:
|
|||
|
|
add_tooltip(self, tooltip)
|
|||
|
|
except Exception:
|
|||
|
|
pass
|
|||
|
|
self._draw()
|
|||
|
|
|
|||
|
|
# ── Farbschema je Variante ────────────────────────────────────────────
|
|||
|
|
|
|||
|
|
def _colors(self):
|
|||
|
|
t = self._theme
|
|||
|
|
if self._kind == "primary":
|
|||
|
|
bg = t["accent_pressed"] if self._pressed else (
|
|||
|
|
t["accent_hover"] if self._active else t["accent"])
|
|||
|
|
return bg, "white", bg
|
|||
|
|
if self._kind == "danger":
|
|||
|
|
bg = t["record_active"] if (self._active or self._pressed) else t["record"]
|
|||
|
|
return bg, "white", bg
|
|||
|
|
if self._kind == "ghost":
|
|||
|
|
bg = t["accent_soft"] if (self._active or self._pressed) else t["surface"]
|
|||
|
|
border = t["accent"] if (self._active or self._pressed) else t["border"]
|
|||
|
|
return bg, t["text"], border
|
|||
|
|
if self._kind == "icon":
|
|||
|
|
bg = t["accent_soft"] if (self._active or self._pressed) else t["surface"]
|
|||
|
|
border = t["accent"] if (self._active or self._pressed) else t["border"]
|
|||
|
|
return bg, t["text"], border
|
|||
|
|
bg = t["accent_soft"] if (self._active or self._pressed) else t["surface_alt"]
|
|||
|
|
border = t["accent"] if (self._active or self._pressed) else t["border"]
|
|||
|
|
return bg, t["text"], border
|
|||
|
|
|
|||
|
|
def _draw(self):
|
|||
|
|
try:
|
|||
|
|
self.delete("all")
|
|||
|
|
w = max(int(self._base_w * self._scale_w), int(self.winfo_width()))
|
|||
|
|
h = max(int(self._base_h * self._scale_w), int(self.winfo_height()))
|
|||
|
|
if w <= 1:
|
|||
|
|
w = int(self._base_w * self._scale_w)
|
|||
|
|
if h <= 1:
|
|||
|
|
h = int(self._base_h * self._scale_w)
|
|||
|
|
fill, fg, border = self._colors()
|
|||
|
|
r = max(2, min(self._radius, h // 2))
|
|||
|
|
self._round_rect(0, 0, w, h, r, fill=fill, outline=border)
|
|||
|
|
font_size = max(7, int(self._font_size_base * self._scale_f))
|
|||
|
|
self.create_text(
|
|||
|
|
w // 2, h // 2, text=self._text, fill=fg,
|
|||
|
|
font=(_FF, font_size, 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)
|
|||
|
|
|
|||
|
|
# ── Events ────────────────────────────────────────────────────────────
|
|||
|
|
|
|||
|
|
def _on_enter(self, _e=None):
|
|||
|
|
self._active = True
|
|||
|
|
self._draw()
|
|||
|
|
|
|||
|
|
def _on_leave(self, _e=None):
|
|||
|
|
self._active = False
|
|||
|
|
self._pressed = False
|
|||
|
|
self._draw()
|
|||
|
|
|
|||
|
|
def _on_press(self, _e=None):
|
|||
|
|
self._pressed = True
|
|||
|
|
self._draw()
|
|||
|
|
|
|||
|
|
def _on_release(self, _e=None):
|
|||
|
|
was_pressed = self._pressed
|
|||
|
|
self._pressed = False
|
|||
|
|
self._draw()
|
|||
|
|
if was_pressed and self._command:
|
|||
|
|
try:
|
|||
|
|
self._command()
|
|||
|
|
except Exception as exc:
|
|||
|
|
print(f"[Shell] Aktion '{self._text}' fehlgeschlagen: {exc}")
|
|||
|
|
|
|||
|
|
# ── Kompatibilitäts-API zu RoundedButton ──────────────────────────────
|
|||
|
|
|
|||
|
|
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 "bg" in kw or "background" in kw:
|
|||
|
|
try:
|
|||
|
|
super().configure(bg=kw.pop("bg", kw.pop("background", None)))
|
|||
|
|
except Exception:
|
|||
|
|
pass
|
|||
|
|
if kw:
|
|||
|
|
try:
|
|||
|
|
super().configure(**kw)
|
|||
|
|
except Exception:
|
|||
|
|
pass
|
|||
|
|
|
|||
|
|
config = configure
|
|||
|
|
|
|||
|
|
def set_text(self, text: str):
|
|||
|
|
self.configure(text=text)
|
|||
|
|
|
|||
|
|
def set_font_size_scale(self, scale: float):
|
|||
|
|
self._scale_f = max(0.4, min(2.0, float(scale)))
|
|||
|
|
self._draw()
|
|||
|
|
|
|||
|
|
def set_button_size_scale(self, scale: float):
|
|||
|
|
self._scale_w = max(0.4, min(2.0, float(scale)))
|
|||
|
|
new_w = int(self._base_w * self._scale_w)
|
|||
|
|
new_h = int(self._base_h * self._scale_w)
|
|||
|
|
try:
|
|||
|
|
super().configure(width=new_w, height=new_h)
|
|||
|
|
except Exception:
|
|||
|
|
pass
|
|||
|
|
self._draw()
|
|||
|
|
|
|||
|
|
def set_font_scale(self, scale: float):
|
|||
|
|
self.set_font_size_scale(scale)
|
|||
|
|
self.set_button_size_scale(scale)
|
|||
|
|
|
|||
|
|
def apply_theme(self, theme: dict):
|
|||
|
|
self._theme = theme
|
|||
|
|
try:
|
|||
|
|
super().configure(bg=self.master.cget("bg"))
|
|||
|
|
except Exception:
|
|||
|
|
pass
|
|||
|
|
self._draw()
|
|||
|
|
|
|||
|
|
|
|||
|
|
# ─── Hülle ───────────────────────────────────────────────────────────────────
|
|||
|
|
|
|||
|
|
class _DesktopShell:
|
|||
|
|
def __init__(self, app):
|
|||
|
|
self.app = app
|
|||
|
|
prefs = _load_prefs()
|
|||
|
|
self._dark: bool = bool(prefs.get("dark_mode", False))
|
|||
|
|
self._font_step: int = max(-2, min(4, int(prefs.get("font_step", 0))))
|
|||
|
|
self._theme: dict = DARK.copy() if self._dark else LIGHT.copy()
|
|||
|
|
self._logo_img = None
|
|||
|
|
self._brand_title_lbl: Optional[tk.Label] = None
|
|||
|
|
self._brand_sub_lbl: Optional[tk.Label] = None
|
|||
|
|
self._logo_lbl: Optional[tk.Label] = None
|
|||
|
|
self._header_frame: Optional[tk.Frame] = None
|
|||
|
|
self._action_frame: Optional[tk.Frame] = None
|
|||
|
|
self._theme_btn: Optional[ShellButton] = None
|
|||
|
|
self._transcript_btn: Optional[ShellButton] = None
|
|||
|
|
self._font_label: Optional[tk.Label] = None
|
|||
|
|
self._all_shell_buttons: list[ShellButton] = []
|
|||
|
|
self._connection_dot: Optional[tk.Canvas] = None
|
|||
|
|
self._connection_lbl: Optional[tk.Label] = None
|
|||
|
|
self._chk_widgets: list = []
|
|||
|
|
|
|||
|
|
# ── Public ───────────────────────────────────────────────────────────
|
|||
|
|
|
|||
|
|
def install(self) -> None:
|
|||
|
|
app = self.app
|
|||
|
|
try:
|
|||
|
|
app.title(self._title())
|
|||
|
|
except Exception:
|
|||
|
|
pass
|
|||
|
|
|
|||
|
|
self._hide_legacy_chrome()
|
|||
|
|
self._restyle_window()
|
|||
|
|
self._build_header()
|
|||
|
|
self._build_action_bar()
|
|||
|
|
self._reorder_main_layout()
|
|||
|
|
self._restyle_inner_panels()
|
|||
|
|
self._suppress_dev_status_popup()
|
|||
|
|
self._collapse_transcript_default()
|
|||
|
|
self._mirror_backend_status()
|
|||
|
|
self._apply_font_step(self._font_step, persist=False)
|
|||
|
|
|
|||
|
|
# ── Titel / Theme ────────────────────────────────────────────────────
|
|||
|
|
|
|||
|
|
def _title(self) -> str:
|
|||
|
|
try:
|
|||
|
|
from aza_version import APP_VERSION
|
|||
|
|
return f"AzA Office (v{APP_VERSION})"
|
|||
|
|
except Exception:
|
|||
|
|
return "AzA Office"
|
|||
|
|
|
|||
|
|
def _restyle_window(self):
|
|||
|
|
app = self.app
|
|||
|
|
t = self._theme
|
|||
|
|
try:
|
|||
|
|
app.configure(bg=t["bg"])
|
|||
|
|
except Exception:
|
|||
|
|
pass
|
|||
|
|
_apply_ttk_theme(app, t)
|
|||
|
|
|
|||
|
|
# ── Altes Chrome verstecken ──────────────────────────────────────────
|
|||
|
|
|
|||
|
|
def _hide_legacy_chrome(self):
|
|||
|
|
"""Versteckt die alte chaotische Toolbar, das Bottom-Logo und die
|
|||
|
|
Modul-Spalte mit dem alten 'An Empfang senden'-Button."""
|
|||
|
|
app = self.app
|
|||
|
|
|
|||
|
|
# Alte obere Toolbar (top-Frame) – komplett verstecken.
|
|||
|
|
try:
|
|||
|
|
top = getattr(app, "_btn_row_left", None)
|
|||
|
|
if top is not None and top.master is not None:
|
|||
|
|
top.master.pack_forget()
|
|||
|
|
except Exception:
|
|||
|
|
pass
|
|||
|
|
|
|||
|
|
# Status-Zeile bleibt sichtbar, wird aber später neu eingeordnet.
|
|||
|
|
# Bottom-Frame ("Ablauf:"-Erklärtext) entfernen – Information ist
|
|||
|
|
# in der neuen Hülle implizit.
|
|||
|
|
try:
|
|||
|
|
if getattr(app, "_bottom_frame", None) is not None:
|
|||
|
|
app._bottom_frame.pack_forget()
|
|||
|
|
except Exception:
|
|||
|
|
pass
|
|||
|
|
|
|||
|
|
# Doppeltes Branding-Logo unten ausblenden.
|
|||
|
|
try:
|
|||
|
|
if getattr(app, "_logo_frame", None) is not None:
|
|||
|
|
try:
|
|||
|
|
app._logo_frame.place_forget()
|
|||
|
|
except Exception:
|
|||
|
|
app._logo_frame.pack_forget()
|
|||
|
|
except Exception:
|
|||
|
|
pass
|
|||
|
|
|
|||
|
|
# "Weitere Module" – die Empfang-Aktion wird in den neuen Header
|
|||
|
|
# gehoben. Die Spalte selbst bleibt funktional erhalten, der
|
|||
|
|
# Empfang-Knopf wird aber dort entfernt, um Doppelaktionen
|
|||
|
|
# zu vermeiden.
|
|||
|
|
rows = getattr(app, "_addon_button_rows", None)
|
|||
|
|
if isinstance(rows, dict) and "empfang" in rows:
|
|||
|
|
try:
|
|||
|
|
rows["empfang"].pack_forget()
|
|||
|
|
except Exception:
|
|||
|
|
pass
|
|||
|
|
|
|||
|
|
# ── Header ───────────────────────────────────────────────────────────
|
|||
|
|
|
|||
|
|
def _build_header(self):
|
|||
|
|
app = self.app
|
|||
|
|
t = self._theme
|
|||
|
|
|
|||
|
|
header = tk.Frame(app, bg=t["surface"], bd=0,
|
|||
|
|
highlightthickness=0)
|
|||
|
|
header.pack(side="top", fill="x", before=app.winfo_children()[0])
|
|||
|
|
self._header_frame = header
|
|||
|
|
|
|||
|
|
# untere Trennlinie
|
|||
|
|
sep = tk.Frame(app, bg=t["border"], height=1)
|
|||
|
|
sep.pack(side="top", fill="x", before=app.winfo_children()[1])
|
|||
|
|
self._header_sep = sep
|
|||
|
|
|
|||
|
|
inner = tk.Frame(header, bg=t["surface"])
|
|||
|
|
inner.pack(fill="x", padx=18, pady=10)
|
|||
|
|
|
|||
|
|
# Links: Branding
|
|||
|
|
brand = tk.Frame(inner, bg=t["surface"])
|
|||
|
|
brand.pack(side="left")
|
|||
|
|
|
|||
|
|
self._logo_img = _load_logo(size=44)
|
|||
|
|
if self._logo_img is not None:
|
|||
|
|
self._logo_lbl = tk.Label(
|
|||
|
|
brand, image=self._logo_img, bg=t["surface"],
|
|||
|
|
bd=0, highlightthickness=0,
|
|||
|
|
)
|
|||
|
|
self._logo_lbl.pack(side="left", padx=(0, 12))
|
|||
|
|
|
|||
|
|
text_block = tk.Frame(brand, bg=t["surface"])
|
|||
|
|
text_block.pack(side="left", anchor="w")
|
|||
|
|
|
|||
|
|
self._brand_title_lbl = tk.Label(
|
|||
|
|
text_block, text="AzA von Arzt zu Arzt",
|
|||
|
|
font=_FONT_BRAND_TITLE, bg=t["surface"], fg=t["text_strong"],
|
|||
|
|
anchor="w",
|
|||
|
|
)
|
|||
|
|
self._brand_title_lbl.pack(anchor="w")
|
|||
|
|
|
|||
|
|
self._brand_sub_lbl = tk.Label(
|
|||
|
|
text_block, text="Informatik zu fairen Preisen",
|
|||
|
|
font=_FONT_BRAND_SUB, bg=t["surface"], fg=t["subtle"],
|
|||
|
|
anchor="w",
|
|||
|
|
)
|
|||
|
|
self._brand_sub_lbl.pack(anchor="w")
|
|||
|
|
|
|||
|
|
# Rechts: Verbindung + Kontoaktionen
|
|||
|
|
right = tk.Frame(inner, bg=t["surface"])
|
|||
|
|
right.pack(side="right")
|
|||
|
|
|
|||
|
|
# Verbindungs-Indikator (nutzt vorhandene App-Variable)
|
|||
|
|
conn_box = tk.Frame(right, bg=t["surface"])
|
|||
|
|
conn_box.pack(side="left", padx=(0, 14))
|
|||
|
|
self._connection_dot = tk.Canvas(
|
|||
|
|
conn_box, width=10, height=10, bg=t["surface"],
|
|||
|
|
highlightthickness=0, bd=0,
|
|||
|
|
)
|
|||
|
|
self._connection_dot.pack(side="left", padx=(0, 6))
|
|||
|
|
self._connection_lbl = tk.Label(
|
|||
|
|
conn_box, text="Verbindung wird geprüft …",
|
|||
|
|
font=_FONT_HEADER_LABEL, bg=t["surface"], fg=t["subtle"],
|
|||
|
|
)
|
|||
|
|
self._connection_lbl.pack(side="left")
|
|||
|
|
|
|||
|
|
self._add_header_btn(
|
|||
|
|
right, "Profil", lambda: self._safe_call(app, "_show_profile_editor"),
|
|||
|
|
tooltip="Profil bearbeiten",
|
|||
|
|
)
|
|||
|
|
self._add_header_btn(
|
|||
|
|
right, "Aktivierung", lambda: self._safe_call(app, "_show_activation_dialog"),
|
|||
|
|
tooltip="Aktivierung verwalten",
|
|||
|
|
)
|
|||
|
|
self._add_header_btn(
|
|||
|
|
right, "Status", lambda: self._safe_call(app, "_open_systemstatus"),
|
|||
|
|
tooltip="Systemstatus und Diagnose",
|
|||
|
|
)
|
|||
|
|
self._add_header_btn(
|
|||
|
|
right, "Einstellungen", lambda: self._safe_call(app, "_open_settings"),
|
|||
|
|
tooltip="Einstellungen öffnen",
|
|||
|
|
)
|
|||
|
|
self._add_header_btn(
|
|||
|
|
right, "Abonnement",
|
|||
|
|
lambda: self._safe_call(app, "_open_billing_portal_from_ui"),
|
|||
|
|
tooltip="Abonnement und Rechnungen verwalten",
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
def _add_header_btn(self, parent, text: str, cmd, *, tooltip: str | None = None):
|
|||
|
|
b = ShellButton(
|
|||
|
|
parent, text, command=cmd, theme=self._theme,
|
|||
|
|
kind="ghost", width=110, height=30, tooltip=tooltip,
|
|||
|
|
)
|
|||
|
|
b.pack(side="left", padx=(0, 6))
|
|||
|
|
self._all_shell_buttons.append(b)
|
|||
|
|
return b
|
|||
|
|
|
|||
|
|
# ── Action-Bar ───────────────────────────────────────────────────────
|
|||
|
|
|
|||
|
|
def _build_action_bar(self):
|
|||
|
|
app = self.app
|
|||
|
|
t = self._theme
|
|||
|
|
|
|||
|
|
bar = tk.Frame(app, bg=t["bg"], bd=0, highlightthickness=0)
|
|||
|
|
# Direkt unter dem Header platzieren (Header + Separator sind als
|
|||
|
|
# erste zwei Kinder gepackt).
|
|||
|
|
bar.pack(side="top", fill="x", padx=18, pady=(10, 6))
|
|||
|
|
self._action_frame = bar
|
|||
|
|
|
|||
|
|
left = tk.Frame(bar, bg=t["bg"])
|
|||
|
|
left.pack(side="left")
|
|||
|
|
|
|||
|
|
right = tk.Frame(bar, bg=t["bg"])
|
|||
|
|
right.pack(side="right")
|
|||
|
|
|
|||
|
|
# Primäre Aufnahme-Aktionen – Re-Bind der zustandsbehafteten Knöpfe.
|
|||
|
|
new_record = ShellButton(
|
|||
|
|
left, "⏺ Aufnahme",
|
|||
|
|
command=self._delegate(app, "toggle_record"),
|
|||
|
|
theme=t, kind="danger", width=_BTN_W_NORMAL, height=_BTN_H,
|
|||
|
|
font_weight="bold", tooltip="Aufnahme starten / stoppen",
|
|||
|
|
)
|
|||
|
|
new_record.pack(side="left", padx=(0, 6))
|
|||
|
|
self._reassign_button(app, "btn_record", new_record)
|
|||
|
|
|
|||
|
|
new_korrigieren = ShellButton(
|
|||
|
|
left, "⏺ Korrigieren",
|
|||
|
|
command=self._delegate(app, "_toggle_record_append"),
|
|||
|
|
theme=t, kind="danger", width=_BTN_W_NORMAL, height=_BTN_H,
|
|||
|
|
font_weight="bold", tooltip="Zusätzliches Diktat anhängen",
|
|||
|
|
)
|
|||
|
|
new_korrigieren.pack(side="left", padx=(0, 12))
|
|||
|
|
self._reassign_button(app, "btn_record_append", new_korrigieren)
|
|||
|
|
|
|||
|
|
# Weitere primäre Aktionen – einheitliche Größe.
|
|||
|
|
for label, attr_method, tip in [
|
|||
|
|
("Audio importieren", "_import_and_transcribe_audio",
|
|||
|
|
"Audiodatei auswählen und transkribieren"),
|
|||
|
|
("Diktat", "open_diktat_window", "Diktatfenster öffnen"),
|
|||
|
|
("Notizen", "open_notizen_window", "Projekt-Notizen anzeigen"),
|
|||
|
|
("Neu", "_new_session", "Neue Sitzung beginnen"),
|
|||
|
|
]:
|
|||
|
|
b = ShellButton(
|
|||
|
|
left, label, command=self._delegate(app, attr_method),
|
|||
|
|
theme=t, kind="default", width=_BTN_W_NORMAL, height=_BTN_H,
|
|||
|
|
tooltip=tip,
|
|||
|
|
)
|
|||
|
|
b.pack(side="left", padx=(0, 6))
|
|||
|
|
self._all_shell_buttons.append(b)
|
|||
|
|
|
|||
|
|
# Rechts: Transkript-Toggle, A−/A+, Hell/Dunkel, An Empfang senden
|
|||
|
|
self._transcript_btn = ShellButton(
|
|||
|
|
right, "Transkript einblenden",
|
|||
|
|
command=self._on_toggle_transcript, theme=t, kind="ghost",
|
|||
|
|
width=_BTN_W_WIDE, height=_BTN_H,
|
|||
|
|
tooltip="Transkript ein-/ausblenden",
|
|||
|
|
)
|
|||
|
|
self._transcript_btn.pack(side="left", padx=(6, 6))
|
|||
|
|
self._all_shell_buttons.append(self._transcript_btn)
|
|||
|
|
|
|||
|
|
# Schriftgrößen-Stepper
|
|||
|
|
font_box = tk.Frame(
|
|||
|
|
right, bg=t["surface"],
|
|||
|
|
highlightthickness=1, highlightbackground=t["border"],
|
|||
|
|
)
|
|||
|
|
font_box.pack(side="left", padx=(0, 6))
|
|||
|
|
self._font_box = font_box
|
|||
|
|
btn_minus = tk.Label(
|
|||
|
|
font_box, text="A−", font=_FONT_FONTSTEP_BTN,
|
|||
|
|
bg=t["surface"], fg=t["text"],
|
|||
|
|
padx=10, pady=4, cursor="hand2",
|
|||
|
|
)
|
|||
|
|
btn_minus.pack(side="left")
|
|||
|
|
btn_minus.bind("<Button-1>", lambda e: self._on_font_step(-1))
|
|||
|
|
try:
|
|||
|
|
add_tooltip(btn_minus, "Schrift verkleinern")
|
|||
|
|
except Exception:
|
|||
|
|
pass
|
|||
|
|
self._font_label = tk.Label(
|
|||
|
|
font_box, text=self._font_step_label(),
|
|||
|
|
font=_FONT_FONTSTEP, bg=t["surface"], fg=t["subtle"],
|
|||
|
|
padx=4, pady=4, width=8, anchor="center",
|
|||
|
|
)
|
|||
|
|
self._font_label.pack(side="left")
|
|||
|
|
btn_plus = tk.Label(
|
|||
|
|
font_box, text="A+", font=_FONT_FONTSTEP_BTN,
|
|||
|
|
bg=t["surface"], fg=t["text"],
|
|||
|
|
padx=10, pady=4, cursor="hand2",
|
|||
|
|
)
|
|||
|
|
btn_plus.pack(side="left")
|
|||
|
|
btn_plus.bind("<Button-1>", lambda e: self._on_font_step(+1))
|
|||
|
|
try:
|
|||
|
|
add_tooltip(btn_plus, "Schrift vergrößern")
|
|||
|
|
except Exception:
|
|||
|
|
pass
|
|||
|
|
self._font_minus, self._font_plus = btn_minus, btn_plus
|
|||
|
|
|
|||
|
|
self._theme_btn = ShellButton(
|
|||
|
|
right, "Dunkel" if not self._dark else "Hell",
|
|||
|
|
command=self._on_toggle_theme, theme=t, kind="ghost",
|
|||
|
|
width=80, height=_BTN_H, tooltip="Hell-/Dunkel-Modus",
|
|||
|
|
)
|
|||
|
|
self._theme_btn.pack(side="left", padx=(0, 6))
|
|||
|
|
self._all_shell_buttons.append(self._theme_btn)
|
|||
|
|
|
|||
|
|
send = ShellButton(
|
|||
|
|
right, "An Empfang senden",
|
|||
|
|
command=self._delegate(app, "_send_to_empfang"),
|
|||
|
|
theme=t, kind="primary", width=_BTN_W_WIDE, height=_BTN_H,
|
|||
|
|
font_weight="bold",
|
|||
|
|
tooltip="Aktuelle Krankengeschichte an den Empfang übergeben",
|
|||
|
|
)
|
|||
|
|
send.pack(side="left")
|
|||
|
|
self._all_shell_buttons.append(send)
|
|||
|
|
|
|||
|
|
# ── Hauptbereich neu einordnen ───────────────────────────────────────
|
|||
|
|
|
|||
|
|
def _reorder_main_layout(self):
|
|||
|
|
"""Bringt die produktive UI (Status-Zeile + Paned Window) sauber
|
|||
|
|
unter Header und Aktionsleiste, ohne das innere Layout zu zerstören."""
|
|||
|
|
app = self.app
|
|||
|
|
|
|||
|
|
# Status-Zeile direkt unter der Action-Bar zeigen, danach Paned.
|
|||
|
|
try:
|
|||
|
|
if getattr(app, "_status_row", None) is not None:
|
|||
|
|
app._status_row.pack_forget()
|
|||
|
|
app._status_row.pack(fill="x", padx=18, pady=(0, 4))
|
|||
|
|
except Exception:
|
|||
|
|
pass
|
|||
|
|
|
|||
|
|
try:
|
|||
|
|
if getattr(app, "paned", None) is not None:
|
|||
|
|
app.paned.pack_forget()
|
|||
|
|
app.paned.pack(fill="both", expand=True,
|
|||
|
|
padx=18, pady=(0, 12))
|
|||
|
|
except Exception:
|
|||
|
|
pass
|
|||
|
|
|
|||
|
|
# ── Innere Panels in Empfang-Look bringen ────────────────────────────
|
|||
|
|
|
|||
|
|
def _restyle_inner_panels(self):
|
|||
|
|
"""Setzt die Hintergrund- und Textfarben der wichtigsten Container
|
|||
|
|
und Labels in der ursprünglichen UI auf das neue Theme.
|
|||
|
|
|
|||
|
|
Wir berühren bewusst nur Frames und Labels – die fertigen
|
|||
|
|
``RoundedButton``-Knöpfe in der KG-Spalte (KG erstellen, Brief,
|
|||
|
|
SOAP-Pfeile, Textblöcke) behalten ihre Form und Funktion, damit
|
|||
|
|
die produktiven Aktionen unverändert nutzbar bleiben."""
|
|||
|
|
app = self.app
|
|||
|
|
t = self._theme
|
|||
|
|
|
|||
|
|
try:
|
|||
|
|
app.configure(bg=t["bg"])
|
|||
|
|
except Exception:
|
|||
|
|
pass
|
|||
|
|
|
|||
|
|
for attr in (
|
|||
|
|
"lbl_status", "_lbl_build", "_token_label", "_backend_status_label",
|
|||
|
|
):
|
|||
|
|
w = getattr(app, attr, None)
|
|||
|
|
if w is None:
|
|||
|
|
continue
|
|||
|
|
try:
|
|||
|
|
w.configure(bg=t["bg"])
|
|||
|
|
except Exception:
|
|||
|
|
pass
|
|||
|
|
|
|||
|
|
for attr in (
|
|||
|
|
"_transcript_toggle_label", "_kg_toggle_label",
|
|||
|
|
"_addon_toggle_label", "_soap_toggle_label",
|
|||
|
|
"_dokumente_toggle_label", "_textbloecke_toggle_label",
|
|||
|
|
):
|
|||
|
|
w = getattr(app, attr, None)
|
|||
|
|
if w is None:
|
|||
|
|
continue
|
|||
|
|
try:
|
|||
|
|
w.configure(bg=t["bg"], fg=t["text"])
|
|||
|
|
except Exception:
|
|||
|
|
pass
|
|||
|
|
|
|||
|
|
# Status-Zeile / Build-Label
|
|||
|
|
try:
|
|||
|
|
if getattr(app, "_status_row", None) is not None:
|
|||
|
|
app._status_row.configure(style="StatusBar.TFrame")
|
|||
|
|
except Exception:
|
|||
|
|
pass
|
|||
|
|
|
|||
|
|
# Checkbuttons (Kommentare/Empfang anzeigen, Rechtsklick-Paste)
|
|||
|
|
for attr in ("_chk_kommentare_auto", "_chk_empfang_auto",
|
|||
|
|
"_rclick_cb"):
|
|||
|
|
w = getattr(app, attr, None)
|
|||
|
|
if w is None:
|
|||
|
|
continue
|
|||
|
|
try:
|
|||
|
|
w.configure(
|
|||
|
|
bg=t["bg"], activebackground=t["bg"],
|
|||
|
|
fg=t["text"], selectcolor=t["bg"],
|
|||
|
|
)
|
|||
|
|
self._chk_widgets.append(w)
|
|||
|
|
except Exception:
|
|||
|
|
pass
|
|||
|
|
|
|||
|
|
# ── Verhalten / Aktionen ─────────────────────────────────────────────
|
|||
|
|
|
|||
|
|
def _suppress_dev_status_popup(self):
|
|||
|
|
app = self.app
|
|||
|
|
try:
|
|||
|
|
existing = getattr(app, "_dev_status_window", None)
|
|||
|
|
if existing is not None and existing.winfo_exists():
|
|||
|
|
existing.destroy()
|
|||
|
|
except Exception:
|
|||
|
|
pass
|
|||
|
|
app._dev_status_window = None
|
|||
|
|
|
|||
|
|
def _collapse_transcript_default(self):
|
|||
|
|
app = self.app
|
|||
|
|
try:
|
|||
|
|
if getattr(app, "_transcript_collapsed", False):
|
|||
|
|
self._sync_transcript_btn()
|
|||
|
|
return
|
|||
|
|
if hasattr(app, "_toggle_transcript_collapse"):
|
|||
|
|
app._toggle_transcript_collapse()
|
|||
|
|
else:
|
|||
|
|
frame = getattr(app, "_transcript_frame", None)
|
|||
|
|
if frame is not None:
|
|||
|
|
frame.pack_forget()
|
|||
|
|
app._transcript_collapsed = True
|
|||
|
|
self._sync_transcript_btn()
|
|||
|
|
except Exception:
|
|||
|
|
pass
|
|||
|
|
|
|||
|
|
def _sync_transcript_btn(self):
|
|||
|
|
if not self._transcript_btn:
|
|||
|
|
return
|
|||
|
|
collapsed = bool(getattr(self.app, "_transcript_collapsed", True))
|
|||
|
|
self._transcript_btn.set_text(
|
|||
|
|
"Transkript einblenden" if collapsed else "Transkript ausblenden"
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
def _on_toggle_transcript(self):
|
|||
|
|
app = self.app
|
|||
|
|
try:
|
|||
|
|
if hasattr(app, "_toggle_transcript_collapse"):
|
|||
|
|
app._toggle_transcript_collapse()
|
|||
|
|
else:
|
|||
|
|
frame = getattr(app, "_transcript_frame", None)
|
|||
|
|
if frame is None:
|
|||
|
|
return
|
|||
|
|
if getattr(app, "_transcript_collapsed", False):
|
|||
|
|
frame.pack(fill="both", expand=True)
|
|||
|
|
app._transcript_collapsed = False
|
|||
|
|
else:
|
|||
|
|
frame.pack_forget()
|
|||
|
|
app._transcript_collapsed = True
|
|||
|
|
finally:
|
|||
|
|
self._sync_transcript_btn()
|
|||
|
|
|
|||
|
|
def _on_toggle_theme(self):
|
|||
|
|
self._dark = not self._dark
|
|||
|
|
self._theme = DARK.copy() if self._dark else LIGHT.copy()
|
|||
|
|
prefs = _load_prefs()
|
|||
|
|
prefs["dark_mode"] = self._dark
|
|||
|
|
_save_prefs(prefs)
|
|||
|
|
self._reapply_theme()
|
|||
|
|
if self._theme_btn:
|
|||
|
|
self._theme_btn.set_text("Hell" if self._dark else "Dunkel")
|
|||
|
|
|
|||
|
|
def _reapply_theme(self):
|
|||
|
|
app = self.app
|
|||
|
|
t = self._theme
|
|||
|
|
try:
|
|||
|
|
app.configure(bg=t["bg"])
|
|||
|
|
except Exception:
|
|||
|
|
pass
|
|||
|
|
_apply_ttk_theme(app, t)
|
|||
|
|
for attr in ("_header_frame",):
|
|||
|
|
w = getattr(self, attr, None)
|
|||
|
|
if w is not None:
|
|||
|
|
try:
|
|||
|
|
w.configure(bg=t["surface"])
|
|||
|
|
self._recolor_descendants(w, bg=t["surface"], fg=t["text"])
|
|||
|
|
except Exception:
|
|||
|
|
pass
|
|||
|
|
try:
|
|||
|
|
if self._header_sep is not None:
|
|||
|
|
self._header_sep.configure(bg=t["border"])
|
|||
|
|
except Exception:
|
|||
|
|
pass
|
|||
|
|
try:
|
|||
|
|
if self._action_frame is not None:
|
|||
|
|
self._action_frame.configure(bg=t["bg"])
|
|||
|
|
self._recolor_descendants(self._action_frame,
|
|||
|
|
bg=t["bg"], fg=t["text"])
|
|||
|
|
except Exception:
|
|||
|
|
pass
|
|||
|
|
try:
|
|||
|
|
self._brand_title_lbl.configure(bg=t["surface"], fg=t["text_strong"])
|
|||
|
|
self._brand_sub_lbl.configure(bg=t["surface"], fg=t["subtle"])
|
|||
|
|
if self._logo_lbl is not None:
|
|||
|
|
self._logo_lbl.configure(bg=t["surface"])
|
|||
|
|
except Exception:
|
|||
|
|
pass
|
|||
|
|
try:
|
|||
|
|
self._font_box.configure(bg=t["surface"],
|
|||
|
|
highlightbackground=t["border"])
|
|||
|
|
self._font_minus.configure(bg=t["surface"], fg=t["text"])
|
|||
|
|
self._font_plus.configure(bg=t["surface"], fg=t["text"])
|
|||
|
|
self._font_label.configure(bg=t["surface"], fg=t["subtle"])
|
|||
|
|
except Exception:
|
|||
|
|
pass
|
|||
|
|
try:
|
|||
|
|
self._connection_dot.configure(bg=t["surface"])
|
|||
|
|
self._connection_lbl.configure(bg=t["surface"], fg=t["subtle"])
|
|||
|
|
except Exception:
|
|||
|
|
pass
|
|||
|
|
for b in self._all_shell_buttons:
|
|||
|
|
try:
|
|||
|
|
b.apply_theme(t)
|
|||
|
|
except Exception:
|
|||
|
|
pass
|
|||
|
|
for w in self._chk_widgets:
|
|||
|
|
try:
|
|||
|
|
w.configure(
|
|||
|
|
bg=t["bg"], activebackground=t["bg"],
|
|||
|
|
fg=t["text"], selectcolor=t["bg"],
|
|||
|
|
)
|
|||
|
|
except Exception:
|
|||
|
|
pass
|
|||
|
|
# Innere Panels mitziehen
|
|||
|
|
self._restyle_inner_panels()
|
|||
|
|
self._mirror_backend_status(force=True)
|
|||
|
|
|
|||
|
|
def _recolor_descendants(self, parent, *, bg: str, fg: str):
|
|||
|
|
try:
|
|||
|
|
for child in parent.winfo_children():
|
|||
|
|
if isinstance(child, tk.Frame):
|
|||
|
|
try:
|
|||
|
|
child.configure(bg=bg)
|
|||
|
|
except Exception:
|
|||
|
|
pass
|
|||
|
|
self._recolor_descendants(child, bg=bg, fg=fg)
|
|||
|
|
elif isinstance(child, tk.Label):
|
|||
|
|
try:
|
|||
|
|
child.configure(bg=bg)
|
|||
|
|
except Exception:
|
|||
|
|
pass
|
|||
|
|
except Exception:
|
|||
|
|
pass
|
|||
|
|
|
|||
|
|
# ── Schriftgröße ──────────────────────────────────────────────────────
|
|||
|
|
|
|||
|
|
_FONT_DELTA = 0.10
|
|||
|
|
|
|||
|
|
def _font_step_label(self) -> str:
|
|||
|
|
if self._font_step == 0:
|
|||
|
|
return "Standard"
|
|||
|
|
sign = "+" if self._font_step > 0 else "−"
|
|||
|
|
return f"{sign}{abs(self._font_step)}"
|
|||
|
|
|
|||
|
|
def _on_font_step(self, delta: int):
|
|||
|
|
new_step = max(-2, min(4, self._font_step + delta))
|
|||
|
|
if new_step == self._font_step:
|
|||
|
|
return
|
|||
|
|
self._font_step = new_step
|
|||
|
|
self._apply_font_step(new_step, persist=True)
|
|||
|
|
|
|||
|
|
def _apply_font_step(self, step: int, *, persist: bool):
|
|||
|
|
app = self.app
|
|||
|
|
try:
|
|||
|
|
base = 1.0
|
|||
|
|
try:
|
|||
|
|
from aza_config import FIXED_FONT_SCALE
|
|||
|
|
base = float(FIXED_FONT_SCALE)
|
|||
|
|
except Exception:
|
|||
|
|
pass
|
|||
|
|
scale = max(0.4, base + step * self._FONT_DELTA)
|
|||
|
|
if hasattr(app, "_apply_font_scale_global"):
|
|||
|
|
app._apply_font_scale_global(scale)
|
|||
|
|
elif hasattr(app, "_apply_font_scale"):
|
|||
|
|
app._apply_font_scale(scale)
|
|||
|
|
except Exception as exc:
|
|||
|
|
print(f"[Shell] Schriftskalierung fehlgeschlagen: {exc}")
|
|||
|
|
try:
|
|||
|
|
if self._font_label is not None:
|
|||
|
|
self._font_label.configure(text=self._font_step_label())
|
|||
|
|
except Exception:
|
|||
|
|
pass
|
|||
|
|
if persist:
|
|||
|
|
prefs = _load_prefs()
|
|||
|
|
prefs["font_step"] = step
|
|||
|
|
_save_prefs(prefs)
|
|||
|
|
|
|||
|
|
# ── Verbindungsanzeige (spiegelt _backend_status_var) ────────────────
|
|||
|
|
|
|||
|
|
def _mirror_backend_status(self, *, force: bool = False):
|
|||
|
|
app = self.app
|
|||
|
|
try:
|
|||
|
|
var = getattr(app, "_backend_status_var", None)
|
|||
|
|
if var is None:
|
|||
|
|
return
|
|||
|
|
if not hasattr(self, "_last_status_text"):
|
|||
|
|
self._last_status_text = None
|
|||
|
|
txt = ""
|
|||
|
|
try:
|
|||
|
|
txt = str(var.get())
|
|||
|
|
except Exception:
|
|||
|
|
pass
|
|||
|
|
if force or txt != self._last_status_text:
|
|||
|
|
self._last_status_text = txt
|
|||
|
|
self._render_connection(txt)
|
|||
|
|
except Exception:
|
|||
|
|
pass
|
|||
|
|
try:
|
|||
|
|
app.after(1500, self._mirror_backend_status)
|
|||
|
|
except Exception:
|
|||
|
|
pass
|
|||
|
|
|
|||
|
|
def _render_connection(self, txt: str):
|
|||
|
|
t = self._theme
|
|||
|
|
if not self._connection_dot or not self._connection_lbl:
|
|||
|
|
return
|
|||
|
|
low = txt.lower()
|
|||
|
|
if any(k in low for k in ("verbund", "online", "ok")):
|
|||
|
|
color, label = t["success"], "Verbunden"
|
|||
|
|
elif "geprüft" in low or "prüf" in low or "warte" in low:
|
|||
|
|
color, label = t["warn"], txt or "Verbindung wird geprüft …"
|
|||
|
|
else:
|
|||
|
|
color = t["danger"]
|
|||
|
|
label = txt if txt else "Nicht verbunden"
|
|||
|
|
try:
|
|||
|
|
self._connection_dot.delete("all")
|
|||
|
|
self._connection_dot.create_oval(
|
|||
|
|
1, 1, 9, 9, fill=color, outline=color,
|
|||
|
|
)
|
|||
|
|
self._connection_lbl.configure(text=label, fg=color)
|
|||
|
|
except Exception:
|
|||
|
|
pass
|
|||
|
|
|
|||
|
|
# ── Helfer ───────────────────────────────────────────────────────────
|
|||
|
|
|
|||
|
|
def _delegate(self, obj, attr: str) -> Callable:
|
|||
|
|
def _do():
|
|||
|
|
self._safe_call(obj, attr)
|
|||
|
|
return _do
|
|||
|
|
|
|||
|
|
@staticmethod
|
|||
|
|
def _safe_call(obj, attr: str):
|
|||
|
|
fn = getattr(obj, attr, None)
|
|||
|
|
if not callable(fn):
|
|||
|
|
return
|
|||
|
|
try:
|
|||
|
|
fn()
|
|||
|
|
except Exception as exc:
|
|||
|
|
print(f"[Shell] Aufruf '{attr}' fehlgeschlagen: {exc}")
|
|||
|
|
|
|||
|
|
def _reassign_button(self, app, attr_name: str, new_btn: ShellButton):
|
|||
|
|
"""Ersetzt einen bestehenden RoundedButton-Attributverweis durch
|
|||
|
|
unseren neuen Pill-Button. State-Updates aus der App
|
|||
|
|
(``self.btn_record.configure(text=...)``) treffen damit den
|
|||
|
|
sichtbaren neuen Button.
|
|||
|
|
|
|||
|
|
Der alte Knopf bleibt weiterhin als Python-Objekt bestehen
|
|||
|
|
(er sitzt im versteckten alten Top-Frame), wird aber nicht mehr
|
|||
|
|
angezeigt und bekommt keine Klicks mehr."""
|
|||
|
|
try:
|
|||
|
|
scalable = getattr(app, "_scalable_widgets", None)
|
|||
|
|
old = getattr(app, attr_name, None)
|
|||
|
|
if isinstance(scalable, list) and old in scalable:
|
|||
|
|
scalable.remove(old)
|
|||
|
|
except Exception:
|
|||
|
|
pass
|
|||
|
|
try:
|
|||
|
|
setattr(app, attr_name, new_btn)
|
|||
|
|
except Exception:
|
|||
|
|
return
|
|||
|
|
self._all_shell_buttons.append(new_btn)
|
|||
|
|
try:
|
|||
|
|
scalable = getattr(app, "_scalable_widgets", None)
|
|||
|
|
if isinstance(scalable, list):
|
|||
|
|
scalable.append(new_btn)
|
|||
|
|
except Exception:
|
|||
|
|
pass
|
|||
|
|
|
|||
|
|
|
|||
|
|
# ─── Öffentliche API ─────────────────────────────────────────────────────────
|
|||
|
|
|
|||
|
|
def apply_desktop_shell(app) -> None:
|
|||
|
|
"""Wendet die saubere AzA-Desktop-Hülle auf eine bestehende
|
|||
|
|
``KGDesktopApp`` an. Idempotent."""
|
|||
|
|
if getattr(app, "_aza_shell_installed", False):
|
|||
|
|
return
|
|||
|
|
try:
|
|||
|
|
shell = _DesktopShell(app)
|
|||
|
|
shell.install()
|
|||
|
|
app._aza_shell = shell
|
|||
|
|
app._aza_shell_installed = True
|
|||
|
|
except Exception as exc:
|
|||
|
|
print(f"[Shell] Installation fehlgeschlagen: {exc}")
|
|||
|
|
import traceback
|
|||
|
|
traceback.print_exc()
|
|||
|
|
|
|||
|
|
|
|||
|
|
__all__ = ["apply_desktop_shell"]
|