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:
@@ -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.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user