From 9b0dbdaa1d902dfca9bba6506ff44c982186ba6a Mon Sep 17 00:00:00 2001 From: cellios-fbr Date: Mon, 15 Dec 2025 17:40:27 +0000 Subject: [PATCH 1/2] feat: add renaming functionality --- frame_editor/CMakeLists.txt | 1 + .../src/frame_editor/FrameEditorGUI.ui | 13 ++++- frame_editor/src/frame_editor/commands.py | 54 +++++++++++++++++++ .../src/frame_editor/interface_services.py | 32 +++++++++++ frame_editor/src/frame_editor/rqt_editor.py | 45 +++++++++++++++- frame_editor/srv/RenameFrame.srv | 4 ++ 6 files changed, 146 insertions(+), 3 deletions(-) create mode 100644 frame_editor/srv/RenameFrame.srv diff --git a/frame_editor/CMakeLists.txt b/frame_editor/CMakeLists.txt index 33d6972..57d4c23 100644 --- a/frame_editor/CMakeLists.txt +++ b/frame_editor/CMakeLists.txt @@ -65,6 +65,7 @@ add_service_files( GetFrame.srv GetFrameNames.srv RemoveFrame.srv + RenameFrame.srv SetFrame.srv SetParentFrame.srv CopyFrame.srv diff --git a/frame_editor/src/frame_editor/FrameEditorGUI.ui b/frame_editor/src/frame_editor/FrameEditorGUI.ui index 3b517ee..655fcc3 100644 --- a/frame_editor/src/frame_editor/FrameEditorGUI.ui +++ b/frame_editor/src/frame_editor/FrameEditorGUI.ui @@ -123,6 +123,17 @@ + + + + Rename + + + + .. + + + @@ -174,9 +185,7 @@ - true - diff --git a/frame_editor/src/frame_editor/commands.py b/frame_editor/src/frame_editor/commands.py index 6c6ece6..dde49fb 100644 --- a/frame_editor/src/frame_editor/commands.py +++ b/frame_editor/src/frame_editor/commands.py @@ -188,7 +188,61 @@ def undo(self): self.element.hidden = True self.editor.add_undo_level(1, [self.element]) +class Command_RenameElement(QUndoCommand): + '''Copys a source frame's transformation and sets a new parent + ''' + + def __init__(self, editor, element, new_name): + QUndoCommand.__init__(self, "Rename") + self.editor = editor + self.element = element + + if editor.active_frame is element: + self.was_active = True + else: + self.was_active = False + + self.new_name = new_name + self.old_name = element.name + + self.element.name = self.new_name + + def redo(self): + if self.was_active: + self.editor.active_frame = self.element + self.editor.add_undo_level(2) + + # Remove old frame + if self.editor.frames.get(self.old_name): + del self.editor.frames[self.old_name] + + # Add new frame + self.element.name = self.new_name + self.editor.frames[self.new_name] = self.element + self.editor.add_undo_level(1, [self.element]) + + # Update parent names + for frame in self.editor.frames.values(): + if frame.parent == self.old_name: + frame.parent = self.new_name + self.editor.add_undo_level(4, [frame]) + + def undo(self): + if self.was_active: + self.editor.active_frame = self.element + self.editor.add_undo_level(2) + + if self.editor.frames.get(self.new_name): + del self.editor.frames[self.new_name] + + self.element.name = self.old_name + self.editor.frames[self.old_name] = self.element + self.editor.add_undo_level(1, [self.element]) + for frame in self.editor.frames.values(): + if frame.parent == self.new_name: + frame.parent = self.old_name + self.editor.add_undo_level(4, [frame]) class Command_RebaseElement(QUndoCommand): '''Copys a source frame's transformation and sets a new parent diff --git a/frame_editor/src/frame_editor/interface_services.py b/frame_editor/src/frame_editor/interface_services.py index 4d6a641..fb64e87 100644 --- a/frame_editor/src/frame_editor/interface_services.py +++ b/frame_editor/src/frame_editor/interface_services.py @@ -30,6 +30,7 @@ def __init__(self, frame_editor): rospy.Service("~set_frame", SetFrame, self.callback_set_frame) rospy.Service("~set_parent", SetParentFrame, self.callback_set_parent_frame) rospy.Service("~copy_frame", CopyFrame, self.callback_copy_frame) + rospy.Service("~rename_frame", RenameFrame, self.callback_rename_frame) rospy.Service("~load_yaml", LoadYaml, self.callback_load_yaml) rospy.Service("~save_yaml", SaveYaml, self.callback_save_yaml) @@ -276,5 +277,36 @@ def callback_copy_frame(self, request): response.error_code = 9 return response + + def callback_rename_frame(self, request): + rospy.loginfo("> Request to rename frame '{}' with new name '{}'".format(request.source_name, request.new_name)) + + response = RenameFrameResponse() + response.error_code = 0 + + if request.new_name == "": + rospy.logerr(" Error: No name given") + response.error_code = 1 + + elif request.source_name == "": + rospy.logerr(" Error: No source name given") + response.error_code = 3 + + elif request.source_name not in self.editor.frames: + rospy.logerr(f" Error: Frame not found: {request.source_name}") + response.error_code = 2 + + elif request.new_name in self.editor.frames: + rospy.logerr(f" Error: New frame name already exists: {request.new_name}") + response.error_code = 4 + + else: + try: + self.editor.command(Command_RenameElement(self.editor, self.editor.frames[request.source_name], request.new_name)) + except Exception as e: + rospy.logerr("Error: unhandled exception {}".format(e)) + response.error_code = 9 + + return response # eof diff --git a/frame_editor/src/frame_editor/rqt_editor.py b/frame_editor/src/frame_editor/rqt_editor.py index feb2499..2822234 100755 --- a/frame_editor/src/frame_editor/rqt_editor.py +++ b/frame_editor/src/frame_editor/rqt_editor.py @@ -116,6 +116,7 @@ def create_main_widget(self): ## widget.btn_add.clicked.connect(self.btn_add_clicked) widget.btn_delete.clicked.connect(self.btn_delete_clicked) + widget.btn_rename.clicked.connect(self.btn_rename_clicked) widget.btn_duplicate.clicked.connect(self.btn_duplicate_clicked) widget.list_frames.currentItemChanged.connect(self.selected_frame_changed) widget.list_tf.currentItemChanged.connect(self.update_measurement) @@ -151,6 +152,7 @@ def create_main_widget(self): widget.txt_c.editingFinished.connect(self.c_valueChanged) widget.txt_group.editingFinished.connect(self.group_valueChanged) + widget.txt_name.editingFinished.connect(self.frameName_valueChanged) widget.btn_rad.toggled.connect(self.update_fields) @@ -559,6 +561,24 @@ def btn_duplicate_clicked(self, checked): self.editor.command(Command_CopyElement(self.editor, name, source_name, parent_name)) self.signal_update_tf.emit(False, False) + + @Slot(bool) + def btn_rename_clicked(self, checked): + item = self.widget.list_frames.currentItem() + if not item: + return + + # Check if the item is a frame. Early returns if selected item is not a frame (e.g. a group) + source_name = item.text(0) + if self.editor.frames.get(source_name) is None: + return + + new_name = self.get_valid_frame_name("Rename Frame", default_name=source_name) + if not new_name: + return + + self.editor.command(Command_RenameElement(self.editor, self.editor.frames[source_name], new_name)) + self.signal_update_tf.emit(True, True) def get_sleep_time(self): return max(5.0 / self.editor.hz, 0.1) @@ -585,7 +605,13 @@ def btn_delete_clicked(self, checked): item = self.widget.list_frames.currentItem() if not item: return - self.editor.command(Command_RemoveElement(self.editor, self.editor.frames[item.text(0)])) + + # Check if the item is a frame. Early returns if selected item is not a frame (e.g. a group) + source_name = item.text(0) + if self.editor.frames.get(source_name) is None: + return + + self.editor.command(Command_RemoveElement(self.editor, self.editor.frames[source_name])) self.signal_update_tf.emit(True, True) ## PARENTING ## @@ -714,7 +740,24 @@ def group_valueChanged(self): if self.editor.active_frame.group != value: self.editor.command(Command_SetGroup(self.editor, self.editor.active_frame, value)) + @Slot() + def frameName_valueChanged(self): + item = self.widget.list_frames.currentItem() + if not item: + return + source_name = item.text(0) + + new_name = self.widget.txt_name.text() + existing_tf_frames = set(self.editor.all_frame_ids()) + existing_editor_frames = set(self.editor.all_editor_frame_ids()) + # allow recreating if frame was published by frameditor node originally + if new_name in existing_editor_frames or (new_name in existing_tf_frames and not Frame.was_published_by_frameeditor(new_name)): + self.widget.txt_name.setText(source_name) + return None + + self.editor.command(Command_RenameElement(self.editor, self.editor.frames[source_name], new_name)) + self.signal_update_tf.emit(True, True) ## FRAME STYLE ## ## diff --git a/frame_editor/srv/RenameFrame.srv b/frame_editor/srv/RenameFrame.srv new file mode 100644 index 0000000..c3b0ba8 --- /dev/null +++ b/frame_editor/srv/RenameFrame.srv @@ -0,0 +1,4 @@ +string source_name +string new_name +--- +int32 error_code \ No newline at end of file From a58c7991629598d054dde32bd90629730921dc71 Mon Sep 17 00:00:00 2001 From: cellios-fbr Date: Mon, 19 Jan 2026 10:04:05 +0000 Subject: [PATCH 2/2] chore: minor adjustments. implemented @ipa-danb suggestions --- frame_editor/src/frame_editor/commands.py | 11 +++-------- frame_editor/src/frame_editor/interface_services.py | 7 +++++-- frame_editor/src/frame_editor/rqt_editor.py | 4 +++- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/frame_editor/src/frame_editor/commands.py b/frame_editor/src/frame_editor/commands.py index dde49fb..62a047b 100644 --- a/frame_editor/src/frame_editor/commands.py +++ b/frame_editor/src/frame_editor/commands.py @@ -197,10 +197,7 @@ def __init__(self, editor, element, new_name): self.editor = editor self.element = element - if editor.active_frame is element: - self.was_active = True - else: - self.was_active = False + self.was_active = editor.active_frame is element self.new_name = new_name self.old_name = element.name @@ -213,8 +210,7 @@ def redo(self): self.editor.add_undo_level(2) # Remove old frame - if self.editor.frames.get(self.old_name): - del self.editor.frames[self.old_name] + self.editor.frames.pop(self.old_name, None) # Add new frame self.element.name = self.new_name @@ -232,8 +228,7 @@ def undo(self): self.editor.active_frame = self.element self.editor.add_undo_level(2) - if self.editor.frames.get(self.new_name): - del self.editor.frames[self.new_name] + self.editor.frames.pop(self.new_name, None) self.element.name = self.old_name self.editor.frames[self.old_name] = self.element diff --git a/frame_editor/src/frame_editor/interface_services.py b/frame_editor/src/frame_editor/interface_services.py index fb64e87..7d767a7 100644 --- a/frame_editor/src/frame_editor/interface_services.py +++ b/frame_editor/src/frame_editor/interface_services.py @@ -293,7 +293,7 @@ def callback_rename_frame(self, request): response.error_code = 3 elif request.source_name not in self.editor.frames: - rospy.logerr(f" Error: Frame not found: {request.source_name}") + rospy.logerr(f" Error: Source Frame not found: {request.source_name}") response.error_code = 2 elif request.new_name in self.editor.frames: @@ -302,7 +302,10 @@ def callback_rename_frame(self, request): else: try: - self.editor.command(Command_RenameElement(self.editor, self.editor.frames[request.source_name], request.new_name)) + self.editor.command(Command_RenameElement(self.editor, self.editor.frames.get(request.source_name), request.new_name)) + except AttributeError as e: + rospy.logerr("Error: The source frame was not found. {}".format(e)) + response.error_code = 9 except Exception as e: rospy.logerr("Error: unhandled exception {}".format(e)) response.error_code = 9 diff --git a/frame_editor/src/frame_editor/rqt_editor.py b/frame_editor/src/frame_editor/rqt_editor.py index 2822234..fcd5d9b 100755 --- a/frame_editor/src/frame_editor/rqt_editor.py +++ b/frame_editor/src/frame_editor/rqt_editor.py @@ -752,8 +752,10 @@ def frameName_valueChanged(self): existing_editor_frames = set(self.editor.all_editor_frame_ids()) # allow recreating if frame was published by frameditor node originally - if new_name in existing_editor_frames or (new_name in existing_tf_frames and not Frame.was_published_by_frameeditor(new_name)): + if source_name != new_name and (new_name in existing_editor_frames or (new_name in existing_tf_frames and not Frame.was_published_by_frameeditor(new_name))): self.widget.txt_name.setText(source_name) + QtWidgets.QMessageBox.warning(self.widget, "Invalid Frame Name", + f"The frame name {new_name} already exists. Cannot create a new frame with the same name.") return None self.editor.command(Command_RenameElement(self.editor, self.editor.frames[source_name], new_name))