SXXXXXXX_PyMsc/pymsc/gui/main_window.py
2025-12-02 11:21:05 +01:00

247 lines
9.7 KiB
Python

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('<<ListboxSelect>>', 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('<<ListboxSelect>>', 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(
"<Configure>",
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("<<ComboboxSelected>>", 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("<Return>", on_entry_change)
entry.bind("<FocusOut>", on_entry_change)
elif node_type == "readonly":
lbl = ttk.Label(parent, text=str(current_val), foreground="gray")
lbl.pack(side='left')