Add Rename to the component right-click menu
Rename is offered from both the canvas and the navigation tree. Names are checked against Sedona's Component.checkName rules (1-7 ASCII chars, a letter first, then letters, digits or '_') and must be unique among siblings. In file mode link paths through the old name are rewritten and the rename is one undo step; in live mode it goes to the device via Sox and warns if the device rejects it. 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.136"
|
||||
VERSION = "0.0.0.137"
|
||||
PROPERTIES_FILE = os.path.join(os.path.dirname(os.path.abspath(__file__)), "editor.properties")
|
||||
DEFAULT_GRID_SIZE = 10
|
||||
DEFAULT_ADD_TIMESTAMP = True
|
||||
|
||||
@@ -1387,6 +1387,71 @@ def open_link_dialog(source_element, target_element):
|
||||
ttk.Button(btn_frame, text="Cancel", width=10, command=dialog.destroy).pack(side=tk.LEFT, padx=5)
|
||||
|
||||
|
||||
def open_rename_dialog(comp):
|
||||
"""Asks for a new name, checking Sedona's naming rules as it is typed."""
|
||||
window = app_state.window
|
||||
root = app_state.xml_root_element
|
||||
if root is None or comp.tag != "comp":
|
||||
return
|
||||
parent = find_parent_element(root, comp)
|
||||
old_name = comp.get("name", "")
|
||||
|
||||
dialog = tk.Toplevel(window)
|
||||
dialog.title("Rename Component")
|
||||
dialog.resizable(False, False)
|
||||
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)
|
||||
|
||||
tk.Label(main_frame, text=f"New name for '{old_name}' [id: {comp.get('id', '?')}]:",
|
||||
anchor="w").grid(row=0, column=0, sticky="w")
|
||||
name_var = tk.StringVar(value=old_name)
|
||||
entry = ttk.Entry(main_frame, textvariable=name_var, width=24)
|
||||
entry.grid(row=1, column=0, sticky="we", pady=(4, 0))
|
||||
tk.Label(main_frame, text="1-7 characters: a letter, then letters, digits or '_'.",
|
||||
anchor="w", fg="#666666").grid(row=2, column=0, sticky="w", pady=(4, 0))
|
||||
error_var = tk.StringVar(value="")
|
||||
tk.Label(main_frame, textvariable=error_var, anchor="w", fg="#b00020",
|
||||
wraplength=320, justify="left").grid(row=3, column=0, sticky="w")
|
||||
|
||||
btn_frame = tk.Frame(main_frame)
|
||||
btn_frame.grid(row=4, column=0, 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 current_error():
|
||||
name = name_var.get()
|
||||
if name == old_name:
|
||||
return None
|
||||
return sax_file.check_component_name(name, parent, comp)
|
||||
|
||||
def on_change(*_):
|
||||
err = current_error()
|
||||
error_var.set(err or "")
|
||||
ok_button.state(["disabled"] if err else ["!disabled"])
|
||||
|
||||
def on_ok(*_):
|
||||
if current_error() is not None:
|
||||
return
|
||||
new_name = name_var.get()
|
||||
dialog.destroy()
|
||||
# An open panel may show the old name, so it is refreshed along with the sheet.
|
||||
if new_name != old_name and sax_file.rename_component(comp, new_name):
|
||||
refresh_all_views()
|
||||
|
||||
ok_button.configure(command=on_ok)
|
||||
name_var.trace_add("write", on_change)
|
||||
entry.bind("<Return>", on_ok)
|
||||
dialog.bind("<Escape>", lambda _: dialog.destroy())
|
||||
entry.focus_set()
|
||||
entry.select_range(0, tk.END)
|
||||
|
||||
|
||||
def fill_component_popup(menu, comp):
|
||||
"""Fills a popup with the actions that apply to one component.
|
||||
|
||||
@@ -1415,6 +1480,12 @@ def fill_component_popup(menu, comp):
|
||||
command=lambda: show_user_service(comp),
|
||||
)
|
||||
menu.add_separator()
|
||||
if comp.tag == "comp":
|
||||
# The app itself is named by its appName property, not by a component name.
|
||||
menu.add_command(
|
||||
label="Rename...",
|
||||
command=lambda: open_rename_dialog(comp),
|
||||
)
|
||||
menu.add_command(
|
||||
label="Delete Component",
|
||||
command=lambda: delete_component(comp),
|
||||
|
||||
+86
@@ -344,6 +344,92 @@ def save_file_as():
|
||||
MAX_NAME_LENGTH = 7 # every component name in a Sedona app observes this
|
||||
|
||||
|
||||
def check_component_name(name, parent=None, element=None):
|
||||
"""Why `name` cannot name a component, or None if it can.
|
||||
|
||||
Mirrors Sedona's Component.checkName -- 1 to 7 ASCII characters, a letter first, then
|
||||
letters, digits or '_' -- plus the uniqueness among siblings that OfflineApp enforces.
|
||||
`element` is the component being renamed, which may keep its own name.
|
||||
"""
|
||||
if len(name) == 0:
|
||||
return "The name is empty."
|
||||
if len(name) > MAX_NAME_LENGTH:
|
||||
return f"The name is longer than {MAX_NAME_LENGTH} characters."
|
||||
for i, c in enumerate(name):
|
||||
if "A" <= c <= "Z" or "a" <= c <= "z":
|
||||
continue
|
||||
if i == 0:
|
||||
return "The name must start with a letter (A-Z or a-z)."
|
||||
if "0" <= c <= "9" or c == "_":
|
||||
continue
|
||||
return f"'{c}' is not allowed; use only letters, digits and '_'."
|
||||
if parent is not None:
|
||||
for sibling in parent:
|
||||
if sibling.tag == "comp" and sibling is not element and sibling.get("name") == name:
|
||||
return f"'{name}' is already used by another component in the same parent."
|
||||
return None
|
||||
|
||||
|
||||
def rename_component(comp_element, new_name):
|
||||
"""Renames a component and rewrites the link paths that ran through it.
|
||||
|
||||
Returns True when the rename happened. The name is validated here as well as in the
|
||||
dialog, so nothing can put a name into the file that Sedona would refuse to load.
|
||||
"""
|
||||
root = app_state.xml_root_element
|
||||
if root is None or comp_element.tag != "comp":
|
||||
return False
|
||||
|
||||
old_name = comp_element.get("name", "")
|
||||
if new_name == old_name:
|
||||
return False
|
||||
|
||||
parent = find_parent_element(root, comp_element)
|
||||
err = check_component_name(new_name, parent, comp_element)
|
||||
if err is not None:
|
||||
messagebox.showwarning("Rename Component", f"'{new_name}' is not a valid name.\n\n{err}")
|
||||
return False
|
||||
|
||||
if app_state.live_mode:
|
||||
if not live_hooks.live_rename_component(comp_element, new_name):
|
||||
live_hooks.warn_failure("Rename Component",
|
||||
f"{old_name} was not renamed on the device")
|
||||
return False
|
||||
|
||||
old_path = get_component_path(comp_element, root)
|
||||
new_path = old_path[:-len(old_name)] + new_name
|
||||
|
||||
# Links address components by path, so every one through the old name moves with it.
|
||||
link_changes = []
|
||||
links_elem = root.find("links")
|
||||
if links_elem is not None:
|
||||
for link in links_elem:
|
||||
if link.tag != "link":
|
||||
continue
|
||||
for attr in ("from", "to"):
|
||||
ref = link.get(attr, "")
|
||||
if ref.startswith(old_path + ".") or ref.startswith(old_path + "/"):
|
||||
link_changes.append((link, attr, ref, new_path + ref[len(old_path):]))
|
||||
|
||||
comp_element.set("name", new_name)
|
||||
for link, attr, _, new_ref in link_changes:
|
||||
link.set(attr, new_ref)
|
||||
|
||||
if not app_state.live_mode:
|
||||
undo.push(undo.RenameComponentCommand(
|
||||
comp_element, old_name, new_name, link_changes,
|
||||
label=f"rename {old_name} to {new_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"{old_name} renamed to {new_name}{where}.")
|
||||
return True
|
||||
|
||||
|
||||
def allocate_component_id():
|
||||
"""Lowest unused id, matching how this app's ids are dense with no gaps."""
|
||||
used = set()
|
||||
|
||||
@@ -248,3 +248,25 @@ class DeleteComponentCommand:
|
||||
links_parent.remove(link)
|
||||
if self.element in list(self.parent):
|
||||
self.parent.remove(self.element)
|
||||
|
||||
|
||||
class RenameComponentCommand:
|
||||
"""A rename, plus every link whose path ran through the component's old name."""
|
||||
|
||||
def __init__(self, element, old_name, new_name, link_changes, label="rename"):
|
||||
# link_changes: list of (link_element, attr, old_value, new_value)
|
||||
self.element = element
|
||||
self.old_name = old_name
|
||||
self.new_name = new_name
|
||||
self.link_changes = link_changes
|
||||
self.label = label
|
||||
|
||||
def undo(self):
|
||||
self.element.set("name", self.old_name)
|
||||
for link, attr, old_value, _ in self.link_changes:
|
||||
link.set(attr, old_value)
|
||||
|
||||
def redo(self):
|
||||
self.element.set("name", self.new_name)
|
||||
for link, attr, _, new_value in self.link_changes:
|
||||
link.set(attr, new_value)
|
||||
|
||||
Reference in New Issue
Block a user