This commit is contained in:
2026-05-04 23:43:34 +02:00
parent 03d188d119
commit 72ecd579de
10 changed files with 422 additions and 277 deletions

View File

@@ -1,6 +1,6 @@
# Auto-generated by aza_build_stamp.py DO NOT EDIT # Auto-generated by aza_build_stamp.py DO NOT EDIT
BUILD_TIME = "2026-05-03 19:14:17" BUILD_TIME = "2026-05-04 23:40:27"
BUILD_TIMESTAMP = "20260503_191417" BUILD_TIMESTAMP = "20260504_234027"
GIT_COMMIT = "d4822fc8" GIT_COMMIT = "03d188d1"
GIT_BRANCH = "main" GIT_BRANCH = "main"
GIT_DIRTY = True GIT_DIRTY = True

View File

@@ -13,10 +13,9 @@ Aufbauend auf V1.1:
``_send_to_empfang``). ``_send_to_empfang``).
* **Erscheinungsbild**: dieselbe **Transparenz-Logik** wie in der klassischen * **Erscheinungsbild**: dieselbe **Transparenz-Logik** wie in der klassischen
Hauptfenster-Kopfzeile (``_opacity_var_main``, ``MIN_OPACITY``, ``save_opacity``) Hauptfenster-Kopfzeile (``_opacity_var_main``, ``MIN_OPACITY``, ``save_opacity``)
sowie Zugriff auf **alle Einstellungen** (``_open_settings``). Zusätzlich sowie Zugriff auf **alle Einstellungen** (``_open_settings``). Die Farbpalette
weiterhin **Hell/Dunkel** für die Office-Hülle-Farbpalette (persistiert), der Office-Hülle folgt der beim Start geladenen Hell/Dunkel-Präferenz;
da das klassische ``basis14``-Hauptfenster keine globale Farb-Umschaltung hat — nur Umschalten nur noch über das klassische Einstellungsfenster, falls dort angeboten.
Transparenz und das separate Einstellungsfenster.
* Footer-Branding und Logo ca. **30 % größer** als in V1.1. * Footer-Branding und Logo ca. **30 % größer** als in V1.1.
Technische Strategie Technische Strategie
@@ -738,7 +737,7 @@ class _OfficeShellV12:
pad = dict( pad = dict(
bg=acc, fg="white", font=FONT_DEFAULT, bg=acc, fg="white", font=FONT_DEFAULT,
activebackground=acc, activeforeground="white", activebackground=acc, activeforeground="white",
selectcolor="#E2EEF6", highlightthickness=0, selectcolor="#1a4d6d", highlightthickness=0,
bd=0, anchor="w", bd=0, anchor="w",
) )
@@ -1015,6 +1014,7 @@ class _OfficeShellV12:
app = self.app app = self.app
acc = self._palette["ACCENT"] acc = self._palette["ACCENT"]
bar = self._sidebar bar = self._sidebar
self._theme_switch_pop = None
for w in list(bar.winfo_children()): for w in list(bar.winfo_children()):
try: try:
w.destroy() w.destroy()
@@ -1024,7 +1024,7 @@ class _OfficeShellV12:
cb_pad = dict( cb_pad = dict(
bg=acc, fg="white", font=FONT_DEFAULT, bg=acc, fg="white", font=FONT_DEFAULT,
activebackground=acc, activeforeground="white", activebackground=acc, activeforeground="white",
selectcolor="#E2EEF6", highlightthickness=0, selectcolor="#1a4d6d", highlightthickness=0,
bd=0, anchor="w", bd=0, anchor="w",
) )
@@ -1147,30 +1147,6 @@ class _OfficeShellV12:
lbl_full.pack(side="left") lbl_full.pack(side="left")
lbl_full.bind("<Button-1>", lambda e: self._apply_opacity_percent_str("100")) 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() tk.Frame(self._sec_ersch_body, bg=acc, height=8).pack()
link = tk.Label( link = tk.Label(

View File

@@ -2066,7 +2066,8 @@ class KGDesktopApp(tk.Tk, TodoMixin, TextWindowsMixin, AzaDiktatMixin, AzaSettin
self._main_hidden = True self._main_hidden = True
self.withdraw() self.withdraw()
return return
self._return_to_launcher = True # Normales Schliessen (X): Programm beenden. Launcher nur wenn
# vorher explizit _go_to_launcher() gesetzt hat (_return_to_launcher True).
self.destroy() self.destroy()
def _go_to_launcher(self): def _go_to_launcher(self):
@@ -5612,11 +5613,15 @@ WICHTIG unbedingt einhalten:
return result return result
def _sync_empfang_after_kg_change(self, *, context: str = "Korrektur") -> None: def _sync_empfang_after_kg_change(self, *, context: str = "Korrektur") -> None:
"""Empfang-Dialog: Felder aus aktueller KG fuellen und Auto-Sektionen """Empfang-Dialog: Felder aus aktueller KG fuellen.
in den Chat unten einfuegen sowie Hinweis im Chat-Verlauf posten.
Bei aktiviertem Autocopy (Therapie / Procedere) wird nur die untere
Nachrichten-Box befuellt — nicht der Chat-Verlauf.
Wird nach «Korrigieren», neuem Transkript und neuer KG aufgerufen. Wird nach «Korrigieren», neuem Transkript und neuer KG aufgerufen.
Das Argument ``context`` bleibt fuer bestehende Aufrufer erhalten.
""" """
_ = context
dlg = getattr(self, "_empfang_dlg", None) dlg = getattr(self, "_empfang_dlg", None)
if dlg is None: if dlg is None:
return return
@@ -5660,77 +5665,44 @@ WICHTIG unbedingt einhalten:
for k, v in section_values.items(): for k, v in section_values.items():
_push_field(k, v) _push_field(k, v)
def _sync_fmt_ther(val: str) -> str:
t = (val or "").strip()
if not t:
return ""
lines = t.split("\n")
first = lines[0].strip().lower().rstrip(":").rstrip(".")
if first in ("therapie", "therapieplan"):
rest = "\n".join(lines[1:]).strip()
return f"Therapie\n{rest}" if rest else "Therapie"
return f"Therapie\n{t}"
def _sync_fmt_proc(val: str) -> str:
t = (val or "").strip()
if not t:
return ""
lines = t.split("\n")
first = lines[0].strip().lower().rstrip(":").rstrip(".")
if first == "procedere":
rest = "\n".join(lines[1:]).strip()
return f"Procedere\n{rest}" if rest else "Procedere"
return f"Procedere\n{t}"
push_fn = copy_fns.get("push") if isinstance(copy_fns, dict) else None push_fn = copy_fns.get("push") if isinstance(copy_fns, dict) else None
auto_pushed: list[str] = []
if callable(push_fn): if callable(push_fn):
for key, label in (("ther", "Therapieplan"), for key in ("ther", "proc"):
("proc", "Procedere"),
("patient", "Nr.")):
var = auto_vars.get(key) var = auto_vars.get(key)
if var is None or not var.get(): if var is None or not var.get():
continue continue
if key == "patient": val = (section_values.get(key) or "").strip()
w = fw.get("patient") if not val:
val = w.get().strip() if isinstance(w, tk.StringVar) else "" continue
else: try:
val = (section_values.get(key) or "").strip() if key == "ther":
if val: push_fn(_sync_fmt_ther(val), source=key)
try: else:
push_fn(f"{label}:\n{val}", source=key) push_fn(_sync_fmt_proc(val), source=key)
auto_pushed.append(label) except Exception:
except Exception: pass
pass
tp = section_values["ther"].strip()
pc = section_values["proc"].strip()
parts: list[str] = []
if tp:
parts.append(f"Therapieplan:\n{tp}")
if pc:
parts.append(f"Procedere:\n{pc}")
if not parts:
try:
kg = self.txt_output.get("1.0", "end").strip()
except Exception:
kg = ""
if kg:
excerpt = kg[:1200] + ("" if len(kg) > 1200 else "")
parts.append(f"Krankengeschichte:\n{excerpt}")
suffix = ""
if auto_pushed:
suffix = (
"\n\n[Auto-Kopie in Chat unten: "
+ ", ".join(auto_pushed) + "]"
)
body = (
f"Krankengeschichte wurde aktualisiert ({context}).\n\n"
+ ("\n\n".join(parts) if parts else "(Kein Text)")
+ suffix
)
overlay = getattr(self, "_empfang_local_chat_overlay", None)
if overlay is None:
overlay = []
self._empfang_local_chat_overlay = overlay
name = (self._user_profile.get("name") or "").strip() or "Benutzer"
overlay.append({
"id": f"local-{time.time_ns()}",
"absender": f"{name} (lokal)",
"zeitstempel": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
"kommentar": body,
})
if len(overlay) > 25:
del overlay[:-25]
updater = getattr(self, "_empfang_update_chat_fn", None)
if callable(updater):
try:
base = getattr(self, "_empfang_last_thread_messages", None)
if base is None:
base = []
updater(base)
except Exception:
pass
def _empfang_background_poll(self): def _empfang_background_poll(self):
"""Prueft im Hintergrund auf neue Empfangs-Nachrichten und zeigt Toast.""" """Prueft im Hintergrund auf neue Empfangs-Nachrichten und zeigt Toast."""
@@ -5882,6 +5854,97 @@ WICHTIG unbedingt einhalten:
outer = tk.Frame(dlg, bg="#E8F4FA") outer = tk.Frame(dlg, bg="#E8F4FA")
outer.pack(fill="both", expand=True) outer.pack(fill="both", expand=True)
def _compact_nr(s: str) -> str:
return re.sub(r"\s+", "", (s or "").strip())
# --- Smart-Pick (Pinsel): vor Patienten-Zeile benoetigt ---
_pick_listener = [None]
def _do_smart_pick(btn, on_result):
if _pick_listener[0] is not None:
try:
_pick_listener[0].stop()
except Exception:
pass
_pick_listener[0] = None
btn.configure(text="\U0001f58c Pinsel", bg="#dde8f0", fg="#1a4d6d")
return
try:
_old_clip = ""
if sys.platform == "win32":
_old_clip = (_win_clipboard_get() or "").strip()
if not _old_clip:
_old_clip = dlg.clipboard_get().strip()
except tk.TclError:
_old_clip = ""
btn.configure(text="\u23f9 Text markieren...", bg="#f8d7da", fg="#721c24")
_started = [time.time()]
_mouse_was_pressed = [False]
_press_time = [0.0]
def _on_click(x, y, button, pressed):
if button != MouseButton.left:
return
if pressed:
if time.time() - _started[0] > 0.3:
_mouse_was_pressed[0] = True
_press_time[0] = time.time()
return
if not _mouse_was_pressed[0]:
return
_mouse_was_pressed[0] = False
hold_duration = time.time() - _press_time[0]
was_drag = hold_duration > 0.20
time.sleep(0.05)
try:
kbd = KbdController()
if not was_drag:
from pynput.mouse import Controller as MController
mc = MController()
mc.click(MouseButton.left, 2)
time.sleep(0.1)
with kbd.pressed(Key.ctrl):
kbd.tap(KeyCode.from_char('c'))
except Exception:
pass
time.sleep(0.38)
def _grab():
cur = ""
try:
for _attempt in range(8):
cur = ""
if sys.platform == "win32":
cur = (_win_clipboard_get() or "").strip()
if not cur:
try:
cur = dlg.clipboard_get().strip()
except tk.TclError:
cur = ""
if cur and cur != _old_clip:
break
time.sleep(0.06)
if cur and cur != _old_clip:
on_result(cur)
except Exception:
pass
btn.configure(text="\U0001f58c Pinsel", bg="#dde8f0", fg="#1a4d6d")
_pick_listener[0] = None
self.after(0, _grab)
return False
if _HAS_PYNPUT_MOUSE:
ml = MouseListener(on_click=_on_click)
_pick_listener[0] = ml
ml.start()
else:
btn.configure(text="\U0001f58c Pinsel", bg="#dde8f0", fg="#1a4d6d")
# --- Header mit Titel und Schriftgröße --- # --- Header mit Titel und Schriftgröße ---
hdr = tk.Frame(outer, bg="#E8F4FA") hdr = tk.Frame(outer, bg="#E8F4FA")
hdr.pack(fill="x", padx=14, pady=(8, 0)) hdr.pack(fill="x", padx=14, pady=(8, 0))
@@ -5889,11 +5952,142 @@ WICHTIG unbedingt einhalten:
tk.Label(hdr, text="An Empfang senden", font=("Segoe UI", 12, "bold"), tk.Label(hdr, text="An Empfang senden", font=("Segoe UI", 12, "bold"),
bg="#E8F4FA", fg="#1a4d6d").pack(side="left") bg="#E8F4FA", fg="#1a4d6d").pack(side="left")
toggle_vars: dict[str, tk.BooleanVar] = {}
field_widgets: dict = {}
# Patienten-Nr. nur in dieser Kopfzeile (kein eigener Nr.-Abschnitt im Scrollbereich)
nr_banner_row = tk.Frame(outer, bg="#E8F4FA")
nr_banner_row.pack(fill="x", padx=14, pady=(2, 2))
tk.Label(nr_banner_row, text="Patienten-Nr.:",
font=("Segoe UI", 9), bg="#E8F4FA", fg="#5B8DB3").pack(side="left")
_pat_ph = "(keine)"
patient_sv = tk.StringVar()
_lp_init = (prefs.get("last_patient") or "").strip()
if _lp_init:
patient_sv.set(_lp_init)
else:
patient_sv.set(_pat_ph)
patient_ent = tk.Entry(
nr_banner_row,
textvariable=patient_sv,
font=("Consolas", 11),
bg="white",
fg="#aaa" if not _lp_init else "#1a4d6d",
relief="solid",
bd=1,
width=24,
)
patient_ent.pack(side="left", padx=(8, 4), fill="x", expand=True)
field_widgets["patient"] = patient_sv
toggle_vars["patient"] = tk.BooleanVar(value=True)
copy_to_chat_fns: dict = {}
def _pat_focus_in(_e=None):
if patient_sv.get().strip() == _pat_ph:
patient_sv.set("")
patient_ent.configure(fg="#1a4d6d")
def _pat_focus_out(_e=None):
if not patient_sv.get().strip():
patient_sv.set(_pat_ph)
patient_ent.configure(fg="#aaa")
else:
patient_ent.configure(fg="#1a4d6d")
patient_ent.bind("<FocusIn>", _pat_focus_in)
patient_ent.bind("<FocusOut>", _pat_focus_out)
def _patient_entry_copy_compact(_e=None):
raw = patient_sv.get().strip()
if raw == _pat_ph:
return "break"
nn = _compact_nr(raw)
if nn:
dlg.clipboard_clear()
dlg.clipboard_append(nn)
dlg.update_idletasks()
try:
self.set_status("Nr. kopiert (ohne Leerzeichen).")
except Exception:
pass
return "break"
patient_ent.bind("<Double-Button-1>", _patient_entry_copy_compact)
def _patient_paste_from_clipboard(_e=None):
try:
txt = ""
if sys.platform == "win32":
txt = (_win_clipboard_get() or "").strip()
if not txt:
txt = dlg.clipboard_get().strip()
nn = _compact_nr(txt)
if len(nn) < 2:
return None
patient_sv.set(nn)
patient_ent.configure(fg="#1a4d6d")
return "break"
except Exception:
return None
patient_ent.bind("<Control-v>", _patient_paste_from_clipboard)
patient_ent.bind("<Control-V>", _patient_paste_from_clipboard)
def _clear_nr_top(_e=None):
patient_sv.set(_pat_ph)
patient_ent.configure(fg="#aaa")
tk.Label(nr_banner_row, text="\u2715", font=("Segoe UI", 9),
bg="#E8F4FA", fg="#aaa", cursor="hand2").pack(side="left", padx=(2, 2))
nr_banner_row.winfo_children()[-1].bind("<Button-1>", _clear_nr_top)
pick_btn_nr_top = tk.Button(
nr_banner_row,
text="\U0001f58c Pinsel",
font=("Segoe UI", 8),
bg="#dde8f0",
fg="#1a4d6d",
relief="flat",
cursor="hand2",
bd=0,
padx=6,
pady=1,
)
pick_btn_nr_top.pack(side="left", padx=(2, 0))
def _start_pick_nr_top():
def _on_pick(t):
nn = _compact_nr(t)
if nn:
patient_sv.set(nn)
patient_ent.configure(fg="#1a4d6d")
def _follow():
fn = copy_to_chat_fns.get("push")
if callable(fn):
fn(f"Nr.: {nn}", source="patient")
dlg.after(120, _follow)
_do_smart_pick(pick_btn_nr_top, _on_pick)
pick_btn_nr_top.configure(command=_start_pick_nr_top)
add_tooltip(
pick_btn_nr_top,
"Text woanders markieren Nr. erscheint hier und oben in der Nachrichten-Box.",
)
add_tooltip(
patient_ent,
"Patienten-Nr.; Doppelklick: ohne Leerzeichen kopieren.",
)
# Section-Sichtbarkeit (immer alle an, ohne Zahnrad-Menue) # Section-Sichtbarkeit (immer alle an, ohne Zahnrad-Menue)
gear_vis = { gear_vis = {
"patient": tk.BooleanVar(value=True), "ther": tk.BooleanVar(value=True),
"ther": tk.BooleanVar(value=True), "proc": tk.BooleanVar(value=True),
"proc": tk.BooleanVar(value=True),
} }
_section_rows: dict[str, tk.Frame] = {} _section_rows: dict[str, tk.Frame] = {}
@@ -6005,81 +6199,6 @@ WICHTIG unbedingt einhalten:
_w.bind("<Enter>", lambda e, ww=_w: ww.configure(fg="#1a4d6d")) _w.bind("<Enter>", lambda e, ww=_w: ww.configure(fg="#1a4d6d"))
_w.bind("<Leave>", lambda e, ww=_w: ww.configure(fg="#8AAFC0")) _w.bind("<Leave>", lambda e, ww=_w: ww.configure(fg="#8AAFC0"))
# --- Smart-Pick: markieren + loslassen = uebernehmen ---
# Benutzer klickt Lupe → wechselt zu fremdem Fenster →
# markiert Text mit Maus → laesst los → Text wird uebernommen.
# KEIN manuelles Ctrl+C noetig.
_pick_listener = [None]
def _do_smart_pick(btn, on_result):
if _pick_listener[0] is not None:
try:
_pick_listener[0].stop()
except Exception:
pass
_pick_listener[0] = None
btn.configure(text="\U0001f58c Pinsel", bg="#dde8f0", fg="#1a4d6d")
return
try:
_old_clip = dlg.clipboard_get()
except tk.TclError:
_old_clip = ""
btn.configure(text="\u23f9 Text markieren...", bg="#f8d7da", fg="#721c24")
_started = [time.time()]
_mouse_was_pressed = [False]
_press_time = [0.0]
def _on_click(x, y, button, pressed):
if button != MouseButton.left:
return
if pressed:
if time.time() - _started[0] > 0.3:
_mouse_was_pressed[0] = True
_press_time[0] = time.time()
return
if not _mouse_was_pressed[0]:
return
_mouse_was_pressed[0] = False
hold_duration = time.time() - _press_time[0]
was_drag = hold_duration > 0.20
time.sleep(0.05)
try:
kbd = KbdController()
if not was_drag:
from pynput.mouse import Controller as MController
mc = MController()
mc.click(MouseButton.left, 2)
time.sleep(0.1)
with kbd.pressed(Key.ctrl):
kbd.tap(KeyCode.from_char('c'))
except Exception:
pass
time.sleep(0.2)
def _grab():
try:
cur = dlg.clipboard_get().strip()
if cur and cur != _old_clip:
on_result(cur)
except tk.TclError:
pass
btn.configure(text="\U0001f58c Pinsel", bg="#dde8f0", fg="#1a4d6d")
_pick_listener[0] = None
self.after(0, _grab)
return False
if _HAS_PYNPUT_MOUSE:
ml = MouseListener(on_click=_on_click)
_pick_listener[0] = ml
ml.start()
else:
btn.configure(text="\U0001f58c Pinsel", bg="#dde8f0", fg="#1a4d6d")
# --- Feste Knopfleiste am unteren Rand --- # --- Feste Knopfleiste am unteren Rand ---
bottom_bar = tk.Frame(outer, bg="#E8F4FA") bottom_bar = tk.Frame(outer, bg="#E8F4FA")
bottom_bar.pack(side="bottom", fill="x", padx=14, pady=(4, 8)) bottom_bar.pack(side="bottom", fill="x", padx=14, pady=(4, 8))
@@ -6107,20 +6226,38 @@ WICHTIG unbedingt einhalten:
dlg.bind("<Destroy>", lambda e: canvas.unbind_all("<MouseWheel>") if e.widget is dlg else None) dlg.bind("<Destroy>", lambda e: canvas.unbind_all("<MouseWheel>") if e.widget is dlg else None)
_field_defs_raw = [ _field_defs_raw = [
("patient", "Nr.", prefs.get("last_patient", "")), ("ther", "Therapieplan", extracted["therapieplan"]),
("ther", "Therapieplan", extracted["therapieplan"]), ("proc", "Procedere", extracted["procedere"]),
("proc", "Procedere", extracted["procedere"]),
] ]
field_defs = list(_field_defs_raw) field_defs = list(_field_defs_raw)
toggle_vars: dict[str, tk.BooleanVar] = {}
content_frames: dict[str, tk.Frame] = {} content_frames: dict[str, tk.Frame] = {}
field_widgets: dict = {}
auto_copy_vars: dict[str, tk.BooleanVar] = { auto_copy_vars: dict[str, tk.BooleanVar] = {
k: tk.BooleanVar(value=bool(prefs.get(f"auto_copy_{k}", False))) k: tk.BooleanVar(value=bool(prefs.get(f"auto_copy_{k}", False)))
for k, _, _ in _field_defs_raw for k, _, _ in _field_defs_raw
} }
copy_to_chat_fns: dict = {}
def _format_ther_for_push(raw: str) -> str:
t = (raw or "").strip()
if not t:
return ""
lines = t.split("\n")
first = lines[0].strip().lower().rstrip(":").rstrip(".")
if first in ("therapie", "therapieplan"):
rest = "\n".join(lines[1:]).strip()
return f"Therapie\n{rest}" if rest else "Therapie"
return f"Therapie\n{t}"
def _format_proc_for_push(raw: str) -> str:
t = (raw or "").strip()
if not t:
return ""
lines = t.split("\n")
first = lines[0].strip().lower().rstrip(":").rstrip(".")
if first == "procedere":
rest = "\n".join(lines[1:]).strip()
return f"Procedere\n{rest}" if rest else "Procedere"
return f"Procedere\n{t}"
for key, label, initial_val in field_defs: for key, label, initial_val in field_defs:
has_content = bool(initial_val and initial_val.strip()) has_content = bool(initial_val and initial_val.strip())
@@ -6154,98 +6291,97 @@ WICHTIG unbedingt einhalten:
if not callable(fn): if not callable(fn):
return return
val = _get_text(_k) val = _get_text(_k)
if val: if not val:
fn(val, source=_k) return
if _k == "ther":
ft = _format_ther_for_push(val)
if ft:
fn(ft, source=_k)
return
fp = _format_proc_for_push(val)
if fp:
fn(fp, source=_k)
copy_btn = tk.Button( def _on_copy_link_click(_e, _k=k):
header, text="Kopieren", font=("Segoe UI", 9), _copy_section_to_chat(_k)
bg="#dde8f0", fg="#1a4d6d", return "break"
activebackground="#c8d8e6", relief="flat", bd=0,
cursor="hand2", padx=10, pady=2, width=9, copy_lbl = tk.Label(
command=_copy_section_to_chat, header,
text="Kopieren",
font=("Segoe UI", 9, "underline"),
bg="#E8F4FA",
fg="#2563ab",
cursor="hand2",
)
copy_lbl.pack(side="left", padx=(8, 6))
copy_lbl.bind("<Button-1>", _on_copy_link_click)
add_tooltip(
copy_lbl,
"Inhalt in die Nachrichten-Box unten einfuegen (nicht in den Chat-Verlauf).",
) )
copy_btn.pack(side="left", padx=(8, 6))
add_tooltip(copy_btn, "Inhalt unten in den Chat einfuegen")
def _on_auto_change(_k=k): def _on_auto_change(_k=k):
prefs[f"auto_copy_{_k}"] = auto_copy_vars[_k].get() acv = auto_copy_vars.get(_k)
if acv is None:
return
prefs[f"auto_copy_{_k}"] = acv.get()
self._autotext_data["empfang_prefs"] = prefs self._autotext_data["empfang_prefs"] = prefs
save_autotext(self._autotext_data) save_autotext(self._autotext_data)
if auto_copy_vars[_k].get(): if acv.get():
_copy_section_to_chat(_k) _copy_section_to_chat(_k)
auto_cb = tk.Checkbutton( auto_cb = tk.Checkbutton(
header, text="Auto", font=("Segoe UI", 9), header,
variable=auto_copy_vars[k], bg="#E8F4FA", fg="#1a4d6d", text="Autocopy",
activebackground="#E8F4FA", activeforeground="#1a4d6d", font=("Segoe UI", 9),
selectcolor="#FFFFFF", highlightthickness=0, bd=0, variable=auto_copy_vars[k],
cursor="hand2", padx=4, bg="#E8F4FA",
fg="#1a4d6d",
activebackground="#E8F4FA",
activeforeground="#1a4d6d",
selectcolor="#FFFFFF",
highlightthickness=0,
bd=0,
cursor="hand2",
padx=4,
command=_on_auto_change, command=_on_auto_change,
) )
auto_cb.pack(side="left") auto_cb.pack(side="left")
add_tooltip( add_tooltip(
auto_cb, auto_cb,
"Automatisch in den Chat unten einfuegen, wenn KG aktualisiert wird", "Wenn aktiv: Abschnitt bei KG-Updates automatisch in die Nachrichten-Box.",
) )
body = tk.Frame(r, bg="#E8F4FA") body = tk.Frame(r, bg="#E8F4FA")
content_frames[k] = body content_frames[k] = body
if k == "patient": t = tk.Text(body, height=3, wrap="word",
sv = tk.StringVar(value=init) font=("Segoe UI", _empfang_font_size[0]),
pat_row = tk.Frame(body, bg="#E8F4FA") bg="white", fg="#1a2a3a", relief="solid", bd=1,
pat_row.pack(fill="x", padx=4, pady=(2, 4)) insertbackground="#1a4d6d", padx=4, pady=4)
e = ttk.Entry(pat_row, textvariable=sv, width=40) t.pack(fill="x", padx=4, pady=(2, 4))
e.pack(side="left", fill="x", expand=True) _ph = "Text eingeben oder diktieren..."
if init:
def _start_pick_nr(): t.insert("1.0", init)
_do_smart_pick(pick_btn_nr, lambda t: sv.set(t))
def _clear_nr():
sv.set("")
tk.Label(pat_row, text="\u2715", font=("Segoe UI", 9),
bg="#E8F4FA", fg="#aaa", cursor="hand2").pack(
side="left", padx=(4, 0))
pat_row.winfo_children()[-1].bind(
"<Button-1>", lambda e: _clear_nr())
pick_btn_nr = tk.Button(
pat_row, text="\U0001f58c Pinsel",
font=("Segoe UI", 8), bg="#dde8f0",
fg="#1a4d6d", relief="flat", cursor="hand2",
command=_start_pick_nr, bd=0, padx=6, pady=1)
pick_btn_nr.pack(side="left", padx=(4, 0))
add_tooltip(pick_btn_nr,
"Text in anderer App markieren - wird automatisch uebernommen")
field_widgets[k] = sv
else: else:
t = tk.Text(body, height=3, wrap="word", t.insert("1.0", _ph)
font=("Segoe UI", _empfang_font_size[0]), t.configure(fg="#aaa")
bg="white", fg="#1a2a3a", relief="solid", bd=1,
insertbackground="#1a4d6d", padx=4, pady=4)
t.pack(fill="x", padx=4, pady=(2, 4))
_ph = "Text eingeben oder diktieren..."
if init:
t.insert("1.0", init)
else:
t.insert("1.0", _ph)
t.configure(fg="#aaa")
def _ph_focus_in(e, _t=t, _p=_ph): def _ph_focus_in(e, _t=t, _p=_ph):
if _t.get("1.0", "end").strip() == _p: if _t.get("1.0", "end").strip() == _p:
_t.delete("1.0", "end") _t.delete("1.0", "end")
_t.configure(fg="#1a2a3a") _t.configure(fg="#1a2a3a")
def _ph_focus_out(e, _t=t, _p=_ph): def _ph_focus_out(e, _t=t, _p=_ph):
if not _t.get("1.0", "end").strip(): if not _t.get("1.0", "end").strip():
_t.insert("1.0", _p) _t.insert("1.0", _p)
_t.configure(fg="#aaa") _t.configure(fg="#aaa")
t.bind("<FocusIn>", _ph_focus_in, add="+") t.bind("<FocusIn>", _ph_focus_in, add="+")
t.bind("<FocusOut>", _ph_focus_out, add="+") t.bind("<FocusOut>", _ph_focus_out, add="+")
field_widgets[k] = t field_widgets[k] = t
_empfang_text_widgets.append(t) _empfang_text_widgets.append(t)
if v.get(): if v.get():
body.pack(fill="x") body.pack(fill="x")
@@ -6282,7 +6418,10 @@ WICHTIG unbedingt einhalten:
if w is None: if w is None:
return "" return ""
if isinstance(w, tk.StringVar): if isinstance(w, tk.StringVar):
return w.get().strip() tt = w.get().strip()
if tt == _pat_ph:
return ""
return tt
val = w.get("1.0", "end").strip() val = w.get("1.0", "end").strip()
if val in _empty_placeholders: if val in _empty_placeholders:
return "" return ""
@@ -6378,13 +6517,14 @@ WICHTIG unbedingt einhalten:
def _flash_bg(success): def _flash_bg(success):
color = "#e8f5e9" if success else "#fce4ec" color = "#e8f5e9" if success else "#fce4ec"
orig = "#E8F4FA" orig = "#E8F4FA"
for w in [outer, hdr] + list(hdr.winfo_children()): _flash_widgets = [outer, hdr, nr_banner_row]
for w in _flash_widgets + list(hdr.winfo_children()):
try: try:
w.configure(bg=color) w.configure(bg=color)
except Exception: except Exception:
pass pass
def _restore(): def _restore():
for w2 in [outer, hdr] + list(hdr.winfo_children()): for w2 in _flash_widgets + list(hdr.winfo_children()):
try: try:
w2.configure(bg=orig) w2.configure(bg=orig)
except Exception: except Exception:
@@ -6419,13 +6559,26 @@ WICHTIG unbedingt einhalten:
font=("Segoe UI", max(7, _empfang_font_size[0] - 2))) font=("Segoe UI", max(7, _empfang_font_size[0] - 2)))
chat_display.tag_configure("new_msg", background="#e8f5e9") chat_display.tag_configure("new_msg", background="#e8f5e9")
reply_entry = tk.Text(inner, height=4, wrap="word", reply_holder = tk.Frame(inner, bg="#E8F4FA")
reply_holder.pack(fill="x", pady=(0, 2))
reply_entry = tk.Text(reply_holder, height=8, wrap="word",
font=("Segoe UI", _empfang_font_size[0]), font=("Segoe UI", _empfang_font_size[0]),
bg="white", fg="#1a2a3a", relief="solid", bd=1, bg="white", fg="#1a2a3a", relief="solid", bd=1,
padx=4, pady=4) padx=4, pady=4)
reply_entry.pack(fill="x", pady=(0, 2)) reply_sb = ttk.Scrollbar(reply_holder, orient="vertical",
command=reply_entry.yview)
reply_entry.configure(yscrollcommand=reply_sb.set)
reply_entry.pack(side="left", fill="both", expand=True)
reply_sb.pack(side="right", fill="y")
_empfang_text_widgets.append(reply_entry) _empfang_text_widgets.append(reply_entry)
def _reply_wheel(_e):
reply_entry.yview_scroll(int(-1 * (_e.delta / 120)), "units")
return "break"
reply_entry.bind("<MouseWheel>", _reply_wheel)
_reply_placeholder = "Antwort eingeben oder diktieren..." _reply_placeholder = "Antwort eingeben oder diktieren..."
reply_entry.insert("1.0", _reply_placeholder) reply_entry.insert("1.0", _reply_placeholder)
reply_entry.configure(fg="#aaa") reply_entry.configure(fg="#aaa")
@@ -6489,14 +6642,27 @@ WICHTIG unbedingt einhalten:
def _push_to_chat_input(text: str, *, source: str = "") -> None: def _push_to_chat_input(text: str, *, source: str = "") -> None:
"""Fuegt Text in das Chat-Eingabefeld unten ein (Platzhalter wird ersetzt).""" """Fuegt Text in das Chat-Eingabefeld unten ein (Platzhalter wird ersetzt)."""
try: try:
chunk = text.strip()
if source == "patient":
line = chunk if chunk.lower().startswith("nr.:") else f"Nr.: {_compact_nr(chunk)}"
cur = reply_entry.get("1.0", "end").strip()
empty = cur == _reply_placeholder or not cur
if empty:
reply_entry.delete("1.0", "end")
reply_entry.insert("1.0", line)
else:
reply_entry.insert("1.0", line + "\n")
reply_entry.configure(fg="#1a2a3a")
reply_entry.see("1.0")
return
cur = reply_entry.get("1.0", "end").strip() cur = reply_entry.get("1.0", "end").strip()
if cur == _reply_placeholder or not cur: if cur == _reply_placeholder or not cur:
reply_entry.delete("1.0", "end") reply_entry.delete("1.0", "end")
reply_entry.insert("1.0", text.strip()) reply_entry.insert("1.0", chunk)
else: else:
if not cur.endswith("\n"): if not cur.endswith("\n"):
reply_entry.insert("end", "\n") reply_entry.insert("end", "\n")
reply_entry.insert("end", text.strip()) reply_entry.insert("end", chunk)
reply_entry.configure(fg="#1a2a3a") reply_entry.configure(fg="#1a2a3a")
reply_entry.see("end") reply_entry.see("end")
except Exception: except Exception:
@@ -6513,8 +6679,7 @@ WICHTIG unbedingt einhalten:
except Exception: except Exception:
base_msgs = [] base_msgs = []
self._empfang_last_thread_messages = base_msgs self._empfang_last_thread_messages = base_msgs
local_extra = list(getattr(self, "_empfang_local_chat_overlay", None) or []) merged = base_msgs
merged = base_msgs + local_extra
chat_display.configure(state="normal") chat_display.configure(state="normal")
chat_display.delete("1.0", "end") chat_display.delete("1.0", "end")
for msg in merged: for msg in merged:

View File

@@ -48,19 +48,22 @@
"dermatology" "dermatology"
], ],
"ui_font_delta": 0, "ui_font_delta": 0,
"global_right_click_paste": true, "global_right_click_paste": false,
"todo_auto_open": false, "todo_auto_open": false,
"autocopy_after_diktat": true, "autocopy_after_diktat": true,
"kommentare_auto_open": true, "kommentare_auto_open": true,
"empfang_auto_open": true, "empfang_auto_open": true,
"empfang_was_open": false, "empfang_was_open": false,
"empfang_prefs": { "empfang_prefs": {
"show_patient": false, "show_patient": true,
"show_ther": false, "show_ther": true,
"show_proc": false, "show_proc": true,
"show_kom": false, "show_kom": true,
"last_patient": "", "last_patient": "24227",
"geometry": "776x779+2208+366" "geometry": "888x974+442+450",
"auto_copy_ther": true,
"auto_copy_patient": false,
"auto_copy_proc": true
}, },
"medikament_quelle": "compendium.ch", "medikament_quelle": "compendium.ch",
"diagnose_quelle": "", "diagnose_quelle": "",

View File

@@ -1,3 +1,4 @@
{ {
"⏺ Start": 16 "⏺ Start": 12,
"⏺ Aufnahme starten": 2
} }

View File

@@ -1 +1 @@
420x380+2034+450 420x380+1187+399

View File

@@ -1 +1 @@
{"A": 0, "S": 0, "O": 0, "B": 0, "D": 3, "T": 0, "P": 0} {"A": 0, "S": 0, "O": 0, "B": 0, "D": 0, "T": 0, "P": 0}

View File

@@ -1 +1 @@
{"used": 534202, "total": 1000000, "budget_dollars": 0, "used_dollars": 0} {"used": 551259, "total": 1000000, "budget_dollars": 0, "used_dollars": 0}

View File

@@ -1 +1 @@
1123 1249 2580 227 0 0 1165 1249 1337 455 0 0

View File

@@ -1,5 +1,5 @@
{ {
"version": "1.0.0", "version": "1.2.0",
"channel": "stable", "channel": "stable",
"release_date": "2026-03-14", "release_date": "2026-03-14",
"minimum_supported_version": "1.0.0", "minimum_supported_version": "1.0.0",