# -*- coding: utf-8 -*- """ Globaler Empfang-Pinsel (Maus-Hook + Clipboard) — gemeinsame Basis fuer Tkinter-Dialog „An Empfang senden“ und Desktop-WebView-Huelle. Keine Netz-/Server- oder Log-Ausgabe von Patientendaten. """ from __future__ import annotations import re import sys import threading import time import tkinter as tk from typing import Any, Callable from aza_persistence import _win_clipboard_get try: from pynput.keyboard import Controller as KbdController, Key, KeyCode except ImportError: KbdController = None # type: ignore[misc, assignment] Key = None # type: ignore[misc, assignment] KeyCode = None # type: ignore[misc, assignment] try: from pynput.mouse import Button as MouseButton, Listener as MouseListener _HAS_PYNPUT_MOUSE = True except ImportError: MouseButton = None # type: ignore[misc, assignment] MouseListener = None # type: ignore[misc, assignment] _HAS_PYNPUT_MOUSE = False _SMART_LISTENER: list[Any] = [None] _SMART_RESET_UI: list[Callable[[], None] | None] = [None] # Set bei Abbruch (Pinsel togglen); verhindert Pick-Erkennung nach dem Stop. _PICK_SUPERSEEDED: threading.Event = threading.Event() def compact_and_validate_patient_nr_pick_text(raw: str) -> str: """ Extrahiert eine plausible Patienten-ID (2–12 Ziffern), konservativ. Ganze Saetze / sehr langer Text werden nicht uebernommen. """ if not raw: return "" s = str(raw).strip() if not s or len(s) > 220: return "" compact_ws = re.sub(r"\s+", "", s) norm = re.sub(r"[\s\-\/\u00ad\.\u202f]+", "", compact_ws) if not norm: return "" letters = sum(1 for ch in norm if ch.isalpha()) if letters > 4 and len(norm) > 28: m = re.search(r"\d{2,12}", norm) return m.group(0) if m else "" m = re.search(r"\d{2,12}", norm) return m.group(0) if m else "" def _smart_pick_run_ui_reset_only() -> None: fn = _SMART_RESET_UI[0] _SMART_RESET_UI[0] = None if fn: try: fn() except Exception: pass def stop_global_patient_nr_pick() -> None: _PICK_SUPERSEEDED.set() lis = _SMART_LISTENER[0] if lis is not None: try: lis.stop() except Exception: pass _SMART_LISTENER[0] = None _smart_pick_run_ui_reset_only() def _grab_thread_finish() -> None: """Nach Clipboard-Polling: Listener ist durch return False schon inactive.""" _SMART_LISTENER[0] = None if not _PICK_SUPERSEEDED.is_set(): _smart_pick_run_ui_reset_only() _PICK_SUPERSEEDED.set() def start_global_patient_nr_pick( on_clipboard_pick: Callable[[str], None], *, reset_pick_ui: Callable[[], None] | None = None, armed_ui: Callable[[], None] | None = None, owner_tk=None, schedule_ui: Callable[[Callable[[], None]], None] | None = None, ) -> str: """ Startet einen globalen Pick-Zyklus (ein Freigabe-Event, dann Clipboard). Returns: "started" — Hoerer aktiv, auf Markierung/Doppelklick warten. "cancelled" — Vorheriger Zyklus wurde abgebrochen (UI-Reset ausgefuehrt). "unavailable" — pynput/Tastatur nicht nutzbar. """ if _SMART_LISTENER[0] is not None: stop_global_patient_nr_pick() return "cancelled" if ( not _HAS_PYNPUT_MOUSE or MouseListener is None or MouseButton is None or KbdController is None or Key is None or KeyCode is None ): return "unavailable" _PICK_SUPERSEEDED.clear() try: old_clip = "" if sys.platform == "win32": old_clip = (_win_clipboard_get() or "").strip() if not old_clip and owner_tk is not None: try: old_clip = owner_tk.clipboard_get().strip() except tk.TclError: old_clip = "" except Exception: old_clip = "" _SMART_RESET_UI[0] = reset_pick_ui if armed_ui: try: armed_ui() except Exception: pass def schedule(fn: Callable[[], None]) -> None: if schedule_ui is not None: schedule_ui(fn) else: # Nicht im pynput-Listener-Thread ausfuehren (lis.stop() / Clipboard wuerden riskieren). threading.Thread(target=fn, daemon=True).start() started_ts = time.time() mouse_was_pressed = False press_time = 0.0 def on_click(x: object, y: object, button: object, pressed: bool) -> bool | None: nonlocal mouse_was_pressed, press_time assert MouseButton is not None if button != MouseButton.left: return None if pressed: if time.time() - started_ts > 0.3: mouse_was_pressed = True press_time = time.time() return None if not mouse_was_pressed: return None mouse_was_pressed = False hold_duration = time.time() - press_time 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() -> None: cur = "" try: for _attempt in range(8): cur = "" if sys.platform == "win32": cur = (_win_clipboard_get() or "").strip() if not cur and owner_tk is not None: try: cur = owner_tk.clipboard_get().strip() except tk.TclError: cur = "" if cur and cur != old_clip: break time.sleep(0.06) if cur and cur != old_clip: if not _PICK_SUPERSEEDED.is_set(): try: on_clipboard_pick(cur) except Exception: pass except Exception: pass _grab_thread_finish() schedule(grab) return False assert MouseListener is not None ml = MouseListener(on_click=on_click) _SMART_LISTENER[0] = ml ml.start() return "started"