Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
48 changes: 28 additions & 20 deletions src/sas/qtgui/Plotting/SlicerModel.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
"""
Expand All @@ -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):
"""
Expand All @@ -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'''
Expand All @@ -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.")

Expand Down
10 changes: 5 additions & 5 deletions src/sas/qtgui/Plotting/Slicers/AnnulusSlicer.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
15 changes: 7 additions & 8 deletions src/sas/qtgui/Plotting/Slicers/BoxSum.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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):
Expand Down Expand Up @@ -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):
"""
Expand Down Expand Up @@ -234,7 +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:
if self._model is not None:
self.setModelFromParams()

def moveend(self, ev):
Expand Down
8 changes: 1 addition & 7 deletions src/sas/qtgui/Plotting/Slicers/MultiSlicerBase.py
Original file line number Diff line number Diff line change
Expand Up @@ -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}")

Expand Down
26 changes: 25 additions & 1 deletion src/sas/qtgui/Plotting/UnitTesting/SlicerModelTest.py
Original file line number Diff line number Diff line change
Expand Up @@ -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()

Expand Down Expand Up @@ -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
Loading