Copy button settings between keys; add window shortcuts
Rechtsklick auf eine Karte im Programmiermodus: Belegung und/oder LED-Farbe kopieren und auf andere Tasten anwenden, Belegung leeren, bearbeiten. Auf Encodern dasselbe fuer die drei Aktionen (dort ohne Farbe -- Encoder haben keine eigene LED). Strg+C/Strg+V wirken auf die Karte unter dem Mauszeiger. Die Ablage ist bewusst eine reine In-Memory-Struktur (self._clip), nicht die System-Zwischenablage: uebertragen werden ganze Action-Dicts, kein Text. Eingefuegt wird immer eine deepcopy, sonst teilen sich zwei Tasten dasselbe Dict und eine spaetere Bearbeitung aendert beide. "Leeren" behaelt die Notiz -- gleiche Regel wie beim Typwechsel im Dialog, die Notiz beschreibt die Taste, nicht die konkrete Aktion. Ausserdem: Makro-Belegungen zeigen im Grid ihre echte Tastenfolge (Makrotabelle wird an annotate_profile() durchgereicht), und das Fenster kennt Strg+1/2/3 (Profil), F2 (umbenennen), Strg+E (Programmiermodus), Esc (ins Tray). Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
aba81463e5
commit
21f9cbc2ed
1 changed files with 196 additions and 14 deletions
|
|
@ -19,6 +19,7 @@ automatisch ausgeschaltet, damit sich beide nicht um den Port streiten).
|
||||||
|
|
||||||
Start: python desktop_viewer.py
|
Start: python desktop_viewer.py
|
||||||
"""
|
"""
|
||||||
|
import copy
|
||||||
import os
|
import os
|
||||||
import queue
|
import queue
|
||||||
import sys
|
import sys
|
||||||
|
|
@ -138,6 +139,12 @@ class VersaPadViewer(tk.Tk):
|
||||||
|
|
||||||
self._link = vs.VersaPadLink()
|
self._link = vs.VersaPadLink()
|
||||||
self._closing = False
|
self._closing = False
|
||||||
|
# Zwischenablage fuer "Belegung/Farbe auf andere Taste uebertragen"
|
||||||
|
# (Rechtsklickmenue bzw. Strg+C/V auf der Karte unter dem Mauszeiger).
|
||||||
|
# Bewusst NUR im Speicher, nicht die System-Zwischenablage: hier
|
||||||
|
# liegen Action-Dicts, kein Text.
|
||||||
|
self._clip = None
|
||||||
|
self._hover = None
|
||||||
self.live_sync = tk.BooleanVar(value=False)
|
self.live_sync = tk.BooleanVar(value=False)
|
||||||
self.editing = tk.BooleanVar(value=False)
|
self.editing = tk.BooleanVar(value=False)
|
||||||
self._serial_results = queue.Queue()
|
self._serial_results = queue.Queue()
|
||||||
|
|
@ -230,6 +237,10 @@ class VersaPadViewer(tk.Tk):
|
||||||
padx=8, pady=2, font=("Segoe UI", 8)).pack(side="left", padx=(0, 6))
|
padx=8, pady=2, font=("Segoe UI", 8)).pack(side="left", padx=(0, 6))
|
||||||
self.prog_status = tk.Label(self.prog_row, text="", bg=BG, fg=TEXT_DIM, font=("Segoe UI", 8))
|
self.prog_status = tk.Label(self.prog_row, text="", bg=BG, fg=TEXT_DIM, font=("Segoe UI", 8))
|
||||||
self.prog_status.pack(side="left", padx=(8, 0))
|
self.prog_status.pack(side="left", padx=(8, 0))
|
||||||
|
# Kurz halten: die Toolbar ist bei der Standardbreite (790px) schon
|
||||||
|
# fast voll, laengerer Text wird rechts abgeschnitten.
|
||||||
|
tk.Label(self.prog_row, text="Rechtsklick = kopieren/einfügen",
|
||||||
|
bg=BG, fg=TEXT_DIM, font=("Segoe UI", 8)).pack(side="right")
|
||||||
# prog_row wird erst bei aktivem Programmiermodus gepackt (siehe _on_toggle_editing)
|
# prog_row wird erst bei aktivem Programmiermodus gepackt (siehe _on_toggle_editing)
|
||||||
|
|
||||||
# Profil-Tabs direkt ueber der Steuermatrix, nicht mehr oben am
|
# Profil-Tabs direkt ueber der Steuermatrix, nicht mehr oben am
|
||||||
|
|
@ -276,6 +287,7 @@ class VersaPadViewer(tk.Tk):
|
||||||
|
|
||||||
self.protocol("WM_DELETE_WINDOW", self._hide_to_tray)
|
self.protocol("WM_DELETE_WINDOW", self._hide_to_tray)
|
||||||
self.bind("<Unmap>", self._on_unmap)
|
self.bind("<Unmap>", self._on_unmap)
|
||||||
|
self._bind_shortcuts()
|
||||||
self._serial_thread.start()
|
self._serial_thread.start()
|
||||||
|
|
||||||
self.set_profile(0)
|
self.set_profile(0)
|
||||||
|
|
@ -617,6 +629,179 @@ class VersaPadViewer(tk.Tk):
|
||||||
except OSError as e:
|
except OSError as e:
|
||||||
self._status(f"Auto-Speichern fehlgeschlagen: {e}", False)
|
self._status(f"Auto-Speichern fehlgeschlagen: {e}", False)
|
||||||
|
|
||||||
|
# ── Kopieren/Einfuegen + Tastenkuerzel ─────────────────────────
|
||||||
|
|
||||||
|
def _bind_shortcuts(self):
|
||||||
|
"""Fensterweite Kuerzel. Waehrend ein Bearbeiten-Dialog offen ist,
|
||||||
|
haelt dessen grab_set() die Tastatur -- die Kuerzel hier koennen ihm
|
||||||
|
also nicht dazwischenfunken."""
|
||||||
|
for seq, handler in (
|
||||||
|
("<Escape>", lambda e: self._hide_to_tray()),
|
||||||
|
("<F2>", lambda e: self._rename_tab(self.profile)),
|
||||||
|
("<Control-e>", lambda e: self._toggle_editing_shortcut()),
|
||||||
|
("<Control-E>", lambda e: self._toggle_editing_shortcut()),
|
||||||
|
("<Control-c>", lambda e: self._copy_hovered()),
|
||||||
|
("<Control-C>", lambda e: self._copy_hovered()),
|
||||||
|
("<Control-v>", lambda e: self._paste_hovered()),
|
||||||
|
("<Control-V>", lambda e: self._paste_hovered()),
|
||||||
|
):
|
||||||
|
self.bind(seq, handler)
|
||||||
|
for p in range(vp.NUM_PROFILES):
|
||||||
|
self.bind(f"<Control-Key-{p + 1}>",
|
||||||
|
lambda e, prof=p: self.set_profile(prof, manual=True))
|
||||||
|
|
||||||
|
def _toggle_editing_shortcut(self):
|
||||||
|
self.editing.set(not self.editing.get())
|
||||||
|
self._on_toggle_editing()
|
||||||
|
|
||||||
|
def _hint(self, text):
|
||||||
|
"""Kurze Rueckmeldung in der Fusszeile -- die Programmiermodus-
|
||||||
|
Statuszeile haengt an der Toolbar und ist sonst leicht zu uebersehen.
|
||||||
|
Der naechste _render() setzt die Quellenangabe wieder ein."""
|
||||||
|
self.footer.configure(text=text)
|
||||||
|
|
||||||
|
def _target_entry(self, target):
|
||||||
|
"""Liefert den Eintrag im combined-State, auf den ein Ziel zeigt --
|
||||||
|
Button-Karte oder Encoder (dort waehlt target["field"] danach noch
|
||||||
|
sw/cw/ccw aus)."""
|
||||||
|
profile = self.combined["profiles"][self.profile]
|
||||||
|
items = profile["buttons"] if target["kind"] == "button" else profile["encoders"]
|
||||||
|
return next(item for item in items if item["index"] == target["index"])
|
||||||
|
|
||||||
|
def _set_hover(self, target):
|
||||||
|
self._hover = target
|
||||||
|
|
||||||
|
def _clear_hover(self, target):
|
||||||
|
"""<Leave> feuert auch beim Wechsel auf ein Kindwidget derselben
|
||||||
|
Karte -- deshalb erst pruefen, ob der Zeiger die Karte wirklich
|
||||||
|
verlassen hat."""
|
||||||
|
card = target["card"]
|
||||||
|
if not card.winfo_exists():
|
||||||
|
self._hover = None
|
||||||
|
return
|
||||||
|
x, y = card.winfo_pointerxy()
|
||||||
|
inside = (card.winfo_rootx() <= x < card.winfo_rootx() + card.winfo_width()
|
||||||
|
and card.winfo_rooty() <= y < card.winfo_rooty() + card.winfo_height())
|
||||||
|
if not inside and self._hover is target:
|
||||||
|
self._hover = None
|
||||||
|
|
||||||
|
def _copy_hovered(self):
|
||||||
|
if self._hover is not None:
|
||||||
|
self._copy_target(self._hover, "all")
|
||||||
|
|
||||||
|
def _paste_hovered(self):
|
||||||
|
if self._hover is not None:
|
||||||
|
self._paste_target(self._hover, "all")
|
||||||
|
|
||||||
|
def _copy_target(self, target, what):
|
||||||
|
if not self.editing.get() or self.combined is None:
|
||||||
|
return
|
||||||
|
entry = self._target_entry(target)
|
||||||
|
if target["kind"] == "button":
|
||||||
|
action, led = entry["action"], entry["led"]
|
||||||
|
else:
|
||||||
|
action, led = entry[target["field"]], None
|
||||||
|
self._clip = {
|
||||||
|
"action": copy.deepcopy(action) if what in ("all", "action") else None,
|
||||||
|
"led": copy.deepcopy(led) if what in ("all", "led") else None,
|
||||||
|
}
|
||||||
|
parts = [name for name, key in (("Belegung", "action"), ("Farbe", "led"))
|
||||||
|
if self._clip[key] is not None]
|
||||||
|
self._hint("kopiert: {} von {}".format(" + ".join(parts) or "nichts", target["label"]))
|
||||||
|
|
||||||
|
def _paste_target(self, target, what):
|
||||||
|
if not self.editing.get() or self.combined is None or not self._clip:
|
||||||
|
return
|
||||||
|
entry = self._target_entry(target)
|
||||||
|
applied = []
|
||||||
|
if what in ("all", "action") and self._clip["action"] is not None:
|
||||||
|
action = copy.deepcopy(self._clip["action"])
|
||||||
|
if target["kind"] == "button":
|
||||||
|
entry["action"] = action
|
||||||
|
else:
|
||||||
|
entry[target["field"]] = action
|
||||||
|
applied.append("Belegung")
|
||||||
|
if what in ("all", "led") and self._clip["led"] is not None and target["kind"] == "button":
|
||||||
|
entry["led"] = copy.deepcopy(self._clip["led"])
|
||||||
|
applied.append("Farbe")
|
||||||
|
if not applied:
|
||||||
|
self._hint("nichts eingefügt -- Zwischenablage passt nicht zu diesem Ziel")
|
||||||
|
return
|
||||||
|
self._autosave_combined()
|
||||||
|
self._render()
|
||||||
|
self._hint("eingefügt: {} → {}".format(" + ".join(applied), target["label"]))
|
||||||
|
|
||||||
|
def _clear_target(self, target):
|
||||||
|
if not self.editing.get() or self.combined is None:
|
||||||
|
return
|
||||||
|
entry = self._target_entry(target)
|
||||||
|
# Notiz bleibt bewusst erhalten (gleiche Regel wie beim Typwechsel im
|
||||||
|
# Dialog): sie beschreibt die Taste, nicht die konkrete Aktion.
|
||||||
|
if target["kind"] == "button":
|
||||||
|
entry["action"] = {"type": "None", "data": 0,
|
||||||
|
"note": entry["action"].get("note", "")}
|
||||||
|
else:
|
||||||
|
old = entry[target["field"]]
|
||||||
|
entry[target["field"]] = {"type": "None", "data": 0, "note": old.get("note", "")}
|
||||||
|
self._autosave_combined()
|
||||||
|
self._render()
|
||||||
|
self._hint("geleert: {}".format(target["label"]))
|
||||||
|
|
||||||
|
def _show_context_menu(self, event, target):
|
||||||
|
menu = tk.Menu(self, tearoff=0, bg=CARD_BG, fg=TEXT, activebackground=ACCENT,
|
||||||
|
activeforeground="#ffffff", bd=0, font=("Segoe UI", 9))
|
||||||
|
is_button = target["kind"] == "button"
|
||||||
|
has_action = bool(self._clip and self._clip.get("action"))
|
||||||
|
has_led = bool(self._clip and self._clip.get("led"))
|
||||||
|
|
||||||
|
menu.add_command(label="Bearbeiten…", command=lambda: self._edit_target(target))
|
||||||
|
menu.add_separator()
|
||||||
|
if is_button:
|
||||||
|
menu.add_command(label="Kopieren: Belegung + Farbe (Strg+C)",
|
||||||
|
command=lambda: self._copy_target(target, "all"))
|
||||||
|
menu.add_command(label="Kopieren: nur Belegung",
|
||||||
|
command=lambda: self._copy_target(target, "action"))
|
||||||
|
menu.add_command(label="Kopieren: nur Farbe",
|
||||||
|
command=lambda: self._copy_target(target, "led"))
|
||||||
|
else:
|
||||||
|
menu.add_command(label="Kopieren: Belegung (Strg+C)",
|
||||||
|
command=lambda: self._copy_target(target, "action"))
|
||||||
|
menu.add_separator()
|
||||||
|
paste_all = has_action or (has_led and is_button)
|
||||||
|
menu.add_command(label="Einfügen: alles (Strg+V)",
|
||||||
|
state="normal" if paste_all else "disabled",
|
||||||
|
command=lambda: self._paste_target(target, "all"))
|
||||||
|
menu.add_command(label="Einfügen: nur Belegung",
|
||||||
|
state="normal" if has_action else "disabled",
|
||||||
|
command=lambda: self._paste_target(target, "action"))
|
||||||
|
if is_button:
|
||||||
|
menu.add_command(label="Einfügen: nur Farbe",
|
||||||
|
state="normal" if has_led else "disabled",
|
||||||
|
command=lambda: self._paste_target(target, "led"))
|
||||||
|
menu.add_separator()
|
||||||
|
menu.add_command(label="Leeren (Belegung entfernen)",
|
||||||
|
command=lambda: self._clear_target(target))
|
||||||
|
try:
|
||||||
|
menu.tk_popup(event.x_root, event.y_root)
|
||||||
|
finally:
|
||||||
|
menu.grab_release()
|
||||||
|
|
||||||
|
def _edit_target(self, target):
|
||||||
|
if target["kind"] == "button":
|
||||||
|
self._edit_button(target["index"])
|
||||||
|
else:
|
||||||
|
self._edit_encoder_action(target["index"], target["field"], target["field_label"])
|
||||||
|
|
||||||
|
def _bind_cell_interaction(self, card, widgets, target):
|
||||||
|
"""Linksklick = bearbeiten, Rechtsklick = Kontextmenue; der
|
||||||
|
Mauszeiger merkt sich das Ziel fuer Strg+C/Strg+V."""
|
||||||
|
card.configure(cursor="hand2")
|
||||||
|
for widget in widgets:
|
||||||
|
widget.bind("<Button-1>", lambda e: self._edit_target(target))
|
||||||
|
widget.bind("<Button-3>", lambda e: self._show_context_menu(e, target))
|
||||||
|
widget.bind("<Enter>", lambda e: self._set_hover(target))
|
||||||
|
card.bind("<Leave>", lambda e: self._clear_hover(target))
|
||||||
|
|
||||||
# ── Rendering ────────────────────────────────────────────────────
|
# ── Rendering ────────────────────────────────────────────────────
|
||||||
|
|
||||||
def _current_profile_view(self):
|
def _current_profile_view(self):
|
||||||
|
|
@ -641,7 +826,7 @@ class VersaPadViewer(tk.Tk):
|
||||||
cfg = vp.annotate_profile({
|
cfg = vp.annotate_profile({
|
||||||
"buttons": [dict(b) for b in raw["buttons"]],
|
"buttons": [dict(b) for b in raw["buttons"]],
|
||||||
"encoders": [dict(e) for e in raw["encoders"]],
|
"encoders": [dict(e) for e in raw["encoders"]],
|
||||||
})
|
}, data.get("macros"))
|
||||||
return cfg, f"Quelle: {vcomb.DEFAULT_PATH}"
|
return cfg, f"Quelle: {vcomb.DEFAULT_PATH}"
|
||||||
except (KeyError, IndexError, ValueError):
|
except (KeyError, IndexError, ValueError):
|
||||||
pass # kaputte/unvollstaendige Datei -- weiter unten ausweichen
|
pass # kaputte/unvollstaendige Datei -- weiter unten ausweichen
|
||||||
|
|
@ -667,7 +852,7 @@ class VersaPadViewer(tk.Tk):
|
||||||
cfg = vp.annotate_profile({
|
cfg = vp.annotate_profile({
|
||||||
"buttons": [dict(b) for b in raw["buttons"]],
|
"buttons": [dict(b) for b in raw["buttons"]],
|
||||||
"encoders": [dict(e) for e in raw["encoders"]],
|
"encoders": [dict(e) for e in raw["encoders"]],
|
||||||
})
|
}, self.combined.get("macros"))
|
||||||
source_text = "Programmiermodus -- nicht gespeichert, bis übertragen/exportiert"
|
source_text = "Programmiermodus -- nicht gespeichert, bis übertragen/exportiert"
|
||||||
else:
|
else:
|
||||||
try:
|
try:
|
||||||
|
|
@ -723,11 +908,9 @@ class VersaPadViewer(tk.Tk):
|
||||||
x=10, y=CARD_H - 16, width=CARD_W - 20, height=13)
|
x=10, y=CARD_H - 16, width=CARD_W - 20, height=13)
|
||||||
|
|
||||||
if editable:
|
if editable:
|
||||||
card.configure(cursor="hand2")
|
target = {"kind": "button", "index": btn["index"], "card": card,
|
||||||
handler = lambda e, idx=btn["index"]: self._edit_button(idx)
|
"label": "Button #{}".format(btn["index"])}
|
||||||
card.bind("<Button-1>", handler)
|
self._bind_cell_interaction(card, [card] + list(card.winfo_children()), target)
|
||||||
for child in card.winfo_children():
|
|
||||||
child.bind("<Button-1>", handler)
|
|
||||||
|
|
||||||
def _render_encoder(self, enc, editable=False):
|
def _render_encoder(self, enc, editable=False):
|
||||||
card = tk.Frame(self.enc_frame, bg=CARD_BG, highlightbackground=CARD_BORDER,
|
card = tk.Frame(self.enc_frame, bg=CARD_BG, highlightbackground=CARD_BORDER,
|
||||||
|
|
@ -755,14 +938,13 @@ class VersaPadViewer(tk.Tk):
|
||||||
wraplength=150, justify="left", anchor="w")
|
wraplength=150, justify="left", anchor="w")
|
||||||
note_label.pack(fill="x")
|
note_label.pack(fill="x")
|
||||||
if editable:
|
if editable:
|
||||||
row.configure(cursor="hand2")
|
target = {"kind": "encoder", "index": enc["index"], "field": field,
|
||||||
handler = lambda e, ei=enc["index"], f=field, lbl=key: self._edit_encoder_action(ei, f, lbl)
|
"field_label": key, "card": row,
|
||||||
row.bind("<Button-1>", handler)
|
"label": "Encoder {} {}".format(enc["index"], key)}
|
||||||
top.bind("<Button-1>", handler)
|
widgets = [row, top] + list(top.winfo_children())
|
||||||
if note_label is not None:
|
if note_label is not None:
|
||||||
note_label.bind("<Button-1>", handler)
|
widgets.append(note_label)
|
||||||
for child in top.winfo_children():
|
self._bind_cell_interaction(row, widgets, target)
|
||||||
child.bind("<Button-1>", handler)
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue