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
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:**
- `desktop_viewer.py` — Tkinter, flaches Design, drei unabhängige Modi
(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
`default_combined()` zurück. Bei jedem "komisches Layout"-Report hier immer
ALLE 3 Profile prüfen, nicht nur das gemeldete.
- **Randloses Fenster (seit 2026-08-15):** Die Titelleiste ist per reinem Tk
`overrideredirect(True)` ausgeblendet -- **niemals** per ctypes/WinAPI
nachhelfen (kein `SetWindowLongW`/`ShowWindow` auf das eigene Fenster),
siehe Speicher-Notiz "keine Selbstversteck-Fenstertricks": genau diese
Kombination hat in einem anderen Projekt Bitdefender-Fehlalarme
ausgelöst. Konsequenzen, die mitgebaut werden müssen: kein
Taskleisten-Eintrag (Rückweg nur über das Tray-Icon), kein Ziehen am
Rahmen (Kopfzeile ist deshalb per `<B1-Motion>` verschiebbar), keine
System-Buttons (eigene `✕`/`—` in der Kopfzeile, beide legen ins Tray)
und **keine Resize-Ränder**.
- **Der Größen-Anfasser hängt per `place()` an der Fensterecke, nicht
gepackt am Ende des Inhalts.** Gepackt verschwindet er, sobald das Fenster
kleiner als der Inhalt ist -- also exakt dann, wenn man ihn zum
Vergrößern bräuchte. `MIN_W/MIN_H` begrenzen das Verkleinern.
- **WinAPI: was verboten bleibt und was nicht.** Verboten sind
**Fenstermanipulation am eigenen Fenster** (`SetWindowLongW`,
`ShowWindow`) und **globale Eingabehooks** (`SetWindowsHookEx`) — genau
diese Kombination hat laut Speicher-Notiz „keine Selbstversteck-
Fenstertricks" in einem anderen Projekt Bitdefender-Fehlalarme ausgeloest.
Erlaubt und seit 2026-08-29 in Benutzung sind **passive Layout-Abfragen**
(`MapVirtualKeyW`, `GetKeyNameTextW` in `versapad_keylayout.py`): sie
lesen nur die Tastaturbelegung, fassen weder Fenster noch Prozesse noch
den Eingabestrom an; die offizielle VersaGUI (C#) benutzt
`GetKeyNameText()` fuer denselben Zweck. Die Grenze verlaeuft also nicht
bei „ctypes", sondern bei „greift ins System ein".
- **Normales Fenster mit Titelleiste (seit 2026-08-29).** Von 2026-08-15
bis dahin lief das Fenster randlos (`overrideredirect(True)`) — das kostet
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
haben (`CARD_H` 84 → 114) braucht das Fenster ~960px, sonst liegen
Encoder-Bereich und Fußzeile unterhalb des Rands. Wer `CARD_H` ändert,
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
(2026-08-28):** `_ModalDialog` baut sich `withdraw()`n auf, positioniert
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:
- Erkannt wird nur, was das fokussierte Fenster erreicht — Win+L,
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
die Erkennung nicht und wird von ihr nicht ersetzt.
- 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.
- Makro-Schritte filtern das Win-Bit weg (`allow_win=False`), passend zur
Firmware-Regel oben.
- Zeichentasten-Labels zeigen eine US-Layout-Näherung, nicht das tatsächlich
aktive Windows-Tastaturlayout (die echte VersaGUI löst das über
`GetKeyNameText()`, das bilden wir ohne WinAPI-Call nicht nach).
**Dieselbe Näherung gilt für die Tastendruck-Erkennung:** HID-Keycodes
sind physische US-Tastenpositionen, Tk liefert aber nur keysym/VK-Code des
aktiven Layouts — die Position (Scan-Code) wäre dafür nötig und ist ohne
WinAPI nicht zu bekommen. Auf deutschem Layout landen Y und Z deshalb
vertauscht auf dem Board, und die einzige echte Keysym-Kollision (`minus`:
US-Position 0x2D vs. deutsche Position 0x38) ist bewusst zugunsten der
US-Position aufgelöst, damit Erkennung und Dropdown-Beschriftung dasselbe
sagen. Bei gehaltenem Shift zählt zuerst der VK-Code, weil der keysym dann
das verschobene Zeichen ist (deutsch: Shift+7 → `slash`, was sonst
fälschlich auf Taste 0x38 zeigen würde).
- **Tastenbeschriftungen kommen vom aktiven Layout** (seit 2026-08-29).
`versapad_data.hid_key_name()` fragt fuer Zeichentasten
`GetKeyNameTextW` (deutsch: HID 0x1C → „Z", 0x34 → „ä"); fuer alles
andere bleiben die gepflegten deutschen Namen aus `_SPECIAL_KEYS`
(„Enter", „Bild↑", „Num5") — die lesen sich besser als das, was Windows
liefert („EINGABE", „4 (ZEHNERTASTATUR)"). Ohne Layout-Abfrage
(Nicht-Windows) faellt alles auf die alte US-Naeherung zurueck.
Konsequenzen, die man kennen muss:
- Bestehende Belegungen aendern ihre **Anzeige**, nicht ihre Daten. Wer
frueher im Dropdown „Z" gewaehlt hat, bekam 0x1D — das steht so in der
Config und zeigt jetzt wahrheitsgemaess „Y", weil es auf dieser Tastatur
ein Y tippt. Das ist keine Regression, sondern der sichtbar gewordene
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
Fremdthread (Serial-Thread, pystray-Thread) — hat in einer früheren
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
- **Tastendruck-Erkennung** — statt die Taste im Dropdown zu suchen,
„⌨ Taste drücken" klicken und die gewünschte Kombination einfach
drücken (Modifier inklusive). Läuft über das Dialogfenster, nicht über
einen System-Hook — vom System abgefangene Kombinationen (Win+L,
Strg+Alt+Entf) kommen deshalb nicht an, und das Dropdown bleibt zum
Nachkorrigieren daneben stehen
drücken (Modifier inklusive). Erkannt wird die *physische* Taste, nicht
das Zeichen — Y/Z, ÄÖÜ, `#`, `+` und `ß` landen also richtig auf dem
Board, auch auf deutschem Layout. Läuft über das Dialogfenster, nicht
ü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-Tabelle vom Board. Schritte einzeln erfassen oder die ganze Folge
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)
- **Tray-Icon** — minimiert/schließt ins Tray statt in die Taskleiste, wie
die offizielle VersaGUI
- **Randloses Fenster** — ohne Windows-Titelleiste, dafür kompakter Kopf
(Modus-Checkboxen direkt neben dem Titel). Verschieben durch Ziehen an
der Kopfzeile, Größe ändern am Anfasser unten rechts, `✕`/`—` legen ins
Tray. Einen Taskleisten-Eintrag gibt es dadurch nicht — das Fenster kommt
über das Tray-Icon zurück.
- **Normales Fenster mit Taskleisten-Eintrag** — Titelleiste, Alt+Tab,
Aero-Snap und Größe ändern am Rahmen funktionieren nativ. `✕` beendet
nicht, sondern legt ins Tray (wie die offizielle VersaGUI — das Programm
läuft im Hintergrund weiter); Minimieren geht normal in die Taskleiste.
- **Tastenkürzel im Hauptfenster**`Strg+1/2/3` Profil wechseln, `F2`
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
@ -123,7 +127,8 @@ automatisch zuerst nach `%TEMP%` und baut nur dort.
| 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_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 |
@ -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
einen globalen WinAPI-Hook — vom System abgefangene Kombinationen (Win+L,
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
aktive Tastatur-Layout. Das betrifft auch die Tastendruck-Erkennung:
HID-Keycodes sind physische US-Tastenpositionen, erkennbar ist ohne
WinAPI aber nur das Zeichen des aktiven Layouts — auf deutschem Layout
landen Y und Z deshalb vertauscht auf dem Board. Das Ergebnis steht immer
sichtbar im Dropdown und lässt sich dort korrigieren
- Ein Wechsel des Tastaturlayouts im laufenden Programm wird nicht bemerkt
(die Tastennamen werden einmal beim ersten Zugriff ermittelt) — Neustart
hilft. Unter Nicht-Windows fällt die Beschriftung auf eine
US-Layout-Näherung zurück
- Unsignierte `.exe` — kann von Antivirus/Smart App Control blockiert
werden; `--onedir` (statt `--onefile`) verringert das Risiko, verhindert
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). |
| 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
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
andere vom Betriebssystem abgefangene Kombinationen kommen nie an. Ein
globaler `SetWindowsHookEx`-Hook würde sie sehen, ist aber ausgeschlossen
(AV-Fehlalarm-Risiko, siehe `AGENTS.md`).
2. **Die Zuordnung ist eine US-Layout-Näherung.** HID-Keycodes bezeichnen
physische Tastenpositionen des US-Layouts; Tk liefert nur `keysym` und
Windows-Virtual-Key-Code, beide vom *aktiven* Layout abgeleitet. Die
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.
Zweite und wichtigere Folge betrifft die *Zuordnung*. HID-Keycodes
bezeichnen **physische Tastenpositionen**: das Board sendet eine Position,
erst Windows macht daraus über das aktive Layout ein Zeichen. Wer die
Zuordnung über das *Zeichen* aufbaut, dreht diese Kette falsch herum — auf
deutschem Layout landete dadurch jedes Y auf der Z-Taste des Boards und
ÄÖÜ/#/+ waren gar nicht erfassbar. `versapad_data.tk_event_to_hid()` löst
deshalb in dieser Reihenfolge auf:
Details der Zuordnungstabellen: `versapad_data.tk_event_to_hid()` und die
`_TK_*`/`_WIN_VK_TO_HID`-Dicts darüber.
1. **Benannte Tasten über den Tk-keysym** (Enter, Escape, Pfeile, F-Tasten,
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