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:
2026-10-10 23:39:06 +03:00
co-authored by Claude Opus 5.5
parent 232eea99f4
commit 08b4dec532
6 changed files with 249 additions and 2 deletions
+132
View File
@@ -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.