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.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.heading("param", text="Parameter")
self.detail_tree.heading("value", text="Value")
@ -57,10 +57,16 @@ class DetailsPane:
self.detail_tree.configure() # placeholder to satisfy possible callers
except Exception:
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
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
setattr(app, 'detail_tree', self.detail_tree)

View File

@ -65,6 +65,8 @@ class MonitorApp(tk.Frame):
self.update_loop_running = False
# cache tree item ids by message label for incremental updates
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.
def create_widgets(self):
@ -306,18 +308,16 @@ class MonitorApp(tk.Frame):
except Exception:
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).
The form is placed into `self.detail_form_container`. Existing form
widgets are destroyed and rebuilt for the new message.
"""
try:
# ensure tree is hidden
try:
self.detail_tree.pack_forget()
except Exception:
pass
# Use the form container for showing message details; do not toggle the
# tree visibility here. The DetailsPane places the form inside a
# scrollable canvas so it remains visible while updating.
# if same form is already shown, just refresh widget values
if getattr(self, 'current_form_label', None) == label and self.form_widgets:
try:
@ -353,13 +353,17 @@ class MonitorApp(tk.Frame):
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.pack(side=tk.LEFT)
apply_btn = tk.Button(hdr, text='Apply', command=lambda: self._apply_form_values(msg_wrapper))
apply_btn.pack(side=tk.RIGHT, padx=6)
# 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.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
frm = tk.Frame(self.detail_form_container)
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
try:
@ -372,7 +376,7 @@ class MonitorApp(tk.Frame):
except Exception:
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."""
if _depth > max_depth:
return
@ -436,7 +440,9 @@ class MonitorApp(tk.Frame):
# show human-friendly label first, then numeric value
choices = [f"{n} ({v})" for (n, v) in enum_items]
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
try:
raw = int(getattr(val, 'raw', getattr(val, 'value', val)))
@ -464,6 +470,12 @@ class MonitorApp(tk.Frame):
except Exception:
var.set('')
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)
widget = ('entry', ent)
@ -532,6 +544,12 @@ class MonitorApp(tk.Frame):
# create parent node with empty value
try:
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
parent = iid
except Exception:
@ -552,7 +570,20 @@ class MonitorApp(tk.Frame):
try:
# keep the param column stable; show only the leaf label
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:
self.detail_accessors[name] = accessor
return iid
@ -562,14 +593,24 @@ class MonitorApp(tk.Frame):
# insert hierarchical node
parent_iid, leaf_label = _ensure_hierarchy(name)
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
if accessor is not None:
self.detail_accessors[name] = accessor
return iid
except Exception:
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
if accessor is not None:
self.detail_accessors[name] = accessor
@ -582,7 +623,12 @@ class MonitorApp(tk.Frame):
# best-effort hierarchical insert
parent_iid, leaf_label = (lambda n: ('' , n))(name)
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:
self.detail_accessors[name] = accessor
return iid
@ -591,7 +637,7 @@ class MonitorApp(tk.Frame):
except Exception:
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`.
- `prefix` is a string prepended to field names (e.g. 'param1').
@ -621,24 +667,42 @@ class MonitorApp(tk.Frame):
return
# 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'):
try:
s = None
# 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:
raw = int(getattr(obj, 'raw')) if hasattr(obj, 'raw') else int(getattr(obj, 'value'))
s = self._fmt_enum(raw, [obj.__class__.__name__])
s = None
try:
raw = int(getattr(obj, 'raw')) if hasattr(obj, 'raw') else int(getattr(obj, 'value'))
s = self._fmt_enum(raw, [obj.__class__.__name__])
except Exception:
try:
s = str(getattr(obj, 'raw', getattr(obj, 'value', obj)))
except Exception:
s = repr(obj)
self._add_detail_row(prefix or obj.__class__.__name__, s)
except Exception:
try:
s = str(getattr(obj, 'raw', getattr(obj, 'value', obj)))
self._add_detail_row(prefix or '<value>', repr(obj))
except Exception:
s = repr(obj)
self._add_detail_row(prefix or obj.__class__.__name__, s)
except Exception:
try:
self._add_detail_row(prefix or '<value>', repr(obj))
except Exception:
pass
return
pass
return
# dict-like
try:
@ -661,22 +725,63 @@ class MonitorApp(tk.Frame):
pass
# otherwise iterate public attributes
import inspect
names = [n for n in dir(obj) if not n.startswith('_')]
for n in names:
try:
v = getattr(obj, n)
except Exception:
continue
if callable(v):
continue
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'):
try:
self._add_detail_row(field_name, self._format_value_for_table(v))
except Exception:
pass
else:
self._add_detail_rows_from_obj(field_name, v, max_depth=max_depth, _depth=_depth+1)
# Recurse into nested objects
try:
self._add_detail_rows_from_obj(field_name, v, max_depth=max_depth, _depth=_depth+1)
except Exception:
pass
def on_init(self):
# 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):
# build table for this label; detail_tree will be populated and updated
try:
self.detail_tree.delete(*self.detail_tree.get_children())
except Exception:
pass
# Do not clear the form here — only clear when we know we'll show
# an error message or rebuild the details. Clearing before the
# MessageDB lookup causes the UI to briefly show then disappear
# if the lookup fails during a refresh.
self.detail_rows = {}
self.detail_selected_label = label
try:
mdb = self._get_message_db()
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:
self.detail_tree.insert('', tk.END, values=("Message DB not available", "Open Diagnostics for details."))
except Exception:
@ -1004,7 +1118,6 @@ class MonitorApp(tk.Frame):
except Exception:
pass
return
return
# Try direct lookup first
try:
msg_wrapper = mdb.getMessage(label)
@ -1035,6 +1148,15 @@ class MonitorApp(tk.Frame):
return
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:
self.detail_tree.insert('', tk.END, values=("No data", f"{label}"))
except Exception:
@ -1074,6 +1196,15 @@ class MonitorApp(tk.Frame):
except Exception:
self.current_msg_wrapper = 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:
self.detail_tree.insert('', tk.END, values=("No message", "<empty>"))
except Exception:
@ -1084,156 +1215,17 @@ class MonitorApp(tk.Frame):
pass
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:
if hasattr(msg, 'rdr_mode_tellback') or hasattr(msg, 'settings_tellback'):
# 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:
mm = getattr(rb, 'raw', rb)
try:
mm_str = self._fmt_enum(mm, ['RdrModes', 'RdrModes'])
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:
pass
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
self.show_message_form(label, editable=False)
except Exception:
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")
self.log.insert(tk.END, f"Failed to render form for {label}\n")
except Exception:
pass
return
def periodic_update(self):
while self.manager.is_running():
@ -1369,8 +1361,17 @@ class MonitorApp(tk.Frame):
pass
if new_str != cur_val:
try:
# update display value
self.detail_tree.item(iid, values=(name, new_str))
# update display value keeping only the leaf name in the param column
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
self._flash_item(iid)
if DEBUG_DETAIL_UPDATES: