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',