diff --git a/pyFAI/gui/CalibrationContext.py b/pyFAI/gui/CalibrationContext.py
index e49c6d509..0155c9077 100644
--- a/pyFAI/gui/CalibrationContext.py
+++ b/pyFAI/gui/CalibrationContext.py
@@ -83,6 +83,8 @@ def __init__(self, settings=None):
self.__scatteringVectorUnit.setValue(units.Unit.INV_ANGSTROM)
self.__markerColors = {}
self.__cacheStyles = {}
+ self.__recentCalibrants = DataModel()
+ self.__recentCalibrants.setValue([])
self.sigStyleChanged = self.__rawColormap.sigChanged
@@ -142,6 +144,9 @@ def restoreSettings(self):
self.__restoreUnit(self.__scatteringVectorUnit, settings, "scattering-vector-unit", units.Unit.INV_ANGSTROM)
settings.endGroup()
+ recentCalibrants = settings.value("recent-calibrations", [], type=list)
+ self.__recentCalibrants.setValue(recentCalibrants)
+
def saveSettings(self):
"""Save the settings of all the application"""
settings = self.__settings
@@ -157,6 +162,8 @@ def saveSettings(self):
settings.setValue("scattering-vector-unit", self.__scatteringVectorUnit.value().name)
settings.endGroup()
+ settings.setValue("recent-calibrations", self.__recentCalibrants.value())
+
# Synchronize the file storage
super(CalibrationContext, self).saveSettings()
@@ -286,7 +293,10 @@ def getCurrentStyle(self):
def getHtmlMarkerColor(self, index):
colors = self.markerColorList()
color = colors[index % len(colors)]
- "#%02X%02X%02X" % (color.red(), color.green(), color.blue())
+ return "#%02X%02X%02X" % (color.red(), color.green(), color.blue())
+
+ def getRecentCalibrants(self) -> DataModel:
+ return self.__recentCalibrants
def disabledMarkerColor(self):
style = self.getCurrentStyle()
diff --git a/pyFAI/gui/dialog/DetectorSelectorDialog.py b/pyFAI/gui/dialog/DetectorSelectorDialog.py
index ea7001002..5f219102d 100644
--- a/pyFAI/gui/dialog/DetectorSelectorDialog.py
+++ b/pyFAI/gui/dialog/DetectorSelectorDialog.py
@@ -34,8 +34,8 @@
import pyFAI.utils
import pyFAI.detectors
-from ..widgets.DetectorModel import AllDetectorModel
-from ..widgets.DetectorModel import DetectorFilter
+from ..widgets.model.AllDetectorItemModel import AllDetectorItemModel
+from ..widgets.model.DetectorFilterProxyModel import DetectorFilterProxyModel
from ..model.DataModel import DataModel
from ..utils import validators
from ..ApplicationContext import ApplicationContext
@@ -62,8 +62,8 @@ def __init__(self, parent=None):
selection = self._manufacturerList.selectionModel()
selection.selectionChanged.connect(self.__manufacturerChanged)
- model = AllDetectorModel(self)
- modelFilter = DetectorFilter(self)
+ model = AllDetectorItemModel(self)
+ modelFilter = DetectorFilterProxyModel(self)
modelFilter.setSourceModel(model)
self._detectorView.setModel(modelFilter)
@@ -512,7 +512,7 @@ def currentDetectorClass(self):
return None
index = indexes[0]
model = self._detectorView.model()
- return model.data(index, role=AllDetectorModel.CLASS_ROLE)
+ return model.data(index, role=AllDetectorItemModel.CLASS_ROLE)
def __modelChanged(self, selected, deselected):
model = self.currentDetectorClass()
diff --git a/pyFAI/gui/tasks/ExperimentTask.py b/pyFAI/gui/tasks/ExperimentTask.py
index 237197369..c9831ee66 100644
--- a/pyFAI/gui/tasks/ExperimentTask.py
+++ b/pyFAI/gui/tasks/ExperimentTask.py
@@ -78,8 +78,10 @@ def _initGui(self):
self._detectorFileDescription.setElideMode(qt.Qt.ElideMiddle)
- self._calibrant.setFileLoadable(True)
+ #self._calibrant.setFileLoadable(True)
self._calibrant.sigLoadFileRequested.connect(self.loadCalibrant)
+ recentCalibrants = CalibrationContext.instance().getRecentCalibrants().value()
+ self._calibrant.setRecentCalibrants(recentCalibrants)
self.__synchronizeRawView = SynchronizeRawView()
self.__synchronizeRawView.registerTask(self)
@@ -93,6 +95,11 @@ def _initGui(self):
self._wavelength.setValidator(validator)
super()._initGui()
+ def aboutToClose(self):
+ super(ExperimentTask, self).aboutToClose()
+ recentCalibrants = self._calibrant.recentCalibrants()
+ CalibrationContext.instance().getRecentCalibrants().setValue(recentCalibrants)
+
def __createPlot(self, parent):
plot = silx.gui.plot.PlotWidget(parent=parent)
plot.setKeepDataAspectRatio(True)
@@ -141,7 +148,7 @@ def _updateModel(self, model):
settings = model.experimentSettingsModel()
- self._calibrant.setModel(settings.calibrantModel())
+ self._calibrant.setCalibrantModel(settings.calibrantModel())
self._detectorLabel.setDetectorModel(settings.detectorModel())
self._image.setModel(settings.image())
self._imageLoader.setModel(settings.image())
diff --git a/pyFAI/gui/widgets/CalibrantSelector.py b/pyFAI/gui/widgets/CalibrantSelector.py
index b32a0add5..980267d6c 100644
--- a/pyFAI/gui/widgets/CalibrantSelector.py
+++ b/pyFAI/gui/widgets/CalibrantSelector.py
@@ -35,12 +35,14 @@
from silx.gui import icons
import pyFAI.calibrant
from ..model.CalibrantModel import CalibrantModel
+from ...utils.decorators import deprecated
class CalibrantSelector(qt.QComboBox):
"""Dropdown widget to select a calibrant.
- It is a view on top of a calibrant model (see :meth:`setModel`, :meth:`model`)
+ It is a view on top of a calibrant model (see :meth:`setCalibrantModel`,
+ :meth:`calibrantModel`)
The calibrant can be selected from a list of calibrant known by pyFAI.
@@ -67,11 +69,11 @@ def __init__(self, parent=None):
self.__isFileLoadable = False
self.__model: CalibrantModel = None
- self.setModel(CalibrantModel())
+ self.setCalibrantModel(CalibrantModel())
self.currentIndexChanged[int].connect(self.__currentIndexChanged)
def __currentIndexChanged(self, index):
- model = self.model()
+ model = self.calibrantModel()
if model is None:
return
if self.__isFileLoadable:
@@ -108,7 +110,7 @@ def setFileLoadable(self, isFileLoadable):
def __loadFileRequested(self):
self.sigLoadFileRequested.emit()
- def setModel(self, model: CalibrantModel):
+ def setCalibrantModel(self, model: CalibrantModel):
if self.__model is not None:
self.__model.changed.disconnect(self.__modelChanged)
self.__model = model
@@ -116,6 +118,10 @@ def setModel(self, model: CalibrantModel):
self.__model.changed.connect(self.__modelChanged)
self.__modelChanged()
+ @deprecated(replacement="setCalibrantModel")
+ def setModel(self, model: CalibrantModel):
+ self.setCalibrantModel(model)
+
def findCalibrant(self, calibrant):
"""Returns the first index containing the requested calibrant.
Else return -1"""
@@ -154,5 +160,9 @@ def __modelChanged(self):
self.__calibrantCount += 1
self.setCurrentIndex(index)
- def model(self) -> CalibrantModel:
+ def calibrantModel(self) -> CalibrantModel:
return self.__model
+
+ @deprecated(replacement="calibrantModel")
+ def model(self) -> CalibrantModel:
+ return self.model()
diff --git a/pyFAI/gui/widgets/CalibrantSelector2.py b/pyFAI/gui/widgets/CalibrantSelector2.py
new file mode 100644
index 000000000..22903fc40
--- /dev/null
+++ b/pyFAI/gui/widgets/CalibrantSelector2.py
@@ -0,0 +1,303 @@
+# coding: utf-8
+# /*##########################################################################
+#
+# Copyright (C) 2016-2018 European Synchrotron Radiation Facility
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+#
+# ###########################################################################*/
+
+__authors__ = ["V. Valls"]
+__license__ = "MIT"
+__date__ = "16/10/2020"
+
+import time
+import logging
+from typing import List
+
+from silx.gui import qt
+from pyFAI.calibrant import Calibrant
+from ..model.CalibrantModel import CalibrantModel
+from .model.CalibrantFilterProxyModel import CalibrantFilterProxyModel
+from .model.CalibrantItemModel import CalibrantItemModel
+from ...utils import get_ui_file
+
+
+_logger = logging.getLogger(__name__)
+
+
+class _CalibrantItemView(qt.QAbstractItemView):
+ """
+ Custom view used as popup view for the main combobox.
+ """
+
+ sigLoadFileRequested = qt.Signal()
+
+ def __init__(self, parent=None):
+ super(_CalibrantItemView, self).__init__(parent=parent)
+ filename = get_ui_file("calibrant-selector2.ui")
+ layout = qt.QVBoxLayout(self)
+ layout.setContentsMargins(0, 0, 0, 0)
+ self.__ui = qt.loadUi(filename)
+ self.__ui.setParent(self)
+ layout.addWidget(self.__ui)
+
+ self.__lastUsed = []
+ self.__dropTime = None
+
+ self.__filter = CalibrantFilterProxyModel(self)
+ self.__ui.listView.setModel(self.__filter)
+ self.__ui.allFilter.clicked.connect(self.__selectAll)
+ self.__ui.lastFilter.clicked.connect(self.__selectLast)
+ self.__ui.userFilter.clicked.connect(self.__selectUser)
+ self.__ui.defaultFilter.clicked.connect(self.__selectDefault)
+ self.__ui.loadButton.clicked.connect(self.__loadFileRequested)
+ self.__ui.listView.clicked.connect(self.__currentChanged)
+ self.__ui.listView.activated.connect(self.__currentChanged)
+
+ # self.setFocusProxy(self.__ui.listView)
+
+ def focusInEvent(self, event: qt.QEvent):
+ # Update the size when the model was initialized
+ self.adjustSize()
+ size = self.size()
+ h = self.__ui.listView.sizeHintForRow(0)
+ size.setHeight(h * 8)
+ self.setFixedSize(size)
+
+ if len(self.__lastUsed) > 0:
+ self.__selectLast()
+ else:
+ self.__selectAll()
+ self.__dropTime = time.time()
+ self.__ui.listView.setFocus()
+
+ def setRecentCalibrants(self, calibrants: List[str]):
+ self.__lastUsed = calibrants
+ self._syncLastUsed()
+
+ def recentCalibrants(self) -> List[str]:
+ return self.__lastUsed
+
+ def touchCalibrant(self, calibrant):
+ filename = calibrant.filename
+ if filename is not None:
+ try:
+ self.__lastUsed.remove(filename)
+ except ValueError:
+ pass
+ self.__lastUsed.insert(0, filename)
+ self.__lastUsed = self.__lastUsed[:8]
+
+ def __currentChanged(self, current: qt.QModelIndex):
+ if time.time() - self.__dropTime < 0.200:
+ # When a mouse press is performed on the combobox, directly followed
+ # a mouse release, if the first item was selected, the change is
+ # triggered
+ # Here is a mitigation
+ return
+ sourceIndex = self.__filter.mapToSource(current)
+ self.setCurrentIndex(sourceIndex)
+ calibrant = sourceIndex.data(CalibrantItemModel.CALIBRANT_ROLE)
+ self.touchCalibrant(calibrant)
+ self.accept()
+
+ def accept(self):
+ """Send event to close and accept the popup"""
+ event = qt.QKeyEvent(qt.QKeyEvent.KeyPress, qt.Qt.Key_Enter, qt.Qt.NoModifier, "x")
+ qt.QApplication.sendEvent(self, event);
+
+ def reject(self):
+ """Send event to close and reject the popup"""
+ event = qt.QKeyEvent(qt.QKeyEvent.KeyPress, qt.Qt.Key_Escape, qt.Qt.NoModifier, "x")
+ qt.QApplication.sendEvent(self, event);
+
+ def __loadFileRequested(self):
+ self.sigLoadFileRequested.emit()
+
+ def restoreState(self, state: qt.QByteArray) -> bool:
+ stream = qt.QDataStream(state, qt.QIODevice.ReadOnly)
+ version = stream.readUInt32()
+ if version != 0:
+ _logger.warning("Serial version mismatch. Found %d." % version)
+ return False
+
+ nb = stream.readUInt32()
+ names = []
+ for _ in range(nb):
+ name = stream.readQString()
+ names.append(name)
+ self.__lastUsed = names
+ self._syncLastUsed()
+ return True
+
+ def saveState(self) -> qt.QByteArray:
+ data = qt.QByteArray()
+ stream = qt.QDataStream(data, qt.QIODevice.WriteOnly)
+ stream.writeUInt32(0)
+ stream.writeUInt32(len(self.__lastUsed))
+ for c in self.__lastUsed:
+ stream.writeQString(c)
+ return data
+
+ def __clearAll(self):
+ self.__ui.allFilter.blockSignals(True)
+ self.__ui.allFilter.setChecked(False)
+ self.__ui.allFilter.blockSignals(False)
+
+ self.__ui.lastFilter.blockSignals(True)
+ self.__ui.lastFilter.setChecked(False)
+ self.__ui.lastFilter.blockSignals(False)
+
+ self.__ui.userFilter.blockSignals(True)
+ self.__ui.userFilter.setChecked(False)
+ self.__ui.userFilter.blockSignals(False)
+
+ self.__ui.defaultFilter.blockSignals(True)
+ self.__ui.defaultFilter.setChecked(False)
+ self.__ui.defaultFilter.blockSignals(False)
+
+ def __selectAll(self):
+ self.__clearAll()
+ self.__filter.setFilter(displayResource=True, displayUser=True)
+ self.__ui.allFilter.blockSignals(True)
+ self.__ui.allFilter.setChecked(True)
+ self.__ui.allFilter.blockSignals(False)
+
+ def __selectLast(self):
+ self.__clearAll()
+ self.__filter.setFilter(displayResource=False, displayUser=False, filenames=set(self.__lastUsed))
+ self.__ui.lastFilter.blockSignals(True)
+ self.__ui.lastFilter.setChecked(True)
+ self.__ui.lastFilter.blockSignals(False)
+
+ def __selectUser(self):
+ self.__clearAll()
+ self.__filter.setFilter(displayResource=False, displayUser=True)
+ self.__ui.userFilter.blockSignals(True)
+ self.__ui.userFilter.setChecked(True)
+ self.__ui.userFilter.blockSignals(False)
+
+ def __selectDefault(self):
+ self.__clearAll()
+ self.__filter.setFilter(displayResource=True, displayUser=False)
+ self.__ui.defaultFilter.blockSignals(True)
+ self.__ui.defaultFilter.setChecked(True)
+ self.__ui.defaultFilter.blockSignals(False)
+
+ def visualRegionForSelection(self, selection: qt.QItemSelection):
+ return qt.QRegion()
+
+ def visualRect(self, index: qt.QModelIndex):
+ return qt.QRect()
+
+ def moveCursor(self, cursorAction, modifiers) -> qt.QModelIndex:
+ return qt.QModelIndex()
+
+ def scrollTo(self, index, hint):
+ self.__ui.listView.scrollTo(index, hint)
+
+ def indexAt(self, point: qt.QPoint) -> qt.QModelIndex:
+ return self.__ui.listView.indexAt(point)
+
+ def setModel(self, model: qt.QStandardItemModel):
+ self.__filter.setSourceModel(model)
+ qt.QAbstractItemView.setModel(self, model)
+ self._syncLastUsed()
+
+ def _syncLastUsed(self):
+ model = self.model()
+ if model is None:
+ return
+ for f in self.__lastUsed:
+ calibrant = Calibrant(f)
+ index = model.indexFromCalibrant(calibrant)
+ if not index.isValid():
+ model.appendCalibrant(calibrant)
+
+ def horizontalOffset(self):
+ return 0
+
+ def verticalOffset(self):
+ return 0
+
+
+class CalibrantSelector2(qt.QComboBox):
+ """Dropdown widget to select a calibrant.
+
+ It is a view on top of a calibrant model (see :meth:`setCalibrantModel`,
+ :meth:`modelCalibrant`)
+
+ The calibrant can be selected from a list of calibrant known by pyFAI.
+ """
+
+ sigLoadFileRequested = qt.Signal()
+
+ def __init__(self, parent=None):
+ super(CalibrantSelector2, self).__init__(parent=parent)
+ model = CalibrantItemModel(self)
+ self.setModel(model)
+ self.setCurrentIndex(-1)
+
+ self.__calibrantModel: CalibrantModel = None
+ self.setCalibrantModel(CalibrantModel())
+
+ view = _CalibrantItemView(self)
+ view.sigLoadFileRequested.connect(self.__loadFileRequested)
+ self.setView(view)
+
+ def __modelChanged(self):
+ calibrant = self.__calibrantModel.calibrant()
+ model = self.model()
+ if calibrant is None or model is None:
+ self.setCurrentIndex(-1)
+ else:
+ index = model.indexFromCalibrant(calibrant)
+ if not index.isValid():
+ model.appendCalibrant(index)
+ index = model.indexFromCalibrant(calibrant)
+ if index.isValid():
+ self.view().touchCalibrant(calibrant)
+ self.setCurrentIndex(index.row())
+
+ def setCalibrantModel(self, model: CalibrantModel):
+ if self.__calibrantModel is not None:
+ self.__calibrantModel.changed.disconnect(self.__modelChanged)
+ self.__calibrantModel = model
+ if self.__calibrantModel is not None:
+ self.__calibrantModel.changed.connect(self.__modelChanged)
+ self.__modelChanged()
+
+ def calibrantModel(self) -> CalibrantModel:
+ return self.__calibrantModel
+
+ def recentCalibrants(self):
+ return self.view().recentCalibrants()
+
+ def setRecentCalibrants(self, recentCalibrants: List[str]):
+ return self.view().setRecentCalibrants(recentCalibrants)
+
+ def restoreState(self, state: qt.QByteArray) -> bool:
+ return self.view().restoreState(state)
+
+ def saveState(self) -> qt.QByteArray:
+ return self.view().saveState()
+
+ def __loadFileRequested(self):
+ self.sigLoadFileRequested.emit()
diff --git a/pyFAI/gui/widgets/DetectorModel.py b/pyFAI/gui/widgets/DetectorModel.py
index 0a1369d32..af9c2792a 100644
--- a/pyFAI/gui/widgets/DetectorModel.py
+++ b/pyFAI/gui/widgets/DetectorModel.py
@@ -28,104 +28,20 @@
__date__ = "16/10/2020"
from silx.gui import qt
-import pyFAI.detectors
+from .model.AllDetectorItemModel import AllDetectorItemModel
+from .model.DetectorFilterProxyModel import DetectorFilterProxyModel
+from ...utils.decorators import deprecated
-class AllDetectorModel(qt.QStandardItemModel):
-
- CLASS_ROLE = qt.Qt.UserRole
- MODEL_ROLE = qt.Qt.UserRole + 1
- MANUFACTURER_ROLE = qt.Qt.UserRole + 2
+class AllDetectorModel(AllDetectorItemModel):
+ @deprecated(replacement="pyFAI.gui.widgets.model.AllDetectorItemModel.AllDetectorItemModel", since_version="2023.6")
def __init__(self, parent):
- qt.QStandardItemModel.__init__(self, parent)
-
- detectorClasses = set(pyFAI.detectors.ALL_DETECTORS.values())
-
- def getNameAndManufacturer(detectorClass):
- modelName = None
- result = []
-
- if hasattr(detectorClass, "MANUFACTURER"):
- manufacturer = detectorClass.MANUFACTURER
- else:
- manufacturer = None
-
- if isinstance(manufacturer, list):
- for index, m in enumerate(manufacturer):
- if m is None:
- continue
- modelName = detectorClass.aliases[index]
- result.append((modelName, m, detectorClass))
- else:
- if hasattr(detectorClass, "aliases"):
- if len(detectorClass.aliases) > 0:
- modelName = detectorClass.aliases[0]
- if modelName is None:
- modelName = detectorClass.__name__
- result.append((modelName, manufacturer, detectorClass))
- return result
-
- def sortingKey(item):
- modelName, manufacturerName, _detector = item
- if modelName:
- modelName = modelName.lower()
- if manufacturerName:
- manufacturerName = manufacturerName.lower()
- return modelName, manufacturerName
+ AllDetectorItemModel.__init__(self, parent=parent)
- items = []
- for c in detectorClasses:
- items.extend(getNameAndManufacturer(c))
- items = sorted(items, key=sortingKey)
- for modelName, manufacturerName, detector in items:
- if detector is pyFAI.detectors.Detector:
- continue
- item = qt.QStandardItem(modelName)
- item.setData(detector, role=self.CLASS_ROLE)
- item.setData(modelName, role=self.MODEL_ROLE)
- item.setData(manufacturerName, role=self.MANUFACTURER_ROLE)
- item2 = qt.QStandardItem(manufacturerName)
- item2.setData(detector, role=self.CLASS_ROLE)
- item2.setData(modelName, role=self.MODEL_ROLE)
- item2.setData(manufacturerName, role=self.MANUFACTURER_ROLE)
- self.appendRow([item, item2])
- def indexFromDetector(self, detector, manufacturer):
- for row in range(self.rowCount()):
- index = self.index(row, 0)
- manufacturerName = self.data(index, role=self.MANUFACTURER_ROLE)
- if manufacturerName != manufacturer:
- continue
- detectorClass = self.data(index, role=self.CLASS_ROLE)
- if detectorClass != detector:
- continue
- return index
- return qt.QModelIndex()
-
-
-class DetectorFilter(qt.QSortFilterProxyModel):
+class DetectorFilter(DetectorFilterProxyModel):
+ @deprecated(replacement="pyFAI.gui.widgets.model.DetectorFilterProxyModel.DetectorFilterProxyModel", since_version="2023.6")
def __init__(self, parent):
- super(DetectorFilter, self).__init__(parent)
- self.__manufacturerFilter = None
-
- def setManufacturerFilter(self, manufacturer):
- if self.__manufacturerFilter == manufacturer:
- return
- self.__manufacturerFilter = manufacturer
- self.invalidateFilter()
-
- def filterAcceptsRow(self, sourceRow, sourceParent):
- if self.__manufacturerFilter == "*":
- return True
- sourceModel = self.sourceModel()
- index = sourceModel.index(sourceRow, 0, sourceParent)
- manufacturer = index.data(AllDetectorModel.MANUFACTURER_ROLE)
- return manufacturer == self.__manufacturerFilter
-
- def indexFromDetector(self, detector, manufacturer):
- sourceModel = self.sourceModel()
- index = sourceModel.indexFromDetector(detector, manufacturer)
- index = self.mapFromSource(index)
- return index
+ DetectorFilterProxyModel.__init__(self, parent=parent)
diff --git a/pyFAI/gui/widgets/DetectorSelector.py b/pyFAI/gui/widgets/DetectorSelector.py
index 95589be7b..9018798db 100644
--- a/pyFAI/gui/widgets/DetectorSelector.py
+++ b/pyFAI/gui/widgets/DetectorSelector.py
@@ -29,8 +29,8 @@
from silx.gui import qt
from ..model.DetectorModel import DetectorModel
-from .DetectorModel import AllDetectorModel
-from .DetectorModel import DetectorFilter
+from .model.AllDetectorItemModel import AllDetectorItemModel
+from .model.DetectorFilterProxyModel import DetectorFilterProxyModel
class DetectorSelector(qt.QComboBox):
@@ -39,8 +39,8 @@ def __init__(self, parent=None):
super(DetectorSelector, self).__init__(parent)
# feed the widget with default detectors
- model = AllDetectorModel(self)
- self.__filter = DetectorFilter(self)
+ model = AllDetectorItemModel(self)
+ self.__filter = DetectorFilterProxyModel(self)
self.__filter.setSourceModel(model)
super(DetectorSelector, self).setModel(self.__filter)
diff --git a/pyFAI/gui/widgets/meson.build b/pyFAI/gui/widgets/meson.build
index 0db7f7da9..dc4b4f378 100644
--- a/pyFAI/gui/widgets/meson.build
+++ b/pyFAI/gui/widgets/meson.build
@@ -1,10 +1,12 @@
subdir('test')
+subdir('model')
py.install_sources(
['AdvancedComboBox.py',
'AdvancedSpinBox.py',
'CalibrantPreview.py',
'CalibrantSelector.py',
+ 'CalibrantSelector2.py',
'ChoiceToolButton.py',
'ColoredCheckBox.py',
'DetectorLabel.py',
diff --git a/pyFAI/gui/widgets/model/AllDetectorItemModel.py b/pyFAI/gui/widgets/model/AllDetectorItemModel.py
new file mode 100644
index 000000000..df5213bdd
--- /dev/null
+++ b/pyFAI/gui/widgets/model/AllDetectorItemModel.py
@@ -0,0 +1,104 @@
+# coding: utf-8
+# /*##########################################################################
+#
+# Copyright (C) 2016-2018 European Synchrotron Radiation Facility
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+#
+# ###########################################################################*/
+
+__authors__ = ["V. Valls"]
+__license__ = "MIT"
+__date__ = "16/10/2020"
+
+from silx.gui import qt
+import pyFAI.detectors
+
+
+class AllDetectorItemModel(qt.QStandardItemModel):
+
+ CLASS_ROLE = qt.Qt.UserRole
+ MODEL_ROLE = qt.Qt.UserRole + 1
+ MANUFACTURER_ROLE = qt.Qt.UserRole + 2
+
+ def __init__(self, parent):
+ qt.QStandardItemModel.__init__(self, parent)
+
+ detectorClasses = set(pyFAI.detectors.ALL_DETECTORS.values())
+
+ def getNameAndManufacturer(detectorClass):
+ modelName = None
+ result = []
+
+ if hasattr(detectorClass, "MANUFACTURER"):
+ manufacturer = detectorClass.MANUFACTURER
+ else:
+ manufacturer = None
+
+ if isinstance(manufacturer, list):
+ for index, m in enumerate(manufacturer):
+ if m is None:
+ continue
+ modelName = detectorClass.aliases[index]
+ result.append((modelName, m, detectorClass))
+ else:
+ if hasattr(detectorClass, "aliases"):
+ if len(detectorClass.aliases) > 0:
+ modelName = detectorClass.aliases[0]
+ if modelName is None:
+ modelName = detectorClass.__name__
+ result.append((modelName, manufacturer, detectorClass))
+ return result
+
+ def sortingKey(item):
+ modelName, manufacturerName, _detector = item
+ if modelName:
+ modelName = modelName.lower()
+ if manufacturerName:
+ manufacturerName = manufacturerName.lower()
+ return modelName, manufacturerName
+
+ items = []
+ for c in detectorClasses:
+ items.extend(getNameAndManufacturer(c))
+ items = sorted(items, key=sortingKey)
+ for modelName, manufacturerName, detector in items:
+ if detector is pyFAI.detectors.Detector:
+ continue
+ item = qt.QStandardItem(modelName)
+ item.setData(detector, role=self.CLASS_ROLE)
+ item.setData(modelName, role=self.MODEL_ROLE)
+ item.setData(manufacturerName, role=self.MANUFACTURER_ROLE)
+ item2 = qt.QStandardItem(manufacturerName)
+ item2.setData(detector, role=self.CLASS_ROLE)
+ item2.setData(modelName, role=self.MODEL_ROLE)
+ item2.setData(manufacturerName, role=self.MANUFACTURER_ROLE)
+ self.appendRow([item, item2])
+
+ def indexFromDetector(self, detector, manufacturer):
+ for row in range(self.rowCount()):
+ index = self.index(row, 0)
+ manufacturerName = self.data(index, role=self.MANUFACTURER_ROLE)
+ if manufacturerName != manufacturer:
+ continue
+ detectorClass = self.data(index, role=self.CLASS_ROLE)
+ if detectorClass != detector:
+ continue
+ return index
+ return qt.QModelIndex()
diff --git a/pyFAI/gui/widgets/model/CalibrantFilterProxyModel.py b/pyFAI/gui/widgets/model/CalibrantFilterProxyModel.py
new file mode 100644
index 000000000..0d7e14b4d
--- /dev/null
+++ b/pyFAI/gui/widgets/model/CalibrantFilterProxyModel.py
@@ -0,0 +1,67 @@
+# coding: utf-8
+# /*##########################################################################
+#
+# Copyright (C) 2016-2018 European Synchrotron Radiation Facility
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+#
+# ###########################################################################*/
+
+__authors__ = ["V. Valls"]
+__license__ = "MIT"
+__date__ = "25/06/2023"
+
+from silx.gui import qt
+from silx.gui import icons
+import pyFAI.calibrant
+from pyFAI.calibrant import Calibrant
+from .CalibrantItemModel import CalibrantItemModel
+
+
+class CalibrantFilterProxyModel(qt.QSortFilterProxyModel):
+
+ def __init__(self, parent):
+ super(CalibrantFilterProxyModel, self).__init__(parent)
+ self.__displayUser: bool = True
+ self.__displayResource: bool = True
+ self.__filenames = None
+
+ def setFilter(self, displayResource: bool, displayUser: bool, filenames=None):
+ if (self.__displayResource == displayResource and self.__displayUser == displayUser):
+ return
+ self.__displayResource = displayResource
+ self.__displayUser = displayUser
+ self.__filenames = filenames
+ self.invalidateFilter()
+
+ def filterAcceptsRow(self, sourceRow, sourceParent):
+ sourceModel = self.sourceModel()
+ index = sourceModel.index(sourceRow, 0, sourceParent)
+ calibrant = index.data(CalibrantItemModel.CALIBRANT_ROLE)
+ if self.__filenames is not None:
+ return calibrant.filename in self.__filenames
+
+ is_user = not calibrant.filename.startswith("pyfai:")
+ return (self.__displayUser and is_user) or (self.__displayResource and not is_user)
+
+ def indexFromCalibrant(self, calibrant: Calibrant):
+ sourceModel = self.sourceModel()
+ index = sourceModel.indexFromCalibrant(calibrant)
+ index = self.mapFromSource(index)
+ return index
diff --git a/pyFAI/gui/widgets/model/CalibrantItemModel.py b/pyFAI/gui/widgets/model/CalibrantItemModel.py
new file mode 100644
index 000000000..a83573dbe
--- /dev/null
+++ b/pyFAI/gui/widgets/model/CalibrantItemModel.py
@@ -0,0 +1,77 @@
+# coding: utf-8
+# /*##########################################################################
+#
+# Copyright (C) 2016-2018 European Synchrotron Radiation Facility
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+#
+# ###########################################################################*/
+
+__authors__ = ["V. Valls"]
+__license__ = "MIT"
+__date__ = "25/06/2023"
+
+import os.path
+from silx.gui import qt
+from silx.gui import icons
+import pyFAI.calibrant
+from pyFAI.calibrant import Calibrant
+
+
+class CalibrantItemModel(qt.QStandardItemModel):
+
+ CALIBRANT_ROLE = qt.Qt.UserRole
+
+ def __init__(self, parent=None):
+ qt.QStandardItemModel.__init__(self, parent=parent)
+
+ calibrants = pyFAI.calibrant.CALIBRANT_FACTORY.items()
+ calibrants = sorted(calibrants, key=lambda x: x[0].lower())
+ for calibrantName, calibrant in calibrants:
+ item = self.createStandardItem(calibrant, calibrantName)
+ self.appendRow(item)
+
+ def createStandardItem(self, calibrant, calibrantName=None) -> qt.QStandardItem:
+ if calibrantName is None:
+ name = os.path.splitext(os.path.basename(calibrant.filename))[0]
+ calibrantName = name
+ item = qt.QStandardItem()
+ item.setText(calibrantName)
+ item.setToolTip(calibrant.filename)
+ item.setData(calibrant, role=self.CALIBRANT_ROLE)
+ if calibrant.filename is None or calibrant.filename.startswith("pyfai:"):
+ icon = icons.getQIcon("pyfai:gui/icons/calibrant")
+ else:
+ icon = icons.getQIcon("pyfai:gui/icons/calibrant-custom")
+
+ item.setIcon(icon)
+ item.setEditable(False)
+ return item
+
+ def indexFromCalibrant(self, calibrant: Calibrant):
+ for row in range(self.rowCount()):
+ index = self.index(row, 0)
+ calibrantObj = self.data(index, role=self.CALIBRANT_ROLE)
+ if calibrant.filename == calibrantObj.filename:
+ return index
+ return qt.QModelIndex()
+
+ def appendCalibrant(self, calibrant) -> qt.QModelIndex:
+ item = self.createStandardItem(calibrant)
+ self.appendRow(item)
diff --git a/pyFAI/gui/widgets/model/DetectorFilterProxyModel.py b/pyFAI/gui/widgets/model/DetectorFilterProxyModel.py
new file mode 100644
index 000000000..f07e5f57e
--- /dev/null
+++ b/pyFAI/gui/widgets/model/DetectorFilterProxyModel.py
@@ -0,0 +1,58 @@
+# coding: utf-8
+# /*##########################################################################
+#
+# Copyright (C) 2016-2018 European Synchrotron Radiation Facility
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+#
+# ###########################################################################*/
+
+__authors__ = ["V. Valls"]
+__license__ = "MIT"
+__date__ = "16/10/2020"
+
+from silx.gui import qt
+from .AllDetectorItemModel import AllDetectorItemModel
+
+
+class DetectorFilterProxyModel(qt.QSortFilterProxyModel):
+
+ def __init__(self, parent):
+ super(DetectorFilterProxyModel, self).__init__(parent)
+ self.__manufacturerFilter = None
+
+ def setManufacturerFilter(self, manufacturer):
+ if self.__manufacturerFilter == manufacturer:
+ return
+ self.__manufacturerFilter = manufacturer
+ self.invalidateFilter()
+
+ def filterAcceptsRow(self, sourceRow, sourceParent):
+ if self.__manufacturerFilter == "*":
+ return True
+ sourceModel = self.sourceModel()
+ index = sourceModel.index(sourceRow, 0, sourceParent)
+ manufacturer = index.data(AllDetectorItemModel.MANUFACTURER_ROLE)
+ return manufacturer == self.__manufacturerFilter
+
+ def indexFromDetector(self, detector, manufacturer):
+ sourceModel = self.sourceModel()
+ index = sourceModel.indexFromDetector(detector, manufacturer)
+ index = self.mapFromSource(index)
+ return index
diff --git a/pyFAI/gui/widgets/model/__init__.py b/pyFAI/gui/widgets/model/__init__.py
new file mode 100644
index 000000000..08a50766c
--- /dev/null
+++ b/pyFAI/gui/widgets/model/__init__.py
@@ -0,0 +1,29 @@
+# coding: utf-8
+# /*##########################################################################
+#
+# Copyright (C) 2016-2018 European Synchrotron Radiation Facility
+#
+# Permission is hereby granted, free of charge, to any person obtaining a copy
+# of this software and associated documentation files (the "Software"), to deal
+# in the Software without restriction, including without limitation the rights
+# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+# copies of the Software, and to permit persons to whom the Software is
+# furnished to do so, subject to the following conditions:
+#
+# The above copyright notice and this permission notice shall be included in
+# all copies or substantial portions of the Software.
+#
+# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
+# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
+# THE SOFTWARE.
+#
+# ###########################################################################*/
+"""Module containing generic widgets"""
+
+__copyright__ = "European Synchrotron Radiation Facility, Grenoble, France"
+__license__ = "MIT"
+__date__ = "25/06/2023"
diff --git a/pyFAI/gui/widgets/model/meson.build b/pyFAI/gui/widgets/model/meson.build
new file mode 100644
index 000000000..ff6954e66
--- /dev/null
+++ b/pyFAI/gui/widgets/model/meson.build
@@ -0,0 +1,10 @@
+
+py.install_sources(
+ ['CalibrantItemModel.py',
+ 'CalibrantFilterProxyModel.py',
+ 'AllDetectorItemModel.py',
+ 'DetectorFilterProxyModel.py',
+ '__init__.py'],
+ pure: false, # Will be installed next to binaries
+ subdir: 'pyFAI/gui/widgets/model' # Folder relative to site-packages to install to
+)
diff --git a/pyFAI/resources/gui/calibrant-selector2.ui b/pyFAI/resources/gui/calibrant-selector2.ui
new file mode 100644
index 000000000..93c57bbe5
--- /dev/null
+++ b/pyFAI/resources/gui/calibrant-selector2.ui
@@ -0,0 +1,170 @@
+
+
+ Form
+
+
+
+ 0
+ 0
+ 250
+ 219
+
+
+
+ Form
+
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+
+ 0
+
+ -
+
+
+
+ 0
+
+
+ 1
+
+
+ 1
+
+
+ 1
+
+
+ 1
+
+
-
+
+
+ Display everything
+
+
+ All
+
+
+ true
+
+
+ true
+
+
+
+ -
+
+
+ Recently used
+
+
+ Recent
+
+
+ true
+
+
+ true
+
+
+
+ -
+
+
+ Only calibrants from pyFAI
+
+
+ Default
+
+
+ true
+
+
+ true
+
+
+
+ -
+
+
+ Only user defined calibrants
+
+
+ User
+
+
+ true
+
+
+ true
+
+
+
+ -
+
+
+ Qt::Vertical
+
+
+
+ 20
+ 40
+
+
+
+
+ -
+
+
+ Load file...
+
+
+ true
+
+
+
+
+
+
+ -
+
+
+ Qt::Vertical
+
+
+
+ -
+
+
+ QListView { background-color: transparent}
+
+
+ QFrame::NoFrame
+
+
+ QAbstractItemView::ScrollPerPixel
+
+
+ QAbstractItemView::ScrollPerPixel
+
+
+ true
+
+
+
+
+
+
+
+
diff --git a/pyFAI/resources/gui/calibration-experiment.ui b/pyFAI/resources/gui/calibration-experiment.ui
index 9426d19c4..1e066b65f 100644
--- a/pyFAI/resources/gui/calibration-experiment.ui
+++ b/pyFAI/resources/gui/calibration-experiment.ui
@@ -120,7 +120,7 @@
-
-
+
-
@@ -435,9 +435,9 @@
pyFAI.gui.widgets.FileEdit
- CalibrantSelector
+ CalibrantSelector2
QComboBox
- pyFAI.gui.widgets.CalibrantSelector
+ pyFAI.gui.widgets.CalibrantSelector2
CalibrantPreview
diff --git a/pyFAI/resources/gui/meson.build b/pyFAI/resources/gui/meson.build
index 6d85440df..7463478d2 100644
--- a/pyFAI/resources/gui/meson.build
+++ b/pyFAI/resources/gui/meson.build
@@ -3,7 +3,8 @@ subdir('images')
subdir('styles')
py.install_sources(
- ['calibration-experiment.ui',
+ ['calibrant-selector2.ui',
+ 'calibration-experiment.ui',
'calibration-geometry.ui',
'calibration-main.ui',
'calibration-mask.ui',