From 8a00b3ae369d7f8ff29cc3e1935cebe6796c22c0 Mon Sep 17 00:00:00 2001 From: Julian Appel Date: Fri, 28 Aug 2026 15:33:38 +0200 Subject: [PATCH] Document key capture, dialog placement and copy/paste AGENTS.md: die Regel "Auswahl ist ein Dropdown, kein Tastendruck-Capture" ist ueberholt -- Capture gibt es jetzt, aber weiterhin ohne WinAPI-Hook, und genau diese Grenze muss bleiben. Dazu die Layout-Naeherung (Y/Z vertauscht auf deutschem Layout, minus-Kollision bewusst zugunsten der US-Position aufgeloest), die neuen UI-Regeln (Dialogposition, Grab-Rueckgabe bei verschachtelten Dialogen, feste Panel-Hoehe, In-Memory-Ablage fuers Kopieren) und ein Verifikationsrezept fuer UI-Aenderungen inkl. der DPI-Falle beim Screenshot-Vergleich. Der Deferred-Work-Eintrag zum Capture faellt weg. README.md: neue Bedienung in den Features, Tastenkuerzel-Uebersicht, und die Einschraenkung praezisiert (nicht mehr "Dropdown statt Capture", sondern was die Fenster-basierte Erkennung nicht sehen kann). docs/architecture.md: _ModalDialog/_KeyCapture in der UI-Schicht, plus zwei neue Abschnitte zu den Grenzen der Erkennung und zur Kopier-Ablage. Co-Authored-By: Claude Opus 5 --- AGENTS.md | 82 ++++++++++++++++++++++++++++++++++++++++---- README.md | 36 ++++++++++++++++--- docs/architecture.md | 46 ++++++++++++++++++++++++- 3 files changed, 152 insertions(+), 12 deletions(-) diff --git a/AGENTS.md b/AGENTS.md index 1d3a001..4971069 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -16,6 +16,11 @@ Nutzerorientierte Einführung: [`README.md`](README.md). - `versapad_data.py` — Decoding für Anzeige: JSON laden, HID-Keycode/ Consumer-Usage/Modifier → lesbarer Text, Grid-Geometrie (`index = spalte*5+reihe`), `hid_key_choices()`/`consumer_choices()`. + Seit 2026-08-28 zusätzlich `tk_event_to_hid()` (Tk-Tastendruck → + HID-Keycode+Modifier, siehe Tastendruck-Erkennung unten) und + `macro_slot_choices()`/`macro_slot_from_choice()` für die Slot-Auswahl. + `action_label()`/`annotate_profile()` nehmen die Makrotabelle optional + entgegen und zeigen dann statt „Makro (Slot 7)" die echte Tastenfolge. `app_dir()` liefert das Verzeichnis für die eigene Config (`versapad_combined.DEFAULT_PATH`) — bei der `.exe` der Installations- ordner, sonst der Projektordner, siehe Installierbarkeit-Notiz unten. @@ -59,7 +64,9 @@ Nutzerorientierte Einführung: [`README.md`](README.md). (Nur lesen / Live-Sync / Programmiermodus, siehe Kritische Domänenregeln), Tray-Icon statt Taskleisten-Minimierung, Info-Button mit MCP-Doku. - `action_dialog.py` — Bearbeiten-Dialoge (`ActionEditDialog`, - `MacroStepsDialog`) für den Programmiermodus. + `MacroStepsDialog`) für den Programmiermodus. Gemeinsame Basis + `_ModalDialog` (Positionierung über dem Elternfenster, Enter/Escape, + Verteilung der Tastenevents) und `_KeyCapture` (Tastendruck-Erkennung). **MCP-Server:** - `versapad_mcp_server.py` — registriert als projektgebundener MCP-Server @@ -246,12 +253,66 @@ Protokoll) liegt in `docs/`, siehe „Dokumentation und Verifikation" unten. 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. -- HID-Tasten-Auswahl im Programmiermodus ist ein Dropdown, kein - Tastendruck-Capture (bewusst — kein WinAPI-Hook, um keinen AV-Fehlalarm - wie bei den Fensterverstecktricks in anderen Projekten zu riskieren). +- **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 + `deiconify()`t. Ohne das legt Tk jeden Toplevel bei `+0+0` an — bei 20 + Tasten hintereinander wandert der Blick jedes Mal in die linke obere Ecke. + Das `withdraw()` gehört zwingend dazu, sonst blitzt der Dialog dort auf, + bevor er springt. +- **Verschachtelte Dialoge geben den Grab zurück:** `MacroStepsDialog` läuft + im `ActionEditDialog`. `_finish()` ruft `grab_release()`, was den Grab des + *aufrufenden* Dialogs mit wegnimmt — `run()` setzt ihn deshalb am Ende + wieder, wenn der Parent ein `_ModalDialog` ist. +- **Panel-Höhe im `ActionEditDialog` ist fix** (`grid_propagate(False)`): + sonst springt die Fenstergröße bei jedem Typwechsel (Keine/Taste/Makro/…) + und OK/Abbrechen wandern unter dem Mauszeiger weg. +- **Kopieren/Einfügen zwischen Tasten** (Rechtsklickmenü bzw. Strg+C/Strg+V + auf der Karte unter dem Mauszeiger) benutzt eine reine In-Memory-Ablage + (`self._clip`), **nicht** die System-Zwischenablage — dort liegen + Action-Dicts, kein Text. Immer `copy.deepcopy()`, sonst teilen sich zwei + Tasten dasselbe Dict und eine spätere Bearbeitung ändert beide. „Leeren" + behält die Notiz (gleiche Regel wie beim Typwechsel im Dialog, siehe + Notizen-Bullet oben). Eine kopierte LED-Farbe auf einen Encoder + einzufügen ist wirkungslos (Encoder haben keine eigene LED) — das ist + Absicht, kein Fehler. +- Die Toolbar des Programmiermodus ist bei der Standardbreite (790px) fast + voll — zusätzliche Hinweistexte dort kurz halten, sonst werden sie rechts + abgeschnitten. +- **Tastendruck-Erkennung (seit 2026-08-28, auf expliziten User-Wunsch):** + Tasten lassen sich im Bearbeiten-Dialog per „⌨ Taste drücken" erfassen, + Makro-Schritte zusätzlich als Folge am Stück („⏺ Folge aufnehmen"). + Umgesetzt **ausschließlich über Tk-Fenster-Events** (``/ + `` auf dem Dialog-Toplevel, ausgewertet in + `versapad_data.tk_event_to_hid()`) — **weiterhin kein WinAPI-Hook** + (`SetWindowsHookEx` o.ä.), aus demselben AV-Fehlalarm-Grund wie bei den + Fensterverstecktricks. Wer hier auf einen globalen Hook „aufrüstet", + 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. + - 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 + *plus* `event.state`), weil ein KeyRelease bei Fokuswechsel verloren + gehen kann. + - `_KeyCapture` hängt an einem `tk.Label`, nicht an einem `tk.Button`: + Buttons reagieren per Klassen-Binding selbst auf Leertaste/Enter und + 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). - 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 @@ -334,8 +395,6 @@ selbst vorgegeben (`SAction.data`), dort beibehalten statt umzubenennen. explizit vom User abgelehnt ("lass uns weg"), Live-Sync bleibt read-only - Profilnamen aufs Board schreiben — technisch unmöglich (kein Platz im Firmware-Struct), bleibt lokal -- Tastendruck-Capture statt Dropdown im Programmiermodus — bewusst - vermieden (WinAPI-Hook-Risiko) - Hintergrund-Thread für "Vom Board laden"/"Zum Board übertragen" — laufen aktuell synchron im UI-Thread (kurzzeitiges Einfrieren möglich) - Vorgefertigte `.exe` im Repo/als Release-Asset — bewusst nicht committet @@ -363,6 +422,17 @@ Prüfungen vor einem Commit an Binärformat/Protokoll: ```bash python -m py_compile *.py ``` + +Für UI-Änderungen (Dialoge, Tastendruck-Erkennung, Kopieren/Einfügen) hat +sich zusätzlich bewährt, ein Wegwerf-Skript im Scratchpad zu fahren, das die +Dialoge ohne Board aufbaut, Tk-Events als kleine Fake-Event-Objekte +(`keysym`/`keycode`/`state`) durchreicht und das Ergebnis-Dict prüft — die +komplette Capture- und Copy/Paste-Logik ist so ohne Klicken verifizierbar. +Wichtig dabei: `versapad_combined.DEFAULT_PATH` vorher auf eine Temp-Datei +umbiegen, sonst schreibt `_autosave_combined()` in die echte Nutzer-Config. +Für Screenshots gilt: Tk rechnet in logischen Pixeln, `ImageGrab` liefert +physische — bei aktiver Windows-Skalierung (hier 125%) sonst ein zu kleiner +Ausschnitt, der wie ein Layout-Fehler aussieht. Danach ein Live-Testskript gegen ein angeschlossenes Board laufen lassen (read → unpack → pack → Bytevergleich, siehe Existing-Codebase-Regel) — es gibt keine automatisierten Unit-Tests dafür, die Verifikation läuft diff --git a/README.md b/README.md index 29e1ea0..bcbb275 100644 --- a/README.md +++ b/README.md @@ -30,9 +30,27 @@ falls gewünscht. und schaltet die Ansicht automatisch mit - **Programmiermodus** — Zellen anklicken und bearbeiten (Taste, Medientaste, Makro, Profilwechsel, LED-Farbe/Animation), direkt aufs Board schreiben - oder als Datei speichern + oder als Datei speichern. Der Dialog öffnet über dem Hauptfenster, + 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 - **Makro-Editor** — bis zu 8 Schritte pro Slot, liest/schreibt die echte - Makro-Tabelle vom Board + Makro-Tabelle vom Board. Schritte einzeln erfassen oder die ganze Folge + am Stück aufnehmen („⏺ Folge aufnehmen"). Die Slot-Auswahl listet alle 32 + Slots samt Inhalt, statt sie einzeln durchklicken zu müssen +- **Makros im Grid lesbar** — eine Makro-Belegung zeigt die tatsächliche + Tastenfolge (`Makro 3: Strg+C → Strg+V`) statt nur der Slot-Nummer; das + gilt auch in der Browser-Ansicht und in den MCP-Antworten +- **Farb-Schnellwahl** — zwölf Grundfarben direkt in der LED-Zeile des + Dialogs, der System-Farbdialog nur noch für den Rest („mehr…") +- **Kopieren/Einfügen zwischen Tasten** — Rechtsklick auf eine Karte im + Programmiermodus: Belegung und/oder Farbe kopieren und auf andere Tasten + anwenden, oder die Belegung leeren. Strg+C/Strg+V wirken auf die Karte + unter dem Mauszeiger - **Notizen** — freier Text pro Button/Encoder-Aktion, was sie tatsächlich tut (z.B. "Speichern in Fusion 360"), zusätzlich zur automatischen Beschriftung ("Strg+S"). Rein lokal wie Profilnamen, geht nie aufs Board, @@ -46,6 +64,9 @@ falls gewünscht. 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. +- **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. ## Voraussetzungen @@ -163,10 +184,15 @@ rechts im Fenster, oder direkt in `versapad_mcp_server.py`. gleichzeitig mit der offiziellen VersaGUI laufen (die läuft dauerhaft als Tray-App weiter, auch wenn nur ihr Konfigurationsfenster geschlossen wird — für Parallelbetrieb muss sie über ihr Tray-Menü beendet werden) -- HID-Tasten-Auswahl im Programmiermodus ist ein Dropdown, kein - Tastendruck-Capture (bewusst, um keinen WinAPI-Hook zu brauchen) +- 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 + 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 - Unsignierte `.exe` — kann von Antivirus/Smart App Control blockiert werden; `--onedir` (statt `--onefile`) verringert das Risiko, verhindert es aber nicht diff --git a/docs/architecture.md b/docs/architecture.md index 0c5dfc0..c16e23f 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -93,7 +93,17 @@ auf. umschaltbaren Modi (siehe „Modi" unten), Tray-Icon, Info-Dialog mit MCP-Doku. - **`action_dialog.py`** — modale Bearbeiten-Dialoge (`ActionEditDialog`, - `MacroStepsDialog`) für den Programmiermodus. + `MacroStepsDialog`) für den Programmiermodus, auf gemeinsamer Basis + `_ModalDialog`: + - positioniert sich beim Öffnen mittig über dem aufrufenden Fenster + (Aufbau `withdraw()`n, `_center_on_parent()`, dann `deiconify()`); + - bindet ``/`` auf dem Toplevel und verteilt sie: + entweder an eine laufende Tastendruck-Aufnahme, sonst als + Enter = OK / Escape = Abbrechen; + - `_KeyCapture` schaltet ein Label in den Aufnahmemodus und schickt jeden + Tastendruck durch `versapad_data.tk_event_to_hid()` — reine + Tk-Fenster-Events, **kein globaler Tastaturhook** (siehe „Grenzen der + Tastendruck-Erkennung" unten). ### MCP-Server @@ -145,6 +155,40 @@ 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 + +Die Erkennung im Bearbeiten-Dialog nutzt ausschließlich Tk-Events des +fokussierten Fensters. Daraus folgt zweierlei, und beides ist bewusst so: + +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. + +Details der Zuordnungstabellen: `versapad_data.tk_event_to_hid()` und die +`_TK_*`/`_WIN_VK_TO_HID`-Dicts darüber. + +### Kopieren/Einfügen zwischen Tasten + +Rechtsklick auf eine Karte im Programmiermodus (bzw. `Strg+C`/`Strg+V` auf +der Karte unter dem Mauszeiger) kopiert Belegung und/oder LED-Farbe auf +andere Tasten. Die Ablage ist eine reine In-Memory-Struktur in +`desktop_viewer.VersaPadViewer._clip` (`{"action": …, "led": …}`), **nicht** +die System-Zwischenablage — dort lägen nur Textrepräsentationen, hier +werden ganze Action-Dicts übertragen. Eingefügt wird immer eine +`copy.deepcopy()`, damit zwei Tasten nicht dasselbe Dict teilen. Encoder +haben keine eigene LED; eine kopierte Farbe auf einen Encoder einzufügen ist +deshalb wirkungslos. + Tk-Aufrufe passieren nie direkt aus dem Serial- oder Tray-Hintergrundthread — Ergebnisse landen in einer `queue.Queue`, der Main-Thread holt sie per `after()`-Polling ab (Absturzrisiko bei Cross-Thread-Tk-Zugriff, siehe