VersaGUI-py/action_dialog.py

329 lines
15 KiB
Python

"""
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)."""
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
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=(12, 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}
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)