Tree: Reorder... for a component's children
Right-click a component (or the app) with two or more children to move them up and down; OK applies the new order, which is the order they run in. On a device the order goes there first via Sox reorder, checked against the device's children, and the file is left alone if it is refused. Undoable. Also: device host in editor.properties. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
+1
-1
@@ -8,7 +8,7 @@ so later rebinding would only ever be seen by the module that did it.
|
||||
import configparser
|
||||
import os
|
||||
|
||||
VERSION = "0.0.0.165"
|
||||
VERSION = "0.0.0.166"
|
||||
PROPERTIES_FILE = os.path.join(os.path.dirname(os.path.abspath(__file__)), "editor.properties")
|
||||
DEFAULT_GRID_SIZE = 10
|
||||
DEFAULT_ADD_TIMESTAMP = True
|
||||
|
||||
+1
-1
@@ -7,7 +7,7 @@ left_pane_width = 230
|
||||
trace_dasp = True
|
||||
|
||||
[Device]
|
||||
host = 192.168.1.29
|
||||
host = 192.168.1.251
|
||||
port = 1876
|
||||
user = admin
|
||||
|
||||
|
||||
@@ -155,6 +155,43 @@ def live_rename_component(element, new_name):
|
||||
return False
|
||||
|
||||
|
||||
# ── Reorder Children ────────────────────────────────────────────
|
||||
|
||||
def live_reorder_children(parent_element, ordered_children):
|
||||
"""In live mode, sends a Sox reorder to the device. Returns True on success.
|
||||
|
||||
The device takes the order only when it names every child it has, no more and no
|
||||
fewer, so the list is checked against what the device last said before it is sent.
|
||||
"""
|
||||
ds = _session()
|
||||
if ds is None:
|
||||
return False
|
||||
parent_id = _comp_id(parent_element)
|
||||
if parent_id is None:
|
||||
_fail(f"'{parent_element.get('name', '?')}' has no component id.")
|
||||
return False
|
||||
child_ids = []
|
||||
for child in ordered_children:
|
||||
cid = _comp_id(child)
|
||||
if cid is None:
|
||||
_fail(f"'{child.get('name', '?')}' has no component id.")
|
||||
return False
|
||||
child_ids.append(cid)
|
||||
|
||||
client = ds.sox_client
|
||||
cached = client.cache_get(parent_id)
|
||||
if cached is not None and set(cached.children) != set(child_ids):
|
||||
_fail("The editor's list of children no longer matches the device's. "
|
||||
"Reconnect to read the device again.")
|
||||
return False
|
||||
try:
|
||||
client.reorder(parent_id, child_ids)
|
||||
return True
|
||||
except Exception as e:
|
||||
_fail(f"The device rejected the reorder: {e}")
|
||||
return False
|
||||
|
||||
|
||||
# ── Link / Unlink ───────────────────────────────────────────────
|
||||
|
||||
def _link_ends(ds, from_element, from_slot_name, to_element, to_slot_name):
|
||||
|
||||
@@ -1370,6 +1370,10 @@ def on_tree_right_click(event):
|
||||
if app_element is not None:
|
||||
tree_root_context_menu.add_command(
|
||||
label="Properties", command=lambda: show_properties(app_element))
|
||||
if _has_reorderable_children(app_element):
|
||||
tree_root_context_menu.add_command(
|
||||
label="Reorder...",
|
||||
command=lambda: open_reorder_dialog(current_element(app_element)))
|
||||
tree_root_context_menu.add_separator()
|
||||
if app_state.live_mode and app_state.device_session and app_state.device_session.is_connected:
|
||||
for action_name in _APP_ACTIONS:
|
||||
@@ -1393,6 +1397,12 @@ def on_tree_right_click(event):
|
||||
selection = [app_state.tree_element_map[row] for row in selected_rows
|
||||
if row in app_state.tree_element_map]
|
||||
fill_component_popup(tree_comp_context_menu, comp_element, selection)
|
||||
if _has_reorderable_children(comp_element):
|
||||
# After Rename..., with the other entries that change the component itself.
|
||||
rename_index = tree_comp_context_menu.index("Rename...")
|
||||
tree_comp_context_menu.insert_command(
|
||||
rename_index + 1, label="Reorder...",
|
||||
command=lambda: open_reorder_dialog(current_element(comp_element)))
|
||||
tree_comp_context_menu.post(event.x_root, event.y_root)
|
||||
|
||||
|
||||
@@ -1806,6 +1816,128 @@ def open_rename_dialog(comp):
|
||||
entry.select_range(0, tk.END)
|
||||
|
||||
|
||||
def _child_display_names(parent, children):
|
||||
"""Names for a reorder list. On a device a child not yet read is a "..." row, so
|
||||
its name is read from the device rather than shown as dots."""
|
||||
names = [child.get("name", "?") for child in children]
|
||||
if not (app_state.live_mode and "..." in names):
|
||||
return names
|
||||
ds = app_state.device_session
|
||||
try:
|
||||
ds.load_children(int(parent.get("id")))
|
||||
except Exception:
|
||||
return names
|
||||
for i, child in enumerate(children):
|
||||
if names[i] == "...":
|
||||
cached = ds.sox_client.cache_get(int(child.get("id", -1)))
|
||||
if cached is not None:
|
||||
names[i] = cached.name
|
||||
return names
|
||||
|
||||
|
||||
def open_reorder_dialog(parent):
|
||||
"""Lists a component's children to move up and down; OK applies the new order."""
|
||||
window = app_state.window
|
||||
original = [child for child in parent if child.tag == "comp"]
|
||||
if len(original) < 2:
|
||||
return
|
||||
names = dict(zip(map(id, original), _child_display_names(parent, original)))
|
||||
order = list(original)
|
||||
|
||||
dialog = tk.Toplevel(window)
|
||||
dialog.title(f"Reorder {parent.get('name', 'app')}")
|
||||
dialog.transient(window)
|
||||
dialog.grab_set()
|
||||
dialog.geometry(f"+{window.winfo_x() + 100}+{window.winfo_y() + 60}")
|
||||
|
||||
main_frame = tk.Frame(dialog, padx=10, pady=10)
|
||||
main_frame.pack(fill=tk.BOTH, expand=True)
|
||||
main_frame.columnconfigure(0, weight=1)
|
||||
main_frame.rowconfigure(0, weight=1)
|
||||
|
||||
list_frame = tk.Frame(main_frame)
|
||||
list_frame.grid(row=0, column=0, sticky="nsew")
|
||||
listbox = tk.Listbox(list_frame, width=28, height=min(max(len(order), 10), 20),
|
||||
exportselection=False, activestyle="none")
|
||||
scrollbar = ttk.Scrollbar(list_frame, orient=tk.VERTICAL, command=listbox.yview)
|
||||
listbox.configure(yscrollcommand=scrollbar.set)
|
||||
listbox.pack(side=tk.LEFT, fill=tk.BOTH, expand=True)
|
||||
scrollbar.pack(side=tk.RIGHT, fill=tk.Y)
|
||||
|
||||
side = tk.Frame(main_frame, padx=10)
|
||||
side.grid(row=0, column=1, sticky="n")
|
||||
up_button = ttk.Button(side, text="Move Up", width=14)
|
||||
up_button.pack(fill=tk.X)
|
||||
down_button = ttk.Button(side, text="Move Down", width=14)
|
||||
down_button.pack(fill=tk.X, pady=(6, 0))
|
||||
reset_button = ttk.Button(side, text="Reset", width=14)
|
||||
reset_button.pack(fill=tk.X, pady=(40, 0))
|
||||
|
||||
btn_frame = tk.Frame(main_frame)
|
||||
btn_frame.grid(row=1, column=0, columnspan=2, pady=(10, 0))
|
||||
ok_button = ttk.Button(btn_frame, text="OK", width=10)
|
||||
ok_button.pack(side=tk.LEFT, padx=5)
|
||||
ttk.Button(btn_frame, text="Cancel", width=10,
|
||||
command=dialog.destroy).pack(side=tk.LEFT, padx=5)
|
||||
|
||||
def selected_index():
|
||||
sel = listbox.curselection()
|
||||
return sel[0] if sel else None
|
||||
|
||||
def update_buttons(*_):
|
||||
index = selected_index()
|
||||
up_button.state(["!disabled"] if index not in (None, 0) else ["disabled"])
|
||||
down_button.state(["!disabled"] if index not in (None, len(order) - 1)
|
||||
else ["disabled"])
|
||||
reset_button.state(["!disabled"] if order != original else ["disabled"])
|
||||
|
||||
def fill(select=None):
|
||||
listbox.delete(0, tk.END)
|
||||
for child in order:
|
||||
listbox.insert(tk.END, names[id(child)])
|
||||
if select is not None:
|
||||
listbox.selection_set(select)
|
||||
listbox.activate(select)
|
||||
listbox.see(select)
|
||||
update_buttons()
|
||||
|
||||
def move(step):
|
||||
index = selected_index()
|
||||
if index is None or not 0 <= index + step < len(order):
|
||||
return
|
||||
order[index], order[index + step] = order[index + step], order[index]
|
||||
fill(index + step)
|
||||
|
||||
def reset():
|
||||
# Back to the order the dialog opened with, keeping the same row selected.
|
||||
index = selected_index()
|
||||
picked = order[index] if index is not None else None
|
||||
order[:] = original
|
||||
fill(order.index(picked) if picked is not None else None)
|
||||
|
||||
def on_ok(*_):
|
||||
dialog.destroy()
|
||||
if sax_file.reorder_children(parent, order):
|
||||
refresh_all_views()
|
||||
|
||||
up_button.configure(command=lambda: move(-1))
|
||||
down_button.configure(command=lambda: move(1))
|
||||
reset_button.configure(command=reset)
|
||||
ok_button.configure(command=on_ok)
|
||||
listbox.bind("<<ListboxSelect>>", update_buttons)
|
||||
dialog.bind("<Alt-Up>", lambda _: move(-1))
|
||||
dialog.bind("<Alt-Down>", lambda _: move(1))
|
||||
dialog.bind("<Return>", on_ok)
|
||||
dialog.bind("<Escape>", lambda _: dialog.destroy())
|
||||
|
||||
fill(0)
|
||||
listbox.focus_set()
|
||||
|
||||
|
||||
def _has_reorderable_children(element):
|
||||
return sum(1 for child in element if child.tag == "comp") >= 2
|
||||
|
||||
|
||||
def fill_component_popup(menu, comp, selection=None):
|
||||
"""Fills a popup with the actions that apply to one component.
|
||||
|
||||
|
||||
+37
@@ -483,6 +483,43 @@ def rename_component(comp_element, new_name):
|
||||
return True
|
||||
|
||||
|
||||
def reorder_children(parent, new_order):
|
||||
"""Gives `parent`'s child components a new order, which is the order they run in.
|
||||
|
||||
Returns True when the order changed. On a device the new order goes there first,
|
||||
and the file is left alone if it does not.
|
||||
"""
|
||||
root = app_state.xml_root_element
|
||||
if root is None:
|
||||
return False
|
||||
old_order = [child for child in parent if child.tag == "comp"]
|
||||
new_order = list(new_order)
|
||||
if new_order == old_order:
|
||||
return False
|
||||
if (len(new_order) != len(old_order)
|
||||
or {id(c) for c in new_order} != {id(c) for c in old_order}):
|
||||
return False
|
||||
|
||||
name = parent.get("name", "app")
|
||||
if app_state.live_mode:
|
||||
if not live_hooks.live_reorder_children(parent, new_order):
|
||||
live_hooks.warn_failure("Reorder", f"{name} was not reordered on the device")
|
||||
return False
|
||||
|
||||
undo.apply_child_order(parent, new_order)
|
||||
command_class = (undo.LiveReorderChildrenCommand if app_state.live_mode
|
||||
else undo.ReorderChildrenCommand)
|
||||
undo.push(command_class(parent, old_order, new_order, label=f"reorder {name}"))
|
||||
|
||||
app_state.mark_dirty()
|
||||
refresh_treeview_from_dom(root)
|
||||
wiresheet.render_current_sheet()
|
||||
|
||||
where = " on the device" if app_state.live_mode else ""
|
||||
app_state.set_status(f"{name} reordered{where}.")
|
||||
return True
|
||||
|
||||
|
||||
def allocate_component_id():
|
||||
"""Lowest unused id, matching how this app's ids are dense with no gaps."""
|
||||
used = set()
|
||||
|
||||
@@ -378,3 +378,44 @@ class LiveRenameCommand(RenameComponentCommand):
|
||||
raise LiveUndoError(f"{self.old_name} was not renamed to {self.new_name} "
|
||||
"on the device")
|
||||
super().redo()
|
||||
|
||||
|
||||
def apply_child_order(parent, ordered_children):
|
||||
"""Puts `parent`'s <comp> children in the given order; its other children stay put."""
|
||||
children = list(parent)
|
||||
comp_slots = [i for i, child in enumerate(children) if child.tag == "comp"]
|
||||
for slot, child in zip(comp_slots, ordered_children):
|
||||
children[slot] = child
|
||||
parent[:] = children
|
||||
|
||||
|
||||
class ReorderChildrenCommand:
|
||||
"""A new order for a component's children."""
|
||||
|
||||
def __init__(self, parent, old_order, new_order, label="reorder"):
|
||||
self.parent = parent
|
||||
self.old_order = list(old_order)
|
||||
self.new_order = list(new_order)
|
||||
self.label = label
|
||||
|
||||
def undo(self):
|
||||
apply_child_order(self.parent, self.old_order)
|
||||
|
||||
def redo(self):
|
||||
apply_child_order(self.parent, self.new_order)
|
||||
|
||||
|
||||
class LiveReorderChildrenCommand(ReorderChildrenCommand):
|
||||
"""A reorder on the device: the old order goes back there before the DOM takes it."""
|
||||
|
||||
def undo(self):
|
||||
if not live_hooks.live_reorder_children(self.parent, self.old_order):
|
||||
raise LiveUndoError(f"The old order of {self.parent.get('name', 'app')} "
|
||||
"could not be put back on the device")
|
||||
super().undo()
|
||||
|
||||
def redo(self):
|
||||
if not live_hooks.live_reorder_children(self.parent, self.new_order):
|
||||
raise LiveUndoError(f"{self.parent.get('name', 'app')} was not reordered "
|
||||
"on the device")
|
||||
super().redo()
|
||||
|
||||
Reference in New Issue
Block a user