# -*- 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 def _pinsel_diag_print(msg: str, enabled: bool) -> None: if enabled: print(msg, file=sys.stderr, flush=True) 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. Ablehnt GUID/UUID, hex-lastige Artefakte und viele kurze Zifferninseln in langem alphanumerischem Schnipsel (keine Roh-Uebernahme aus Clipboard). """ if not raw: return "" s = str(raw).strip() if not s or len(s) > 220: return "" compact_ws = re.sub(r"\s+", "", s) if re.search( r"[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}", compact_ws, ): return "" norm = re.sub(r"[\s\-\/\u00ad\.\u202f]+", "", compact_ws) if not norm: return "" digits = sum(1 for ch in norm if ch.isdigit()) hexish = sum(1 for ch in norm.lower() if "a" <= ch <= "f") letters = sum(1 for ch in norm if ch.isalpha()) if len(norm) >= 18 and hexish >= 6 and digits <= max(12, hexish): return "" all_matches = list(re.finditer(r"\d{2,12}", norm)) if not all_matches: return "" longest = max((m.group(0) for m in all_matches), key=len) if ( len(norm) > 24 and len(all_matches) >= 3 and max(len(m.group(0)) for m in all_matches) <= 4 ): return "" if letters > 4 and len(norm) > 28 and len(longest) <= 6 and hexish >= 4: return "" if len(longest) < 4 and len(norm) > 16 and (hexish >= 4 or letters > 6): return "" return longest 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, diagnostics: bool = False, ) -> 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() _pinsel_diag_print("[pinsel] pick cycle: cancelled (previous listener stopped)", diagnostics) return "cancelled" if not _HAS_PYNPUT_MOUSE or MouseListener is None or MouseButton is None: _pinsel_diag_print("[pinsel] unavailable: pynput mouse missing", diagnostics) return "unavailable" if KbdController is None: _pinsel_diag_print( "[pinsel] unavailable: keyboard controller missing (pynput.keyboard Import?)", diagnostics, ) return "unavailable" if Key is None: _pinsel_diag_print("[pinsel] unavailable: keyboard Key missing", diagnostics) return "unavailable" if KeyCode is None: _pinsel_diag_print("[pinsel] unavailable: keyboard KeyCode missing", diagnostics) return "unavailable" _pinsel_diag_print("[pinsel] imports_ok", diagnostics) _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 _pinsel_diag_print("[pinsel] mouse_up_seen", diagnostics) 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 as ex: _pinsel_diag_print( f"[pinsel] copy_sent failed: {type(ex).__name__}: {ex}", diagnostics, ) else: _pinsel_diag_print("[pinsel] copy_sent", diagnostics) time.sleep(0.38) def grab() -> None: cur = "" changed = False 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: changed = True break time.sleep(0.06) if changed: _pinsel_diag_print("[pinsel] clipboard_changed", diagnostics) else: _pinsel_diag_print("[pinsel] clipboard_unchanged", diagnostics) if cur and cur != old_clip: if not _PICK_SUPERSEEDED.is_set(): try: _pinsel_diag_print("[pinsel] dispatching on_clipboard_pick", diagnostics) on_clipboard_pick(cur) except Exception as ex: _pinsel_diag_print( f"[pinsel] on_clipboard_pick error: {type(ex).__name__}: {ex}", diagnostics, ) except Exception as ex: _pinsel_diag_print( f"[pinsel] grab error: {type(ex).__name__}: {ex}", diagnostics, ) _grab_thread_finish() schedule(grab) return False assert MouseListener is not None ml = MouseListener(on_click=on_click) _SMART_LISTENER[0] = ml ml.start() _pinsel_diag_print("[pinsel] mouse_listener_started", diagnostics) return "started"