""" Abfragen ans AKTIVE Windows-Tastaturlayout: physische Tastenposition (Scan-Code) und der Name, den diese Taste auf dem aktuellen Layout traegt. Warum ueberhaupt WinAPI, wo dieses Projekt sonst konsequent darauf verzichtet: HID-Keycodes bezeichnen **physische Tastenpositionen** (das Board sendet eine Position, erst Windows macht daraus ein Zeichen). Aus einem Tk-Event kommen aber nur `keysym` und Virtual-Key-Code -- beides ist bereits durch das Layout gefiltert. Auf deutschem Layout landete deshalb jedes Y auf der Z-Taste des Boards und umgekehrt, und AeOeUe/#/+ waren gar nicht erfassbar. Die Position ist ohne `MapVirtualKeyW` schlicht nicht zu bekommen. Abgrenzung zu den Domaenenregeln in AGENTS.md: verboten sind dort **Fenstermanipulation** (`SetWindowLongW`/`ShowWindow` aufs eigene Fenster) und **globale Tastaturhooks** (`SetWindowsHookEx`) -- genau die Aufrufe, die in einem anderen Projekt AV-Fehlalarme ausgeloest haben. Hier passiert nichts davon: `MapVirtualKeyW` und `GetKeyNameTextW` sind passive, lesende Layout-Abfragen ohne Fenster-, Prozess- oder Eingabezugriff. Die offizielle VersaGUI (C#) benutzt `GetKeyNameText()` fuer denselben Zweck. Alles hier ist optional: auf Nicht-Windows oder wenn `user32` nicht laedt, bleibt AVAILABLE False und alle Funktionen liefern None -- `versapad_data` faellt dann auf seine US-Layout-Naeherung zurueck. """ import sys # Scan-Code (PS/2 Set 1, wie MapVirtualKeyW ihn liefert) -> HID Usage Page # 0x07. Diese Tabelle ist layoutunabhaengig und damit der eigentliche Kern: # eine physische Taste hat immer denselben Scan-Code und denselben HID-Code, # egal welches Zeichen das Layout daraufschreibt. SCANCODE_TO_HID = { 0x01: 0x29, # Esc 0x02: 0x1E, 0x03: 0x1F, 0x04: 0x20, 0x05: 0x21, 0x06: 0x22, 0x07: 0x23, 0x08: 0x24, 0x09: 0x25, 0x0A: 0x26, 0x0B: 0x27, # 1-9, 0 0x0C: 0x2D, 0x0D: 0x2E, # US -/=, deutsch ss/Akut 0x0E: 0x2A, 0x0F: 0x2B, # Backspace, Tab 0x10: 0x14, 0x11: 0x1A, 0x12: 0x08, 0x13: 0x15, 0x14: 0x17, # Q W E R T 0x15: 0x1C, 0x16: 0x18, 0x17: 0x0C, 0x18: 0x12, 0x19: 0x13, # US Y U I O P 0x1A: 0x2F, 0x1B: 0x30, # US [/], deutsch Ue/+ 0x1C: 0x28, # Enter 0x1E: 0x04, 0x1F: 0x16, 0x20: 0x07, 0x21: 0x09, 0x22: 0x0A, # A S D F G 0x23: 0x0B, 0x24: 0x0D, 0x25: 0x0E, 0x26: 0x0F, # H J K L 0x27: 0x33, 0x28: 0x34, # US ;/', deutsch Oe/Ae 0x29: 0x35, # US Backtick, deutsch Zirkumflex 0x2B: 0x31, # US Backslash, deutsch # 0x2C: 0x1D, 0x2D: 0x1B, 0x2E: 0x06, 0x2F: 0x19, 0x30: 0x05, # US Z X C V B 0x31: 0x11, 0x32: 0x10, # N M 0x33: 0x36, 0x34: 0x37, # Komma, Punkt (in beiden Layouts gleich) 0x35: 0x38, # US /, deutsch - 0x37: 0x55, # Num * 0x39: 0x2C, # Leertaste 0x3A: 0x39, # Caps 0x45: 0x53, 0x46: 0x47, # NumLock, Rollen 0x47: 0x5F, 0x48: 0x60, 0x49: 0x61, 0x4A: 0x56, # Num 7 8 9 - 0x4B: 0x5C, 0x4C: 0x5D, 0x4D: 0x5E, 0x4E: 0x57, # Num 4 5 6 + 0x4F: 0x59, 0x50: 0x5A, 0x51: 0x5B, # Num 1 2 3 0x52: 0x62, 0x53: 0x63, # Num 0 . 0x56: 0x64, # ISO-Zusatztaste (deutsch <>|) 0x57: 0x44, 0x58: 0x45, # F11, F12 } for _i in range(10): SCANCODE_TO_HID[0x3B + _i] = 0x3A + _i # F1-F10 HID_TO_SCANCODE = {hid: scan for scan, hid in SCANCODE_TO_HID.items()} # Nur fuer diese HID-Codes lohnt der Layout-Name: es sind die Tasten, deren # Beschriftung sich zwischen Layouts unterscheidet (Buchstaben, Ziffern, # Satzzeichen). Fuer Enter/F5/Entf/Numpad bleiben die gepflegten deutschen # Namen aus versapad_data._SPECIAL_KEYS besser als das, was Windows liefert # ("EINGABE", "4 (ZEHNERTASTATUR)"). CHARACTER_HID_CODES = ( frozenset(range(0x04, 0x1E)) # A-Z (US-Positionen) | frozenset(range(0x1E, 0x28)) # 1-9, 0 | frozenset(range(0x2D, 0x39)) # Satzzeichen | frozenset({0x64}) # ISO-Zusatztaste ) MAPVK_VK_TO_VSC_EX = 4 AVAILABLE = False _user32 = None if sys.platform == "win32": try: import ctypes from ctypes import wintypes _user32 = ctypes.WinDLL("user32", use_last_error=True) _user32.MapVirtualKeyW.argtypes = [wintypes.UINT, wintypes.UINT] _user32.MapVirtualKeyW.restype = wintypes.UINT _user32.GetKeyNameTextW.argtypes = [wintypes.LONG, wintypes.LPWSTR, ctypes.c_int] _user32.GetKeyNameTextW.restype = ctypes.c_int AVAILABLE = True except (ImportError, OSError, AttributeError): _user32 = None # z.B. exotische Python-Builds ohne ctypes def scancode_for_vk(vk): """Virtual-Key-Code -> Scan-Code der physischen Taste, oder None. MAPVK_VK_TO_VSC_EX setzt fuer manche Tasten ein 0xE0-Praefix ins High-Byte (erweiterte Tasten), fuer die Pfeiltasten hier aber gemessen NICHT -- die liefern denselben Scan-Code wie ihre Numpad-Zwillinge. Genau deshalb laeuft die Aufloesung in versapad_data zuerst ueber die eindeutigen Tk-keysyms und benutzt diesen Weg nur fuer Zeichentasten.""" if not AVAILABLE: return None scan = _user32.MapVirtualKeyW(vk, MAPVK_VK_TO_VSC_EX) return scan or None def hid_for_vk(vk): """Virtual-Key-Code -> HID-Keycode ueber die physische Tastenposition. Das ist der eigentliche Fix gegen vertauschtes Y/Z und nicht erfassbares AeOeUe/#/+: der Umweg ueber das Zeichen entfaellt komplett.""" scan = scancode_for_vk(vk) if scan is None: return None if (scan >> 8) == 0xE0: return None # erweiterte Taste -- kommt hier nicht vor, siehe oben return SCANCODE_TO_HID.get(scan & 0xFF) def key_name(hid_code): """HID-Keycode -> Beschriftung dieser Taste auf dem aktiven Layout (deutsch: HID 0x1C -> "Z", 0x34 -> "ä"), oder None wenn nicht ermittelbar. Achtung: das Ergebnis gilt fuer das Layout, das beim Aufruf aktiv ist. Ein Layoutwechsel zur Laufzeit wird nicht bemerkt -- fuer ein Tool, das eine Tastatur konfiguriert, ist das vertretbar (der Cache in versapad_data haelt entsprechend auch nur eine Fassung).""" if not AVAILABLE: return None scan = HID_TO_SCANCODE.get(hid_code) if scan is None: return None buffer = ctypes.create_unicode_buffer(64) # lParam-Layout von GetKeyNameTextW: Bits 16-23 Scan-Code, # Bit 24 "erweiterte Taste". written = _user32.GetKeyNameTextW((scan & 0xFF) << 16, buffer, 64) if not written: return None name = buffer.value.strip() if not name: return None # Windows liefert Mehrzeichen-Namen in Grossbuchstaben ("AKUT", # "ZIRKUMFLEX") -- als Dropdown-Eintrag zwischen "A" und "ä" liest sich # Titelschreibung deutlich ruhiger. if len(name) > 1 and name == name.upper(): return name.capitalize() return name