247 lines
9.7 KiB
Python
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') |