From 59d4eec796d73c64cf17912884c95d223dd526ae Mon Sep 17 00:00:00 2001 From: tetektoza Date: Wed, 20 Aug 2025 00:35:21 +0200 Subject: [PATCH] Assembly: Allow editing joint references As the title says - root cause of this problem was imprecise reference matching by moving_part and TNP string processing incosistencies, so the solution is to give enhanced matching and TNP string mapping for reliable deletion. Also added context menu with keyboard shortucs so user is able to delete stuff with right click. --- src/Mod/Assembly/JointObject.py | 103 +++++++++++++++++++++++++------- 1 file changed, 83 insertions(+), 20 deletions(-) diff --git a/src/Mod/Assembly/JointObject.py b/src/Mod/Assembly/JointObject.py index 4cbf7628ba..55ad10d09c 100644 --- a/src/Mod/Assembly/JointObject.py +++ b/src/Mod/Assembly/JointObject.py @@ -32,7 +32,7 @@ from collections.abc import Sequence if App.GuiUp: import FreeCADGui as Gui - from PySide import QtWidgets + from PySide import QtGui, QtWidgets __title__ = "Assembly Joint object" __author__ = "Ondsel" @@ -1386,6 +1386,8 @@ class TaskAssemblyCreateJoint(QtCore.QObject): self.jForm.featureList.installEventFilter(self) + self.createDeleteAction() + self.addition_rejected = False def accept(self): @@ -1784,34 +1786,80 @@ class TaskAssemblyCreateJoint(QtCore.QObject): if info["State"] == "UP" and info["Key"] == "RETURN": self.accept() + def _removeSelectedItems(self, selected_indexes): + for index in selected_indexes: + row = index.row() + if row < len(self.refs): + ref = self.refs[row] + + ref_id = id(ref) + if hasattr(self, "_original_tnp_map") and ref_id in self._original_tnp_map: + # use original TNP string for newly added references + removal_string = self._original_tnp_map[ref_id] + else: + # use processed element name for reloaded references + removal_string = ref[1][0] + + Gui.Selection.removeSelection(ref[0], removal_string) + else: + print(f"Row {row} is out of bounds for refs (length: {len(self.refs)})") + def eventFilter(self, watched, event): if self.jForm is not None and watched == self.jForm.featureList: if event.type() == QtCore.QEvent.ShortcutOverride: - if event.key() == QtCore.Qt.Key_Delete: - event.accept() # Accept the event only if the key is Delete - return True # Indicate that the event has been handled + if ( + hasattr(self, "deleteAction") + and self.deleteAction.shortcut().matches(event.key()) + != QtGui.QKeySequence.NoMatch + ): + event.accept() + return True return False elif event.type() == QtCore.QEvent.KeyPress: - if event.key() == QtCore.Qt.Key_Delete: - selected_indexes = self.jForm.featureList.selectedIndexes() - - for index in selected_indexes: - row = index.row() - if row < len(self.refs): - ref = self.refs[row] - - Gui.Selection.removeSelection(ref[0], ref[1][0]) - + if ( + hasattr(self, "deleteAction") + and self.deleteAction.shortcut().matches(event.key()) + != QtGui.QKeySequence.NoMatch + ): + self.deleteAction.trigger() return True # Consume the event return super().eventFilter(watched, event) + def createDeleteAction(self): + """Create delete action with shortcut""" + try: + delete_sequence = Gui.QtTools.deleteKeySequence() + except AttributeError: + # fallback to standard key if there is no sequence defined + delete_sequence = QtCore.Qt.Key_Delete + + self.deleteAction = QtGui.QAction("Remove", self.jForm) + self.deleteAction.setShortcut(delete_sequence) + + self.deleteAction.setIcon( + QtWidgets.QApplication.style().standardIcon(QtWidgets.QStyle.SP_DialogCancelButton) + ) + + self.deleteAction.setShortcutVisibleInContextMenu(True) + + self.jForm.featureList.addAction(self.deleteAction) + self.jForm.featureList.setContextMenuPolicy(QtCore.Qt.ActionsContextMenu) + + self.deleteAction.triggered.connect(self.deleteSelectedItems) + + def deleteSelectedItems(self): + """Delete selected items from the feature list - same logic as Delete key in ev filter""" + selected_indexes = self.jForm.featureList.selectedIndexes() + self._removeSelectedItems(selected_indexes) + def getMovingPart(self, ref): return UtilsAssembly.getMovingPart(self.assembly, ref) # selectionObserver stuff def addSelection(self, doc_name, obj_name, sub_name, mousePos): + original_sub_name = sub_name rootObj = App.getDocument(doc_name).getObject(obj_name) # We do not need the full TNP string like :"Part.Body.Pad.;#a:1;:G0;XTR;:Hc94:8,F.Face6" @@ -1849,6 +1897,12 @@ class TaskAssemblyCreateJoint(QtCore.QObject): # add the vertex name to the reference ref = UtilsAssembly.addVertexToReference(ref, vertex_name) + # store the original TNP string for deletion purposes + if hasattr(self, "_original_tnp_map"): + self._original_tnp_map[id(ref)] = original_sub_name + else: + self._original_tnp_map = {id(ref): original_sub_name} + self.refs.append(ref) self.updateJoint() @@ -1860,15 +1914,24 @@ class TaskAssemblyCreateJoint(QtCore.QObject): self.addition_rejected = False return - ref = [App.getDocument(doc_name).getObject(obj_name), [sub_name]] - moving_part = self.getMovingPart(ref) + rootObj = App.getDocument(doc_name).getObject(obj_name) - # Find and remove the corresponding dictionary from the combined list - for reference in self.refs: - sel_moving_part = self.getMovingPart(reference) - if sel_moving_part == moving_part: + # Apply the same processing as in addSelection to ensure consistent comparison + resolved = rootObj.resolveSubElement(sub_name, True) + sub_name = resolved[2] + + sub_name = UtilsAssembly.fixBodyExtraFeatureInSub(doc_name, sub_name) + + for reference in self.refs[:]: + ref_obj = reference[0] + ref_element_name = reference[1][0] if len(reference[1]) > 0 else "" + + # match both object and processed element name for precise identification + if ref_obj == rootObj and ref_element_name == sub_name: self.refs.remove(reference) break + else: + print("No matching ref found for removal!") self.updateJoint()