import tkinter as tk from tkinter import ttk from typing import TYPE_CHECKING from pymsc.core.introspection import inspect_structure if TYPE_CHECKING: from ..core.app_controller import AppController class MainWindow: """ Main GUI Window class using Tkinter. """ def __init__(self, root: tk.Tk, controller: "AppController"): self.root = root self.controller = controller # Configuration styles style = ttk.Style() style.theme_use('clam') # Usually looks better than default # Main container (Notebook for tabs) self.notebook = ttk.Notebook(root) self.notebook.pack(fill='both', expand=True, padx=5, pady=5) # Tab 1: Parameter Control (Send) self.tab_control = ttk.Frame(self.notebook) self.notebook.add(self.tab_control, text='Control Panel') self._init_control_tab() # Tab 2: Bus Monitor (Receive) self.tab_monitor = ttk.Frame(self.notebook) self.notebook.add(self.tab_monitor, text='Bus Monitor') self._init_monitor_tab() # Tab 3: Settings self.tab_settings = ttk.Frame(self.notebook) self.notebook.add(self.tab_settings, text='Settings') self._init_settings_tab() # Status Bar self.status_var = tk.StringVar() self.status_var.set("Ready") self.status_bar = ttk.Label(root, textvariable=self.status_var, relief=tk.SUNKEN, anchor=tk.W) self.status_bar.pack(side=tk.BOTTOM, fill=tk.X) self._refresh_msg_list() def _init_control_tab(self): """UI for modifying parameters of outgoing messages.""" # Split into Left (Message List) and Right (Parameters) paned = ttk.PanedWindow(self.tab_control, orient=tk.HORIZONTAL) paned.pack(fill='both', expand=True) # Left Frame frame_list = ttk.LabelFrame(paned, text="Active Messages", padding=5) paned.add(frame_list, weight=1) self.msg_listbox = tk.Listbox(frame_list) self.msg_listbox.pack(fill='both', expand=True) # Bind selection event # self.msg_listbox.bind('<>', self._on_msg_select) # Right Frame frame_params = ttk.LabelFrame(paned, text="Parameters", padding=5) paned.add(frame_params, weight=3) self.lbl_placeholder = ttk.Label(frame_params, text="Select a message to edit parameters") self.lbl_placeholder.pack(pady=20) # BINDING: Quando si seleziona una riga nella lista self.msg_listbox.bind('<>', self._on_msg_select) # Right Frame Container per lo scroll dei parametri self.param_canvas = tk.Canvas(frame_params) self.scrollbar = ttk.Scrollbar(frame_params, orient="vertical", command=self.param_canvas.yview) self.scrollable_frame = ttk.Frame(self.param_canvas) self.scrollable_frame.bind( "", lambda e: self.param_canvas.configure(scrollregion=self.param_canvas.bbox("all")) ) self.param_canvas.create_window((0, 0), window=self.scrollable_frame, anchor="nw") self.param_canvas.configure(yscrollcommand=self.scrollbar.set) self.param_canvas.pack(side="left", fill="both", expand=True) self.scrollbar.pack(side="right", fill="y") def _init_monitor_tab(self): """UI for viewing decoded incoming traffic.""" # Treeview for logs columns = ("Time", "Label", "Direction", "Data Snippet") self.tree = ttk.Treeview(self.tab_monitor, columns=columns, show='headings') for col in columns: self.tree.heading(col, text=col) self.tree.column(col, width=100) self.tree.column("Data Snippet", width=300) scrollbar = ttk.Scrollbar(self.tab_monitor, orient=tk.VERTICAL, command=self.tree.yview) self.tree.configure(yscroll=scrollbar.set) self.tree.pack(side=tk.LEFT, fill='both', expand=True) scrollbar.pack(side=tk.RIGHT, fill='y') def _init_settings_tab(self): """UI for network configuration.""" frm = ttk.Frame(self.tab_settings, padding=20) frm.pack(fill='both') ttk.Label(frm, text="UDP Target IP:").grid(row=0, column=0, sticky=tk.W, pady=5) self.ent_ip = ttk.Entry(frm) self.ent_ip.insert(0, self.controller.udp_ip) self.ent_ip.grid(row=0, column=1, sticky=tk.EW, pady=5) ttk.Label(frm, text="Send Port:").grid(row=1, column=0, sticky=tk.W, pady=5) self.ent_tx_port = ttk.Entry(frm) self.ent_tx_port.insert(0, str(self.controller.udp_send_port)) self.ent_tx_port.grid(row=1, column=1, sticky=tk.EW, pady=5) ttk.Button(frm, text="Apply & Restart Core", command=self._apply_settings).grid(row=3, column=0, columnspan=2, pady=20) def _apply_settings(self): # Placeholder for applying settings self.status_var.set("Settings applied (Not implemented yet)") def _refresh_msg_list(self): """Popola la listbox con i messaggi registrati nel controller.""" self.msg_listbox.delete(0, tk.END) for label in self.controller.messages.keys(): self.msg_listbox.insert(tk.END, label) def _on_msg_select(self, event): """Gestisce la selezione di un messaggio.""" selection = self.msg_listbox.curselection() if not selection: return label = self.msg_listbox.get(selection[0]) msg = self.controller.messages.get(label) if msg: self._build_dynamic_form(msg) def _build_dynamic_form(self, msg): """Pulisce e ricostruisce il form dei parametri.""" # Pulisci frame precedente for widget in self.scrollable_frame.winfo_children(): widget.destroy() # Introspezione structure_map = inspect_structure(msg.payload) # Generazione ricorsiva UI self._render_node(self.scrollable_frame, structure_map, level=0) def _render_node(self, parent_widget, node, level=0): """Funzione ricorsiva per disegnare i widget.""" node_type = node.get("type") if node_type == "compound": # Per i compound, disegna i figli for name, child in node["children"].items(): if child.get("type") == "compound": # Crea un LabelFrame per i gruppi lf = ttk.LabelFrame(parent_widget, text=name, padding=5) lf.pack(fill='x', padx=5 + (level*5), pady=2) self._render_node(lf, child, level + 1) else: # Crea un frame per la riga Campo: Valore row = ttk.Frame(parent_widget) row.pack(fill='x', padx=10 + (level*5), pady=1) lbl = ttk.Label(row, text=name, width=20) lbl.pack(side='left') self._create_input_widget(row, child) def _create_input_widget(self, parent, node): """Crea il widget specifico (Entry, Combobox) e gestisce il callback.""" node_type = node.get("type") obj_ref = node.get("obj_ref") attr_name = node.get("attr_name") current_val = node.get("value") if node_type == "enum": # COMBOBOX enum_cls = node["enum_cls"] # Creiamo una mappa Nome -> MembroEnum options = {e.name: e for e in enum_cls} combo = ttk.Combobox(parent, values=list(options.keys()), state="readonly") # Se il valore attuale รจ un Enum, prendi il nome, altrimenti gestisci errore try: if hasattr(current_val, 'name'): combo.set(current_val.name) else: combo.set(str(current_val)) except: combo.set("INVALID") combo.pack(side='left', fill='x', expand=True) def on_combo_change(event): sel_name = combo.get() new_enum_val = options[sel_name] # Aggiorna il backend (ctypes scrive direttamente in memoria) setattr(obj_ref, attr_name, new_enum_val.value) print(f"Updated {attr_name} -> {new_enum_val}") combo.bind("<>", on_combo_change) elif node_type in ["primitive", "smart_value", "smart_angle"]: # ENTRY var = tk.StringVar(value=str(current_val)) entry = ttk.Entry(parent, textvariable=var) entry.pack(side='left', fill='x', expand=True) def on_entry_change(event): try: val_str = var.get() if node_type == "primitive": # Cerca di mantenere il tipo originale (int o float) if isinstance(current_val, int): new_val = int(val_str) else: new_val = float(val_str) else: # Smart values sono tipicamente float new_val = float(val_str) setattr(obj_ref, attr_name, new_val) entry.configure(foreground="black") print(f"Updated {attr_name} -> {new_val}") except ValueError: entry.configure(foreground="red") # Feedback visivo errore print(f"Error updating {attr_name}") entry.bind("", on_entry_change) entry.bind("", on_entry_change) elif node_type == "readonly": lbl = ttk.Label(parent, text=str(current_val), foreground="gray") lbl.pack(side='left')