"""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("", 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("", 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 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("", _on_wheel) def _unbind_wheel(_): self.content_canvas.unbind_all("") for w in (self.content_canvas, self.content_inner): w.bind("", _bind_wheel) w.bind("", _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()