fix(datum): prevent RuntimeError from deleted Qt widgets in params UI

QFormLayout.removeRow() destroys the widgets it owns, transferring
ownership to Qt which immediately deletes the underlying C++ objects.
The update_params_ui() method was calling removeRow(0) in a loop to
clear the parameter fields before re-populating them for the new mode.
This destroyed the long-lived QDoubleSpinBox instances (offset_spin,
angle_spin, param_spin, x/y/z_spin) stored as instance attributes.

On the next call to update_params_ui() — triggered by selection
changes, row removal, or mode override — the method attempted to
addRow() with these same Python references, but the C++ objects behind
them had already been freed by Qt. This produced:

  RuntimeError: Internal C++ object (PySide6.QtWidgets.QDoubleSpinBox)
  already deleted.

The fix replaces the removeRow() loop with a new _clear_params_layout()
method that uses QLayout.takeAt() to detach items from the layout
without destroying them. Each widget is hidden and reparented to None
(releasing Qt's ownership) so it survives the layout clearing and can
be safely re-added with addRow() and show() on the next mode switch.
This commit is contained in:
Zoe Forbes
2026-01-31 20:39:20 -06:00
parent 8d1f195e56
commit 6f4ac5ffb1

View File

@@ -458,11 +458,24 @@ class DatumCreatorTaskPanel:
self.update_params_ui(mode_id)
return
def _clear_params_layout(self):
"""Remove all rows from params_layout without deleting the widgets.
QFormLayout.removeRow() destroys the widgets it owns. Instead we
detach every item from the layout (which relinquishes ownership)
and hide the widgets so they can be re-added later.
"""
while self.params_layout.count():
item = self.params_layout.takeAt(0)
widget = item.widget()
if widget is not None:
widget.hide()
widget.setParent(None)
def update_params_ui(self, mode_id):
"""Update parameters UI based on mode."""
# Clear existing params
while self.params_layout.rowCount() > 0:
self.params_layout.removeRow(0)
# Clear existing params without destroying widgets
self._clear_params_layout()
if mode_id is None:
self.params_group.setVisible(False)
@@ -471,18 +484,26 @@ class DatumCreatorTaskPanel:
self.params_group.setVisible(True)
if mode_id in ("offset_face", "offset_plane"):
self.offset_spin.show()
self.params_layout.addRow("Offset:", self.offset_spin)
elif mode_id == "angled":
self.angle_spin.show()
self.params_layout.addRow("Angle:", self.angle_spin)
elif mode_id == "normal_edge":
self.param_spin.show()
self.params_layout.addRow("Position (0-1):", self.param_spin)
elif mode_id == "tangent_cyl":
self.angle_spin.show()
self.params_layout.addRow("Angle:", self.angle_spin)
elif mode_id == "point_xyz":
self.x_spin.show()
self.y_spin.show()
self.z_spin.show()
self.params_layout.addRow("X:", self.x_spin)
self.params_layout.addRow("Y:", self.y_spin)
self.params_layout.addRow("Z:", self.z_spin)
elif mode_id == "point_edge":
self.param_spin.show()
self.params_layout.addRow("Position (0-1):", self.param_spin)
else:
# No parameters needed