329 lines
15 KiB
Python
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)
|