Rework the edit dialogs: key capture, placement, shortcuts, quick colours
Der Bearbeiten-Dialog war der langsamste Teil der Bedienung. Fuenf Punkte,
alle auf gemeinsamer Basis _ModalDialog:
- Oeffnet mittig ueber dem aufrufenden Fenster statt bei +0+0 in der
Bildschirmecke -- bei 20 Tasten hintereinander wanderte der Blick sonst
jedes Mal dorthin. Aufbau erfolgt withdraw()'t, damit er nicht kurz in der
Ecke aufblitzt, bevor er springt.
- Enter = OK, Escape = Abbrechen. Die Sequenzen sind nicht einzeln
gebunden, sondern werden aus dem <KeyPress>-Handler verteilt -- waehrend
einer laufenden Tastendruck-Aufnahme muessen sie als normale Tasten
erfassbar bleiben.
- "Taste druecken" erfasst Taste + Modifier per Tastendruck, Makro-Schritte
zusaetzlich als Folge am Stueck ("Folge aufnehmen"). _KeyCapture haengt an
einem Label, nicht an einem Button: Buttons reagieren per Klassen-Binding
selbst auf Leertaste/Enter und wuerden die Aufnahme mit ihrem eigenen
Klick beantworten. Das Dropdown bleibt daneben stehen, damit ein falsch
erkanntes Zeichen (US-Layout-Naeherung) korrigierbar ist.
- Die Makro-Slot-Auswahl ist ein Dropdown mit allen 32 Slots samt Inhalt
statt einer Spinbox, durch die man sich klicken musste.
- Zwoelf Grundfarben direkt in der LED-Zeile, der System-Farbdialog nur noch
fuer den Rest.
Die Panel-Hoehe ist fix (grid_propagate(False)), sonst springt die
Fenstergroesse bei jedem Typwechsel und OK/Abbrechen wandern unter dem
Mauszeiger weg. run() gibt den Grab an einen aufrufenden Dialog zurueck --
grab_release() des verschachtelten MacroStepsDialog nimmt ihn sonst mit.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
73c1135655
commit
f6f4edbdde
1 changed files with 392 additions and 117 deletions
509
action_dialog.py
509
action_dialog.py
|
|
@ -1,12 +1,19 @@
|
|||
"""
|
||||
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()).
|
||||
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
|
||||
|
|
@ -18,6 +25,7 @@ BG2 = "#14161b"
|
|||
TEXT = "#e8e8ec"
|
||||
TEXT_DIM = "#8a8d98"
|
||||
ACCENT = "#3a6ff0"
|
||||
CAPTURE_BG = "#c04a2a"
|
||||
|
||||
TYPE_CHOICES = [
|
||||
("None", "Keine"),
|
||||
|
|
@ -34,8 +42,100 @@ PROFILE_SWITCH_CHOICES = [
|
|||
("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("<Button-1>", 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)
|
||||
|
|
@ -43,18 +143,110 @@ class _ModalDialog(tk.Toplevel):
|
|||
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("<KeyPress>", self._on_key_press)
|
||||
self.bind("<KeyRelease>", 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."""
|
||||
|
|
@ -68,43 +260,81 @@ class MacroStepsDialog(_ModalDialog):
|
|||
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))
|
||||
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 + 1
|
||||
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")
|
||||
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)
|
||||
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 in enumerate(("Strg", "Shift", "Alt")):
|
||||
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")
|
||||
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))
|
||||
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)
|
||||
|
||||
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)
|
||||
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 = []
|
||||
|
|
@ -112,11 +342,8 @@ class MacroStepsDialog(_ModalDialog):
|
|||
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)
|
||||
)
|
||||
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)
|
||||
|
|
@ -140,8 +367,8 @@ class ActionEditDialog(_ModalDialog):
|
|||
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.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
|
||||
|
|
@ -151,13 +378,15 @@ class ActionEditDialog(_ModalDialog):
|
|||
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))
|
||||
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)
|
||||
# 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
|
||||
|
|
@ -174,17 +403,16 @@ class ActionEditDialog(_ModalDialog):
|
|||
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()
|
||||
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
|
||||
modifier = (action["data"] >> 8) & 0xFF
|
||||
self._hidkey_key_var.set(self._key_name_by_code.get(keycode, "(leer)"))
|
||||
self._hidkey_mods_init = modifier
|
||||
self._hidkey_mods_init = (action["data"] >> 8) & 0xFF
|
||||
else:
|
||||
self._hidkey_key_var.set("(leer)")
|
||||
self._hidkey_mods_init = 0
|
||||
|
|
@ -206,45 +434,67 @@ class ActionEditDialog(_ModalDialog):
|
|||
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)
|
||||
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("<Button-1>", lambda e, c=rgb: self._set_color(c))
|
||||
# Tk kennt keine Tooltips -- der Farbname landet in der Zeile darunter.
|
||||
chip.bind("<Enter>", lambda e, n=name: self._color_hint.configure(text=n))
|
||||
chip.bind("<Leave>", 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")
|
||||
result = colorchooser.askcolor(color=self._hex(), title="LED-Farbe", parent=self)
|
||||
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())
|
||||
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():
|
||||
|
|
@ -252,80 +502,105 @@ class ActionEditDialog(_ModalDialog):
|
|||
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)
|
||||
|
||||
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")
|
||||
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)
|
||||
|
||||
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":
|
||||
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()
|
||||
|
||||
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)
|
||||
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 _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("<<ComboboxSelected>>", 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_var.get()
|
||||
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_preview()
|
||||
self._refresh_macro_choices()
|
||||
|
||||
# ── Ergebnis ──────────────────────────────────────────────────────
|
||||
|
||||
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)
|
||||
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_var.get()}
|
||||
action = {"type": "Macro", "data": self._macro_slot}
|
||||
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}
|
||||
action = {"type": "ProfileSwitch",
|
||||
"data": next(v for n, v in PROFILE_SWITCH_CHOICES if n == name)}
|
||||
else:
|
||||
action = {"type": "None", "data": 0}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue