From 47791a627c2b5803b6225f9c93115ce5016992d0 Mon Sep 17 00:00:00 2001 From: jellybean2004 Date: Wed, 22 Apr 2026 09:34:30 +0100 Subject: [PATCH 1/5] Add blocker in setModelFromParams --- src/sas/qtgui/Plotting/SlicerModel.py | 48 ++++++++++++++++----------- 1 file changed, 28 insertions(+), 20 deletions(-) diff --git a/src/sas/qtgui/Plotting/SlicerModel.py b/src/sas/qtgui/Plotting/SlicerModel.py index 6844acf045..d4b3e4429a 100644 --- a/src/sas/qtgui/Plotting/SlicerModel.py +++ b/src/sas/qtgui/Plotting/SlicerModel.py @@ -16,19 +16,23 @@ def setModelFromParams(self): Set up the Qt model for data handling between controls """ parameters = self.getParams() - self._model.removeRows(0, self._model.rowCount()) - # Crete/overwrite model items - for parameter in list(parameters.keys()): - item1 = QtGui.QStandardItem(parameter) - if isinstance(parameters[parameter], bool): - item2 = QtGui.QStandardItem(parameters[parameter]) - item2.setCheckable(True) - item2.setCheckState(QtCore.Qt.Checked if parameters[parameter] else QtCore.Qt.Unchecked) - else: - item2 = QtGui.QStandardItem(GuiUtils.formatNumber(parameters[parameter])) - self._model.appendRow([item1, item2]) - self._model.setHeaderData(0, QtCore.Qt.Horizontal, "Parameter") - self._model.setHeaderData(1, QtCore.Qt.Horizontal, "Value") + blocked = self._model.blockSignals(True) + try: + self._model.removeRows(0, self._model.rowCount()) + # Crete/overwrite model items + for parameter in list(parameters.keys()): + item1 = QtGui.QStandardItem(parameter) + if isinstance(parameters[parameter], bool): + item2 = QtGui.QStandardItem(parameters[parameter]) + item2.setCheckable(True) + item2.setCheckState(QtCore.Qt.Checked if parameters[parameter] else QtCore.Qt.Unchecked) + else: + item2 = QtGui.QStandardItem(GuiUtils.formatNumber(parameters[parameter])) + self._model.appendRow([item1, item2]) + self._model.setHeaderData(0, QtCore.Qt.Horizontal, "Parameter") + self._model.setHeaderData(1, QtCore.Qt.Horizontal, "Value") + finally: + self._model.blockSignals(blocked) def setParamsFromModel(self): """ @@ -44,9 +48,11 @@ def setParamsFromModel(self): else: params[param_name] = float(self._model.item(row_index, 1).text()) - self.update_model = False - self.setParams(params) - self.update_model = True + blocked = self._model.blockSignals(True) + try: + self.setParams(params) + finally: + self._model.blockSignals(blocked) def setParamsFromModelItem(self, item): """ @@ -61,9 +67,11 @@ def setParamsFromModelItem(self, item): else: params[param_name] = float(self._model.item(row_index, 1).text()) - self.update_model = False - self.setParams(params) - self.update_model = True + blocked = self._model.blockSignals(True) + try: + self.setParams(params) + finally: + self._model.blockSignals(blocked) def model(self): '''getter for the model''' @@ -73,7 +81,7 @@ def getParams(self): ''' pure virtual ''' raise NotImplementedError("Parameter getter must be implemented in derived class.") - def setParams(self): + def setParams(self, params): ''' pure virtual ''' raise NotImplementedError("Parameter setter must be implemented in derived class.") From 54beafe55f2de90247275a649e3021a1fe7a121e Mon Sep 17 00:00:00 2001 From: jellybean2004 Date: Wed, 22 Apr 2026 09:49:43 +0100 Subject: [PATCH 2/5] Changes references --- src/sas/qtgui/Plotting/Slicers/BoxSum.py | 16 +++++++--------- .../qtgui/Plotting/Slicers/MultiSlicerBase.py | 8 +------- 2 files changed, 8 insertions(+), 16 deletions(-) diff --git a/src/sas/qtgui/Plotting/Slicers/BoxSum.py b/src/sas/qtgui/Plotting/Slicers/BoxSum.py index a987ce0a5e..a5905b6be9 100644 --- a/src/sas/qtgui/Plotting/Slicers/BoxSum.py +++ b/src/sas/qtgui/Plotting/Slicers/BoxSum.py @@ -39,7 +39,6 @@ def __init__(self, base, axes, color="black", zorder=3): self.markers = [] self.axes = axes self._model = None - self.update_model = False # connect the artist for the motion self.connect = self.base.connect # Reference to the widget (if any) @@ -98,14 +97,12 @@ def __init__(self, base, axes, color="black", zorder=3): # Save the name of the slicer panel associate with this slicer self.panel_name = "" # Update and post slicer parameters - self.update_model = False self.update() self.postData() # set up the model self._model = QtGui.QStandardItemModel(1, 9) self.setModelFromParams() - self.update_model = True self._model.itemChanged.connect(self.setParamsFromModel) def validate(self, param_name, param_value): @@ -151,10 +148,12 @@ def setParamsFromModel(self): params["Width"] = toDouble(self.model().item(0, 1).text()) params["center_x"] = toDouble(self.model().item(0, 2).text()) params["center_y"] = toDouble(self.model().item(0, 3).text()) - self.update_model = False - self.setParams(params) - self.setReadOnlyParametersFromModel() - self.update_model = True + blocked = self._model.blockSignals(True) + try: + self.setParams(params) + self.setReadOnlyParametersFromModel() + finally: + self._model.blockSignals(blocked) def setPanelName(self, name): """ @@ -234,8 +233,7 @@ def postData(self): # Dig out number of points summed, SMK & PDB, 04/03/2013 boxtotal = Boxsum(x_min=x_min, x_max=x_max, y_min=y_min, y_max=y_max) self.total, self.totalerror, self.points = boxtotal(self.data) - if self.update_model: - self.setModelFromParams() + self.setModelFromParams() def moveend(self, ev): """ diff --git a/src/sas/qtgui/Plotting/Slicers/MultiSlicerBase.py b/src/sas/qtgui/Plotting/Slicers/MultiSlicerBase.py index 71e34d589d..946b7a8a42 100644 --- a/src/sas/qtgui/Plotting/Slicers/MultiSlicerBase.py +++ b/src/sas/qtgui/Plotting/Slicers/MultiSlicerBase.py @@ -333,18 +333,12 @@ def _synchronized_moveend(self, ev, interactor_name): except (ValueError, RuntimeError) as e: logger.warning(f"Failed to post data for master slicer: {e}") - # Now post data for all secondary slicers (with update_model temporarily enabled) + # Now post data for all secondary slicers for i, slicer in enumerate(self.slicers[1:], start=1): try: - # Temporarily enable model updates for this slicer - old_update_model = slicer.update_model - slicer.update_model = True with warnings.catch_warnings(): warnings.simplefilter("ignore") slicer._post_data() - - # Restore original state - slicer.update_model = old_update_model except (ValueError, RuntimeError) as e: logger.warning(f"Failed to post data for slicer {i + 1}: {e}") From 93f4b8bd59ac70319a52aa1ba5918a641fdccb21 Mon Sep 17 00:00:00 2001 From: jellybean2004 Date: Wed, 22 Apr 2026 09:55:50 +0100 Subject: [PATCH 3/5] Code improvements --- src/sas/qtgui/Plotting/Slicers/AnnulusSlicer.py | 10 +++++----- src/sas/qtgui/Plotting/UnitTesting/SlicerModelTest.py | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/sas/qtgui/Plotting/Slicers/AnnulusSlicer.py b/src/sas/qtgui/Plotting/Slicers/AnnulusSlicer.py index 59cf0e5124..a4f2ff600f 100644 --- a/src/sas/qtgui/Plotting/Slicers/AnnulusSlicer.py +++ b/src/sas/qtgui/Plotting/Slicers/AnnulusSlicer.py @@ -173,23 +173,23 @@ def validate(self, param_name, param_value): if param_name == "inner_radius": # First, check the closeness if numpy.fabs(param_value - self.getParams()["outer_radius"]) < MIN_DIFFERENCE: - print("Inner and outer radii too close. Please adjust.") + logger.warning("Inner and outer radii too close. Please adjust.") isValid = False elif param_value > self.qmax: - print("Inner radius exceeds maximum range. Please adjust.") + logger.warning("Inner radius exceeds maximum range. Please adjust.") isValid = False elif param_name == "outer_radius": # First, check the closeness if numpy.fabs(param_value - self.getParams()["inner_radius"]) < MIN_DIFFERENCE: - print("Inner and outer radii too close. Please adjust.") + logger.warning("Inner and outer radii too close. Please adjust.") isValid = False elif param_value > self.qmax: - print("Outer radius exceeds maximum range. Please adjust.") + logger.warning("Outer radius exceeds maximum range. Please adjust.") isValid = False elif param_name == "nbins": # Can't be 0 if param_value < 1: - print("Number of bins cannot be less than or equal to 0. Please adjust.") + logger.warning("Number of bins cannot be less than or equal to 0. Please adjust.") isValid = False return isValid diff --git a/src/sas/qtgui/Plotting/UnitTesting/SlicerModelTest.py b/src/sas/qtgui/Plotting/UnitTesting/SlicerModelTest.py index 0257c4f2bf..2b4dce0466 100644 --- a/src/sas/qtgui/Plotting/UnitTesting/SlicerModelTest.py +++ b/src/sas/qtgui/Plotting/UnitTesting/SlicerModelTest.py @@ -28,7 +28,7 @@ def testBaseClass(self, qapp): '''Assure that SlicerModel contains pure virtuals''' model = SlicerModel() with pytest.raises(NotImplementedError): - model.setParams() + model.setParams({}) with pytest.raises(NotImplementedError): model.setModelFromParams() From b7a07ca12c8b0b48b6fa3648d111071dd99a6cf7 Mon Sep 17 00:00:00 2001 From: jellybean2004 Date: Wed, 22 Apr 2026 10:03:21 +0100 Subject: [PATCH 4/5] Adds tests --- .../Plotting/UnitTesting/SlicerModelTest.py | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/src/sas/qtgui/Plotting/UnitTesting/SlicerModelTest.py b/src/sas/qtgui/Plotting/UnitTesting/SlicerModelTest.py index 2b4dce0466..9e163ed110 100644 --- a/src/sas/qtgui/Plotting/UnitTesting/SlicerModelTest.py +++ b/src/sas/qtgui/Plotting/UnitTesting/SlicerModelTest.py @@ -64,3 +64,27 @@ def testSetParamsFromModel(self, model): # Check the new model. The update should be automatic assert model.model().rowCount() == 3 assert model.model().columnCount() == 2 + + + def testSetModelFromParams_blocks_itemChanged(self, model): + """Programmatic model writes should not emit itemChanged.""" + emissions = [] + model.model().itemChanged.connect(lambda *args: emissions.append(args)) + + model.setModelFromParams() + + assert emissions == [] + + def testSetModelFromParams_restores_signal_state_on_exception(self, model, monkeypatch): + """Signal blocking should be restored even if model population fails.""" + def boom(value): + if value == 2: + raise RuntimeError("boom") + return str(value) + + monkeypatch.setattr("sas.qtgui.Plotting.SlicerModel.GuiUtils.formatNumber", boom) + + with pytest.raises(RuntimeError): + model.setModelFromParams() + + assert model.model().signalsBlocked() is False From dee2bb3eff3264ff5e31d59f9501502df6f6abe3 Mon Sep 17 00:00:00 2001 From: jellybean2004 Date: Wed, 22 Apr 2026 10:25:17 +0100 Subject: [PATCH 5/5] Check model before call --- src/sas/qtgui/Plotting/Slicers/BoxSum.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/sas/qtgui/Plotting/Slicers/BoxSum.py b/src/sas/qtgui/Plotting/Slicers/BoxSum.py index a5905b6be9..d9cd05c332 100644 --- a/src/sas/qtgui/Plotting/Slicers/BoxSum.py +++ b/src/sas/qtgui/Plotting/Slicers/BoxSum.py @@ -233,7 +233,8 @@ def postData(self): # Dig out number of points summed, SMK & PDB, 04/03/2013 boxtotal = Boxsum(x_min=x_min, x_max=x_max, y_min=y_min, y_max=y_max) self.total, self.totalerror, self.points = boxtotal(self.data) - self.setModelFromParams() + if self._model is not None: + self.setModelFromParams() def moveend(self, ev): """