""" Modale Bearbeiten-Dialoge fuer den Programmiermodus -- Pendant zu VersaGUI/src/ActionDialog.cs, in Tkinter. ActionEditDialog: Action-Typ + Daten + optional LED (nur MX-Buttons). MacroStepsDialog: bis zu 8 Schritte (Taste + Strg/Shift/Alt), passend zur echten GUI ("8 Step-Buttons + je Strg/Shift/Alt-Checkboxen", kein Win). Tastendruck-Erkennung (_KeyCapture): Tasten lassen sich statt per Dropdown auch einfach druecken. Das laeuft ueber ganz normale Tk-Fokus-Events des Dialogfensters -- KEIN globaler WinAPI-Tastaturhook (siehe Domaenenregel "keine Selbstversteck-/Hook-Fenstertricks"). Konsequenz: erkannt wird nur, was das fokussierte Fenster erreicht, und die HID-Zuordnung ist dieselbe US-Layout-Naeherung wie bei den Dropdown-Labels (siehe versapad_data.tk_event_to_hid). Das Dropdown bleibt deshalb daneben stehen, damit ein falsch erkanntes Zeichen von Hand korrigiert werden kann. """ import tkinter as tk from tkinter import colorchooser, ttk import versapad_data as vp BG = "#1e2129" BG2 = "#14161b" TEXT = "#e8e8ec" TEXT_DIM = "#8a8d98" ACCENT = "#3a6ff0" CAPTURE_BG = "#c04a2a" TYPE_CHOICES = [ ("None", "Keine"), ("HidKey", "Taste (HID)"), ("HidConsumer", "Medientaste"), ("Macro", "Makro"), ("ProfileSwitch", "Profilwechsel"), ] PROFILE_SWITCH_CHOICES = [ ("Nächstes Profil (Zyklus)", 0xFFFF), ("Profil 1", 0), ("Profil 2", 1), ("Profil 3", 2), ] # Schnellzugriff direkt in der LED-Zeile -- der Umweg ueber den # System-Farbdialog lohnt sich fuer die paar Standardfarben nicht. # ("aus" ist Schwarz: LED bleibt dunkel, die Firmware kennt kein # separates Enable-Flag.) QUICK_COLORS = [ ("aus", (0, 0, 0)), ("weiß", (255, 255, 255)), ("rot", (255, 0, 0)), ("orange", (255, 80, 0)), ("gelb", (255, 200, 0)), ("grün", (0, 255, 0)), ("türkis", (0, 255, 160)), ("cyan", (0, 200, 255)), ("blau", (0, 64, 255)), ("violett", (128, 0, 255)), ("magenta", (255, 0, 200)), ("rosa", (255, 128, 128)), ] MOD_CHECKBOXES = (("Strg", 0x01), ("Shift", 0x02), ("Alt", 0x04), ("Win", 0x08)) MACRO_MOD_CHECKBOXES = MOD_CHECKBOXES[:3] # Firmware kennt in Makros kein Win class _KeyCapture: """Macht ein Label zum "Taste drücken"-Schalter: solange er aktiv ist, wandert jeder Tastendruck im Dialog nicht ins Widget, sondern durch versapad_data.tk_event_to_hid() in ein (keycode, modifier)-Paar. on_result(keycode, modifier) wird im Tk-Main-Thread aufgerufen. repeat=True laesst die Aufnahme nach einem Treffer weiterlaufen (fuer "Folge aufnehmen" im Makro-Dialog), sonst endet sie nach dem ersten. allow_win=False filtert das Win-Bit weg (Makro-Schritte). """ def __init__(self, dialog, label, on_result, idle_text, allow_win=True, repeat=False): self.dialog = dialog self.label = label self.on_result = on_result self.idle_text = idle_text self.allow_win = allow_win self.repeat = repeat self.active = False self._held = 0 label.configure(text=idle_text, cursor="hand2") label.bind("", lambda e: self.toggle()) def toggle(self): self.stop() if self.active else self.start() def start(self): self.active = True self._held = 0 self.dialog.begin_capture(self) self.label.configure(text="… jetzt drücken (Klick = Stopp)", bg=CAPTURE_BG, fg="#fff") self.label.focus_set() def stop(self): if not self.active: return self.active = False self._held = 0 self.dialog.end_capture(self) self.label.configure(text=self.idle_text, bg=BG, fg=TEXT) def handle_key(self, event, pressed): """Vom Dialog aufgerufen, solange diese Aufnahme aktiv ist. Gibt immer "break" zurueck -- waehrend der Aufnahme darf kein Tastendruck als Text im Notizfeld oder als Dialog-Shortcut landen.""" mod = vp.TK_MODIFIER_KEYSYMS.get(event.keysym) if mod is not None: # Modifier selbst sind nie das Ziel, sie sammeln nur Bits -- # ein KeyRelease sieht der Dialog nicht immer (Fokuswechsel), # das state-Feld im Treffer-Event faengt das ab. self._held = (self._held | mod) if pressed else (self._held & ~mod) return "break" if not pressed: return "break" hit = vp.tk_event_to_hid(event.keysym, event.keycode, event.state, self._held) if hit is None: return "break" # nicht zuordenbar -> einfach weiter warten keycode, modifier = hit if not self.allow_win: modifier &= ~0x08 if not self.repeat: self.stop() self.on_result(keycode, modifier) return "break" class _ModalDialog(tk.Toplevel): """Basis: oeffnet mittig ueber dem aufrufenden Fenster (nicht in der Bildschirmecke), Enter = OK, Escape = Abbrechen, und verteilt Tastendruecke an eine ggf. laufende _KeyCapture.""" def __init__(self, parent, title): super().__init__(parent) self.title(title) self.configure(bg=BG2) self.transient(parent) self.resizable(False, False) self.cancelled = True self._active_capture = None # Erst unsichtbar aufbauen, in run() positionieren und dann zeigen -- # sonst blitzt der Dialog kurz in der linken oberen Bildschirmecke auf. self.withdraw() self.bind("", self._on_key_press) self.bind("", self._on_key_release) self.protocol("WM_DELETE_WINDOW", lambda: self._finish(True)) # ── Tastendruck-Aufnahme ────────────────────────────────────────── def begin_capture(self, capture): if self._active_capture is not None and self._active_capture is not capture: self._active_capture.stop() self._active_capture = capture def end_capture(self, capture): if self._active_capture is capture: self._active_capture = None def _on_key_press(self, event): if self._active_capture is not None: return self._active_capture.handle_key(event, pressed=True) if event.keysym in ("Return", "KP_Enter"): self._on_ok() return "break" if event.keysym == "Escape": self._finish(True) return "break" return None def _on_key_release(self, event): if self._active_capture is not None: return self._active_capture.handle_key(event, pressed=False) return None def _on_ok(self): """Von Enter und vom OK-Knopf -- Unterklassen ueberschreiben das.""" self._finish(False) def _finish(self, cancelled): if self._active_capture is not None: self._active_capture.stop() self.cancelled = cancelled self.grab_release() self.destroy() # ── Positionierung + Ablauf ─────────────────────────────────────── def _center_on_parent(self): """Mittig ueber dem Elternfenster, aber immer vollstaendig auf dem Bildschirm. Ohne das oeffnet Tk jeden Toplevel bei +0+0, also bei jeder bearbeiteten Taste erneut in der Bildschirmecke -- weit weg vom Hauptfenster, in dem gerade geklickt wurde.""" parent = self.master width, height = self.winfo_reqwidth(), self.winfo_reqheight() try: px, py = parent.winfo_rootx(), parent.winfo_rooty() pw, ph = parent.winfo_width(), parent.winfo_height() except tk.TclError: px = py = 0 pw, ph = self.winfo_screenwidth(), self.winfo_screenheight() x = px + (pw - width) // 2 y = py + (ph - height) // 3 # etwas oberhalb der Mitte wirkt ruhiger x = max(0, min(x, self.winfo_screenwidth() - width)) y = max(0, min(y, self.winfo_screenheight() - height)) self.geometry(f"+{x}+{y}") def run(self): self.update_idletasks() self._center_on_parent() self.deiconify() self.grab_set() self.focus_force() self.wait_window(self) # Ein verschachtelter Dialog (Makro-Schritte aus dem Action-Dialog) # nimmt beim Schliessen den Grab mit -- ohne Rueckgabe waere der # aufrufende Dialog danach nicht mehr modal. parent = self.master if isinstance(parent, _ModalDialog) and parent.winfo_exists(): parent.grab_set() parent.focus_force() return not self.cancelled # ── gemeinsame kleine Bausteine ─────────────────────────────────── def _capture_label(self, parent, on_result, idle_text="⌨ Taste drücken", allow_win=True, repeat=False): """Bewusst ein Label statt tk.Button: Buttons reagieren per Klassen-Binding selbst auf Leertaste/Enter und wuerden die laufende Aufnahme mit ihrem eigenen Klick beantworten.""" label = tk.Label(parent, bg=BG, fg=TEXT, font=("Segoe UI", 9), padx=10, pady=3, relief="flat") _KeyCapture(self, label, on_result, idle_text, allow_win=allow_win, repeat=repeat) return label def _button_row(self, row, columnspan, ok_text="OK"): btns = tk.Frame(self, bg=BG2) btns.grid(row=row, column=0, columnspan=columnspan, pady=14) tk.Button(btns, text=f"{ok_text} (Enter)", command=self._on_ok, bg=ACCENT, fg="#fff", activebackground=ACCENT, relief="flat", padx=16).pack(side="left", padx=4) tk.Button(btns, text="Abbrechen (Esc)", command=lambda: self._finish(True), bg=BG, fg=TEXT, activebackground=BG, relief="flat", padx=16).pack(side="left", padx=4) return btns class MacroStepsDialog(_ModalDialog): """Ergebnis in self.steps nach run()==True.""" def __init__(self, parent, steps): super().__init__(parent, "Makro-Schritte") self.steps = [] key_choices = vp.hid_key_choices() self._name_by_code = {code: name for code, name in key_choices} self._code_by_name = {name: code for code, name in key_choices} names = ["(leer)"] + [name for _, name in key_choices] head = tk.Frame(self, bg=BG2) head.grid(row=0, column=0, columnspan=6, sticky="w", padx=12, pady=(12, 6)) tk.Label(head, text="Bis zu 8 Schritte, Ausführung stoppt beim ersten leeren.", bg=BG2, fg=TEXT_DIM, font=("Segoe UI", 8)).pack(anchor="w") tk.Label(head, text="Einzeln erfassen mit ⌨ je Zeile, oder die ganze Folge am Stück " "aufnehmen. Win ist in Makros nicht möglich (Firmware).", bg=BG2, fg=TEXT_DIM, font=("Segoe UI", 8)).pack(anchor="w") tools = tk.Frame(self, bg=BG2) tools.grid(row=1, column=0, columnspan=6, sticky="w", padx=12, pady=(0, 6)) self._record_label = tk.Label(tools, bg=BG, fg=TEXT, font=("Segoe UI", 9), padx=10, pady=3) self._record = _KeyCapture(self, self._record_label, self._on_record_step, "⏺ Folge aufnehmen", allow_win=False, repeat=True) self._record_label.pack(side="left") tk.Button(tools, text="Alle leeren", command=self._clear_all, bg=BG, fg=TEXT, activebackground=BG, relief="flat", padx=10).pack(side="left", padx=6) self._record_hint = tk.Label(tools, text="", bg=BG2, fg=TEXT_DIM, font=("Segoe UI", 8)) self._record_hint.pack(side="left", padx=6) self._key_vars = [] self._mod_vars = [] for i in range(8): r = i + 2 tk.Label(self, text=f"{i + 1}.", bg=BG2, fg=TEXT_DIM, font=("Segoe UI", 9)).grid(row=r, column=0, padx=(12, 4), pady=2, sticky="e") key_var = tk.StringVar(value="(leer)") ttk.Combobox(self, textvariable=key_var, values=names, width=16, state="readonly").grid(row=r, column=1, padx=4, pady=2) self._key_vars.append(key_var) mods = {} for j, (label, _bit) in enumerate(MACRO_MOD_CHECKBOXES): v = tk.BooleanVar(value=False) tk.Checkbutton(self, text=label, variable=v, bg=BG2, fg=TEXT, selectcolor=BG, activebackground=BG2, activeforeground=TEXT, font=("Segoe UI", 8)).grid(row=r, column=2 + j, padx=2, pady=2, sticky="w") mods[label] = v self._mod_vars.append(mods) self._capture_label( self, lambda code, mod, idx=i: self._apply_step(idx, code, mod), idle_text="⌨", allow_win=False, ).grid(row=r, column=5, padx=(6, 12), pady=2) for i, step in enumerate(steps[:8]): self._apply_step(i, step["keycode"], step["modifier"]) self._button_row(row=10, columnspan=6) def _apply_step(self, index, keycode, modifier): self._key_vars[index].set(self._name_by_code.get(keycode, "(leer)")) for label, bit in MACRO_MOD_CHECKBOXES: self._mod_vars[index][label].set(bool(modifier & bit)) def _clear_all(self): for i in range(8): self._apply_step(i, 0, 0) self._record_hint.configure(text="") def _on_record_step(self, keycode, modifier): """Aufnahmemodus: jeder Tastendruck fuellt den naechsten freien Schritt. Nach dem 8. stoppt die Aufnahme selbst (mehr Schritte kennt SMacroTable nicht).""" free = next((i for i in range(8) if self._key_vars[i].get() == "(leer)"), None) if free is None: self._record.stop() self._record_hint.configure(text="alle 8 Schritte belegt") return self._apply_step(free, keycode, modifier) self._record_hint.configure(text=f"Schritt {free + 1} aufgenommen") if free == 7: self._record.stop() def _on_ok(self): steps = [] for i in range(8): name = self._key_vars[i].get() if name == "(leer)": break # Firmware: keycode=0 beendet die Sequenz -> Rest ignorieren modifier = sum(bit for label, bit in MACRO_MOD_CHECKBOXES if self._mod_vars[i][label].get()) steps.append({"keycode": self._code_by_name[name], "modifier": modifier}) self.steps = steps self._finish(False) class ActionEditDialog(_ModalDialog): """Ergebnis in self.action / self.led nach run()==True. led bleibt None wenn led_in None war (Encoder -- keine eigene Farbe). self.action enthaelt immer ein "note"-Feld (freie Notiz, was die Aktion tut -- rein lokal wie Profilnamen, geht nie aufs Board).""" def __init__(self, parent, title, action, led, macros): super().__init__(parent, title) self._macros = macros self._led = dict(led) if led is not None else None self.action = None self.led = None row = 0 note_row = tk.Frame(self, bg=BG2) note_row.grid(row=row, column=0, columnspan=3, sticky="w", padx=12, pady=(12, 4)); row += 1 tk.Label(note_row, text="Notiz:", bg=BG2, fg=TEXT_DIM, font=("Segoe UI", 9)).pack(side="left") self._note_var = tk.StringVar(value=action.get("note", "")) tk.Entry(note_row, textvariable=self._note_var, width=52, bg=BG, fg=TEXT, insertbackground=TEXT, relief="flat").pack(side="left", padx=6) tk.Label(self, text="Aktion", bg=BG2, fg=TEXT_DIM, font=("Segoe UI", 9, "bold")).grid( row=row, column=0, sticky="w", padx=12, pady=(4, 4)); row += 1 self._type_var = tk.StringVar(value=action["type"]) type_frame = tk.Frame(self, bg=BG2) type_frame.grid(row=row, column=0, columnspan=3, sticky="w", padx=12); row += 1 for value, label in TYPE_CHOICES: tk.Radiobutton(type_frame, text=label, variable=self._type_var, value=value, command=self._on_type_change, bg=BG2, fg=TEXT, selectcolor=BG, activebackground=BG2, activeforeground=TEXT, font=("Segoe UI", 9)).pack(side="left", padx=(0, 8)) # Feste Groesse: sonst springt die Fensterhoehe (und damit die # Position von OK/Abbrechen) bei jedem Typwechsel. self._panel = tk.Frame(self, bg=BG2, width=600, height=90) self._panel.grid(row=row, column=0, columnspan=3, sticky="ew", padx=12, pady=8) self._panel.grid_propagate(False) row += 1 # HidKey-Panel key_choices = vp.hid_key_choices() self._key_name_by_code = {code: name for code, name in key_choices} self._key_code_by_name = {name: code for code, name in key_choices} self._hidkey_mods = {} self._hidkey_key_var = tk.StringVar() # HidConsumer-Panel cons_choices = vp.consumer_choices() self._cons_name_by_id = {cid: name for cid, name in cons_choices} self._cons_id_by_name = {name: cid for cid, name in cons_choices} self._consumer_var = tk.StringVar() # Macro-Panel self._macro_slot = action["data"] if action["type"] == "Macro" else 0 self._macro_choice_var = tk.StringVar() # ProfileSwitch-Panel self._profile_switch_var = tk.StringVar(value=PROFILE_SWITCH_CHOICES[0][0]) if action["type"] == "HidKey": keycode = action["data"] & 0xFF self._hidkey_key_var.set(self._key_name_by_code.get(keycode, "(leer)")) self._hidkey_mods_init = (action["data"] >> 8) & 0xFF else: self._hidkey_key_var.set("(leer)") self._hidkey_mods_init = 0 if action["type"] == "HidConsumer": self._consumer_var.set(self._cons_name_by_id.get(action["data"], cons_choices[0][1])) else: self._consumer_var.set(cons_choices[0][1]) if action["type"] == "ProfileSwitch": for name, val in PROFILE_SWITCH_CHOICES: if val == action["data"] or (val == 0xFFFF and action["data"] in (0xFFFF, 0x00FF)): self._profile_switch_var.set(name) break # LED-Panel (nur wenn showColor) self._anim_var = tk.StringVar(value=self._led["anim"] if self._led else "Static") self._period_var = tk.IntVar(value=self._led["period_ms"] if self._led else 4000) self._led_color = (self._led["r"], self._led["g"], self._led["b"]) if self._led else (80, 40, 0) if self._led is not None: row = self._build_led_panel(row) self._button_row(row=row, columnspan=3) self._on_type_change() # ── LED ─────────────────────────────────────────────────────────── def _build_led_panel(self, row): led_frame = tk.Frame(self, bg=BG2) led_frame.grid(row=row, column=0, columnspan=3, sticky="w", padx=12, pady=(4, 8)) tk.Label(led_frame, text="LED", bg=BG2, fg=TEXT_DIM, font=("Segoe UI", 9, "bold")).pack(anchor="w") color_row = tk.Frame(led_frame, bg=BG2) color_row.pack(anchor="w", pady=4) self._swatch = tk.Label(color_row, text=" ", bg=self._hex(), relief="flat", width=4) self._swatch.pack(side="left") self._hex_label = tk.Label(color_row, text=self._hex(), bg=BG2, fg=TEXT_DIM, font=("Consolas", 9), width=9) self._hex_label.pack(side="left", padx=(6, 6)) for name, rgb in QUICK_COLORS: chip = tk.Label(color_row, bg="#%02x%02x%02x" % rgb, width=2, height=1, cursor="hand2", relief="flat", highlightbackground="#3a3d47", highlightthickness=1) chip.pack(side="left", padx=1) chip.bind("", lambda e, c=rgb: self._set_color(c)) # Tk kennt keine Tooltips -- der Farbname landet in der Zeile darunter. chip.bind("", lambda e, n=name: self._color_hint.configure(text=n)) chip.bind("", lambda e: self._color_hint.configure(text="")) tk.Button(color_row, text="mehr…", command=self._pick_color, bg=BG, fg=TEXT, activebackground=BG, relief="flat", padx=8).pack(side="left", padx=(8, 0)) self._color_hint = tk.Label(led_frame, text="", bg=BG2, fg=TEXT_DIM, font=("Segoe UI", 8)) self._color_hint.pack(anchor="w") anim_row = tk.Frame(led_frame, bg=BG2) anim_row.pack(anchor="w", pady=4) tk.Label(anim_row, text="Animation:", bg=BG2, fg=TEXT_DIM, font=("Segoe UI", 9)).pack(side="left") ttk.Combobox(anim_row, textvariable=self._anim_var, values=list(vp.ANIM_LABELS.keys()), width=12, state="readonly").pack(side="left", padx=6) tk.Label(anim_row, text="Periode (ms):", bg=BG2, fg=TEXT_DIM, font=("Segoe UI", 9)).pack(side="left", padx=(12, 0)) tk.Spinbox(anim_row, from_=2, to=10000, increment=100, textvariable=self._period_var, width=7).pack(side="left", padx=6) return row + 1 def _hex(self): r, g, b = self._led_color return f"#{r:02x}{g:02x}{b:02x}" def _set_color(self, rgb): self._led_color = tuple(rgb) self._swatch.configure(bg=self._hex()) self._hex_label.configure(text=self._hex()) def _pick_color(self): result = colorchooser.askcolor(color=self._hex(), title="LED-Farbe", parent=self) if result and result[0]: self._set_color(tuple(int(c) for c in result[0])) # ── Action-Panels ───────────────────────────────────────────────── def _on_type_change(self): for w in self._panel.winfo_children(): w.destroy() t = self._type_var.get() if t == "HidKey": self._build_hidkey_panel() elif t == "HidConsumer": row1 = tk.Frame(self._panel, bg=BG2); row1.pack(anchor="w", pady=2) tk.Label(row1, text="Medienaktion:", bg=BG2, fg=TEXT_DIM, font=("Segoe UI", 9)).pack(side="left") ttk.Combobox(row1, textvariable=self._consumer_var, values=[n for _, n in vp.consumer_choices()], width=20, state="readonly").pack(side="left", padx=6) elif t == "Macro": self._build_macro_panel() elif t == "ProfileSwitch": row1 = tk.Frame(self._panel, bg=BG2); row1.pack(anchor="w", pady=2) tk.Label(row1, text="Ziel:", bg=BG2, fg=TEXT_DIM, font=("Segoe UI", 9)).pack(side="left") ttk.Combobox(row1, textvariable=self._profile_switch_var, values=[n for n, _ in PROFILE_SWITCH_CHOICES], width=22, state="readonly").pack(side="left", padx=6) self.update_idletasks() def _build_hidkey_panel(self): row1 = tk.Frame(self._panel, bg=BG2); row1.pack(anchor="w", pady=2) self._hidkey_mods = {} for label, bit in MOD_CHECKBOXES: v = tk.BooleanVar(value=bool(self._hidkey_mods_init & bit)) tk.Checkbutton(row1, text=label, variable=v, bg=BG2, fg=TEXT, selectcolor=BG, activebackground=BG2, activeforeground=TEXT, font=("Segoe UI", 9)).pack(side="left", padx=(0, 6)) self._hidkey_mods[bit] = v row2 = tk.Frame(self._panel, bg=BG2); row2.pack(anchor="w", pady=4) tk.Label(row2, text="Taste:", bg=BG2, fg=TEXT_DIM, font=("Segoe UI", 9)).pack(side="left") names = ["(leer)"] + [n for _, n in vp.hid_key_choices()] ttk.Combobox(row2, textvariable=self._hidkey_key_var, values=names, width=18, state="readonly").pack(side="left", padx=6) self._capture_label(row2, self._apply_captured_key).pack(side="left", padx=(8, 0)) tk.Label(self._panel, text="Erkennung läuft über das Dialogfenster, nicht über einen " "System-Hook: Win+L o.ä. fängt Windows selbst ab.", bg=BG2, fg=TEXT_DIM, font=("Segoe UI", 8)).pack(anchor="w") def _apply_captured_key(self, keycode, modifier): self._hidkey_key_var.set(self._key_name_by_code.get(keycode, "(leer)")) self._hidkey_mods_init = modifier for bit, var in self._hidkey_mods.items(): var.set(bool(modifier & bit)) def _build_macro_panel(self): row1 = tk.Frame(self._panel, bg=BG2); row1.pack(anchor="w", pady=2) tk.Label(row1, text="Slot:", bg=BG2, fg=TEXT_DIM, font=("Segoe UI", 9)).pack(side="left") # Dropdown listet alle 32 Slots MIT Inhalt -- vorher musste man sich # per Spinbox durch die Tabelle klicken, um ein Makro wiederzufinden. self._macro_combo = ttk.Combobox(row1, textvariable=self._macro_choice_var, values=vp.macro_slot_choices(self._macros), width=52, state="readonly") self._macro_combo.pack(side="left", padx=6) self._macro_combo.bind("<>", self._on_macro_slot_selected) tk.Button(row1, text="Schritte bearbeiten…", command=self._edit_macro_steps, bg=BG, fg=TEXT, activebackground=BG, relief="flat", padx=8).pack(side="left", padx=8) tk.Label(self._panel, text="Slots sind global (nicht pro Profil) – dasselbe Makro auf " "zwei Tasten meint denselben Slot.", bg=BG2, fg=TEXT_DIM, font=("Segoe UI", 8)).pack(anchor="w", pady=(6, 0)) self._refresh_macro_choices() def _on_macro_slot_selected(self, _event=None): self._macro_slot = vp.macro_slot_from_choice(self._macro_choice_var.get()) def _refresh_macro_choices(self): choices = vp.macro_slot_choices(self._macros) self._macro_combo.configure(values=choices) self._macro_choice_var.set(choices[self._macro_slot]) def _edit_macro_steps(self): slot = self._macro_slot current = self._macros[slot] if 0 <= slot < len(self._macros) else [] dlg = MacroStepsDialog(self, current) if dlg.run(): while len(self._macros) <= slot: self._macros.append([]) self._macros[slot] = dlg.steps self._refresh_macro_choices() # ── Ergebnis ────────────────────────────────────────────────────── def _on_ok(self): t = self._type_var.get() if t == "HidKey": keycode = self._key_code_by_name.get(self._hidkey_key_var.get(), 0) modifier = sum(bit for bit, v in self._hidkey_mods.items() if v.get()) action = {"type": "HidKey", "data": (modifier << 8) | keycode} elif t == "HidConsumer": action = {"type": "HidConsumer", "data": self._cons_id_by_name[self._consumer_var.get()]} elif t == "Macro": action = {"type": "Macro", "data": self._macro_slot} elif t == "ProfileSwitch": name = self._profile_switch_var.get() action = {"type": "ProfileSwitch", "data": next(v for n, v in PROFILE_SWITCH_CHOICES if n == name)} else: action = {"type": "None", "data": 0} action["note"] = self._note_var.get() self.action = action if self._led is not None: r, g, b = self._led_color period = max(2, self._period_var.get()) if self._anim_var.get() == "Pulse" else self._period_var.get() self.led = {"r": r, "g": g, "b": b, "brightness": self._led.get("brightness", 255), "anim": self._anim_var.get(), "period_ms": period} self._finish(False)