Document the layout fix and the return of the title bar

AGENTS.md: die WinAPI-Regel praeziser gefasst -- verboten bleiben
Fenstermanipulation und globale Hooks, passive Layout-Abfragen sind es
nicht (und die offizielle VersaGUI macht dasselbe). Dazu die neue
Aufloesungsreihenfolge samt Warnung, nicht auf "Zeichen auswerten"
zurueckzubauen, die Kollisionsregel bei Tastennamen, der Mehrmonitor-Fix
und die Folgen der Titelleiste (welche Eigenbauten dadurch entfallen sind
und warum randlos nicht ohne Ruecksprache zurueckkommt).

README.md und docs/architecture.md entsprechend: Position statt Zeichen,
layoutrichtige Beschriftungen inkl. der Anzeigeaenderung an bestehenden
Belegungen, Taskleisten-Eintrag statt randlosem Fenster.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Julian Appel 2026-08-29 01:02:18 +02:00
parent e1e3d0e466
commit 78e9640dec
3 changed files with 152 additions and 61 deletions

112
AGENTS.md
View file

@ -59,6 +59,13 @@ Nutzerorientierte Einführung: [`README.md`](README.md).
`read_profile_names()` liest nur die Namen (kein Board-Zugriff, fuer `read_profile_names()` liest nur die Namen (kein Board-Zugriff, fuer
Tab-Beschriftungen im Nur-Lese-Modus, siehe Installierbarkeit-Notiz). Tab-Beschriftungen im Nur-Lese-Modus, siehe Installierbarkeit-Notiz).
- `versapad_keylayout.py` (seit 2026-08-29) — passive Abfragen ans aktive
Windows-Tastaturlayout: `hid_for_vk()` (Virtual-Key → Scan-Code → HID über
`MapVirtualKeyW`) und `key_name()` (Beschriftung über `GetKeyNameTextW`).
Optional: auf Nicht-Windows/ohne ctypes bleibt `AVAILABLE` False und
`versapad_data` faellt auf seine US-Naeherung zurueck. Zur Abgrenzung
gegen die WinAPI-Verbote siehe Domaenenregeln.
**UI:** **UI:**
- `desktop_viewer.py` — Tkinter, flaches Design, drei unabhängige Modi - `desktop_viewer.py` — Tkinter, flaches Design, drei unabhängige Modi
(Nur lesen / Live-Sync / Programmiermodus, siehe Kritische Domänenregeln), (Nur lesen / Live-Sync / Programmiermodus, siehe Kritische Domänenregeln),
@ -235,24 +242,47 @@ Protokoll) liegt in `docs/`, siehe „Dokumentation und Verifikation" unten.
`vcomb.DEFAULT_PATH`, fällt nur bei fehlender/kaputter Datei auf `vcomb.DEFAULT_PATH`, fällt nur bei fehlender/kaputter Datei auf
`default_combined()` zurück. Bei jedem "komisches Layout"-Report hier immer `default_combined()` zurück. Bei jedem "komisches Layout"-Report hier immer
ALLE 3 Profile prüfen, nicht nur das gemeldete. ALLE 3 Profile prüfen, nicht nur das gemeldete.
- **Randloses Fenster (seit 2026-08-15):** Die Titelleiste ist per reinem Tk - **WinAPI: was verboten bleibt und was nicht.** Verboten sind
`overrideredirect(True)` ausgeblendet -- **niemals** per ctypes/WinAPI **Fenstermanipulation am eigenen Fenster** (`SetWindowLongW`,
nachhelfen (kein `SetWindowLongW`/`ShowWindow` auf das eigene Fenster), `ShowWindow`) und **globale Eingabehooks** (`SetWindowsHookEx`) — genau
siehe Speicher-Notiz "keine Selbstversteck-Fenstertricks": genau diese diese Kombination hat laut Speicher-Notiz „keine Selbstversteck-
Kombination hat in einem anderen Projekt Bitdefender-Fehlalarme Fenstertricks" in einem anderen Projekt Bitdefender-Fehlalarme ausgeloest.
ausgelöst. Konsequenzen, die mitgebaut werden müssen: kein Erlaubt und seit 2026-08-29 in Benutzung sind **passive Layout-Abfragen**
Taskleisten-Eintrag (Rückweg nur über das Tray-Icon), kein Ziehen am (`MapVirtualKeyW`, `GetKeyNameTextW` in `versapad_keylayout.py`): sie
Rahmen (Kopfzeile ist deshalb per `<B1-Motion>` verschiebbar), keine lesen nur die Tastaturbelegung, fassen weder Fenster noch Prozesse noch
System-Buttons (eigene `✕`/`—` in der Kopfzeile, beide legen ins Tray) den Eingabestrom an; die offizielle VersaGUI (C#) benutzt
und **keine Resize-Ränder**. `GetKeyNameText()` fuer denselben Zweck. Die Grenze verlaeuft also nicht
- **Der Größen-Anfasser hängt per `place()` an der Fensterecke, nicht bei „ctypes", sondern bei „greift ins System ein".
gepackt am Ende des Inhalts.** Gepackt verschwindet er, sobald das Fenster - **Normales Fenster mit Titelleiste (seit 2026-08-29).** Von 2026-08-15
kleiner als der Inhalt ist -- also exakt dann, wenn man ihn zum bis dahin lief das Fenster randlos (`overrideredirect(True)`) — das kostet
Vergrößern bräuchte. `MIN_W/MIN_H` begrenzen das Verkleinern. unter Windows zwingend den Taskleisten-Eintrag. Als der ausdruecklich
gebraucht wurde, gab es nur drei Wege: Titelleiste zurueck,
`SetWindowLongW`+WS_EX_APPWINDOW (siehe Verbot oben) oder ein
unsichtbares Proxy-Fenster. Der User hat sich fuer die Titelleiste
entschieden. Damit sind ersatzlos entfallen: ziehbare Kopfzeile
(`_start_move`/`_on_move`), Groessen-Anfasser unten rechts
(`_start_resize`/`_on_resize`, ersetzt durch `self.minsize()`), die
eigenen `✕`/`—`-Knoepfe und der `<Unmap>`-Handler, der Minimieren ins Tray
umgeleitet hat. **Wer das Fenster wieder randlos machen will, nimmt dem
User den Taskleisten-Eintrag weg** — nicht ohne Ruecksprache.
- Schliessen (`✕`) legt weiterhin ins Tray statt zu beenden (wie die
offizielle VersaGUI, das Programm laeuft im Hintergrund weiter),
Minimieren geht jetzt aber ganz normal in die Taskleiste. `Escape`
minimiert ebenfalls, statt wie frueher ins Tray zu legen — mit
Taskleisten-Eintrag waere „verschwindet spurlos" die unangenehmere
Ueberraschung.
- Fensterhöhe muss zum Karteninhalt passen: seit die Karten eine Notizzeile - Fensterhöhe muss zum Karteninhalt passen: seit die Karten eine Notizzeile
haben (`CARD_H` 84 → 114) braucht das Fenster ~960px, sonst liegen haben (`CARD_H` 84 → 114) braucht das Fenster ~960px, sonst liegen
Encoder-Bereich und Fußzeile unterhalb des Rands. Wer `CARD_H` ändert, Encoder-Bereich und Fußzeile unterhalb des Rands. Wer `CARD_H` ändert,
muss `geometry()` mit anpassen. muss `geometry()` mit anpassen.
- **Dialogposition wird gegen das ELTERNFENSTER begrenzt, nie gegen
`winfo_screenwidth()` (2026-08-29):** Tk meldet dort nur den
Hauptbildschirm. Die erste Fassung hat damit geklemmt — lag das
Hauptfenster auf einem zweiten Monitor, zog genau diese Begrenzung den
Dialog zurueck an den Rand des ersten. Passt der Dialog nicht ins
Elternfenster (der Makro-Schritte-Dialog ist breiter als der
Action-Dialog), wird er an dessen linker oberer Ecke ausgerichtet statt
zentriert.
- **Dialoge öffnen über dem Hauptfenster, nicht in der Bildschirmecke - **Dialoge öffnen über dem Hauptfenster, nicht in der Bildschirmecke
(2026-08-28):** `_ModalDialog` baut sich `withdraw()`n auf, positioniert (2026-08-28):** `_ModalDialog` baut sich `withdraw()`n auf, positioniert
sich in `run()` per `_center_on_parent()` und wird erst dann sich in `run()` per `_center_on_parent()` und wird erst dann
@ -290,6 +320,23 @@ Protokoll) liegt in `docs/`, siehe „Dokumentation und Verifikation" unten.
baut genau dieses Risiko ein. Konsequenzen, die so bleiben müssen: baut genau dieses Risiko ein. Konsequenzen, die so bleiben müssen:
- Erkannt wird nur, was das fokussierte Fenster erreicht — Win+L, - Erkannt wird nur, was das fokussierte Fenster erreicht — Win+L,
Strg+Alt+Entf und andere vom System abgefangene Kombinationen nicht. Strg+Alt+Entf und andere vom System abgefangene Kombinationen nicht.
- **Welche Taste gemeint ist, wird ueber die PHYSISCHE POSITION
aufgeloest, nie ueber das erzeugte Zeichen** (2026-08-29, nach
Fehlermeldung aus der Praxis). HID-Keycodes *sind* Positionen: das Board
sendet eine Position, erst Windows macht daraus ein Zeichen. Die erste
Fassung ging ueber keysym/Zeichen und war auf deutschem Layout
entsprechend kaputt — Y und Z landeten vertauscht auf dem Board, ÄÖÜ
und #/+ waren gar nicht erfassbar. Reihenfolge jetzt: (1) benannte
Tasten ueber den keysym, (2) Zeichentasten ueber
`versapad_keylayout.hid_for_vk()`, (3) die alte Naeherung nur noch als
Fallback ohne WinAPI. **Nicht auf "Zeichen auswerten" zurueckbauen**
das ist genau der Fehler, der hier behoben wurde.
- Schritt (1) ist keine Bequemlichkeit, sondern noetig: `MapVirtualKeyW`
liefert fuer die Pfeiltasten denselben Scan-Code wie fuer ihre
Numpad-Zwillinge (gemessen: VK_LEFT und VK_NUMPAD4 beide 0x4B, das
E0-Praefix von `MAPVK_VK_TO_VSC_EX` bleibt dort aus). Ueber den
Scan-Code allein waeren Pfeiltasten nicht von Numpad-Tasten zu
unterscheiden.
- Das Dropdown bleibt daneben stehen (Korrekturmöglichkeit), es ersetzt - Das Dropdown bleibt daneben stehen (Korrekturmöglichkeit), es ersetzt
die Erkennung nicht und wird von ihr nicht ersetzt. die Erkennung nicht und wird von ihr nicht ersetzt.
- Modifier werden doppelt ermittelt (selbst mitgeführte Press/Release-Bits - Modifier werden doppelt ermittelt (selbst mitgeführte Press/Release-Bits
@ -300,19 +347,30 @@ Protokoll) liegt in `docs/`, siehe „Dokumentation und Verifikation" unten.
würden die laufende Aufnahme mit ihrem eigenen Klick beantworten. würden die laufende Aufnahme mit ihrem eigenen Klick beantworten.
- Makro-Schritte filtern das Win-Bit weg (`allow_win=False`), passend zur - Makro-Schritte filtern das Win-Bit weg (`allow_win=False`), passend zur
Firmware-Regel oben. Firmware-Regel oben.
- Zeichentasten-Labels zeigen eine US-Layout-Näherung, nicht das tatsächlich - **Tastenbeschriftungen kommen vom aktiven Layout** (seit 2026-08-29).
aktive Windows-Tastaturlayout (die echte VersaGUI löst das über `versapad_data.hid_key_name()` fragt fuer Zeichentasten
`GetKeyNameText()`, das bilden wir ohne WinAPI-Call nicht nach). `GetKeyNameTextW` (deutsch: HID 0x1C → „Z", 0x34 → „ä"); fuer alles
**Dieselbe Näherung gilt für die Tastendruck-Erkennung:** HID-Keycodes andere bleiben die gepflegten deutschen Namen aus `_SPECIAL_KEYS`
sind physische US-Tastenpositionen, Tk liefert aber nur keysym/VK-Code des („Enter", „Bild↑", „Num5") — die lesen sich besser als das, was Windows
aktiven Layouts — die Position (Scan-Code) wäre dafür nötig und ist ohne liefert („EINGABE", „4 (ZEHNERTASTATUR)"). Ohne Layout-Abfrage
WinAPI nicht zu bekommen. Auf deutschem Layout landen Y und Z deshalb (Nicht-Windows) faellt alles auf die alte US-Naeherung zurueck.
vertauscht auf dem Board, und die einzige echte Keysym-Kollision (`minus`: Konsequenzen, die man kennen muss:
US-Position 0x2D vs. deutsche Position 0x38) ist bewusst zugunsten der - Bestehende Belegungen aendern ihre **Anzeige**, nicht ihre Daten. Wer
US-Position aufgelöst, damit Erkennung und Dropdown-Beschriftung dasselbe frueher im Dropdown „Z" gewaehlt hat, bekam 0x1D — das steht so in der
sagen. Bei gehaltenem Shift zählt zuerst der VK-Code, weil der keysym dann Config und zeigt jetzt wahrheitsgemaess „Y", weil es auf dieser Tastatur
das verschobene Zeichen ist (deutsch: Shift+7 → `slash`, was sonst ein Y tippt. Das ist keine Regression, sondern der sichtbar gewordene
fälschlich auf Taste 0x38 zeigen würde). Altfehler.
- Namen sind Schluessel (Dropdown, `hid_key_code_for_name()`) und muessen
eindeutig bleiben. Es gibt echte Kollisionen: auf deutschem Layout heisst
HID 0x31 schlicht „#" — den Namen trug bisher HID 0x32. Der Layoutname
gewinnt, der verdraengte US-Name wird als „# (US-Layout)" gekennzeichnet
statt verworfen, damit die Taste ansprechbar bleibt.
- `hid_key_code_for_name()` akzeptiert weiterhin beide Schreibweisen, bei
Kollision gewinnt das Layout. Fuer den MCP-Server heisst das:
`set_button_key(key="Z")` trifft die Taste, die auf dieser Tastatur ein
Z tippt (0x1C), nicht mehr die US-Position 0x1D.
- Ein Layoutwechsel zur Laufzeit wird nicht bemerkt (Namen werden einmal
ermittelt und behalten).
- Tk-Aufrufe (`self.after()`, Widget-Konfiguration) NIE direkt aus einem - Tk-Aufrufe (`self.after()`, Widget-Konfiguration) NIE direkt aus einem
Fremdthread (Serial-Thread, pystray-Thread) — hat in einer früheren Fremdthread (Serial-Thread, pystray-Thread) — hat in einer früheren
Version einen stillen Absturz verursacht. Threads legen Ergebnisse nur in Version einen stillen Absturz verursacht. Threads legen Ergebnisse nur in

View file

@ -34,10 +34,15 @@ falls gewünscht.
Enter bestätigt, Escape bricht ab Enter bestätigt, Escape bricht ab
- **Tastendruck-Erkennung** — statt die Taste im Dropdown zu suchen, - **Tastendruck-Erkennung** — statt die Taste im Dropdown zu suchen,
„⌨ Taste drücken" klicken und die gewünschte Kombination einfach „⌨ Taste drücken" klicken und die gewünschte Kombination einfach
drücken (Modifier inklusive). Läuft über das Dialogfenster, nicht über drücken (Modifier inklusive). Erkannt wird die *physische* Taste, nicht
einen System-Hook — vom System abgefangene Kombinationen (Win+L, das Zeichen — Y/Z, ÄÖÜ, `#`, `+` und `ß` landen also richtig auf dem
Strg+Alt+Entf) kommen deshalb nicht an, und das Dropdown bleibt zum Board, auch auf deutschem Layout. Läuft über das Dialogfenster, nicht
Nachkorrigieren daneben stehen über einen System-Hook: vom System abgefangene Kombinationen (Win+L,
Strg+Alt+Entf) kommen nicht an
- **Layoutrichtige Tastennamen** — Beschriftungen kommen vom aktiven
Windows-Layout (`Strg+Z` heißt auf deutscher Tastatur auch `Strg+Z`, und
`ä`/`ö`/`ü` heißen so). Bestehende Belegungen ändern dadurch ihre
Anzeige, nicht ihre Funktion
- **Makro-Editor** — bis zu 8 Schritte pro Slot, liest/schreibt die echte - **Makro-Editor** — bis zu 8 Schritte pro Slot, liest/schreibt die echte
Makro-Tabelle vom Board. Schritte einzeln erfassen oder die ganze Folge Makro-Tabelle vom Board. Schritte einzeln erfassen oder die ganze Folge
am Stück aufnehmen („⏺ Folge aufnehmen"). Die Slot-Auswahl listet alle 32 am Stück aufnehmen („⏺ Folge aufnehmen"). Die Slot-Auswahl listet alle 32
@ -59,14 +64,13 @@ falls gewünscht.
Tool-Aufruf ändern, ohne Klicks in der GUI (siehe unten) Tool-Aufruf ändern, ohne Klicks in der GUI (siehe unten)
- **Tray-Icon** — minimiert/schließt ins Tray statt in die Taskleiste, wie - **Tray-Icon** — minimiert/schließt ins Tray statt in die Taskleiste, wie
die offizielle VersaGUI die offizielle VersaGUI
- **Randloses Fenster** — ohne Windows-Titelleiste, dafür kompakter Kopf - **Normales Fenster mit Taskleisten-Eintrag** — Titelleiste, Alt+Tab,
(Modus-Checkboxen direkt neben dem Titel). Verschieben durch Ziehen an Aero-Snap und Größe ändern am Rahmen funktionieren nativ. `✕` beendet
der Kopfzeile, Größe ändern am Anfasser unten rechts, `✕`/`—` legen ins nicht, sondern legt ins Tray (wie die offizielle VersaGUI — das Programm
Tray. Einen Taskleisten-Eintrag gibt es dadurch nicht — das Fenster kommt läuft im Hintergrund weiter); Minimieren geht normal in die Taskleiste.
über das Tray-Icon zurück.
- **Tastenkürzel im Hauptfenster**`Strg+1/2/3` Profil wechseln, `F2` - **Tastenkürzel im Hauptfenster**`Strg+1/2/3` Profil wechseln, `F2`
Profil umbenennen, `Strg+E` Programmiermodus an/aus, `Strg+C`/`Strg+V` Profil umbenennen, `Strg+E` Programmiermodus an/aus, `Strg+C`/`Strg+V`
Taste unter dem Mauszeiger kopieren/einfügen, `Esc` ins Tray. Taste unter dem Mauszeiger kopieren/einfügen, `Esc` minimieren.
## Voraussetzungen ## Voraussetzungen
@ -123,7 +127,8 @@ automatisch zuerst nach `%TEMP%` und baut nur dort.
| Datei | Zweck | | Datei | Zweck |
|---|---| |---|---|
| `versapad_data.py` | Decoding für die Anzeige: JSON laden, HID-Keycodes/Consumer-IDs/Modifier → lesbarer Text | | `versapad_data.py` | Decoding für die Anzeige: JSON laden, HID-Keycodes/Consumer-IDs/Modifier → lesbarer Text, Tastendruck → HID-Keycode |
| `versapad_keylayout.py` | Abfragen ans aktive Windows-Tastaturlayout: physische Tastenposition und Tastenname (optional, nur Windows) |
| `versapad_protocol.py` | Binäres NVM-Layout des Boards (740B Config + 512B Makros), CRC16 — pack/unpack | | `versapad_protocol.py` | Binäres NVM-Layout des Boards (740B Config + 512B Makros), CRC16 — pack/unpack |
| `versapad_serial.py` | Serial-Client: liest/schreibt Config + Makros per 8-Byte-Paket-Protokoll | | `versapad_serial.py` | Serial-Client: liest/schreibt Config + Makros per 8-Byte-Paket-Protokoll |
| `versapad_combined.py` | Ein-Datei-Format für alle 3 Profile + Makros + lokale Profilnamen | | `versapad_combined.py` | Ein-Datei-Format für alle 3 Profile + Makros + lokale Profilnamen |
@ -187,12 +192,10 @@ rechts im Fenster, oder direkt in `versapad_mcp_server.py`.
- Die Tastendruck-Erkennung läuft bewusst über das Dialogfenster statt über - Die Tastendruck-Erkennung läuft bewusst über das Dialogfenster statt über
einen globalen WinAPI-Hook — vom System abgefangene Kombinationen (Win+L, einen globalen WinAPI-Hook — vom System abgefangene Kombinationen (Win+L,
Strg+Alt+Entf) erreichen das Fenster nie und lassen sich so nicht erfassen Strg+Alt+Entf) erreichen das Fenster nie und lassen sich so nicht erfassen
- Zeichentasten-Labels zeigen eine US-Layout-Näherung, nicht das tatsächlich - Ein Wechsel des Tastaturlayouts im laufenden Programm wird nicht bemerkt
aktive Tastatur-Layout. Das betrifft auch die Tastendruck-Erkennung: (die Tastennamen werden einmal beim ersten Zugriff ermittelt) — Neustart
HID-Keycodes sind physische US-Tastenpositionen, erkennbar ist ohne hilft. Unter Nicht-Windows fällt die Beschriftung auf eine
WinAPI aber nur das Zeichen des aktiven Layouts — auf deutschem Layout US-Layout-Näherung zurück
landen Y und Z deshalb vertauscht auf dem Board. Das Ergebnis steht immer
sichtbar im Dropdown und lässt sich dort korrigieren
- Unsignierte `.exe` — kann von Antivirus/Smart App Control blockiert - Unsignierte `.exe` — kann von Antivirus/Smart App Control blockiert
werden; `--onedir` (statt `--onefile`) verringert das Risiko, verhindert werden; `--onedir` (statt `--onefile`) verringert das Risiko, verhindert
es aber nicht es aber nicht

View file

@ -155,27 +155,57 @@ Drei Checkboxen, unabhängig voneinander:
| Live-Sync | Fragt per Serial das aktuell aktive Profil ab, schaltet die Ansicht mit | Hintergrund-Thread pollt `read_active_profile()`, hält dafür den COM-Port dauerhaft offen, solange die Checkbox an ist. Schließt sich mit VersaGUI/Programmiermodus/MCP-Board-Zugriff gegenseitig aus (exklusiver Port). | | Live-Sync | Fragt per Serial das aktuell aktive Profil ab, schaltet die Ansicht mit | Hintergrund-Thread pollt `read_active_profile()`, hält dafür den COM-Port dauerhaft offen, solange die Checkbox an ist. Schließt sich mit VersaGUI/Programmiermodus/MCP-Board-Zugriff gegenseitig aus (exklusiver Port). |
| Programmiermodus | Zellen anklicken zum Bearbeiten | Lädt `self.combined` **einmalig pro Prozesslauf** beim ersten Aktivieren (bevorzugt `DEFAULT_PATH`, sonst `default_combined()`). Jede Bearbeitung speichert sofort automatisch (`_autosave_combined()`). **Achtung:** Da der Snapshot nur einmal geladen wird, sieht der Programmiermodus externe Änderungen (z.B. per MCP) erst nach einem Neustart der exe oder einem expliziten „Datei laden…“. | | Programmiermodus | Zellen anklicken zum Bearbeiten | Lädt `self.combined` **einmalig pro Prozesslauf** beim ersten Aktivieren (bevorzugt `DEFAULT_PATH`, sonst `default_combined()`). Jede Bearbeitung speichert sofort automatisch (`_autosave_combined()`). **Achtung:** Da der Snapshot nur einmal geladen wird, sieht der Programmiermodus externe Änderungen (z.B. per MCP) erst nach einem Neustart der exe oder einem expliziten „Datei laden…“. |
### Grenzen der Tastendruck-Erkennung ### Tastendruck-Erkennung: Position statt Zeichen
Die Erkennung im Bearbeiten-Dialog nutzt ausschließlich Tk-Events des Die Erkennung im Bearbeiten-Dialog nutzt ausschließlich Tk-Events des
fokussierten Fensters. Daraus folgt zweierlei, und beides ist bewusst so: fokussierten Fensters — kein globaler `SetWindowsHookEx`-Hook (siehe
`AGENTS.md`). Erste Folge: **nur was das Fenster erreicht, wird erkannt.**
Win+L, Strg+Alt+Entf und andere vom Betriebssystem abgefangene
Kombinationen kommen nie an.
1. **Nur was das Fenster erreicht, wird erkannt.** Win+L, Strg+Alt+Entf und Zweite und wichtigere Folge betrifft die *Zuordnung*. HID-Keycodes
andere vom Betriebssystem abgefangene Kombinationen kommen nie an. Ein bezeichnen **physische Tastenpositionen**: das Board sendet eine Position,
globaler `SetWindowsHookEx`-Hook würde sie sehen, ist aber ausgeschlossen erst Windows macht daraus über das aktive Layout ein Zeichen. Wer die
(AV-Fehlalarm-Risiko, siehe `AGENTS.md`). Zuordnung über das *Zeichen* aufbaut, dreht diese Kette falsch herum — auf
2. **Die Zuordnung ist eine US-Layout-Näherung.** HID-Keycodes bezeichnen deutschem Layout landete dadurch jedes Y auf der Z-Taste des Boards und
physische Tastenpositionen des US-Layouts; Tk liefert nur `keysym` und ÄÖÜ/#/+ waren gar nicht erfassbar. `versapad_data.tk_event_to_hid()` löst
Windows-Virtual-Key-Code, beide vom *aktiven* Layout abgeleitet. Die deshalb in dieser Reihenfolge auf:
physische Position (Scan-Code) wäre nötig, um das exakt aufzulösen, und
ist ohne WinAPI-Aufruf nicht verfügbar. Praktische Folge auf deutschem
Layout: Y und Z landen vertauscht auf dem Board. Das Ergebnis wird immer
ins Dropdown und in die Modifier-Checkboxen geschrieben und ist dort
korrigierbar — die Erkennung ersetzt die manuelle Auswahl nicht, sie
beschleunigt sie nur.
Details der Zuordnungstabellen: `versapad_data.tk_event_to_hid()` und die 1. **Benannte Tasten über den Tk-keysym** (Enter, Escape, Pfeile, F-Tasten,
`_TK_*`/`_WIN_VK_TO_HID`-Dicts darüber. Numpad, Entf …). Layoutunabhängig eindeutig — und hier zwingend, weil
`MapVirtualKeyW` für die Pfeiltasten denselben Scan-Code liefert wie für
ihre Numpad-Zwillinge (gemessen: VK_LEFT und VK_NUMPAD4 beide 0x4B).
2. **Zeichentasten über die physische Position**
`versapad_keylayout.hid_for_vk()`: Virtual-Key → Scan-Code
(`MapVirtualKeyW`) → HID über die layoutunabhängige Tabelle
`SCANCODE_TO_HID`. Der Virtual-Key ist unabhängig davon, ob Shift oder
AltGr mitgehalten wird.
3. **Näherung ohne WinAPI** (keysym-Zeichentabelle, dann VK-Tabelle) — nur
relevant, wenn `versapad_keylayout` nicht verfügbar ist (Nicht-Windows,
kein ctypes). Auf dieser Ebene bleibt es bei der US-Layout-Näherung
inklusive vertauschtem Y/Z.
### Tastenbeschriftungen
`versapad_data.hid_key_name()` fragt für Zeichentasten `GetKeyNameTextW`
und zeigt damit den Namen des aktiven Layouts (deutsch: HID 0x1C → „Z“,
0x34 → „ä“). Für alles andere bleiben die gepflegten deutschen Namen aus
`_SPECIAL_KEYS` („Enter“, „Bild↑“, „Num5“) — die lesen sich besser als das,
was Windows dafür liefert („EINGABE“, „4 (ZEHNERTASTATUR)“).
Diese Namen sind zugleich Schlüssel (Dropdown-Einträge,
`hid_key_code_for_name()` für den MCP-Server) und müssen eindeutig bleiben.
Echte Kollisionen kommen vor: auf deutschem Layout heißt HID 0x31 schlicht
„#“, und diesen Namen trug bisher HID 0x32. Der Layoutname gewinnt, der
verdrängte US-Name wird als „# (US-Layout)“ gekennzeichnet statt verworfen.
`hid_key_code_for_name()` akzeptiert beide Schreibweisen; bei Kollision
gewinnt das Layout, damit `set_button_key(key="Z")` die Taste trifft, die
auf dieser Tastatur ein Z tippt.
Bestehende Belegungen ändern dadurch ihre **Anzeige, nicht ihre Daten**:
eine früher über das Dropdown gesetzte „Z“ steht als 0x1D in der Config und
wird jetzt wahrheitsgemäß als „Y“ angezeigt, weil sie auf dieser Tastatur
ein Y tippt. Ein Layoutwechsel zur Laufzeit wird nicht nachgezogen.
### Kopieren/Einfügen zwischen Tasten ### Kopieren/Einfügen zwischen Tasten