Resolve captured keys by physical position, not by character
Die Tastendruck-Erkennung ging bisher ueber das erzeugte Zeichen (keysym bzw. Virtual-Key). Das dreht die Kette falsch herum: HID-Keycodes SIND physische Tastenpositionen -- das Board sendet eine Position, erst Windows macht daraus ueber das aktive Layout ein Zeichen. Auf deutschem Layout landete deshalb jedes Y auf der Z-Taste des Boards und umgekehrt, und AeOeUe/#/+/ss waren gar nicht erfassbar. versapad_keylayout.py loest das ueber MapVirtualKeyW (Virtual-Key -> Scan-Code) und eine layoutunabhaengige Scan-Code-zu-HID-Tabelle. GetKeyNameTextW liefert dazu den Namen, den eine Taste auf dem aktiven Layout traegt, so dass Dropdown und Grid "Strg+Z" zeigen, wenn Strg+Z gemeint ist. Beides sind passive Layout-Abfragen -- kein Hook, keine Fenstermanipulation, also nicht das, was AGENTS.md verbietet (die offizielle VersaGUI benutzt GetKeyNameText fuer denselben Zweck). Benannte Tasten laufen weiterhin zuerst ueber den Tk-keysym, und das ist kein Schoenheitsfehler: MapVirtualKeyW liefert fuer die Pfeiltasten denselben Scan-Code wie fuer ihre Numpad-Zwillinge (gemessen: VK_LEFT und VK_NUMPAD4 beide 0x4B). Ohne diesen Schritt waeren beide nicht zu unterscheiden. Namen sind zugleich Schluessel (Dropdown, hid_key_code_for_name), muessen also eindeutig bleiben -- auf deutschem Layout heisst HID 0x31 "#", ein Name den bisher HID 0x32 trug. Der Layoutname gewinnt, der verdraengte US-Name wird gekennzeichnet statt verworfen. Bestehende Belegungen aendern damit ihre Anzeige, nicht ihre Daten. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
8a00b3ae36
commit
42897d553c
2 changed files with 252 additions and 27 deletions
127
versapad_data.py
127
versapad_data.py
|
|
@ -8,6 +8,8 @@ Kein Schreibzugriff auf die JSONs -- reines Lesen/Anzeigen.
|
|||
import json
|
||||
import os
|
||||
|
||||
import versapad_keylayout as kl
|
||||
|
||||
NUM_PROFILES = 3
|
||||
|
||||
# Groesse der globalen Makrotabelle (SMacroTable, siehe versapad_protocol.
|
||||
|
|
@ -228,29 +230,41 @@ def tk_event_to_hid(keysym, keycode, state, held_modifier=0):
|
|||
Modifier (siehe TK_MODIFIER_KEYSYMS) -- beides wird verodert, damit ein
|
||||
verlorenes KeyRelease die Erkennung nicht verfaelscht.
|
||||
|
||||
Bei gehaltenem Shift zaehlt zuerst der Virtual-Key-Code: der keysym ist
|
||||
dann das *verschobene* Zeichen (deutsches Layout: Shift+7 -> "slash",
|
||||
was sonst faelschlich auf die Taste 0x38 zeigen wuerde), der VK-Code
|
||||
bleibt derselbe wie ohne Shift.
|
||||
Aufloesungsreihenfolge, und warum genau so:
|
||||
|
||||
Layout-Vorbehalt (derselbe wie bei den Dropdown-Labels, siehe
|
||||
_SPECIAL_KEYS): HID-Keycodes sind US-Tastenpositionen, Tk liefert aber
|
||||
nur Zeichen/VK-Codes des aktiven Layouts -- die physische Position
|
||||
(Scan-Code) waere dafuer noetig und ist ohne WinAPI nicht zu bekommen.
|
||||
Auf deutschem Layout landen Y und Z deshalb vertauscht auf dem Board.
|
||||
Das Ergebnis ist im Dialog sichtbar (Dropdown + Modifier-Checkboxen
|
||||
werden gefuellt) und laesst sich dort von Hand korrigieren."""
|
||||
shifted = bool(state & 0x0001) or bool(held_modifier & 0x02)
|
||||
if shifted:
|
||||
code = _WIN_VK_TO_HID.get(keycode)
|
||||
if code is None:
|
||||
code = tk_keysym_to_hid(keysym)
|
||||
else:
|
||||
code = tk_keysym_to_hid(keysym)
|
||||
if code is None:
|
||||
code = _WIN_VK_TO_HID.get(keycode)
|
||||
1. **Benannte Tasten ueber den keysym** (Enter, Pfeile, F-Tasten,
|
||||
Numpad, Entf ...). Die sind layoutunabhaengig eindeutig -- und der
|
||||
Weg ueber den Scan-Code waere hier sogar gefaehrlich: Windows liefert
|
||||
fuer die Pfeiltasten denselben Scan-Code wie fuer ihre
|
||||
Numpad-Zwillinge (gemessen: VK_LEFT und VK_NUMPAD4 beide 0x4B).
|
||||
2. **Zeichentasten ueber die physische Position**
|
||||
(`versapad_keylayout.hid_for_vk()`, Virtual-Key -> Scan-Code -> HID).
|
||||
HID-Keycodes SIND Positionen; alles, was ueber das erzeugte Zeichen
|
||||
geht, ist auf nicht-US-Layouts falsch. Genau hier lag der Fehler, der
|
||||
auf deutschem Layout Y und Z vertauscht hat und AeOeUe/#/+ gar nicht
|
||||
erfassbar machte. Der Virtual-Key ist ausserdem unabhaengig davon, ob
|
||||
Shift oder AltGr mitgehalten wird.
|
||||
3. **Naeherung ohne WinAPI** (keysym-Zeichentabelle, dann VK-Tabelle) --
|
||||
nur relevant, wenn `versapad_keylayout` nicht verfuegbar ist
|
||||
(Nicht-Windows, kein ctypes). Auf dieser Ebene bleibt es bei der
|
||||
US-Layout-Naeherung inklusive vertauschtem Y/Z; bei gehaltenem Shift
|
||||
zaehlt dort zuerst der VK-Code, weil der keysym dann das verschobene
|
||||
Zeichen ist (deutsch: Shift+7 -> "slash").
|
||||
"""
|
||||
if keysym in TK_MODIFIER_KEYSYMS:
|
||||
return None # Modifier sind nie das Ziel, sie setzen nur Bits
|
||||
|
||||
code = _TK_NAMED_KEYSYMS.get(keysym)
|
||||
if code is None:
|
||||
code = kl.hid_for_vk(keycode)
|
||||
if code is None:
|
||||
if state & 0x0001 or held_modifier & 0x02:
|
||||
code = _WIN_VK_TO_HID.get(keycode) or tk_keysym_to_hid(keysym)
|
||||
else:
|
||||
code = tk_keysym_to_hid(keysym) or _WIN_VK_TO_HID.get(keycode)
|
||||
if code is None:
|
||||
return None
|
||||
|
||||
modifier = held_modifier
|
||||
for bit, mod in TK_STATE_MODIFIER_BITS:
|
||||
if state & bit:
|
||||
|
|
@ -269,9 +283,55 @@ ANIM_LABELS = {
|
|||
}
|
||||
|
||||
|
||||
_display_name_cache = None
|
||||
|
||||
|
||||
def _display_key_names():
|
||||
"""{hid_code: Anzeigename} -- Zeichentasten mit dem Namen des aktiven
|
||||
Windows-Layouts, alles andere mit der gepflegten deutschen Bezeichnung
|
||||
aus _SPECIAL_KEYS ("Enter", "Bild↑", "Num5"); die liest sich besser als
|
||||
das, was Windows liefert ("EINGABE", "4 (ZEHNERTASTATUR)").
|
||||
|
||||
Einmal ermittelt und behalten -- ein Layoutwechsel zur Laufzeit wird
|
||||
bewusst nicht nachgezogen (siehe versapad_keylayout.key_name()).
|
||||
|
||||
Die Namen sind gleichzeitig Schluessel im Dropdown und in
|
||||
hid_key_code_for_name(), muessen also eindeutig bleiben. Kollisionen
|
||||
entstehen real: auf deutschem Layout heisst HID 0x31 (US-Backslash-
|
||||
Position) schlicht "#" -- und diesen Namen trug bisher HID 0x32
|
||||
(Non-US-#). Der Layoutname gewinnt, der verdraengte US-Name wird
|
||||
gekennzeichnet statt verworfen, damit die Taste ansprechbar bleibt."""
|
||||
global _display_name_cache
|
||||
if _display_name_cache is None:
|
||||
names = dict(_SPECIAL_KEYS)
|
||||
layout = {}
|
||||
for code in sorted(_SPECIAL_KEYS):
|
||||
if code not in kl.CHARACTER_HID_CODES:
|
||||
continue
|
||||
name = kl.key_name(code)
|
||||
if name:
|
||||
layout[code] = name
|
||||
names.update(layout)
|
||||
claimed = set(layout.values())
|
||||
for code, name in list(names.items()):
|
||||
if code not in layout and name in claimed:
|
||||
names[code] = f"{name} (US-Layout)"
|
||||
elif code not in layout:
|
||||
claimed.add(name)
|
||||
_display_name_cache = names
|
||||
return _display_name_cache
|
||||
|
||||
|
||||
def hid_key_name(keycode):
|
||||
"""Anzeigename einer Taste, layoutrichtig wo es darauf ankommt
|
||||
(deutsch: 0x1C -> "Z", 0x34 -> "ä"). Ohne verfuegbare Layout-Abfrage
|
||||
(Nicht-Windows) bleibt es bei der US-Naeherung aus _SPECIAL_KEYS."""
|
||||
return _display_key_names().get(keycode, f"0x{keycode:02X}")
|
||||
|
||||
|
||||
def hid_key_choices():
|
||||
"""Sortierte [(keycode, name), ...] fuer Dropdown-Auswahl beim Editieren."""
|
||||
return sorted(_SPECIAL_KEYS.items())
|
||||
return [(code, hid_key_name(code)) for code in sorted(_SPECIAL_KEYS)]
|
||||
|
||||
|
||||
def consumer_choices():
|
||||
|
|
@ -279,15 +339,28 @@ def consumer_choices():
|
|||
return sorted(_CONSUMER_NAMES.items())
|
||||
|
||||
|
||||
_KEY_CODE_BY_NAME = {name: code for code, name in _SPECIAL_KEYS.items()}
|
||||
_CONSUMER_ID_BY_NAME = {name: cid for cid, name in _CONSUMER_NAMES.items()}
|
||||
|
||||
|
||||
def _key_code_by_name():
|
||||
"""Namen -> Keycode, Layoutnamen haben Vorrang vor den US-Namen.
|
||||
|
||||
Beide Schreibweisen bleiben gueltig, damit aeltere Aufrufe nicht brechen.
|
||||
Bei einer echten Kollision (deutsch: "Z" ist US-Position 0x1D *und*
|
||||
Layoutname von 0x1C) gewinnt bewusst das Layout: wer "Z" sagt, will die
|
||||
Taste, die auf dieser Tastatur ein Z tippt -- alles andere waere genau
|
||||
der Fehler, der hier gerade behoben wurde."""
|
||||
by_name = {name: code for code, name in _SPECIAL_KEYS.items()}
|
||||
by_name.update({name: code for code, name in _display_key_names().items()})
|
||||
return by_name
|
||||
|
||||
|
||||
def hid_key_code_for_name(name):
|
||||
"""z.B. 'S' -> 0x16. Wirft ValueError mit Vorschlaegen bei unbekanntem Namen."""
|
||||
if name not in _KEY_CODE_BY_NAME:
|
||||
raise ValueError(f"Unbekannte Taste {name!r}. Gueltige Namen: {sorted(_KEY_CODE_BY_NAME)}")
|
||||
return _KEY_CODE_BY_NAME[name]
|
||||
by_name = _key_code_by_name()
|
||||
if name not in by_name:
|
||||
raise ValueError(f"Unbekannte Taste {name!r}. Gueltige Namen: {sorted(by_name)}")
|
||||
return by_name[name]
|
||||
|
||||
|
||||
def consumer_id_for_name(name):
|
||||
|
|
@ -312,7 +385,7 @@ def macro_step_label(step):
|
|||
"""step: {'keycode','modifier'} -> z.B. 'Strg+S'. Reine Keycode/Modifier-Variante
|
||||
von hid_key_label() (dort steckt beides in einem 16-Bit data-Feld, hier getrennt)."""
|
||||
mods = [name for bit, name in MODIFIER_BITS if step["modifier"] & bit]
|
||||
key = _SPECIAL_KEYS.get(step["keycode"], f"0x{step['keycode']:02X}")
|
||||
key = hid_key_name(step["keycode"])
|
||||
return "+".join(mods + [key]) if mods else key
|
||||
|
||||
|
||||
|
|
@ -340,7 +413,7 @@ def hid_key_label(data):
|
|||
keycode = data & 0xFF
|
||||
modifier = (data >> 8) & 0xFF
|
||||
mods = [name for bit, name in MODIFIER_BITS if modifier & bit]
|
||||
key = _SPECIAL_KEYS.get(keycode, f"0x{keycode:02X}")
|
||||
key = hid_key_name(keycode)
|
||||
return "+".join(mods + [key]) if mods else key
|
||||
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue