update
This commit is contained in:
@@ -19,6 +19,7 @@ import os
|
||||
import subprocess
|
||||
import sys
|
||||
import threading
|
||||
import time
|
||||
from pathlib import Path
|
||||
from urllib.parse import parse_qs, urlencode, urlparse, urlunparse
|
||||
|
||||
@@ -124,7 +125,7 @@ def _focus_other_empfang_host_window() -> bool:
|
||||
return True
|
||||
buf = ctypes.create_unicode_buffer(260)
|
||||
user32.GetWindowTextW(hwnd, buf, 260)
|
||||
if buf.value != "AzA-Empfang":
|
||||
if not (buf.value == "AzA-Empfang" or buf.value.startswith("AzA-Empfang")):
|
||||
return True
|
||||
pid = wintypes.DWORD()
|
||||
user32.GetWindowThreadProcessId(hwnd, ctypes.byref(pid))
|
||||
@@ -197,7 +198,13 @@ class EmpfangWebviewApi:
|
||||
self._window = window
|
||||
|
||||
def toggle_on_top(self):
|
||||
"""Wechselt always-on-top zuverlaessig: pywebview-Setter UND Win32 SetWindowPos."""
|
||||
"""Wechselt always-on-top: persistiert sofort, fuehrt OS-Setter NICHT-BLOCKIEREND aus.
|
||||
|
||||
Wichtig: gibt sofort den neuen Booleschen Zustand zurueck. Die WinForms-/Win32-
|
||||
Aufrufe (`win.on_top = X`, SetWindowPos) laufen in einem Daemon-Thread, damit
|
||||
der pywebview-API-Thread und damit auch der UI-Thread NICHT durch synchrone
|
||||
WinForms-Marshalls blockiert werden. Kein SetForegroundWindow im Pin-Pfad.
|
||||
"""
|
||||
if not self._pin_lock.acquire(blocking=False):
|
||||
return self._on_top
|
||||
try:
|
||||
@@ -207,40 +214,117 @@ class EmpfangWebviewApi:
|
||||
self._pin_lock.release()
|
||||
|
||||
_save_shell_pin_on_top(new_val)
|
||||
print(
|
||||
f"[EMPFANG_TOPMOST] stage=manual_toggle requested={new_val}",
|
||||
flush=True,
|
||||
)
|
||||
|
||||
win = self._window
|
||||
|
||||
def _apply_async() -> None:
|
||||
err_type = ""
|
||||
try:
|
||||
if win is not None:
|
||||
win.on_top = new_val
|
||||
except Exception as exc_a:
|
||||
err_type = type(exc_a).__name__
|
||||
try:
|
||||
_apply_win32_topmost(new_val)
|
||||
except Exception as exc_b:
|
||||
err_type = err_type or type(exc_b).__name__
|
||||
if err_type:
|
||||
print(
|
||||
f"[EMPFANG_TOPMOST] stage=apply_error err_type={err_type}",
|
||||
flush=True,
|
||||
)
|
||||
else:
|
||||
print(
|
||||
f"[EMPFANG_TOPMOST] stage=manual_toggle ok=True value={new_val}",
|
||||
flush=True,
|
||||
)
|
||||
|
||||
try:
|
||||
win = self._window
|
||||
if win is not None:
|
||||
win.on_top = new_val
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
_apply_win32_topmost(new_val)
|
||||
except Exception:
|
||||
pass
|
||||
threading.Thread(target=_apply_async, daemon=True).start()
|
||||
except Exception as exc:
|
||||
print(
|
||||
f"[EMPFANG_TOPMOST] stage=manual_toggle_thread_spawn_failed err_type={type(exc).__name__}",
|
||||
flush=True,
|
||||
)
|
||||
return new_val
|
||||
|
||||
def get_on_top(self):
|
||||
return self._on_top
|
||||
|
||||
def apply_saved_on_top(self) -> dict:
|
||||
"""Persistierten Pin-Zustand einmal anwenden (nach pywebviewready vom JS aufgerufen).
|
||||
"""Pin-Wiederanwendung beim Start: NICHT-BLOCKIEREND, optional verzoegert.
|
||||
|
||||
Synchron im pywebview-API-Thread, KEIN Daemon-Thread, KEIN loaded-Hook.
|
||||
Setzt zusaetzlich SetWindowPos(HWND_TOPMOST), weil pywebview/WinForms
|
||||
unter WebView2 sonst oft kein echtes TopMost erzwingt.
|
||||
Frueher wurde ``win.on_top = X`` und SetWindowPos(HWND_TOPMOST) hier
|
||||
SYNCHRON im API-/UI-Aufrufpfad gemacht. Das konnte ``AZA_EmpfangShell.exe``
|
||||
beim WebView2-Coldstart einfrieren ("Keine Rueckmeldung"), bevor die
|
||||
Chat-Oberflaeche geladen war.
|
||||
|
||||
Verhalten ab jetzt:
|
||||
* Standard: NICHTS tun (Pin wird erst auf manuellen Klick erneut angewendet).
|
||||
* Optional verzoegert NEU anwenden, wenn Umgebungsvariable
|
||||
AZA_EMPFANG_AUTO_TOPMOST_ON_START=1 gesetzt ist. Dann erfolgt der
|
||||
tatsaechliche Aufruf in einem kurzen Daemon-Thread nach ~3 s.
|
||||
* Kein SetForegroundWindow, kein Loop, kein Spam.
|
||||
* Liefert sofort {"ok": True, "on_top": <gespeicherter Zustand>} zurueck.
|
||||
"""
|
||||
saved = bool(self._on_top)
|
||||
auto_env = (os.environ.get("AZA_EMPFANG_AUTO_TOPMOST_ON_START") or "").strip()
|
||||
do_auto = auto_env == "1" and saved
|
||||
|
||||
if not do_auto:
|
||||
print(
|
||||
f"[EMPFANG_TOPMOST] stage=startup_apply_skipped saved={saved} "
|
||||
f"auto_env_set={int(auto_env == '1')}",
|
||||
flush=True,
|
||||
)
|
||||
return {"ok": True, "on_top": saved, "applied": False}
|
||||
|
||||
win = self._window
|
||||
|
||||
def _delayed_apply() -> None:
|
||||
try:
|
||||
time.sleep(3.0)
|
||||
except Exception:
|
||||
pass
|
||||
err_type = ""
|
||||
try:
|
||||
if win is not None:
|
||||
win.on_top = saved
|
||||
except Exception as exc_a:
|
||||
err_type = type(exc_a).__name__
|
||||
try:
|
||||
_apply_win32_topmost(saved)
|
||||
except Exception as exc_b:
|
||||
err_type = err_type or type(exc_b).__name__
|
||||
if err_type:
|
||||
print(
|
||||
f"[EMPFANG_TOPMOST] stage=apply_error err_type={err_type}",
|
||||
flush=True,
|
||||
)
|
||||
else:
|
||||
print(
|
||||
f"[EMPFANG_TOPMOST] stage=startup_apply_done value={saved}",
|
||||
flush=True,
|
||||
)
|
||||
|
||||
try:
|
||||
win = self._window
|
||||
if win is not None:
|
||||
win.on_top = self._on_top
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
_apply_win32_topmost(self._on_top)
|
||||
except Exception:
|
||||
pass
|
||||
return {"ok": True, "on_top": bool(self._on_top)}
|
||||
threading.Thread(target=_delayed_apply, daemon=True).start()
|
||||
print(
|
||||
f"[EMPFANG_TOPMOST] stage=startup_apply_scheduled saved={saved} delay_s=3",
|
||||
flush=True,
|
||||
)
|
||||
except Exception as exc:
|
||||
print(
|
||||
f"[EMPFANG_TOPMOST] stage=startup_apply_thread_spawn_failed "
|
||||
f"err_type={type(exc).__name__}",
|
||||
flush=True,
|
||||
)
|
||||
|
||||
return {"ok": True, "on_top": saved, "applied": False, "scheduled": True}
|
||||
|
||||
def open_minichat(self, href: str) -> dict:
|
||||
"""Zweites schmales Empfang-Fenster (subprocess wie Desktop-Starter).
|
||||
@@ -391,6 +475,32 @@ class EmpfangWebviewApi:
|
||||
stop_global_patient_nr_pick()
|
||||
return {"ok": True}
|
||||
|
||||
def play_notification_sound(self, kind: str = "") -> dict:
|
||||
"""Spielt einen kurzen Systemton ab (Windows: winsound.MessageBeep).
|
||||
|
||||
Native, non-blocking, unabhaengig von Browser-Autoplay-Policies und
|
||||
Hintergrundtab-Drosselung. Keine Patientendaten/Chatdaten/Tokens werden
|
||||
geloggt oder verarbeitet. ``kind`` ist nur ein Hinweis (z.B. "incoming").
|
||||
"""
|
||||
def _do() -> None:
|
||||
if sys.platform != "win32":
|
||||
return
|
||||
try:
|
||||
import winsound # noqa: WPS433 (lazy)
|
||||
# MB_OK ist nicht zu laut, gut hoerbar; SND_ASYNC blockiert nicht.
|
||||
winsound.MessageBeep(winsound.MB_OK)
|
||||
except Exception as exc:
|
||||
print(
|
||||
f"[empfang] play_notification_sound failed: {type(exc).__name__}",
|
||||
flush=True,
|
||||
)
|
||||
|
||||
try:
|
||||
threading.Thread(target=_do, daemon=True).start()
|
||||
except Exception:
|
||||
pass
|
||||
return {"ok": True, "kind": str(kind or "")[:32]}
|
||||
|
||||
def bring_to_front(self) -> dict:
|
||||
"""JS-API: Empfang-Huelle in den Vordergrund holen. Nicht-blockierend.
|
||||
|
||||
@@ -418,7 +528,26 @@ class EmpfangWebviewApi:
|
||||
import ctypes # noqa: WPS433
|
||||
|
||||
user32 = ctypes.windll.user32
|
||||
hwnd = user32.FindWindowW(None, "AzA-Empfang")
|
||||
handles: list[int] = []
|
||||
my_pid = int(os.getpid())
|
||||
|
||||
@ctypes.WINFUNCTYPE(ctypes.c_bool, ctypes.c_void_p, ctypes.c_void_p)
|
||||
def _enum(hwnd, _lp):
|
||||
if not user32.IsWindowVisible(hwnd):
|
||||
return True
|
||||
buf = ctypes.create_unicode_buffer(260)
|
||||
user32.GetWindowTextW(hwnd, buf, 260)
|
||||
t = buf.value or ""
|
||||
if not (t == "AzA-Empfang" or t.startswith("AzA-Empfang")):
|
||||
return True
|
||||
pid = ctypes.c_ulong()
|
||||
user32.GetWindowThreadProcessId(hwnd, ctypes.byref(pid))
|
||||
if int(pid.value) == my_pid and int(hwnd):
|
||||
handles.append(int(hwnd))
|
||||
return True
|
||||
|
||||
user32.EnumWindows(_enum, 0)
|
||||
hwnd = handles[0] if handles else 0
|
||||
if not hwnd:
|
||||
return
|
||||
sw_restore = 9
|
||||
@@ -591,13 +720,27 @@ def main(argv: list[str] | None = None) -> int:
|
||||
)
|
||||
return 11
|
||||
|
||||
def _window_title_for_start_url(start_url: str) -> str:
|
||||
"""Titel nach Start-URL: Arzt-Desktop-Shell vs. separate Chat-Huelle vs. manueller Test."""
|
||||
u = (start_url or "")
|
||||
low = u.lower()
|
||||
if "/empfang/shell/launch" in u and "token=" in low:
|
||||
if "target=empfang_chat_shell" in low or "target%3Dempfang_chat_shell" in low:
|
||||
return "AzA-Empfang \u00b7 Chat-H\u00fclle"
|
||||
return "AzA-Empfang \u00b7 Desktop"
|
||||
if "shell_source=aza_desktop" in u or "shell_source%3Daza_desktop" in low:
|
||||
return "AzA-Empfang \u00b7 Desktop"
|
||||
return "AzA-Empfang"
|
||||
|
||||
api = EmpfangWebviewApi()
|
||||
start_kw = {"storage_path": storage_path, "private_mode": False}
|
||||
_ico = _empfang_shell_icon_path()
|
||||
if _ico:
|
||||
start_kw["icon"] = _ico
|
||||
try:
|
||||
win = webview.create_window("AzA-Empfang", url, width=w, height=h, js_api=api)
|
||||
win = webview.create_window(
|
||||
_window_title_for_start_url(url), url, width=w, height=h, js_api=api,
|
||||
)
|
||||
api.bind_window(win)
|
||||
webview.start(**start_kw)
|
||||
return 0
|
||||
|
||||
Reference in New Issue
Block a user