sistemata la visualizzazione dei campi details con form

This commit is contained in:
VALLONGOL 2025-12-17 14:21:06 +01:00
parent fc52579330
commit 01936497a0
2 changed files with 194 additions and 187 deletions

View File

@ -47,7 +47,7 @@ class DetailsPane:
self._details_canvas_frame.bind('<Configure>', _on_frame_configure) self._details_canvas_frame.bind('<Configure>', _on_frame_configure)
self._details_canvas.bind('<Configure>', _on_canvas_configure) self._details_canvas.bind('<Configure>', _on_canvas_configure)
# details Treeview # details Treeview (kept for backwards compatibility but not packed)
self.detail_tree = ttk.Treeview(self._details_canvas_frame, columns=("param", "value"), show="headings", height=18) self.detail_tree = ttk.Treeview(self._details_canvas_frame, columns=("param", "value"), show="headings", height=18)
self.detail_tree.heading("param", text="Parameter") self.detail_tree.heading("param", text="Parameter")
self.detail_tree.heading("value", text="Value") self.detail_tree.heading("value", text="Value")
@ -57,10 +57,16 @@ class DetailsPane:
self.detail_tree.configure() # placeholder to satisfy possible callers self.detail_tree.configure() # placeholder to satisfy possible callers
except Exception: except Exception:
pass pass
self.detail_tree.pack(side=tk.TOP, fill=tk.BOTH, expand=False, padx=2, pady=2)
# form container in the interior frame so it scrolls with canvas # form container in the interior frame so it scrolls with canvas
self.detail_form_container = tk.Frame(self._details_canvas_frame) self.detail_form_container = tk.Frame(self._details_canvas_frame)
# ensure the form container is visible by default (the form builder will
# populate it). We do not pack the Treeview to avoid an empty tree
# overlaying the form; the form is the primary details UI.
try:
self.detail_form_container.pack(side=tk.LEFT, fill=tk.BOTH, expand=True, padx=2, pady=2)
except Exception:
pass
# assign attributes onto the app so existing MonitorApp code works # assign attributes onto the app so existing MonitorApp code works
setattr(app, 'detail_tree', self.detail_tree) setattr(app, 'detail_tree', self.detail_tree)

View File

@ -65,6 +65,8 @@ class MonitorApp(tk.Frame):
self.update_loop_running = False self.update_loop_running = False
# cache tree item ids by message label for incremental updates # cache tree item ids by message label for incremental updates
self._tree_items = {} self._tree_items = {}
# parents we want to keep always expanded in detail tree
self._always_open_parents = {}
# No background bind or retry at startup; user must press Initialize. # No background bind or retry at startup; user must press Initialize.
def create_widgets(self): def create_widgets(self):
@ -306,18 +308,16 @@ class MonitorApp(tk.Frame):
except Exception: except Exception:
return None return None
def show_message_form(self, label: str): def show_message_form(self, label: str, editable: bool = True):
"""Build an editable form for message `label` (A messages). """Build an editable form for message `label` (A messages).
The form is placed into `self.detail_form_container`. Existing form The form is placed into `self.detail_form_container`. Existing form
widgets are destroyed and rebuilt for the new message. widgets are destroyed and rebuilt for the new message.
""" """
try: try:
# ensure tree is hidden # Use the form container for showing message details; do not toggle the
try: # tree visibility here. The DetailsPane places the form inside a
self.detail_tree.pack_forget() # scrollable canvas so it remains visible while updating.
except Exception:
pass
# if same form is already shown, just refresh widget values # if same form is already shown, just refresh widget values
if getattr(self, 'current_form_label', None) == label and self.form_widgets: if getattr(self, 'current_form_label', None) == label and self.form_widgets:
try: try:
@ -353,13 +353,17 @@ class MonitorApp(tk.Frame):
hdr.pack(fill=tk.X, padx=6, pady=(6,0)) hdr.pack(fill=tk.X, padx=6, pady=(6,0))
lbl_hdr = tk.Label(hdr, text=f"Edit message {label}", anchor=tk.W) lbl_hdr = tk.Label(hdr, text=f"Edit message {label}", anchor=tk.W)
lbl_hdr.pack(side=tk.LEFT) lbl_hdr.pack(side=tk.LEFT)
# For editable forms show Apply button, otherwise present a label
if editable:
apply_btn = tk.Button(hdr, text='Apply', command=lambda: self._apply_form_values(msg_wrapper)) apply_btn = tk.Button(hdr, text='Apply', command=lambda: self._apply_form_values(msg_wrapper))
apply_btn.pack(side=tk.RIGHT, padx=6) apply_btn.pack(side=tk.RIGHT, padx=6)
else:
tk.Label(hdr, text='Read-only', anchor=tk.E).pack(side=tk.RIGHT, padx=6)
# recursive builder for fields # recursive builder for fields
frm = tk.Frame(self.detail_form_container) frm = tk.Frame(self.detail_form_container)
frm.pack(fill=tk.BOTH, expand=True, padx=6, pady=6) frm.pack(fill=tk.BOTH, expand=True, padx=6, pady=6)
self._build_form_fields(frm, msg, prefix='') self._build_form_fields(frm, msg, prefix='', editable=editable)
# show form container # show form container
try: try:
@ -372,7 +376,7 @@ class MonitorApp(tk.Frame):
except Exception: except Exception:
pass pass
def _build_form_fields(self, parent, obj, prefix='', max_depth=3, _depth=0): def _build_form_fields(self, parent, obj, prefix='', max_depth=3, _depth=0, editable=True):
"""Recursively create labeled widgets for public fields of obj.""" """Recursively create labeled widgets for public fields of obj."""
if _depth > max_depth: if _depth > max_depth:
return return
@ -436,7 +440,9 @@ class MonitorApp(tk.Frame):
# show human-friendly label first, then numeric value # show human-friendly label first, then numeric value
choices = [f"{n} ({v})" for (n, v) in enum_items] choices = [f"{n} ({v})" for (n, v) in enum_items]
var = tk.StringVar() var = tk.StringVar()
cb = ttk.Combobox(row_fr, values=choices, textvariable=var, state='readonly') # combobox is selectable for editable forms, otherwise disabled
cb_state = 'readonly' if editable else 'disabled'
cb = ttk.Combobox(row_fr, values=choices, textvariable=var, state=cb_state)
# set current value # set current value
try: try:
raw = int(getattr(val, 'raw', getattr(val, 'value', val))) raw = int(getattr(val, 'raw', getattr(val, 'value', val)))
@ -464,6 +470,12 @@ class MonitorApp(tk.Frame):
except Exception: except Exception:
var.set('') var.set('')
ent = tk.Entry(row_fr, textvariable=var) ent = tk.Entry(row_fr, textvariable=var)
# disable entry if not editable
if not editable:
try:
ent.config(state='disabled')
except Exception:
pass
ent.pack(side=tk.RIGHT, fill=tk.X, expand=True) ent.pack(side=tk.RIGHT, fill=tk.X, expand=True)
widget = ('entry', ent) widget = ('entry', ent)
@ -532,6 +544,12 @@ class MonitorApp(tk.Frame):
# create parent node with empty value # create parent node with empty value
try: try:
iid = self.detail_tree.insert(parent, tk.END, values=(p, '')) iid = self.detail_tree.insert(parent, tk.END, values=(p, ''))
# ensure parent is expanded so nested fields remain visible
try:
self.detail_tree.item(iid, open=True)
self._always_open_parents[key] = True
except Exception:
pass
self.detail_rows[key] = iid self.detail_rows[key] = iid
parent = iid parent = iid
except Exception: except Exception:
@ -552,7 +570,20 @@ class MonitorApp(tk.Frame):
try: try:
# keep the param column stable; show only the leaf label # keep the param column stable; show only the leaf label
leaf = name.split('.')[-1] leaf = name.split('.')[-1]
self.detail_tree.item(iid, values=(leaf, value)) parent_key = '.'.join(name.replace('/', '.').split('.')[:-1])
if parent_key and parent_key in self.detail_rows:
disp = f"-> {leaf}"
else:
disp = leaf
self.detail_tree.item(iid, values=(disp, value))
# if this row represents a parent that we marked to remain open,
# ensure it's still expanded after value updates
try:
parent_key = name
if parent_key in self._always_open_parents:
self.detail_tree.item(iid, open=True)
except Exception:
pass
if accessor is not None: if accessor is not None:
self.detail_accessors[name] = accessor self.detail_accessors[name] = accessor
return iid return iid
@ -562,14 +593,24 @@ class MonitorApp(tk.Frame):
# insert hierarchical node # insert hierarchical node
parent_iid, leaf_label = _ensure_hierarchy(name) parent_iid, leaf_label = _ensure_hierarchy(name)
try: try:
iid = self.detail_tree.insert(parent_iid, tk.END, values=(leaf_label, value)) leaf_parent_key = '.'.join(name.replace('/', '.').split('.')[:-1])
if leaf_parent_key:
disp_label = f"-> {leaf_label}"
else:
disp_label = leaf_label
iid = self.detail_tree.insert(parent_iid, tk.END, values=(disp_label, value))
self.detail_rows[name] = iid self.detail_rows[name] = iid
if accessor is not None: if accessor is not None:
self.detail_accessors[name] = accessor self.detail_accessors[name] = accessor
return iid return iid
except Exception: except Exception:
try: try:
iid = self.detail_tree.insert('', tk.END, values=(leaf_label, value)) leaf_parent_key = '.'.join(name.replace('/', '.').split('.')[:-1])
if leaf_parent_key:
disp_label = f"-> {leaf_label}"
else:
disp_label = leaf_label
iid = self.detail_tree.insert('', tk.END, values=(disp_label, value))
self.detail_rows[name] = iid self.detail_rows[name] = iid
if accessor is not None: if accessor is not None:
self.detail_accessors[name] = accessor self.detail_accessors[name] = accessor
@ -582,7 +623,12 @@ class MonitorApp(tk.Frame):
# best-effort hierarchical insert # best-effort hierarchical insert
parent_iid, leaf_label = (lambda n: ('' , n))(name) parent_iid, leaf_label = (lambda n: ('' , n))(name)
try: try:
iid = self.detail_tree.insert(parent_iid, tk.END, values=(leaf_label, str(value))) leaf_parent_key = '.'.join(name.replace('/', '.').split('.')[:-1])
if leaf_parent_key:
disp_label = f"-> {leaf_label}"
else:
disp_label = leaf_label
iid = self.detail_tree.insert(parent_iid, tk.END, values=(disp_label, str(value)))
if accessor is not None: if accessor is not None:
self.detail_accessors[name] = accessor self.detail_accessors[name] = accessor
return iid return iid
@ -591,7 +637,7 @@ class MonitorApp(tk.Frame):
except Exception: except Exception:
return None return None
def _add_detail_rows_from_obj(self, prefix: str, obj, max_depth: int = 3, _depth: int = 0): def _add_detail_rows_from_obj(self, prefix: str, obj, max_depth: int = 4, _depth: int = 0):
"""Recursively add detail rows for public fields of `obj`. """Recursively add detail rows for public fields of `obj`.
- `prefix` is a string prepended to field names (e.g. 'param1'). - `prefix` is a string prepended to field names (e.g. 'param1').
@ -621,7 +667,25 @@ class MonitorApp(tk.Frame):
return return
# Enum-like or ctypes scalar handling # Enum-like or ctypes scalar handling
def _is_struct_like_local(v):
# heuristics similar to form builder: ctypes.Structure or many public attrs
if v is None:
return False
if isinstance(v, (int, float, str, bytes)):
return False
if hasattr(v, '_fields_'):
return True
public = [n for n in dir(v) if not n.startswith('_')]
if len(public) > 2:
return True
return False
if hasattr(obj, 'raw') or hasattr(obj, 'value'): if hasattr(obj, 'raw') or hasattr(obj, 'value'):
# If the object also looks like a struct (has nested fields), recurse
if _is_struct_like_local(obj):
# fall through to attribute iteration to expand nested fields
pass
else:
try: try:
s = None s = None
try: try:
@ -661,22 +725,63 @@ class MonitorApp(tk.Frame):
pass pass
# otherwise iterate public attributes # otherwise iterate public attributes
import inspect
names = [n for n in dir(obj) if not n.startswith('_')] names = [n for n in dir(obj) if not n.startswith('_')]
for n in names: for n in names:
try: try:
v = getattr(obj, n) v = getattr(obj, n)
except Exception: except Exception:
continue continue
if callable(v):
continue
field_name = f"{prefix}.{n}" if prefix else n field_name = f"{prefix}.{n}" if prefix else n
# If attribute is a ctypes Union with a '.str' bitfield, expand it
try:
if hasattr(v, 'str') and (hasattr(getattr(v, 'str'), '__dict__') or hasattr(getattr(v, 'str'), '_fields_')):
try:
self._add_detail_rows_from_obj(field_name, getattr(v, 'str'), max_depth=max_depth, _depth=_depth+1)
continue
except Exception:
pass
except Exception:
pass
# If callable, try to call zero-arg getters (get_*/is_*) to obtain a value
if callable(v):
called = False
try:
sig = inspect.signature(v)
if len(sig.parameters) == 0:
try:
val = v()
self._add_detail_row(field_name, self._format_value_for_table(val))
called = True
except Exception:
called = False
except Exception:
# fallback: attempt to call and ignore errors
try:
val = v()
self._add_detail_row(field_name, self._format_value_for_table(val))
called = True
except Exception:
called = False
if called:
continue
# otherwise skip arbitrary callables
continue
# Primitive or scalar-like values
if isinstance(v, (int, float, str)) or hasattr(v, 'raw') or hasattr(v, 'value'): if isinstance(v, (int, float, str)) or hasattr(v, 'raw') or hasattr(v, 'value'):
try: try:
self._add_detail_row(field_name, self._format_value_for_table(v)) self._add_detail_row(field_name, self._format_value_for_table(v))
except Exception: except Exception:
pass pass
else: else:
# Recurse into nested objects
try:
self._add_detail_rows_from_obj(field_name, v, max_depth=max_depth, _depth=_depth+1) self._add_detail_rows_from_obj(field_name, v, max_depth=max_depth, _depth=_depth+1)
except Exception:
pass
def on_init(self): def on_init(self):
# On-demand import/creation of the connection manager when the user requests initialization. # On-demand import/creation of the connection manager when the user requests initialization.
@ -986,15 +1091,24 @@ class MonitorApp(tk.Frame):
def show_message_detail(self, label: str): def show_message_detail(self, label: str):
# build table for this label; detail_tree will be populated and updated # build table for this label; detail_tree will be populated and updated
try: # Do not clear the form here — only clear when we know we'll show
self.detail_tree.delete(*self.detail_tree.get_children()) # an error message or rebuild the details. Clearing before the
except Exception: # MessageDB lookup causes the UI to briefly show then disappear
pass # if the lookup fails during a refresh.
self.detail_rows = {} self.detail_rows = {}
self.detail_selected_label = label self.detail_selected_label = label
try: try:
mdb = self._get_message_db() mdb = self._get_message_db()
if mdb is None: if mdb is None:
# clear any previous form/tree content before showing diagnostic
try:
for w in getattr(self, 'detail_form_container', []).winfo_children():
try:
w.destroy()
except Exception:
pass
except Exception:
pass
try: try:
self.detail_tree.insert('', tk.END, values=("Message DB not available", "Open Diagnostics for details.")) self.detail_tree.insert('', tk.END, values=("Message DB not available", "Open Diagnostics for details."))
except Exception: except Exception:
@ -1004,7 +1118,6 @@ class MonitorApp(tk.Frame):
except Exception: except Exception:
pass pass
return return
return
# Try direct lookup first # Try direct lookup first
try: try:
msg_wrapper = mdb.getMessage(label) msg_wrapper = mdb.getMessage(label)
@ -1035,6 +1148,15 @@ class MonitorApp(tk.Frame):
return return
if not msg_wrapper: if not msg_wrapper:
# clear any existing form before showing 'No data'
try:
for w in getattr(self, 'detail_form_container', []).winfo_children():
try:
w.destroy()
except Exception:
pass
except Exception:
pass
try: try:
self.detail_tree.insert('', tk.END, values=("No data", f"{label}")) self.detail_tree.insert('', tk.END, values=("No data", f"{label}"))
except Exception: except Exception:
@ -1074,6 +1196,15 @@ class MonitorApp(tk.Frame):
except Exception: except Exception:
self.current_msg_wrapper = None self.current_msg_wrapper = None
if msg is None: if msg is None:
# Clear any existing form before showing 'No message'
try:
for w in getattr(self, 'detail_form_container', []).winfo_children():
try:
w.destroy()
except Exception:
pass
except Exception:
pass
try: try:
self.detail_tree.insert('', tk.END, values=("No message", "<empty>")) self.detail_tree.insert('', tk.END, values=("No message", "<empty>"))
except Exception: except Exception:
@ -1084,157 +1215,18 @@ class MonitorApp(tk.Frame):
pass pass
return return
# Handle known tellbacks # Use the same form renderer for B messages (read-only) to avoid toggling
# between tree and form and to keep the UI stable. This will build a
# non-editable form for the message so values can be observed live.
try: try:
if hasattr(msg, 'rdr_mode_tellback') or hasattr(msg, 'settings_tellback'): self.show_message_form(label, editable=False)
# B7 - status tellback
if hasattr(msg, 'rdr_mode_tellback'):
rb = msg.rdr_mode_tellback
# master mode and flags
try:
mm = rb.get_master_mode()
except Exception: except Exception:
mm = getattr(rb, 'raw', rb)
try: try:
mm_str = self._fmt_enum(mm, ['RdrModes', 'RdrModes']) self.log.insert(tk.END, f"Failed to render form for {label}\n")
except Exception:
mm_str = str(mm)
# store accessor to refresh live value
try:
self._add_detail_row('master_mode', mm_str, accessor=(lambda rb=rb: self._fmt_enum(rb.get_master_mode(), ['RdrModes'])))
except Exception:
self._add_detail_row('master_mode', mm_str)
try:
des = rb.get_des_ctrl()
des_str = self._fmt_enum(des, ['DesControl', 'DesignationStatus'])
try:
self._add_detail_row('designation_ctrl', des_str, accessor=(lambda rb=rb: self._fmt_enum(rb.get_des_ctrl(), ['DesControl', 'DesignationStatus'])))
except Exception:
self._add_detail_row('designation_ctrl', des_str)
except Exception:
pass
try:
ib = rb.get_ibit()
ib_str = self._fmt_enum(ib, ['IbitRequest', 'BITReportAvailable'])
try:
self._add_detail_row('ibit', ib_str, accessor=(lambda rb=rb: self._fmt_enum(rb.get_ibit(), ['IbitRequest', 'BITReportAvailable'])))
except Exception:
self._add_detail_row('ibit', ib_str)
except Exception:
pass
# also expand nested fields for richer detail
try:
self._add_detail_rows_from_obj('rdr_mode_tellback', rb, max_depth=3)
except Exception:
pass
# B6 - settings tellback
if hasattr(msg, 'settings_tellback'):
st = msg.settings_tellback
try:
hist = st.get_history_level()
except Exception:
hist = getattr(st, 'raw', '')
try:
sym = st.get_sym_intensity()
except Exception:
sym = ''
try:
self._add_detail_row('history_level', self._fmt_enum(hist, ['TargetHistory']), accessor=(lambda st=st: self._fmt_enum(st.get_history_level(), ['TargetHistory'])))
except Exception:
self._add_detail_row('history_level', self._fmt_enum(hist, ['TargetHistory']))
try:
self._add_detail_row('symbology_intensity', str(sym), accessor=(lambda st=st: str(st.get_sym_intensity())))
except Exception:
self._add_detail_row('symbology_intensity', str(sym))
try:
self._add_detail_rows_from_obj('settings_tellback', st, max_depth=3)
except Exception:
pass
# param1/param2 fields (if present)
try:
if hasattr(msg, 'param1_tellback'):
p1 = msg.param1_tellback
for fn, enum_names in (
('get_rws_submode', ['RwsSubmode']),
('get_spot', ['SpotSelection','SpotSelection']),
('get_acm_submode', ['AcmSubmode']),
('get_gm_submode', ['GmSubmode','GmSubmode']),
('get_expand', ['Expand']),
('get_range_scale', ['RangeScale']),
('get_bars_num', ['BarsNum']),
('get_scan_width', ['ScanWidth','AzimuthScanWidth']),
):
if hasattr(p1, fn):
try:
v = getattr(p1, fn)()
v_str = self._fmt_enum(v, enum_names)
try:
self._add_detail_row(fn, v_str, accessor=(lambda p1=p1, fn=fn, enum_names=enum_names: self._fmt_enum(getattr(p1, fn)(), enum_names)))
except Exception:
self._add_detail_row(fn, v_str)
except Exception:
pass
# after common getters, also walk the p1 struct for other fields
try:
self._add_detail_rows_from_obj('param1_tellback', p1, max_depth=3)
except Exception:
pass
except Exception:
pass
try:
if hasattr(msg, 'param2_tellback'):
p2 = msg.param2_tellback
self._add_detail_row('param2_raw', str(getattr(p2,'raw', '')))
try:
self._add_detail_rows_from_obj('param2_tellback', p2, max_depth=3)
except Exception:
pass
except Exception: except Exception:
pass pass
return return
# Default: dump raw fields of ctypes message
self.detail_tree.insert('', tk.END, values=(f"# {label}", ""))
# default: show top-level public attributes as rows
try:
for name in dir(msg):
if name.startswith('_'):
continue
try:
val = getattr(msg, name)
except Exception:
continue
if callable(val):
continue
# add row and remember item id
vstr = self._format_value_for_table(val)
try:
self._add_detail_row(name, vstr, accessor=(lambda v=val: v))
except Exception:
try:
iid = self.detail_tree.insert('', tk.END, values=(name, vstr))
self.detail_rows[name] = iid
except Exception:
pass
except Exception as e:
# fallback: ensure at least one row
try:
self.detail_tree.insert('', tk.END, values=("<error>", str(e)))
except Exception:
pass
except Exception as e:
try:
self.detail_tree.insert('', tk.END, values=("<error>", str(e)))
except Exception:
pass
try:
self.log.insert(tk.END, f"Error while decoding: {e}\n")
except Exception:
pass
def periodic_update(self): def periodic_update(self):
while self.manager.is_running(): while self.manager.is_running():
try: try:
@ -1369,8 +1361,17 @@ class MonitorApp(tk.Frame):
pass pass
if new_str != cur_val: if new_str != cur_val:
try: try:
# update display value # update display value keeping only the leaf name in the param column
self.detail_tree.item(iid, values=(name, new_str)) try:
leaf_label = name.split('.')[-1]
except Exception:
leaf_label = name
parent_key = '.'.join(name.replace('/', '.').split('.')[:-1])
if parent_key and parent_key in self.detail_rows:
disp = f"-> {leaf_label}"
else:
disp = leaf_label
self.detail_tree.item(iid, values=(disp, new_str))
# flash the changed row # flash the changed row
self._flash_item(iid) self._flash_item(iid)
if DEBUG_DETAIL_UPDATES: if DEBUG_DETAIL_UPDATES: