From 01936497a06c2a0a72472a67a8daa926b79858eb Mon Sep 17 00:00:00 2001 From: VALLONGOL Date: Wed, 17 Dec 2025 14:21:06 +0100 Subject: [PATCH] sistemata la visualizzazione dei campi details con form --- pybusmonitor1553/gui/details_pane.py | 10 +- pybusmonitor1553/gui/monitor.py | 371 ++++++++++++++------------- 2 files changed, 194 insertions(+), 187 deletions(-) diff --git a/pybusmonitor1553/gui/details_pane.py b/pybusmonitor1553/gui/details_pane.py index 1744b1a..950e92f 100644 --- a/pybusmonitor1553/gui/details_pane.py +++ b/pybusmonitor1553/gui/details_pane.py @@ -47,7 +47,7 @@ class DetailsPane: self._details_canvas_frame.bind('', _on_frame_configure) self._details_canvas.bind('', _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) diff --git a/pybusmonitor1553/gui/monitor.py b/pybusmonitor1553/gui/monitor.py index f95c7cb..026aea0 100644 --- a/pybusmonitor1553/gui/monitor.py +++ b/pybusmonitor1553/gui/monitor.py @@ -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 '', 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 '', 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", "")) 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=("", str(e))) - except Exception: - pass - except Exception as e: - try: - self.detail_tree.insert('', tk.END, values=("", 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: