""" 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 -- Win+L, Strg+Alt+Entf und aehnliche vom System abgefangene Kombinationen also nicht. Welche physische Taste gemeint ist, loest versapad_data.tk_event_to_hid() ueber den Scan-Code des aktiven Layouts auf (versapad_keylayout), nicht ueber das erzeugte Zeichen -- sonst landet auf deutschem Layout jedes Y auf der Z-Taste des Boards und AeOeUe/#/+ sind gar nicht erfassbar. Das Dropdown daneben zeigt dieselbe Taste unter ihrem Layout-Namen und bleibt als Korrekturmoeglichkeit stehen. """ 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. Ohne das oeffnet Tk jeden Toplevel bei +0+0, also bei jeder bearbeiteten Taste erneut in der Bildschirmecke -- weit weg vom Fenster, in dem gerade geklickt wurde. Begrenzt wird bewusst gegen das ELTERNFENSTER, nicht gegen `winfo_screenwidth()`: Tk meldet dort nur den Hauptbildschirm. Lag das Hauptfenster auf einem zweiten Monitor, hat genau diese Begrenzung den Dialog wieder auf den Rand des ersten Monitors gezogen. 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 -- so bleibt er in jedem Fall auf dem Monitor, auf dem gearbeitet wird.""" 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 + max(0, (pw - width) // 2) y = py + max(0, (ph - height) // 3) # etwas oberhalb der Mitte wirkt ruhiger 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)