|
|
|
@@ -1109,7 +1109,6 @@ def on_tree_select(event):
|
|
|
|
|
|
|
|
|
|
selected_id = selected_items[0]
|
|
|
|
|
element = app_state.tree_element_map.get(selected_id)
|
|
|
|
|
components_to_render = []
|
|
|
|
|
|
|
|
|
|
# Remembered so undo and tidy can repaint this same sheet from the DOM.
|
|
|
|
|
app_state.current_sheet_parent = element
|
|
|
|
@@ -1118,42 +1117,16 @@ def on_tree_select(event):
|
|
|
|
|
if app_state.live_mode and app_state.device_session and element is not None:
|
|
|
|
|
comp_id_str = element.get("id")
|
|
|
|
|
if comp_id_str is not None:
|
|
|
|
|
ds = app_state.device_session
|
|
|
|
|
ds.close_wiresheet()
|
|
|
|
|
try:
|
|
|
|
|
ds.open_wiresheet(int(comp_id_str))
|
|
|
|
|
sax_root = ds.build_dom_tree(ds.sox_client.cache_get(0))
|
|
|
|
|
ds.build_links_elements(sax_root)
|
|
|
|
|
app_state.xml_root_element = sax_root
|
|
|
|
|
sax_file.refresh_treeview_from_dom(sax_root)
|
|
|
|
|
# re-find element by comp id after DOM rebuild (tree item IDs changed)
|
|
|
|
|
element = None
|
|
|
|
|
for tid, elem in app_state.tree_element_map.items():
|
|
|
|
|
if elem.get("id") == comp_id_str:
|
|
|
|
|
element = elem
|
|
|
|
|
selected_id = tid
|
|
|
|
|
break
|
|
|
|
|
if element is not None:
|
|
|
|
|
_restoring_selection = True
|
|
|
|
|
tree.selection_set(selected_id)
|
|
|
|
|
tree.see(selected_id)
|
|
|
|
|
tree.after_idle(_clear_restoring_flag)
|
|
|
|
|
app_state.current_sheet_parent = element
|
|
|
|
|
except Exception:
|
|
|
|
|
import traceback; traceback.print_exc()
|
|
|
|
|
_load_live_sheet(app_state.device_session, comp_id_str, pending_link)
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
# The DOM rebuild replaced every element object, so a pending link from the
|
|
|
|
|
# old tree can no longer be found by identity. Re-resolve it by from/to.
|
|
|
|
|
if pending_link is not None:
|
|
|
|
|
old_from = pending_link.get("from", "")
|
|
|
|
|
old_to = pending_link.get("to", "")
|
|
|
|
|
links_container = app_state.xml_root_element.find("links") if app_state.xml_root_element is not None else None
|
|
|
|
|
if links_container is not None:
|
|
|
|
|
for lnk in links_container:
|
|
|
|
|
if lnk.get("from") == old_from and lnk.get("to") == old_to:
|
|
|
|
|
pending_link = lnk
|
|
|
|
|
break
|
|
|
|
|
_paint_sheet(element, selected_id, pending_link)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _paint_sheet(element, selected_id, pending_link):
|
|
|
|
|
"""Paints the sheet of `element` (or of the tree row `selected_id` when it has none)."""
|
|
|
|
|
tree = app_state.tree
|
|
|
|
|
components_to_render = []
|
|
|
|
|
if element is not None:
|
|
|
|
|
components_to_render = [child for child in element if child.tag == "comp"]
|
|
|
|
|
elif tree.exists(selected_id):
|
|
|
|
@@ -1170,6 +1143,98 @@ def on_tree_select(event):
|
|
|
|
|
select_link_on_open_sheet(pending_link)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
# Bumped by every live sheet load, so a load the user has clicked away from is dropped
|
|
|
|
|
# when it finishes rather than painted over the sheet they moved on to.
|
|
|
|
|
_sheet_load_gen = 0
|
|
|
|
|
# One load talks to the device at a time: each starts by unsubscribing the sheet the
|
|
|
|
|
# previous one subscribed.
|
|
|
|
|
_sheet_load_lock = threading.Lock()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _load_live_sheet(ds, comp_id_str, pending_link):
|
|
|
|
|
"""Reads a folder's sheet from the device on a background thread, then paints it.
|
|
|
|
|
|
|
|
|
|
Sox requests wait for the device. A device that has gone away holds them until DASP
|
|
|
|
|
gives up (30 s), which on the UI thread froze the whole window.
|
|
|
|
|
"""
|
|
|
|
|
global _sheet_load_gen
|
|
|
|
|
_sheet_load_gen += 1
|
|
|
|
|
gen = _sheet_load_gen
|
|
|
|
|
|
|
|
|
|
# Blank until the device answers: the rows on screen belong to the DOM the load
|
|
|
|
|
# is about to replace.
|
|
|
|
|
wiresheet.render_components([])
|
|
|
|
|
app_state.set_status("Loading sheet...")
|
|
|
|
|
|
|
|
|
|
def _bg():
|
|
|
|
|
with _sheet_load_lock:
|
|
|
|
|
if gen != _sheet_load_gen:
|
|
|
|
|
return # clicked away while waiting for the previous load
|
|
|
|
|
try:
|
|
|
|
|
ds.close_wiresheet()
|
|
|
|
|
ds.open_wiresheet(int(comp_id_str))
|
|
|
|
|
sax_root = ds.build_dom_tree(ds.sox_client.cache_get(0))
|
|
|
|
|
ds.build_links_elements(sax_root)
|
|
|
|
|
except Exception as exc:
|
|
|
|
|
app_state.window.after(
|
|
|
|
|
0, lambda err=exc: _live_sheet_failed(ds, gen, err))
|
|
|
|
|
return
|
|
|
|
|
app_state.window.after(
|
|
|
|
|
0, lambda: _live_sheet_loaded(ds, gen, comp_id_str, pending_link, sax_root))
|
|
|
|
|
|
|
|
|
|
threading.Thread(target=_bg, daemon=True).start()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _live_sheet_failed(ds, gen, exc):
|
|
|
|
|
if gen != _sheet_load_gen or app_state.device_session is not ds:
|
|
|
|
|
return
|
|
|
|
|
if not ds.is_connected:
|
|
|
|
|
_on_connection_lost(ds, ds.close_cause)
|
|
|
|
|
return
|
|
|
|
|
app_state.set_status(f"Could not open sheet: {exc}", warn=True)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _live_sheet_loaded(ds, gen, comp_id_str, pending_link, sax_root):
|
|
|
|
|
"""UI thread: swaps in the freshly read DOM and paints the sheet."""
|
|
|
|
|
global _restoring_selection
|
|
|
|
|
if gen != _sheet_load_gen or app_state.device_session is not ds:
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
tree = app_state.tree
|
|
|
|
|
app_state.xml_root_element = sax_root
|
|
|
|
|
sax_file.refresh_treeview_from_dom(sax_root)
|
|
|
|
|
# re-find element by comp id after DOM rebuild (tree item IDs changed)
|
|
|
|
|
element = None
|
|
|
|
|
selected_id = None
|
|
|
|
|
for tid, elem in app_state.tree_element_map.items():
|
|
|
|
|
if elem.get("id") == comp_id_str:
|
|
|
|
|
element = elem
|
|
|
|
|
selected_id = tid
|
|
|
|
|
break
|
|
|
|
|
if element is not None:
|
|
|
|
|
_restoring_selection = True
|
|
|
|
|
tree.selection_set(selected_id)
|
|
|
|
|
tree.see(selected_id)
|
|
|
|
|
tree.after_idle(_clear_restoring_flag)
|
|
|
|
|
app_state.current_sheet_parent = element
|
|
|
|
|
|
|
|
|
|
# The DOM rebuild replaced every element object, so a pending link from the
|
|
|
|
|
# old tree can no longer be found by identity. Re-resolve it by from/to.
|
|
|
|
|
if pending_link is not None:
|
|
|
|
|
old_from = pending_link.get("from", "")
|
|
|
|
|
old_to = pending_link.get("to", "")
|
|
|
|
|
links_container = sax_root.find("links")
|
|
|
|
|
if links_container is not None:
|
|
|
|
|
for lnk in links_container:
|
|
|
|
|
if lnk.get("from") == old_from and lnk.get("to") == old_to:
|
|
|
|
|
pending_link = lnk
|
|
|
|
|
break
|
|
|
|
|
|
|
|
|
|
app_state.clear_status()
|
|
|
|
|
if element is not None:
|
|
|
|
|
_paint_sheet(element, selected_id, pending_link)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def delete_selected_link():
|
|
|
|
|
"""Removes every selected link from the DOM, as one undoable step, and repaints."""
|
|
|
|
|
link_elems = []
|
|
|
|
@@ -3157,6 +3222,15 @@ def do_device_disconnect(device_down=False):
|
|
|
|
|
wiresheet.render_components([])
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _on_connection_lost(ds, cause):
|
|
|
|
|
"""Main thread: the device stopped answering (closed simulator, unplugged cable) or
|
|
|
|
|
ended the session. Goes offline as Disconnect does, without asking anything of it."""
|
|
|
|
|
if app_state.device_session is not ds:
|
|
|
|
|
return # already disconnected, or a newer connection has taken its place
|
|
|
|
|
do_device_disconnect(device_down=True)
|
|
|
|
|
app_state.set_status(f"Connection lost: {cause}", warn=True)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def quit_app():
|
|
|
|
|
"""File > Exit and the window's close box: let go of the device before going.
|
|
|
|
|
|
|
|
|
@@ -3190,11 +3264,13 @@ def _start_cov_polling():
|
|
|
|
|
ds = app_state.device_session
|
|
|
|
|
if ds is None or not ds.is_connected:
|
|
|
|
|
_cov_poll_id = None
|
|
|
|
|
if app_state.live_mode:
|
|
|
|
|
do_device_disconnect()
|
|
|
|
|
app_state.set_status("Connection lost", warn=True)
|
|
|
|
|
if ds is not None and app_state.live_mode:
|
|
|
|
|
_on_connection_lost(ds, ds.close_cause)
|
|
|
|
|
return
|
|
|
|
|
|
|
|
|
|
# Scheduled first, so one bad event cannot stop the polling for good.
|
|
|
|
|
_cov_poll_id = app_state.window.after(100, poll)
|
|
|
|
|
|
|
|
|
|
events = ds.poll_events()
|
|
|
|
|
if events and app_state.xml_root_element is not None:
|
|
|
|
|
needs_repaint = False
|
|
|
|
@@ -3210,8 +3286,6 @@ def _start_cov_polling():
|
|
|
|
|
except Exception:
|
|
|
|
|
pass
|
|
|
|
|
|
|
|
|
|
_cov_poll_id = app_state.window.after(100, poll)
|
|
|
|
|
|
|
|
|
|
_cov_poll_id = app_state.window.after(100, poll)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@@ -3272,8 +3346,11 @@ def _live_tree_expand(comp_id, tree_item_id, select=True, on_loaded=None):
|
|
|
|
|
|
|
|
|
|
app_state.window.after(0, _ui)
|
|
|
|
|
except Exception as exc:
|
|
|
|
|
if not ds.is_connected:
|
|
|
|
|
# The COV poll sees the closed session, goes offline and says so.
|
|
|
|
|
return
|
|
|
|
|
app_state.window.after(
|
|
|
|
|
0, lambda: app_state.set_status(f"Load error: {exc}", warn=True)
|
|
|
|
|
0, lambda msg=f"Load error: {exc}": app_state.set_status(msg, warn=True)
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
threading.Thread(target=_bg, daemon=True).start()
|
|
|
|
|