Files
2026-07-17 15:29:53 -04:00

195 lines
8.4 KiB
Python

"""Right sidebar - node property editor panel."""
import tkinter as tk
from utils.constants import NODE_TYPES
from node_editor.nodes import get_property_editor
class PropertiesPanel(tk.Frame):
"""Dynamic property editor for the selected node."""
def __init__(self, parent, on_change=None):
super().__init__(parent, bg="#252535", width=250)
self.on_change = on_change
self.current_node = None
self.current_editor = None
self._node_canvas = None
self._profile_manager = None
self._project = None
self.pack_propagate(False)
self._build_ui()
def set_node_canvas(self, canvas):
self._node_canvas = canvas
def set_macro_list(self, macro_list):
self._macro_list = macro_list
def set_profile_manager(self, profile_manager):
self._profile_manager = profile_manager
def set_project(self, project):
"""Wire the Project so editors can read live settings (pause-text margins, etc.)."""
self._project = project
def _build_ui(self):
self.header = tk.Frame(self, bg="#1E1E2E")
self.header.pack(fill="x")
self.title_label = tk.Label(self.header, text="Properties", bg="#1E1E2E",
fg="white", font=("Segoe UI", 11, "bold"), pady=8)
self.title_label.pack(side="left", padx=10)
content_frame = tk.Frame(self, bg="#252535")
content_frame.pack(fill="both", expand=True)
self.content_canvas = tk.Canvas(content_frame, bg="#252535", highlightthickness=0)
scrollbar = tk.Scrollbar(content_frame, orient="vertical",
command=self.content_canvas.yview)
self.content_inner = tk.Frame(self.content_canvas, bg="#252535")
self.content_inner.bind("<Configure>",
lambda e: self.content_canvas.configure(
scrollregion=self.content_canvas.bbox("all")))
self.content_canvas.create_window((0, 0), window=self.content_inner, anchor="nw",
tags="inner")
self.content_canvas.configure(yscrollcommand=scrollbar.set)
self.content_canvas.bind("<Configure>",
lambda e: self.content_canvas.itemconfig("inner", width=e.width))
# Bind wheel scrolling only while the cursor is inside the panel so
# we don't fight other widgets for the global <MouseWheel> event
def _on_wheel(event):
self.content_canvas.yview_scroll(int(-1 * (event.delta / 120)), "units")
return "break"
def _bind_wheel(_):
self.content_canvas.bind_all("<MouseWheel>", _on_wheel)
def _unbind_wheel(_):
self.content_canvas.unbind_all("<MouseWheel>")
for w in (self.content_canvas, self.content_inner):
w.bind("<Enter>", _bind_wheel)
w.bind("<Leave>", _unbind_wheel)
self.content_canvas.pack(side="left", fill="both", expand=True)
scrollbar.pack(side="right", fill="y")
self.empty_label = tk.Label(self.content_inner,
text="Select a node to\nedit its properties",
bg="#252535", fg="#666666",
font=("Segoe UI", 10), justify="center")
self.empty_label.pack(expand=True, pady=40)
def show_node(self, node_widget):
"""Display properties for the selected node (or multi-select summary)."""
self._clear_editor()
if node_widget is None:
self.current_node = None
self.title_label.config(text="Properties")
canvas = self._node_canvas
if canvas and len(canvas.selected_nodes) > 1:
count = len(canvas.selected_nodes)
tk.Label(self.content_inner,
text=f"{count} nodes selected",
bg="#252535", fg="#AAAAAA",
font=("Segoe UI", 11, "bold"), justify="center").pack(pady=(30, 10))
tk.Button(self.content_inner, text="Delete All Selected",
bg="#D94A4A", fg="white", font=("Segoe UI", 9), relief="flat",
command=self._delete_current).pack(padx=20, pady=4, fill="x")
return
self.empty_label = tk.Label(self.content_inner,
text="Select a node to\nedit its properties",
bg="#252535", fg="#666666",
font=("Segoe UI", 10), justify="center")
self.empty_label.pack(expand=True, pady=40)
return
self.current_node = node_widget
node_data = node_widget.data
type_info = NODE_TYPES.get(node_data.type, {"label": "Unknown"})
self.title_label.config(text=type_info["label"])
type_frame = tk.Frame(self.content_inner, bg="#2D2D3D")
type_frame.pack(fill="x", padx=4, pady=4)
color = type_info.get("color", "#555555")
tk.Frame(type_frame, bg=color, width=4).pack(side="left", fill="y")
tk.Label(type_frame, text=f" {type_info['label']} Node",
bg="#2D2D3D", fg="white", font=("Segoe UI", 10, "bold"),
pady=6).pack(side="left")
tk.Button(type_frame, text="Delete", bg="#D94A4A", fg="white",
font=("Segoe UI", 8), relief="flat",
command=self._delete_current).pack(side="right", padx=8, pady=4)
editor_factory = get_property_editor(node_data.type)
if editor_factory:
def on_prop_change():
if self.current_node and self._node_canvas:
self._node_canvas.refresh_node(self.current_node.data.id)
if self.on_change:
self.on_change()
if node_data.type == "start" and self._node_canvas and self._node_canvas.macro:
def on_rename():
if hasattr(self, '_macro_list') and self._macro_list:
self._macro_list.refresh()
editor = editor_factory(
self.content_inner, node_data.data, on_prop_change,
macro=self._node_canvas.macro, on_rename=on_rename,
)
elif node_data.type == "subroutine" and self._profile_manager:
sub_names = self._profile_manager.get_subroutine_names()
editor = editor_factory(
self.content_inner, node_data.data, on_prop_change,
subroutine_names=sub_names,
)
elif node_data.type == "iteration_branch" and self._node_canvas:
editor = editor_factory(
self.content_inner, node_data.data, on_prop_change,
macro=self._node_canvas.macro,
node_canvas=self._node_canvas,
widget=node_widget,
)
elif node_data.type == "aggregator" and self._node_canvas:
editor = editor_factory(
self.content_inner, node_data.data, on_prop_change,
node_canvas=self._node_canvas,
widget=node_widget,
)
elif node_data.type in ("branch", "bluetooth") and self._node_canvas:
editor = editor_factory(
self.content_inner, node_data.data, on_prop_change,
node_canvas=self._node_canvas,
widget=node_widget,
)
elif node_data.type == "pause":
editor = editor_factory(
self.content_inner, node_data.data, on_prop_change,
project=self._project,
)
elif node_data.type == "text":
editor = editor_factory(
self.content_inner, node_data.data, on_prop_change,
project=self._project,
)
else:
editor = editor_factory(self.content_inner, node_data.data, on_prop_change)
editor.pack(fill="x", padx=4, pady=4)
self.current_editor = editor
def _clear_editor(self):
for widget in self.content_inner.winfo_children():
widget.destroy()
self.current_editor = None
def _delete_current(self):
if self._node_canvas:
self._node_canvas.delete_selected()