""" Modale Bearbeiten-Dialoge fuer den Programmiermodus -- Pendant zu VersaGUI/src/ActionDialog.cs, aber in Tkinter und ohne Tastendruck-Capture (kein WinAPI-Hook -- stattdessen Tasten-Auswahl per Dropdown, siehe versapad_data.hid_key_choices()). 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). """ 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" 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), ] class _ModalDialog(tk.Toplevel): 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 def _finish(self, cancelled): self.cancelled = cancelled self.grab_release() self.destroy() def run(self): self.update_idletasks() self.grab_set() self.wait_window(self) return not self.cancelled 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] tk.Label(self, text="Bis zu 8 Schritte, Ausführung stoppt beim ersten leeren.", bg=BG2, fg=TEXT_DIM, font=("Segoe UI", 8)).grid( row=0, column=0, columnspan=5, sticky="w", padx=12, pady=(12, 6)) self._key_vars = [] self._mod_vars = [] for i in range(8): r = i + 1 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)") combo = ttk.Combobox(self, textvariable=key_var, values=names, width=16, state="readonly") combo.grid(row=r, column=1, padx=4, pady=2) self._key_vars.append(key_var) mods = {} for j, label in enumerate(("Strg", "Shift", "Alt")): 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) for i, step in enumerate(steps[:8]): self._key_vars[i].set(self._name_by_code.get(step["keycode"], "(leer)")) self._mod_vars[i]["Strg"].set(bool(step["modifier"] & 0x01)) self._mod_vars[i]["Shift"].set(bool(step["modifier"] & 0x02)) self._mod_vars[i]["Alt"].set(bool(step["modifier"] & 0x04)) btns = tk.Frame(self, bg=BG2) btns.grid(row=9, column=0, columnspan=5, pady=12) tk.Button(btns, text="OK", command=self._on_ok, bg=ACCENT, fg="#fff", activebackground=ACCENT, relief="flat", padx=16).pack(side="left", padx=4) tk.Button(btns, text="Abbrechen", command=lambda: self._finish(True), bg=BG, fg=TEXT, activebackground=BG, relief="flat", padx=16).pack(side="left", padx=4) 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 = ( (0x01 if self._mod_vars[i]["Strg"].get() else 0) | (0x02 if self._mod_vars[i]["Shift"].get() else 0) | (0x04 if self._mod_vars[i]["Alt"].get() else 0) ) 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=36, 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)) self._panel_row = row self._panel = tk.Frame(self, bg=BG2) self._panel.grid(row=row, column=0, columnspan=3, sticky="w", padx=12, pady=8) 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_var = tk.IntVar(value=action["data"] if action["type"] == "Macro" else 0) self._macro_preview_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 modifier = (action["data"] >> 8) & 0xFF self._hidkey_key_var.set(self._key_name_by_code.get(keycode, "(leer)")) self._hidkey_mods_init = modifier 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: led_frame = tk.Frame(self, bg=BG2) led_frame.grid(row=row, column=0, columnspan=3, sticky="w", padx=12, pady=(4, 8)); row += 1 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") tk.Button(color_row, text="Farbe wählen...", command=self._pick_color, bg=BG, fg=TEXT, activebackground=BG, relief="flat").pack(side="left", padx=8) 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) btns = tk.Frame(self, bg=BG2) btns.grid(row=row, column=0, columnspan=3, pady=14) tk.Button(btns, text="OK", command=self._on_ok, bg=ACCENT, fg="#fff", activebackground=ACCENT, relief="flat", padx=16).pack(side="left", padx=4) tk.Button(btns, text="Abbrechen", command=lambda: self._finish(True), bg=BG, fg=TEXT, activebackground=BG, relief="flat", padx=16).pack(side="left", padx=4) self._on_type_change() def _hex(self): r, g, b = self._led_color return f"#{r:02x}{g:02x}{b:02x}" def _pick_color(self): result = colorchooser.askcolor(color=self._hex(), title="LED-Farbe") if result and result[0]: r, g, b = (int(c) for c in result[0]) self._led_color = (r, g, b) self._swatch.configure(bg=self._hex()) def _on_type_change(self): for w in self._panel.winfo_children(): w.destroy() t = self._type_var.get() if t == "HidKey": row1 = tk.Frame(self._panel, bg=BG2); row1.pack(anchor="w", pady=2) self._hidkey_mods = {} for label, bit in (("Strg", 0x01), ("Shift", 0x02), ("Alt", 0x04), ("Win", 0x08)): 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) 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") names = [n for _, n in vp.consumer_choices()] ttk.Combobox(row1, textvariable=self._consumer_var, values=names, width=20, state="readonly").pack(side="left", padx=6) elif t == "Macro": row1 = tk.Frame(self._panel, bg=BG2); row1.pack(anchor="w", pady=2) tk.Label(row1, text="Slot (0-31):", bg=BG2, fg=TEXT_DIM, font=("Segoe UI", 9)).pack(side="left") tk.Spinbox(row1, from_=0, to=31, textvariable=self._macro_slot_var, width=5, command=self._refresh_macro_preview).pack(side="left", padx=6) tk.Button(row1, text="Schritte bearbeiten...", command=self._edit_macro_steps, bg=BG, fg=TEXT, activebackground=BG, relief="flat").pack(side="left", padx=8) row2 = tk.Frame(self._panel, bg=BG2); row2.pack(anchor="w", pady=(4, 0)) tk.Label(row2, textvariable=self._macro_preview_var, bg=BG2, fg=TEXT_DIM, font=("Segoe UI", 8), wraplength=340, justify="left").pack(anchor="w") self._refresh_macro_preview() 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 _refresh_macro_preview(self): slot = self._macro_slot_var.get() steps = self._macros[slot] if 0 <= slot < len(self._macros) else [] self._macro_preview_var.set(f"Slot {slot}: {vp.macro_slot_label(steps)}") def _edit_macro_steps(self): slot = self._macro_slot_var.get() 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_preview() def _on_ok(self): t = self._type_var.get() if t == "None": action = {"type": "None", "data": 0} elif t == "HidKey": name = self._hidkey_key_var.get() keycode = self._key_code_by_name.get(name, 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_var.get()} elif t == "ProfileSwitch": name = self._profile_switch_var.get() data = next(v for n, v in PROFILE_SWITCH_CHOICES if n == name) action = {"type": "ProfileSwitch", "data": data} 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)