diff options
Diffstat (limited to 'silx/app/view')
-rw-r--r-- | silx/app/view/About.py | 257 | ||||
-rw-r--r-- | silx/app/view/ApplicationContext.py | 194 | ||||
-rw-r--r-- | silx/app/view/CustomNxdataWidget.py | 1008 | ||||
-rw-r--r-- | silx/app/view/DataPanel.py | 192 | ||||
-rw-r--r-- | silx/app/view/Viewer.py | 971 | ||||
-rw-r--r-- | silx/app/view/__init__.py | 28 | ||||
-rw-r--r-- | silx/app/view/main.py | 171 | ||||
-rw-r--r-- | silx/app/view/setup.py | 40 | ||||
-rw-r--r-- | silx/app/view/test/__init__.py | 41 | ||||
-rw-r--r-- | silx/app/view/test/test_launcher.py | 151 | ||||
-rw-r--r-- | silx/app/view/test/test_view.py | 394 | ||||
-rw-r--r-- | silx/app/view/utils.py | 45 |
12 files changed, 0 insertions, 3492 deletions
diff --git a/silx/app/view/About.py b/silx/app/view/About.py deleted file mode 100644 index a2b430f..0000000 --- a/silx/app/view/About.py +++ /dev/null @@ -1,257 +0,0 @@ -# coding: utf-8 -# /*########################################################################## -# Copyright (C) 2016-2019 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. -# -# ############################################################################*/ -"""About box for Silx viewer""" - -__authors__ = ["V. Valls"] -__license__ = "MIT" -__date__ = "05/07/2018" - -import os -import sys - -from silx.gui import qt -from silx.gui import icons - -_LICENSE_TEMPLATE = """<p align="center"> -<b>Copyright (C) {year} European Synchrotron Radiation Facility</b> -</p> - -<p align="justify"> -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: -</p> - -<p align="justify"> -The above copyright notice and this permission notice shall be included in -all copies or substantial portions of the Software. -</p> - -<p align="justify"> -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. -</p> -""" - - -class About(qt.QDialog): - """ - Util dialog to display an common about box for all the silx GUIs. - """ - - def __init__(self, parent=None): - """ - :param files_: List of HDF5 or Spec files (pathes or - :class:`silx.io.spech5.SpecH5` or :class:`h5py.File` - instances) - """ - super(About, self).__init__(parent) - self.__createLayout() - self.setSizePolicy(qt.QSizePolicy.Fixed, qt.QSizePolicy.Fixed) - self.setModal(True) - self.setApplicationName(None) - - def __createLayout(self): - layout = qt.QVBoxLayout(self) - layout.setContentsMargins(24, 15, 24, 20) - layout.setSpacing(8) - - self.__label = qt.QLabel(self) - self.__label.setWordWrap(True) - flags = self.__label.textInteractionFlags() - flags = flags | qt.Qt.TextSelectableByKeyboard - flags = flags | qt.Qt.TextSelectableByMouse - self.__label.setTextInteractionFlags(flags) - self.__label.setOpenExternalLinks(True) - self.__label.setSizePolicy(qt.QSizePolicy.Minimum, qt.QSizePolicy.Preferred) - - licenseButton = qt.QPushButton(self) - licenseButton.setText("License...") - licenseButton.clicked.connect(self.__displayLicense) - licenseButton.setAutoDefault(False) - - self.__options = qt.QDialogButtonBox() - self.__options.addButton(licenseButton, qt.QDialogButtonBox.ActionRole) - okButton = self.__options.addButton(qt.QDialogButtonBox.Ok) - okButton.setDefault(True) - okButton.clicked.connect(self.accept) - - layout.addWidget(self.__label) - layout.addWidget(self.__options) - layout.setStretch(0, 100) - layout.setStretch(1, 0) - - def getHtmlLicense(self): - """Returns the text license in HTML format. - - :rtype: str - """ - from silx._version import __date__ as date - year = date.split("/")[2] - info = dict( - year=year - ) - textLicense = _LICENSE_TEMPLATE.format(**info) - return textLicense - - def __displayLicense(self): - """Displays the license used by silx.""" - text = self.getHtmlLicense() - licenseDialog = qt.QMessageBox(self) - licenseDialog.setWindowTitle("License") - licenseDialog.setText(text) - licenseDialog.exec_() - - def setApplicationName(self, name): - self.__applicationName = name - if name is None: - self.setWindowTitle("About") - else: - self.setWindowTitle("About %s" % name) - self.__updateText() - - @staticmethod - def __formatOptionalLibraries(name, isAvailable): - """Utils to format availability of features""" - if isAvailable: - template = '<b>%s</b> is <font color="green">loaded</font>' - else: - template = '<b>%s</b> is <font color="red">not loaded</font>' - return template % name - - @staticmethod - def __formatOptionalFilters(name, isAvailable): - """Utils to format availability of features""" - if isAvailable: - template = '<b>%s</b> is <font color="green">available</font>' - else: - template = '<b>%s</b> is <font color="red">not available</font>' - return template % name - - def __updateText(self): - """Update the content of the dialog according to the settings.""" - import silx._version - - message = """<table> - <tr><td width="50%" align="center" valign="middle"> - <img src="{silx_image_path}" width="100" /> - </td><td width="50%" align="center" valign="middle"> - <b>{application_name}</b> - <br /> - <br />{silx_version} - <br /> - <br /><a href="{project_url}">Upstream project on GitHub</a> - </td></tr> - </table> - <dl> - <dt><b>Silx version</b></dt><dd>{silx_version}</dd> - <dt><b>Qt version</b></dt><dd>{qt_version}</dd> - <dt><b>Qt binding</b></dt><dd>{qt_binding}</dd> - <dt><b>Python version</b></dt><dd>{python_version}</dd> - <dt><b>Optional libraries</b></dt><dd>{optional_lib}</dd> - </dl> - <p> - Copyright (C) <a href="{esrf_url}">European Synchrotron Radiation Facility</a> - </p> - """ - - optionals = [] - optionals.append(self.__formatOptionalLibraries("H5py", "h5py" in sys.modules)) - optionals.append(self.__formatOptionalLibraries("FabIO", "fabio" in sys.modules)) - - try: - import h5py.version - if h5py.version.hdf5_version_tuple >= (1, 10, 2): - # Previous versions only return True if the filter was first used - # to decode a dataset - import h5py.h5z - FILTER_LZ4 = 32004 - FILTER_BITSHUFFLE = 32008 - filters = [ - ("HDF5 LZ4 filter", FILTER_LZ4), - ("HDF5 Bitshuffle filter", FILTER_BITSHUFFLE), - ] - for name, filterId in filters: - isAvailable = h5py.h5z.filter_avail(filterId) - optionals.append(self.__formatOptionalFilters(name, isAvailable)) - else: - optionals.append(self.__formatOptionalLibraries("hdf5plugin", "hdf5plugin" in sys.modules)) - except ImportError: - pass - - # Access to the logo in SVG or PNG - logo = icons.getQFile("silx:" + os.path.join("gui", "logo", "silx")) - - info = dict( - application_name=self.__applicationName, - esrf_url="http://www.esrf.eu", - project_url="https://github.com/silx-kit/silx", - silx_version=silx._version.version, - qt_binding=qt.BINDING, - qt_version=qt.qVersion(), - python_version=sys.version.replace("\n", "<br />"), - optional_lib="<br />".join(optionals), - silx_image_path=logo.fileName() - ) - - self.__label.setText(message.format(**info)) - self.__updateSize() - - def __updateSize(self): - """Force the size to a QMessageBox like size.""" - screenSize = qt.QApplication.desktop().availableGeometry(qt.QCursor.pos()).size() - hardLimit = min(screenSize.width() - 480, 1000) - if screenSize.width() <= 1024: - hardLimit = screenSize.width() - softLimit = min(screenSize.width() / 2, 420) - - layoutMinimumSize = self.layout().totalMinimumSize() - width = layoutMinimumSize.width() - if width > softLimit: - width = softLimit - if width > hardLimit: - width = hardLimit - - height = layoutMinimumSize.height() - self.setFixedSize(width, height) - - @staticmethod - def about(parent, applicationName): - """Displays a silx about box with title and text text. - - :param qt.QWidget parent: The parent widget - :param str title: The title of the dialog - :param str applicationName: The content of the dialog - """ - dialog = About(parent) - dialog.setApplicationName(applicationName) - dialog.exec_() diff --git a/silx/app/view/ApplicationContext.py b/silx/app/view/ApplicationContext.py deleted file mode 100644 index 8693848..0000000 --- a/silx/app/view/ApplicationContext.py +++ /dev/null @@ -1,194 +0,0 @@ -# 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. -# -# ############################################################################*/ -"""Browse a data file with a GUI""" - -__authors__ = ["V. Valls"] -__license__ = "MIT" -__date__ = "23/05/2018" - -import weakref -import logging - -import silx -from silx.gui.data.DataViews import DataViewHooks -from silx.gui.colors import Colormap -from silx.gui.dialog.ColormapDialog import ColormapDialog - - -_logger = logging.getLogger(__name__) - - -class ApplicationContext(DataViewHooks): - """ - Store the conmtext of the application - - It overwrites the DataViewHooks to custom the use of the DataViewer for - the silx view application. - - - Create a single colormap shared with all the views - - Create a single colormap dialog shared with all the views - """ - - def __init__(self, parent, settings=None): - self.__parent = weakref.ref(parent) - self.__defaultColormap = None - self.__defaultColormapDialog = None - self.__settings = settings - self.__recentFiles = [] - - def getSettings(self): - """Returns actual application settings. - - :rtype: qt.QSettings - """ - return self.__settings - - def restoreLibrarySettings(self): - """Restore the library settings, which must be done early""" - settings = self.__settings - if settings is None: - return - settings.beginGroup("library") - plotBackend = settings.value("plot.backend", "") - plotImageYAxisOrientation = settings.value("plot-image.y-axis-orientation", "") - settings.endGroup() - - if plotBackend != "": - silx.config.DEFAULT_PLOT_BACKEND = plotBackend - if plotImageYAxisOrientation != "": - silx.config.DEFAULT_PLOT_IMAGE_Y_AXIS_ORIENTATION = plotImageYAxisOrientation - - def restoreSettings(self): - """Restore the settings of all the application""" - settings = self.__settings - if settings is None: - return - parent = self.__parent() - parent.restoreSettings(settings) - - settings.beginGroup("colormap") - byteArray = settings.value("default", None) - if byteArray is not None: - try: - colormap = Colormap() - colormap.restoreState(byteArray) - self.__defaultColormap = colormap - except Exception: - _logger.debug("Backtrace", exc_info=True) - settings.endGroup() - - self.__recentFiles = [] - settings.beginGroup("recent-files") - for index in range(1, 10 + 1): - if not settings.contains("path%d" % index): - break - filePath = settings.value("path%d" % index) - self.__recentFiles.append(filePath) - settings.endGroup() - - def saveSettings(self): - """Save the settings of all the application""" - settings = self.__settings - if settings is None: - return - parent = self.__parent() - parent.saveSettings(settings) - - if self.__defaultColormap is not None: - settings.beginGroup("colormap") - settings.setValue("default", self.__defaultColormap.saveState()) - settings.endGroup() - - settings.beginGroup("library") - settings.setValue("plot.backend", silx.config.DEFAULT_PLOT_BACKEND) - settings.setValue("plot-image.y-axis-orientation", silx.config.DEFAULT_PLOT_IMAGE_Y_AXIS_ORIENTATION) - settings.endGroup() - - settings.beginGroup("recent-files") - for index in range(0, 11): - key = "path%d" % (index + 1) - if index < len(self.__recentFiles): - filePath = self.__recentFiles[index] - settings.setValue(key, filePath) - else: - settings.remove(key) - settings.endGroup() - - def getRecentFiles(self): - """Returns the list of recently opened files. - - The list is limited to the last 10 entries. The newest file path is - in first. - - :rtype: List[str] - """ - return self.__recentFiles - - def pushRecentFile(self, filePath): - """Push a new recent file to the list. - - If the file is duplicated in the list, all duplications are removed - before inserting the new filePath. - - If the list becan bigger than 10 items, oldest paths are removed. - - :param filePath: File path to push - """ - # Remove old occurencies - self.__recentFiles[:] = (f for f in self.__recentFiles if f != filePath) - self.__recentFiles.insert(0, filePath) - while len(self.__recentFiles) > 10: - self.__recentFiles.pop() - - def clearRencentFiles(self): - """Clear the history of the rencent files. - """ - self.__recentFiles[:] = [] - - def getColormap(self, view): - """Returns a default colormap. - - Override from DataViewHooks - - :rtype: Colormap - """ - if self.__defaultColormap is None: - self.__defaultColormap = Colormap(name="viridis") - return self.__defaultColormap - - def getColormapDialog(self, view): - """Returns a shared color dialog as default for all the views. - - Override from DataViewHooks - - :rtype: ColorDialog - """ - if self.__defaultColormapDialog is None: - parent = self.__parent() - if parent is None: - return None - dialog = ColormapDialog(parent=parent) - dialog.setModal(False) - self.__defaultColormapDialog = dialog - return self.__defaultColormapDialog diff --git a/silx/app/view/CustomNxdataWidget.py b/silx/app/view/CustomNxdataWidget.py deleted file mode 100644 index 72c9940..0000000 --- a/silx/app/view/CustomNxdataWidget.py +++ /dev/null @@ -1,1008 +0,0 @@ -# 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. -# -# ############################################################################*/ - -"""Widget to custom NXdata groups""" - -__authors__ = ["V. Valls"] -__license__ = "MIT" -__date__ = "15/06/2018" - -import logging -import numpy -import weakref - -from silx.gui import qt -from silx.io import commonh5 -import silx.io.nxdata -from silx.gui.hdf5._utils import Hdf5DatasetMimeData -from silx.gui.data.TextFormatter import TextFormatter -from silx.gui.hdf5.Hdf5Formatter import Hdf5Formatter -from silx.gui import icons - - -_logger = logging.getLogger(__name__) -_formatter = TextFormatter() -_hdf5Formatter = Hdf5Formatter(textFormatter=_formatter) - - -class _RowItems(qt.QStandardItem): - """Define the list of items used for a specific row.""" - - def type(self): - return qt.QStandardItem.UserType + 1 - - def getRowItems(self): - """Returns the list of items used for a specific row. - - The first item should be this class. - - :rtype: List[qt.QStandardItem] - """ - raise NotImplementedError() - - -class _DatasetItemRow(_RowItems): - """Define a row which can contain a dataset.""" - - def __init__(self, label="", dataset=None): - """Constructor""" - super(_DatasetItemRow, self).__init__(label) - self.setEditable(False) - self.setDropEnabled(False) - self.setDragEnabled(False) - - self.__name = qt.QStandardItem() - self.__name.setEditable(False) - self.__name.setDropEnabled(True) - - self.__type = qt.QStandardItem() - self.__type.setEditable(False) - self.__type.setDropEnabled(False) - self.__type.setDragEnabled(False) - - self.__shape = qt.QStandardItem() - self.__shape.setEditable(False) - self.__shape.setDropEnabled(False) - self.__shape.setDragEnabled(False) - - self.setDataset(dataset) - - def getDefaultFormatter(self): - """Get the formatter used to display dataset informations. - - :rtype: Hdf5Formatter - """ - return _hdf5Formatter - - def setDataset(self, dataset): - """Set the dataset stored in this item. - - :param Union[numpy.ndarray,h5py.Dataset,silx.io.commonh5.Dataset] dataset: - The dataset to store. - """ - self.__dataset = dataset - if self.__dataset is not None: - name = self.__dataset.name - - if silx.io.is_dataset(dataset): - type_ = self.getDefaultFormatter().humanReadableType(dataset) - shape = self.getDefaultFormatter().humanReadableShape(dataset) - - if dataset.shape is None: - icon_name = "item-none" - elif len(dataset.shape) < 4: - icon_name = "item-%ddim" % len(dataset.shape) - else: - icon_name = "item-ndim" - icon = icons.getQIcon(icon_name) - else: - type_ = "" - shape = "" - icon = qt.QIcon() - else: - name = "" - type_ = "" - shape = "" - icon = qt.QIcon() - - self.__icon = icon - self.__name.setText(name) - self.__name.setDragEnabled(self.__dataset is not None) - self.__name.setIcon(self.__icon) - self.__type.setText(type_) - self.__shape.setText(shape) - - parent = self.parent() - if parent is not None: - self.parent()._datasetUpdated() - - def getDataset(self): - """Returns the dataset stored within the item.""" - return self.__dataset - - def getRowItems(self): - """Returns the list of items used for a specific row. - - The first item should be this class. - - :rtype: List[qt.QStandardItem] - """ - return [self, self.__name, self.__type, self.__shape] - - -class _DatasetAxisItemRow(_DatasetItemRow): - """Define a row describing an axis.""" - - def __init__(self): - """Constructor""" - super(_DatasetAxisItemRow, self).__init__() - - def setAxisId(self, axisId): - """Set the id of the axis (the first axis is 0) - - :param int axisId: Identifier of this axis. - """ - self.__axisId = axisId - label = "Axis %d" % (axisId + 1) - self.setText(label) - - def getAxisId(self): - """Returns the identifier of this axis. - - :rtype: int - """ - return self.__axisId - - -class _NxDataItem(qt.QStandardItem): - """ - Define a custom NXdata. - """ - - def __init__(self): - """Constructor""" - qt.QStandardItem.__init__(self) - self.__error = None - self.__title = None - self.__axes = [] - self.__virtual = None - - item = _DatasetItemRow("Signal", None) - self.appendRow(item.getRowItems()) - self.__signal = item - - self.setEditable(False) - self.setDragEnabled(False) - self.setDropEnabled(False) - self.__setError(None) - - def getRowItems(self): - """Returns the list of items used for a specific row. - - The first item should be this class. - - :rtype: List[qt.QStandardItem] - """ - row = [self] - for _ in range(3): - item = qt.QStandardItem("") - item.setEditable(False) - item.setDragEnabled(False) - item.setDropEnabled(False) - row.append(item) - return row - - def _datasetUpdated(self): - """Called when the NXdata contained of the item have changed. - - It invalidates the NXdata stored and send an event `sigNxdataUpdated`. - """ - self.__virtual = None - self.__setError(None) - model = self.model() - if model is not None: - model.sigNxdataUpdated.emit(self.index()) - - def createVirtualGroup(self): - """Returns a new virtual Group using a NeXus NXdata structure to store - data - - :rtype: silx.io.commonh5.Group - """ - name = "" - if self.__title is not None: - name = self.__title - virtual = commonh5.Group(name) - virtual.attrs["NX_class"] = "NXdata" - - if self.__title is not None: - virtual.attrs["title"] = self.__title - - if self.__signal is not None: - signal = self.__signal.getDataset() - if signal is not None: - # Could be done using a link instead of a copy - node = commonh5.DatasetProxy("signal", target=signal) - virtual.attrs["signal"] = "signal" - virtual.add_node(node) - - axesAttr = [] - for i, axis in enumerate(self.__axes): - if axis is None: - name = "." - else: - axis = axis.getDataset() - if axis is None: - name = "." - else: - name = "axis%d" % i - node = commonh5.DatasetProxy(name, target=axis) - virtual.add_node(node) - axesAttr.append(name) - - if axesAttr != []: - virtual.attrs["axes"] = numpy.array(axesAttr) - - validator = silx.io.nxdata.NXdata(virtual) - if not validator.is_valid: - message = "<html>" - message += "This NXdata is not consistant" - message += "<ul>" - for issue in validator.issues: - message += "<li>%s</li>" % issue - message += "</ul>" - message += "</html>" - self.__setError(message) - else: - self.__setError(None) - return virtual - - def isValid(self): - """Returns true if the stored NXdata is valid - - :rtype: bool - """ - return self.__error is None - - def getVirtualGroup(self): - """Returns a cached virtual Group using a NeXus NXdata structure to - store data. - - If the stored NXdata was invalidated, :meth:`createVirtualGroup` is - internally called to update the cache. - - :rtype: silx.io.commonh5.Group - """ - if self.__virtual is None: - self.__virtual = self.createVirtualGroup() - return self.__virtual - - def getTitle(self): - """Returns the title of the NXdata - - :rtype: str - """ - return self.text() - - def setTitle(self, title): - """Set the title of the NXdata - - :param str title: The title of this NXdata - """ - self.setText(title) - - def __setError(self, error): - """Set the error message in case of the current state of the stored - NXdata is not valid. - - :param str error: Message to display - """ - self.__error = error - style = qt.QApplication.style() - if error is None: - message = "" - icon = style.standardIcon(qt.QStyle.SP_DirLinkIcon) - else: - message = error - icon = style.standardIcon(qt.QStyle.SP_MessageBoxCritical) - self.setIcon(icon) - self.setToolTip(message) - - def getError(self): - """Returns the error message in case the NXdata is not valid. - - :rtype: str""" - return self.__error - - def setSignalDataset(self, dataset): - """Set the dataset to use as signal with this NXdata. - - :param Union[numpy.ndarray,h5py.Dataset,silx.io.commonh5.Dataset] dataset: - The dataset to use as signal. - """ - - self.__signal.setDataset(dataset) - self._datasetUpdated() - - def getSignalDataset(self): - """Returns the dataset used as signal. - - :rtype: Union[numpy.ndarray,h5py.Dataset,silx.io.commonh5.Dataset] - """ - return self.__signal.getDataset() - - def setAxesDatasets(self, datasets): - """Set all the available dataset used as axes. - - Axes will be created or removed from the GUI in order to provide the - same amount of requested axes. - - A `None` element is an axes with no dataset. - - :param List[Union[numpy.ndarray,h5py.Dataset,silx.io.commonh5.Dataset,None]] datasets: - List of dataset to use as axes. - """ - for i, dataset in enumerate(datasets): - if i < len(self.__axes): - mustAppend = False - item = self.__axes[i] - else: - mustAppend = True - item = _DatasetAxisItemRow() - item.setAxisId(i) - item.setDataset(dataset) - if mustAppend: - self.__axes.append(item) - self.appendRow(item.getRowItems()) - - # Clean up extra axis - for i in range(len(datasets), len(self.__axes)): - item = self.__axes.pop(len(datasets)) - self.removeRow(item.row()) - - self._datasetUpdated() - - def getAxesDatasets(self): - """Returns available axes as dataset. - - A `None` element is an axes with no dataset. - - :rtype: List[Union[numpy.ndarray,h5py.Dataset,silx.io.commonh5.Dataset,None]] - """ - datasets = [] - for axis in self.__axes: - datasets.append(axis.getDataset()) - return datasets - - -class _Model(qt.QStandardItemModel): - """Model storing a list of custom NXdata items. - - Supports drag and drop of datasets. - """ - - sigNxdataUpdated = qt.Signal(qt.QModelIndex) - """Emitted when stored NXdata was edited""" - - def __init__(self, parent=None): - """Constructor""" - qt.QStandardItemModel.__init__(self, parent) - root = self.invisibleRootItem() - root.setDropEnabled(True) - root.setDragEnabled(False) - - def supportedDropActions(self): - """Inherited method to redefine supported drop actions.""" - return qt.Qt.CopyAction | qt.Qt.MoveAction - - def mimeTypes(self): - """Inherited method to redefine draggable mime types.""" - return [Hdf5DatasetMimeData.MIME_TYPE] - - def mimeData(self, indexes): - """ - Returns an object that contains serialized items of data corresponding - to the list of indexes specified. - - :param List[qt.QModelIndex] indexes: List of indexes - :rtype: qt.QMimeData - """ - if len(indexes) > 1: - return None - if len(indexes) == 0: - return None - - qindex = indexes[0] - qindex = self.index(qindex.row(), 0, parent=qindex.parent()) - item = self.itemFromIndex(qindex) - if isinstance(item, _DatasetItemRow): - dataset = item.getDataset() - if dataset is None: - return None - else: - mimeData = Hdf5DatasetMimeData(dataset=item.getDataset()) - else: - mimeData = None - return mimeData - - def dropMimeData(self, mimedata, action, row, column, parentIndex): - """Inherited method to handle a drop operation to this model.""" - if action == qt.Qt.IgnoreAction: - return True - - if mimedata.hasFormat(Hdf5DatasetMimeData.MIME_TYPE): - if row != -1 or column != -1: - # It is not a drop on a specific item - return False - item = self.itemFromIndex(parentIndex) - if item is None or item is self.invisibleRootItem(): - # Drop at the end - dataset = mimedata.dataset() - if silx.io.is_dataset(dataset): - self.createFromSignal(dataset) - elif silx.io.is_group(dataset): - nxdata = dataset - try: - self.createFromNxdata(nxdata) - except ValueError: - _logger.error("Error while dropping a group as an NXdata") - _logger.debug("Backtrace", exc_info=True) - return False - else: - _logger.error("Dropping a wrong object") - return False - else: - item = item.parent().child(item.row(), 0) - if not isinstance(item, _DatasetItemRow): - # Dropped at a bad place - return False - dataset = mimedata.dataset() - if silx.io.is_dataset(dataset): - item.setDataset(dataset) - else: - _logger.error("Dropping a wrong object") - return False - return True - - return False - - def __getNxdataByTitle(self, title): - """Returns an NXdata item by its title, else None. - - :rtype: Union[_NxDataItem,None] - """ - for row in range(self.rowCount()): - qindex = self.index(row, 0) - item = self.itemFromIndex(qindex) - if item.getTitle() == title: - return item - return None - - def findFreeNxdataTitle(self): - """Returns an NXdata title which is not yet used. - - :rtype: str - """ - for i in range(self.rowCount() + 1): - name = "NXData #%d" % (i + 1) - group = self.__getNxdataByTitle(name) - if group is None: - break - return name - - def createNewNxdata(self, name=None): - """Create a new NXdata item. - - :param Union[str,None] name: A title for the new NXdata - """ - item = _NxDataItem() - if name is None: - name = self.findFreeNxdataTitle() - item.setTitle(name) - self.appendRow(item.getRowItems()) - - def createFromSignal(self, dataset): - """Create a new NXdata item from a signal dataset. - - This signal will also define an amount of axes according to its number - of dimensions. - - :param Union[numpy.ndarray,h5py.Dataset,silx.io.commonh5.Dataset] dataset: - A dataset uses as signal. - """ - - item = _NxDataItem() - name = self.findFreeNxdataTitle() - item.setTitle(name) - item.setSignalDataset(dataset) - item.setAxesDatasets([None] * len(dataset.shape)) - self.appendRow(item.getRowItems()) - - def createFromNxdata(self, nxdata): - """Create a new custom NXdata item from an existing NXdata group. - - If the NXdata is not valid, nothing is created, and an exception is - returned. - - :param Union[h5py.Group,silx.io.commonh5.Group] nxdata: An h5py group - following the NXData specification. - :raise ValueError:If `nxdata` is not valid. - """ - validator = silx.io.nxdata.NXdata(nxdata) - if validator.is_valid: - item = _NxDataItem() - title = validator.title - if title in [None or ""]: - title = self.findFreeNxdataTitle() - item.setTitle(title) - item.setSignalDataset(validator.signal) - item.setAxesDatasets(validator.axes) - self.appendRow(item.getRowItems()) - else: - raise ValueError("Not a valid NXdata") - - def removeNxdataItem(self, item): - """Remove an NXdata item from this model. - - :param _NxDataItem item: An item - """ - if isinstance(item, _NxDataItem): - parent = item.parent() - assert(parent is None) - model = item.model() - model.removeRow(item.row()) - else: - _logger.error("Unexpected item") - - def appendAxisToNxdataItem(self, item): - """Append a new axes to this item (or the NXdata item own by this item). - - :param Union[_NxDataItem,qt.QStandardItem] item: An item - """ - if item is not None and not isinstance(item, _NxDataItem): - item = item.parent() - nxdataItem = item - if isinstance(item, _NxDataItem): - datasets = nxdataItem.getAxesDatasets() - datasets.append(None) - nxdataItem.setAxesDatasets(datasets) - else: - _logger.error("Unexpected item") - - def removeAxisItem(self, item): - """Remove an axis item from this model. - - :param _DatasetAxisItemRow item: An axis item - """ - if isinstance(item, _DatasetAxisItemRow): - axisId = item.getAxisId() - nxdataItem = item.parent() - datasets = nxdataItem.getAxesDatasets() - del datasets[axisId] - nxdataItem.setAxesDatasets(datasets) - else: - _logger.error("Unexpected item") - - -class CustomNxDataToolBar(qt.QToolBar): - """A specialised toolbar to manage custom NXdata model and items.""" - - def __init__(self, parent=None): - """Constructor""" - super(CustomNxDataToolBar, self).__init__(parent=parent) - self.__nxdataWidget = None - self.__initContent() - # Initialize action state - self.__currentSelectionChanged(qt.QModelIndex(), qt.QModelIndex()) - - def __initContent(self): - """Create all expected actions and set the content of this toolbar.""" - action = qt.QAction("Create a new custom NXdata", self) - action.setIcon(icons.getQIcon("nxdata-create")) - action.triggered.connect(self.__createNewNxdata) - self.addAction(action) - self.__addNxDataAction = action - - action = qt.QAction("Remove the selected NXdata", self) - action.setIcon(icons.getQIcon("nxdata-remove")) - action.triggered.connect(self.__removeSelectedNxdata) - self.addAction(action) - self.__removeNxDataAction = action - - self.addSeparator() - - action = qt.QAction("Create a new axis to the selected NXdata", self) - action.setIcon(icons.getQIcon("nxdata-axis-add")) - action.triggered.connect(self.__appendNewAxisToSelectedNxdata) - self.addAction(action) - self.__addNxDataAxisAction = action - - action = qt.QAction("Remove the selected NXdata axis", self) - action.setIcon(icons.getQIcon("nxdata-axis-remove")) - action.triggered.connect(self.__removeSelectedAxis) - self.addAction(action) - self.__removeNxDataAxisAction = action - - def __getSelectedItem(self): - """Get the selected item from the linked CustomNxdataWidget. - - :rtype: qt.QStandardItem - """ - selectionModel = self.__nxdataWidget.selectionModel() - index = selectionModel.currentIndex() - if not index.isValid(): - return - model = self.__nxdataWidget.model() - index = model.index(index.row(), 0, index.parent()) - item = model.itemFromIndex(index) - return item - - def __createNewNxdata(self): - """Create a new NXdata item to the linked CustomNxdataWidget.""" - if self.__nxdataWidget is None: - return - model = self.__nxdataWidget.model() - model.createNewNxdata() - - def __removeSelectedNxdata(self): - """Remove the NXdata item currently selected in the linked - CustomNxdataWidget.""" - if self.__nxdataWidget is None: - return - model = self.__nxdataWidget.model() - item = self.__getSelectedItem() - model.removeNxdataItem(item) - - def __appendNewAxisToSelectedNxdata(self): - """Append a new axis to the NXdata item currently selected in the - linked CustomNxdataWidget.""" - if self.__nxdataWidget is None: - return - model = self.__nxdataWidget.model() - item = self.__getSelectedItem() - model.appendAxisToNxdataItem(item) - - def __removeSelectedAxis(self): - """Remove the axis item currently selected in the linked - CustomNxdataWidget.""" - if self.__nxdataWidget is None: - return - model = self.__nxdataWidget.model() - item = self.__getSelectedItem() - model.removeAxisItem(item) - - def setCustomNxDataWidget(self, widget): - """Set the linked CustomNxdataWidget to this toolbar.""" - assert(isinstance(widget, CustomNxdataWidget)) - if self.__nxdataWidget is not None: - selectionModel = self.__nxdataWidget.selectionModel() - selectionModel.currentChanged.disconnect(self.__currentSelectionChanged) - self.__nxdataWidget = widget - if self.__nxdataWidget is not None: - selectionModel = self.__nxdataWidget.selectionModel() - selectionModel.currentChanged.connect(self.__currentSelectionChanged) - - def __currentSelectionChanged(self, current, previous): - """Update the actions according to the linked CustomNxdataWidget - item selection""" - if not current.isValid(): - item = None - else: - model = self.__nxdataWidget.model() - index = model.index(current.row(), 0, current.parent()) - item = model.itemFromIndex(index) - self.__removeNxDataAction.setEnabled(isinstance(item, _NxDataItem)) - self.__removeNxDataAxisAction.setEnabled(isinstance(item, _DatasetAxisItemRow)) - self.__addNxDataAxisAction.setEnabled(isinstance(item, _NxDataItem) or isinstance(item, _DatasetItemRow)) - - -class _HashDropZones(qt.QStyledItemDelegate): - """Delegate item displaying a drop zone when the item do not contains - dataset.""" - - def __init__(self, parent=None): - """Constructor""" - super(_HashDropZones, self).__init__(parent) - pen = qt.QPen() - pen.setColor(qt.QColor("#D0D0D0")) - pen.setStyle(qt.Qt.DotLine) - pen.setWidth(2) - self.__dropPen = pen - - def paint(self, painter, option, index): - """ - Paint the item - - :param qt.QPainter painter: A painter - :param qt.QStyleOptionViewItem option: Options of the item to paint - :param qt.QModelIndex index: Index of the item to paint - """ - displayDropZone = False - if index.isValid(): - model = index.model() - rowIndex = model.index(index.row(), 0, index.parent()) - rowItem = model.itemFromIndex(rowIndex) - if isinstance(rowItem, _DatasetItemRow): - displayDropZone = rowItem.getDataset() is None - - if displayDropZone: - painter.save() - - # Draw background if selected - if option.state & qt.QStyle.State_Selected: - colorGroup = qt.QPalette.Inactive - if option.state & qt.QStyle.State_Active: - colorGroup = qt.QPalette.Active - if not option.state & qt.QStyle.State_Enabled: - colorGroup = qt.QPalette.Disabled - brush = option.palette.brush(colorGroup, qt.QPalette.Highlight) - painter.fillRect(option.rect, brush) - - painter.setPen(self.__dropPen) - painter.drawRect(option.rect.adjusted(3, 3, -3, -3)) - painter.restore() - else: - qt.QStyledItemDelegate.paint(self, painter, option, index) - - -class CustomNxdataWidget(qt.QTreeView): - """Widget providing a table displaying and allowing to custom virtual - NXdata.""" - - sigNxdataItemUpdated = qt.Signal(qt.QStandardItem) - """Emitted when the NXdata from an NXdata item was edited""" - - sigNxdataItemRemoved = qt.Signal(qt.QStandardItem) - """Emitted when an NXdata item was removed""" - - def __init__(self, parent=None): - """Constructor""" - qt.QTreeView.__init__(self, parent=None) - self.__model = _Model(self) - self.__model.setColumnCount(4) - self.__model.setHorizontalHeaderLabels(["Name", "Dataset", "Type", "Shape"]) - self.setModel(self.__model) - - self.setItemDelegateForColumn(1, _HashDropZones(self)) - - self.__model.sigNxdataUpdated.connect(self.__nxdataUpdate) - self.__model.rowsAboutToBeRemoved.connect(self.__rowsAboutToBeRemoved) - self.__model.rowsAboutToBeInserted.connect(self.__rowsAboutToBeInserted) - - header = self.header() - if qt.qVersion() < "5.0": - setResizeMode = header.setResizeMode - else: - setResizeMode = header.setSectionResizeMode - setResizeMode(0, qt.QHeaderView.ResizeToContents) - setResizeMode(1, qt.QHeaderView.Stretch) - setResizeMode(2, qt.QHeaderView.ResizeToContents) - setResizeMode(3, qt.QHeaderView.ResizeToContents) - - self.setSelectionMode(qt.QAbstractItemView.SingleSelection) - self.setDropIndicatorShown(True) - self.setDragDropOverwriteMode(True) - self.setDragEnabled(True) - self.viewport().setAcceptDrops(True) - - self.setContextMenuPolicy(qt.Qt.CustomContextMenu) - self.customContextMenuRequested[qt.QPoint].connect(self.__executeContextMenu) - - def __rowsAboutToBeInserted(self, parentIndex, start, end): - if qt.qVersion()[0:2] == "5.": - # FIXME: workaround for https://github.com/silx-kit/silx/issues/1919 - # Uses of ResizeToContents looks to break nice update of cells with Qt5 - # This patch make the view blinking - self.repaint() - - def __rowsAboutToBeRemoved(self, parentIndex, start, end): - """Called when an item was removed from the model.""" - items = [] - model = self.model() - for index in range(start, end): - qindex = model.index(index, 0, parent=parentIndex) - item = self.__model.itemFromIndex(qindex) - if isinstance(item, _NxDataItem): - items.append(item) - for item in items: - self.sigNxdataItemRemoved.emit(item) - - if qt.qVersion()[0:2] == "5.": - # FIXME: workaround for https://github.com/silx-kit/silx/issues/1919 - # Uses of ResizeToContents looks to break nice update of cells with Qt5 - # This patch make the view blinking - self.repaint() - - def __nxdataUpdate(self, index): - """Called when a virtual NXdata was updated from the model.""" - model = self.model() - item = model.itemFromIndex(index) - self.sigNxdataItemUpdated.emit(item) - - def createDefaultContextMenu(self, index): - """Create a default context menu at this position. - - :param qt.QModelIndex index: Index of the item - """ - index = self.__model.index(index.row(), 0, parent=index.parent()) - item = self.__model.itemFromIndex(index) - - menu = qt.QMenu() - - weakself = weakref.proxy(self) - - if isinstance(item, _NxDataItem): - action = qt.QAction("Add a new axis", menu) - action.triggered.connect(lambda: weakself.model().appendAxisToNxdataItem(item)) - action.setIcon(icons.getQIcon("nxdata-axis-add")) - action.setIconVisibleInMenu(True) - menu.addAction(action) - menu.addSeparator() - action = qt.QAction("Remove this NXdata", menu) - action.triggered.connect(lambda: weakself.model().removeNxdataItem(item)) - action.setIcon(icons.getQIcon("remove")) - action.setIconVisibleInMenu(True) - menu.addAction(action) - else: - if isinstance(item, _DatasetItemRow): - if item.getDataset() is not None: - action = qt.QAction("Remove this dataset", menu) - action.triggered.connect(lambda: item.setDataset(None)) - menu.addAction(action) - - if isinstance(item, _DatasetAxisItemRow): - menu.addSeparator() - action = qt.QAction("Remove this axis", menu) - action.triggered.connect(lambda: weakself.model().removeAxisItem(item)) - action.setIcon(icons.getQIcon("remove")) - action.setIconVisibleInMenu(True) - menu.addAction(action) - - return menu - - def __executeContextMenu(self, point): - """Execute the context menu at this position.""" - index = self.indexAt(point) - menu = self.createDefaultContextMenu(index) - if menu is None or menu.isEmpty(): - return - menu.exec_(qt.QCursor.pos()) - - def removeDatasetsFrom(self, root): - """ - Remove all datasets provided by this root - - :param root: The root file of datasets to remove - """ - for row in range(self.__model.rowCount()): - qindex = self.__model.index(row, 0) - item = self.model().itemFromIndex(qindex) - - edited = False - datasets = item.getAxesDatasets() - for i, dataset in enumerate(datasets): - if dataset is not None: - # That's an approximation, IS can't be used as h5py generates - # To objects for each requests to a node - if dataset.file.filename == root.file.filename: - datasets[i] = None - edited = True - if edited: - item.setAxesDatasets(datasets) - - dataset = item.getSignalDataset() - if dataset is not None: - # That's an approximation, IS can't be used as h5py generates - # To objects for each requests to a node - if dataset.file.filename == root.file.filename: - item.setSignalDataset(None) - - def replaceDatasetsFrom(self, removedRoot, loadedRoot): - """ - Replace any dataset from any NXdata items using the same dataset name - from another root. - - Usually used when a file was synchronized. - - :param removedRoot: The h5py root file which is replaced - (which have to be removed) - :param loadedRoot: The new h5py root file which have to be used - instread. - """ - for row in range(self.__model.rowCount()): - qindex = self.__model.index(row, 0) - item = self.model().itemFromIndex(qindex) - - edited = False - datasets = item.getAxesDatasets() - for i, dataset in enumerate(datasets): - newDataset = self.__replaceDatasetRoot(dataset, removedRoot, loadedRoot) - if dataset is not newDataset: - datasets[i] = newDataset - edited = True - if edited: - item.setAxesDatasets(datasets) - - dataset = item.getSignalDataset() - newDataset = self.__replaceDatasetRoot(dataset, removedRoot, loadedRoot) - if dataset is not newDataset: - item.setSignalDataset(newDataset) - - def __replaceDatasetRoot(self, dataset, fromRoot, toRoot): - """ - Replace the dataset by the same dataset name from another root. - """ - if dataset is None: - return None - - if dataset.file is None: - # Not from the expected root - return dataset - - # That's an approximation, IS can't be used as h5py generates - # To objects for each requests to a node - if dataset.file.filename == fromRoot.file.filename: - # Try to find the same dataset name - try: - return toRoot[dataset.name] - except Exception: - _logger.debug("Backtrace", exc_info=True) - return None - else: - # Not from the expected root - return dataset - - def selectedItems(self): - """Returns the list of selected items containing NXdata - - :rtype: List[qt.QStandardItem] - """ - result = [] - for qindex in self.selectedIndexes(): - if qindex.column() != 0: - continue - if not qindex.isValid(): - continue - item = self.__model.itemFromIndex(qindex) - if not isinstance(item, _NxDataItem): - continue - result.append(item) - return result - - def selectedNxdata(self): - """Returns the list of selected NXdata - - :rtype: List[silx.io.commonh5.Group] - """ - result = [] - for qindex in self.selectedIndexes(): - if qindex.column() != 0: - continue - if not qindex.isValid(): - continue - item = self.__model.itemFromIndex(qindex) - if not isinstance(item, _NxDataItem): - continue - result.append(item.getVirtualGroup()) - return result diff --git a/silx/app/view/DataPanel.py b/silx/app/view/DataPanel.py deleted file mode 100644 index 5d87381..0000000 --- a/silx/app/view/DataPanel.py +++ /dev/null @@ -1,192 +0,0 @@ -# coding: utf-8 -# /*########################################################################## -# Copyright (C) 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. -# -# ############################################################################*/ -"""Browse a data file with a GUI""" - -__authors__ = ["V. Valls"] -__license__ = "MIT" -__date__ = "12/10/2018" - -import logging -import os.path - -from silx.gui import qt -from silx.gui.data.DataViewerFrame import DataViewerFrame - - -_logger = logging.getLogger(__name__) - - -class _HeaderLabel(qt.QLabel): - - def __init__(self, parent=None): - qt.QLabel.__init__(self, parent=parent) - self.setFrameShape(qt.QFrame.StyledPanel) - - def sizeHint(self): - return qt.QSize(10, 30) - - def minimumSizeHint(self): - return qt.QSize(10, 30) - - def setData(self, filename, path): - if filename == "" and path == "": - text = "" - elif filename == "": - text = path - else: - text = "%s::%s" % (filename, path) - self.setText(text) - tooltip = "" - template = "<li><b>%s</b>: %s</li>" - tooltip += template % ("Directory", os.path.dirname(filename)) - tooltip += template % ("File name", os.path.basename(filename)) - tooltip += template % ("Data path", path) - tooltip = "<ul>%s</ul>" % tooltip - tooltip = "<html>%s</html>" % tooltip - self.setToolTip(tooltip) - - def paintEvent(self, event): - painter = qt.QPainter(self) - - opt = qt.QStyleOptionHeader() - opt.orientation = qt.Qt.Horizontal - opt.text = self.text() - opt.textAlignment = self.alignment() - opt.direction = self.layoutDirection() - opt.fontMetrics = self.fontMetrics() - opt.palette = self.palette() - opt.state = qt.QStyle.State_Active - opt.position = qt.QStyleOptionHeader.Beginning - style = self.style() - - # Background - margin = -1 - opt.rect = self.rect().adjusted(margin, margin, -margin, -margin) - style.drawControl(qt.QStyle.CE_HeaderSection, opt, painter, None) - - # Frame border and text - super(_HeaderLabel, self).paintEvent(event) - - -class DataPanel(qt.QWidget): - - def __init__(self, parent=None, context=None): - qt.QWidget.__init__(self, parent=parent) - - self.__customNxdataItem = None - - self.__dataTitle = _HeaderLabel(self) - self.__dataTitle.setVisible(False) - - self.__dataViewer = DataViewerFrame(self) - self.__dataViewer.setGlobalHooks(context) - - layout = qt.QVBoxLayout(self) - layout.setSpacing(0) - layout.setContentsMargins(0, 0, 0, 0) - layout.addWidget(self.__dataTitle) - layout.addWidget(self.__dataViewer) - - def getData(self): - return self.__dataViewer.data() - - def getCustomNxdataItem(self): - return self.__customNxdataItem - - def setData(self, data): - self.__customNxdataItem = None - self.__dataViewer.setData(data) - self.__dataTitle.setVisible(data is not None) - if data is not None: - self.__dataTitle.setVisible(True) - if hasattr(data, "name"): - if hasattr(data, "file"): - filename = str(data.file.filename) - else: - filename = "" - path = data.name - else: - filename = "" - path = "" - self.__dataTitle.setData(filename, path) - - def setCustomDataItem(self, item): - self.__customNxdataItem = item - if item is not None: - data = item.getVirtualGroup() - else: - data = None - self.__dataViewer.setData(data) - self.__dataTitle.setVisible(item is not None) - if item is not None: - text = item.text() - self.__dataTitle.setText(text) - - def removeDatasetsFrom(self, root): - """ - Remove all datasets provided by this root - - .. note:: This function do not update data stored inside - customNxdataItem cause in the silx-view context this item is - already updated on his own. - - :param root: The root file of datasets to remove - """ - data = self.__dataViewer.data() - if data is not None: - if data.file is not None: - # That's an approximation, IS can't be used as h5py generates - # To objects for each requests to a node - if data.file.filename == root.file.filename: - self.__dataViewer.setData(None) - - def replaceDatasetsFrom(self, removedH5, loadedH5): - """ - Replace any dataset from any NXdata items using the same dataset name - from another root. - - Usually used when a file was synchronized. - - .. note:: This function do not update data stored inside - customNxdataItem cause in the silx-view context this item is - already updated on his own. - - :param removedRoot: The h5py root file which is replaced - (which have to be removed) - :param loadedRoot: The new h5py root file which have to be used - instread. - """ - - data = self.__dataViewer.data() - if data is not None: - if data.file is not None: - if data.file.filename == removedH5.file.filename: - # Try to synchonize the viewed data - try: - # TODO: It have to update the data without changing the - # view which is not so easy - newData = loadedH5[data.name] - self.__dataViewer.setData(newData) - except Exception: - _logger.debug("Backtrace", exc_info=True) diff --git a/silx/app/view/Viewer.py b/silx/app/view/Viewer.py deleted file mode 100644 index dd4d075..0000000 --- a/silx/app/view/Viewer.py +++ /dev/null @@ -1,971 +0,0 @@ -# coding: utf-8 -# /*########################################################################## -# Copyright (C) 2016-2020 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. -# -# ############################################################################*/ -"""Browse a data file with a GUI""" - -__authors__ = ["V. Valls"] -__license__ = "MIT" -__date__ = "15/01/2019" - - -import os -import collections -import logging -import functools - -import silx.io.nxdata -from silx.gui import qt -from silx.gui import icons -import silx.gui.hdf5 -from .ApplicationContext import ApplicationContext -from .CustomNxdataWidget import CustomNxdataWidget -from .CustomNxdataWidget import CustomNxDataToolBar -from . import utils -from silx.gui.utils import projecturl -from .DataPanel import DataPanel - - -_logger = logging.getLogger(__name__) - - -class Viewer(qt.QMainWindow): - """ - This window allows to browse a data file like images or HDF5 and it's - content. - """ - - def __init__(self, parent=None, settings=None): - """ - Constructor - """ - - qt.QMainWindow.__init__(self, parent) - self.setWindowTitle("Silx viewer") - - silxIcon = icons.getQIcon("silx") - self.setWindowIcon(silxIcon) - - self.__context = self.createApplicationContext(settings) - self.__context.restoreLibrarySettings() - - self.__dialogState = None - self.__customNxDataItem = None - self.__treeview = silx.gui.hdf5.Hdf5TreeView(self) - self.__treeview.setExpandsOnDoubleClick(False) - """Silx HDF5 TreeView""" - - rightPanel = qt.QSplitter(self) - rightPanel.setOrientation(qt.Qt.Vertical) - self.__splitter2 = rightPanel - - self.__displayIt = None - self.__treeWindow = self.__createTreeWindow(self.__treeview) - - # Custom the model to be able to manage the life cycle of the files - treeModel = silx.gui.hdf5.Hdf5TreeModel(self.__treeview, ownFiles=False) - treeModel.sigH5pyObjectLoaded.connect(self.__h5FileLoaded) - treeModel.sigH5pyObjectRemoved.connect(self.__h5FileRemoved) - treeModel.sigH5pyObjectSynchronized.connect(self.__h5FileSynchonized) - treeModel.setDatasetDragEnabled(True) - self.__treeModelSorted = silx.gui.hdf5.NexusSortFilterProxyModel(self.__treeview) - self.__treeModelSorted.setSourceModel(treeModel) - self.__treeModelSorted.sort(0, qt.Qt.AscendingOrder) - self.__treeModelSorted.setSortCaseSensitivity(qt.Qt.CaseInsensitive) - - self.__treeview.setModel(self.__treeModelSorted) - rightPanel.addWidget(self.__treeWindow) - - self.__customNxdata = CustomNxdataWidget(self) - self.__customNxdata.setSelectionBehavior(qt.QAbstractItemView.SelectRows) - # optimise the rendering - self.__customNxdata.setUniformRowHeights(True) - self.__customNxdata.setIconSize(qt.QSize(16, 16)) - self.__customNxdata.setExpandsOnDoubleClick(False) - - self.__customNxdataWindow = self.__createCustomNxdataWindow(self.__customNxdata) - self.__customNxdataWindow.setVisible(False) - rightPanel.addWidget(self.__customNxdataWindow) - - rightPanel.setStretchFactor(1, 1) - rightPanel.setCollapsible(0, False) - rightPanel.setCollapsible(1, False) - - self.__dataPanel = DataPanel(self, self.__context) - - spliter = qt.QSplitter(self) - spliter.addWidget(rightPanel) - spliter.addWidget(self.__dataPanel) - spliter.setStretchFactor(1, 1) - spliter.setCollapsible(0, False) - spliter.setCollapsible(1, False) - self.__splitter = spliter - - main_panel = qt.QWidget(self) - layout = qt.QVBoxLayout() - layout.addWidget(spliter) - layout.setStretchFactor(spliter, 1) - main_panel.setLayout(layout) - - self.setCentralWidget(main_panel) - - self.__treeview.activated.connect(self.displaySelectedData) - self.__customNxdata.activated.connect(self.displaySelectedCustomData) - self.__customNxdata.sigNxdataItemRemoved.connect(self.__customNxdataRemoved) - self.__customNxdata.sigNxdataItemUpdated.connect(self.__customNxdataUpdated) - self.__treeview.addContextMenuCallback(self.customContextMenu) - - treeModel = self.__treeview.findHdf5TreeModel() - columns = list(treeModel.COLUMN_IDS) - columns.remove(treeModel.VALUE_COLUMN) - columns.remove(treeModel.NODE_COLUMN) - columns.remove(treeModel.DESCRIPTION_COLUMN) - columns.insert(1, treeModel.DESCRIPTION_COLUMN) - self.__treeview.header().setSections(columns) - - self._iconUpward = icons.getQIcon('plot-yup') - self._iconDownward = icons.getQIcon('plot-ydown') - - self.createActions() - self.createMenus() - self.__context.restoreSettings() - - def createApplicationContext(self, settings): - return ApplicationContext(self, settings) - - def __createTreeWindow(self, treeView): - toolbar = qt.QToolBar(self) - toolbar.setIconSize(qt.QSize(16, 16)) - toolbar.setStyleSheet("QToolBar { border: 0px }") - - action = qt.QAction(toolbar) - action.setIcon(icons.getQIcon("view-refresh")) - action.setText("Refresh") - action.setToolTip("Refresh all selected items") - action.triggered.connect(self.__refreshSelected) - action.setShortcut(qt.QKeySequence(qt.Qt.Key_F5)) - toolbar.addAction(action) - treeView.addAction(action) - self.__refreshAction = action - - # Another shortcut for refresh - action = qt.QAction(toolbar) - action.setShortcut(qt.QKeySequence(qt.Qt.ControlModifier + qt.Qt.Key_R)) - treeView.addAction(action) - action.triggered.connect(self.__refreshSelected) - - action = qt.QAction(toolbar) - # action.setIcon(icons.getQIcon("view-refresh")) - action.setText("Close") - action.setToolTip("Close selected item") - action.triggered.connect(self.__removeSelected) - action.setShortcut(qt.QKeySequence(qt.Qt.Key_Delete)) - treeView.addAction(action) - self.__closeAction = action - - toolbar.addSeparator() - - action = qt.QAction(toolbar) - action.setIcon(icons.getQIcon("tree-expand-all")) - action.setText("Expand all") - action.setToolTip("Expand all selected items") - action.triggered.connect(self.__expandAllSelected) - action.setShortcut(qt.QKeySequence(qt.Qt.ControlModifier + qt.Qt.Key_Plus)) - toolbar.addAction(action) - treeView.addAction(action) - self.__expandAllAction = action - - action = qt.QAction(toolbar) - action.setIcon(icons.getQIcon("tree-collapse-all")) - action.setText("Collapse all") - action.setToolTip("Collapse all selected items") - action.triggered.connect(self.__collapseAllSelected) - action.setShortcut(qt.QKeySequence(qt.Qt.ControlModifier + qt.Qt.Key_Minus)) - toolbar.addAction(action) - treeView.addAction(action) - self.__collapseAllAction = action - - action = qt.QAction("&Sort file content", toolbar) - action.setIcon(icons.getQIcon("tree-sort")) - action.setToolTip("Toggle sorting of file content") - action.setCheckable(True) - action.setChecked(True) - action.triggered.connect(self.setContentSorted) - toolbar.addAction(action) - treeView.addAction(action) - self._sortContentAction = action - - widget = qt.QWidget(self) - layout = qt.QVBoxLayout(widget) - layout.setContentsMargins(0, 0, 0, 0) - layout.setSpacing(0) - layout.addWidget(toolbar) - layout.addWidget(treeView) - return widget - - def __removeSelected(self): - """Close selected items""" - qt.QApplication.setOverrideCursor(qt.Qt.WaitCursor) - - selection = self.__treeview.selectionModel() - indexes = selection.selectedIndexes() - selectedItems = [] - model = self.__treeview.model() - h5files = set([]) - while len(indexes) > 0: - index = indexes.pop(0) - if index.column() != 0: - continue - h5 = model.data(index, role=silx.gui.hdf5.Hdf5TreeModel.H5PY_OBJECT_ROLE) - rootIndex = index - # Reach the root of the tree - while rootIndex.parent().isValid(): - rootIndex = rootIndex.parent() - rootRow = rootIndex.row() - relativePath = self.__getRelativePath(model, rootIndex, index) - selectedItems.append((rootRow, relativePath)) - h5files.add(h5.file) - - if len(h5files) != 0: - model = self.__treeview.findHdf5TreeModel() - for h5 in h5files: - row = model.h5pyObjectRow(h5) - model.removeH5pyObject(h5) - - qt.QApplication.restoreOverrideCursor() - - def __refreshSelected(self): - """Refresh all selected items - """ - qt.QApplication.setOverrideCursor(qt.Qt.WaitCursor) - - selection = self.__treeview.selectionModel() - indexes = selection.selectedIndexes() - selectedItems = [] - model = self.__treeview.model() - h5files = set([]) - while len(indexes) > 0: - index = indexes.pop(0) - if index.column() != 0: - continue - h5 = model.data(index, role=silx.gui.hdf5.Hdf5TreeModel.H5PY_OBJECT_ROLE) - rootIndex = index - # Reach the root of the tree - while rootIndex.parent().isValid(): - rootIndex = rootIndex.parent() - rootRow = rootIndex.row() - relativePath = self.__getRelativePath(model, rootIndex, index) - selectedItems.append((rootRow, relativePath)) - h5files.add(h5.file) - - if len(h5files) == 0: - qt.QApplication.restoreOverrideCursor() - return - - model = self.__treeview.findHdf5TreeModel() - for h5 in h5files: - self.__synchronizeH5pyObject(h5) - - model = self.__treeview.model() - itemSelection = qt.QItemSelection() - for rootRow, relativePath in selectedItems: - rootIndex = model.index(rootRow, 0, qt.QModelIndex()) - index = self.__indexFromPath(model, rootIndex, relativePath) - if index is None: - continue - indexEnd = model.index(index.row(), model.columnCount() - 1, index.parent()) - itemSelection.select(index, indexEnd) - selection.select(itemSelection, qt.QItemSelectionModel.ClearAndSelect) - - qt.QApplication.restoreOverrideCursor() - - def __synchronizeH5pyObject(self, h5): - model = self.__treeview.findHdf5TreeModel() - # This is buggy right now while h5py do not allow to close a file - # while references are still used. - # FIXME: The architecture have to be reworked to support this feature. - # model.synchronizeH5pyObject(h5) - - filename = h5.filename - row = model.h5pyObjectRow(h5) - index = self.__treeview.model().index(row, 0, qt.QModelIndex()) - paths = self.__getPathFromExpandedNodes(self.__treeview, index) - model.removeH5pyObject(h5) - model.insertFile(filename, row) - index = self.__treeview.model().index(row, 0, qt.QModelIndex()) - self.__expandNodesFromPaths(self.__treeview, index, paths) - - def __getRelativePath(self, model, rootIndex, index): - """Returns a relative path from an index to his rootIndex. - - If the path is empty the index is also the rootIndex. - """ - path = "" - while index.isValid(): - if index == rootIndex: - return path - name = model.data(index) - if path == "": - path = name - else: - path = name + "/" + path - index = index.parent() - - # index is not a children of rootIndex - raise ValueError("index is not a children of the rootIndex") - - def __getPathFromExpandedNodes(self, view, rootIndex): - """Return relative path from the root index of the extended nodes""" - model = view.model() - rootPath = None - paths = [] - indexes = [rootIndex] - while len(indexes): - index = indexes.pop(0) - if not view.isExpanded(index): - continue - - node = model.data(index, role=silx.gui.hdf5.Hdf5TreeModel.H5PY_ITEM_ROLE) - path = node._getCanonicalName() - if rootPath is None: - rootPath = path - path = path[len(rootPath):] - paths.append(path) - - for child in range(model.rowCount(index)): - childIndex = model.index(child, 0, index) - indexes.append(childIndex) - return paths - - def __indexFromPath(self, model, rootIndex, path): - elements = path.split("/") - if elements[0] == "": - elements.pop(0) - index = rootIndex - while len(elements) != 0: - element = elements.pop(0) - found = False - for child in range(model.rowCount(index)): - childIndex = model.index(child, 0, index) - name = model.data(childIndex) - if element == name: - index = childIndex - found = True - break - if not found: - return None - return index - - def __expandNodesFromPaths(self, view, rootIndex, paths): - model = view.model() - for path in paths: - index = self.__indexFromPath(model, rootIndex, path) - if index is not None: - view.setExpanded(index, True) - - def __expandAllSelected(self): - """Expand all selected items of the tree. - - The depth is fixed to avoid infinite loop with recurssive links. - """ - qt.QApplication.setOverrideCursor(qt.Qt.WaitCursor) - - selection = self.__treeview.selectionModel() - indexes = selection.selectedIndexes() - model = self.__treeview.model() - while len(indexes) > 0: - index = indexes.pop(0) - if isinstance(index, tuple): - index, depth = index - else: - depth = 0 - if index.column() != 0: - continue - - if depth > 10: - # Avoid infinite loop with recursive links - break - - if model.hasChildren(index): - self.__treeview.setExpanded(index, True) - for row in range(model.rowCount(index)): - childIndex = model.index(row, 0, index) - indexes.append((childIndex, depth + 1)) - qt.QApplication.restoreOverrideCursor() - - def __collapseAllSelected(self): - """Collapse all selected items of the tree. - - The depth is fixed to avoid infinite loop with recurssive links. - """ - selection = self.__treeview.selectionModel() - indexes = selection.selectedIndexes() - model = self.__treeview.model() - while len(indexes) > 0: - index = indexes.pop(0) - if isinstance(index, tuple): - index, depth = index - else: - depth = 0 - if index.column() != 0: - continue - - if depth > 10: - # Avoid infinite loop with recursive links - break - - if model.hasChildren(index): - self.__treeview.setExpanded(index, False) - for row in range(model.rowCount(index)): - childIndex = model.index(row, 0, index) - indexes.append((childIndex, depth + 1)) - - def __createCustomNxdataWindow(self, customNxdataWidget): - toolbar = CustomNxDataToolBar(self) - toolbar.setCustomNxDataWidget(customNxdataWidget) - toolbar.setIconSize(qt.QSize(16, 16)) - toolbar.setStyleSheet("QToolBar { border: 0px }") - - widget = qt.QWidget(self) - layout = qt.QVBoxLayout(widget) - layout.setContentsMargins(0, 0, 0, 0) - layout.setSpacing(0) - layout.addWidget(toolbar) - layout.addWidget(customNxdataWidget) - return widget - - def __h5FileLoaded(self, loadedH5): - self.__context.pushRecentFile(loadedH5.file.filename) - if loadedH5.file.filename == self.__displayIt: - self.__displayIt = None - self.displayData(loadedH5) - - def __h5FileRemoved(self, removedH5): - self.__dataPanel.removeDatasetsFrom(removedH5) - self.__customNxdata.removeDatasetsFrom(removedH5) - removedH5.close() - - def __h5FileSynchonized(self, removedH5, loadedH5): - self.__dataPanel.replaceDatasetsFrom(removedH5, loadedH5) - self.__customNxdata.replaceDatasetsFrom(removedH5, loadedH5) - removedH5.close() - - def closeEvent(self, event): - self.__context.saveSettings() - - # Clean up as much as possible Python objects - self.displayData(None) - customModel = self.__customNxdata.model() - customModel.clear() - hdf5Model = self.__treeview.findHdf5TreeModel() - hdf5Model.clear() - - def saveSettings(self, settings): - """Save the window settings to this settings object - - :param qt.QSettings settings: Initialized settings - """ - isFullScreen = bool(self.windowState() & qt.Qt.WindowFullScreen) - if isFullScreen: - # show in normal to catch the normal geometry - self.showNormal() - - settings.beginGroup("mainwindow") - settings.setValue("size", self.size()) - settings.setValue("pos", self.pos()) - settings.setValue("full-screen", isFullScreen) - settings.endGroup() - - settings.beginGroup("mainlayout") - settings.setValue("spliter", self.__splitter.sizes()) - settings.setValue("spliter2", self.__splitter2.sizes()) - isVisible = self.__customNxdataWindow.isVisible() - settings.setValue("custom-nxdata-window-visible", isVisible) - settings.endGroup() - - settings.beginGroup("content") - isSorted = self._sortContentAction.isChecked() - settings.setValue("is-sorted", isSorted) - settings.endGroup() - - if isFullScreen: - self.showFullScreen() - - def restoreSettings(self, settings): - """Restore the window settings using this settings object - - :param qt.QSettings settings: Initialized settings - """ - settings.beginGroup("mainwindow") - size = settings.value("size", qt.QSize(640, 480)) - pos = settings.value("pos", qt.QPoint()) - isFullScreen = settings.value("full-screen", False) - try: - if not isinstance(isFullScreen, bool): - isFullScreen = utils.stringToBool(isFullScreen) - except ValueError: - isFullScreen = False - settings.endGroup() - - settings.beginGroup("mainlayout") - try: - data = settings.value("spliter") - data = [int(d) for d in data] - self.__splitter.setSizes(data) - except Exception: - _logger.debug("Backtrace", exc_info=True) - try: - data = settings.value("spliter2") - data = [int(d) for d in data] - self.__splitter2.setSizes(data) - except Exception: - _logger.debug("Backtrace", exc_info=True) - isVisible = settings.value("custom-nxdata-window-visible", False) - try: - if not isinstance(isVisible, bool): - isVisible = utils.stringToBool(isVisible) - except ValueError: - isVisible = False - self.__customNxdataWindow.setVisible(isVisible) - self._displayCustomNxdataWindow.setChecked(isVisible) - - settings.endGroup() - - settings.beginGroup("content") - isSorted = settings.value("is-sorted", True) - try: - if not isinstance(isSorted, bool): - isSorted = utils.stringToBool(isSorted) - except ValueError: - isSorted = True - self.setContentSorted(isSorted) - settings.endGroup() - - if not pos.isNull(): - self.move(pos) - if not size.isNull(): - self.resize(size) - if isFullScreen: - self.showFullScreen() - - def createActions(self): - action = qt.QAction("E&xit", self) - action.setShortcuts(qt.QKeySequence.Quit) - action.setStatusTip("Exit the application") - action.triggered.connect(self.close) - self._exitAction = action - - action = qt.QAction("&Open...", self) - action.setStatusTip("Open a file") - action.triggered.connect(self.open) - self._openAction = action - - action = qt.QAction("Open Recent", self) - action.setStatusTip("Open a recently openned file") - action.triggered.connect(self.open) - self._openRecentAction = action - - action = qt.QAction("Close All", self) - action.setStatusTip("Close all opened files") - action.triggered.connect(self.closeAll) - self._closeAllAction = action - - action = qt.QAction("&About", self) - action.setStatusTip("Show the application's About box") - action.triggered.connect(self.about) - self._aboutAction = action - - action = qt.QAction("&Documentation", self) - action.setStatusTip("Show the Silx library's documentation") - action.triggered.connect(self.showDocumentation) - self._documentationAction = action - - # Plot backend - - action = qt.QAction("Plot rendering backend", self) - action.setStatusTip("Select plot rendering backend") - self._plotBackendSelection = action - - menu = qt.QMenu() - action.setMenu(menu) - group = qt.QActionGroup(self) - group.setExclusive(True) - - action = qt.QAction("matplotlib", self) - action.setStatusTip("Plot will be rendered using matplotlib") - action.setCheckable(True) - action.triggered.connect(self.__forceMatplotlibBackend) - group.addAction(action) - menu.addAction(action) - self._usePlotWithMatplotlib = action - - action = qt.QAction("OpenGL", self) - action.setStatusTip("Plot will be rendered using OpenGL") - action.setCheckable(True) - action.triggered.connect(self.__forceOpenglBackend) - group.addAction(action) - menu.addAction(action) - self._usePlotWithOpengl = action - - # Plot image orientation - - action = qt.QAction("Default plot image y-axis orientation", self) - action.setStatusTip("Select the default y-axis orientation used by plot displaying images") - self._plotImageOrientation = action - - menu = qt.QMenu() - action.setMenu(menu) - group = qt.QActionGroup(self) - group.setExclusive(True) - - action = qt.QAction("Downward, origin on top", self) - action.setIcon(self._iconDownward) - action.setStatusTip("Plot images will use a downward Y-axis orientation") - action.setCheckable(True) - action.triggered.connect(self.__forcePlotImageDownward) - group.addAction(action) - menu.addAction(action) - self._useYAxisOrientationDownward = action - - action = qt.QAction("Upward, origin on bottom", self) - action.setIcon(self._iconUpward) - action.setStatusTip("Plot images will use a upward Y-axis orientation") - action.setCheckable(True) - action.triggered.connect(self.__forcePlotImageUpward) - group.addAction(action) - menu.addAction(action) - self._useYAxisOrientationUpward = action - - # Windows - - action = qt.QAction("Show custom NXdata selector", self) - action.setStatusTip("Show a widget which allow to create plot by selecting data and axes") - action.setCheckable(True) - action.setShortcut(qt.QKeySequence(qt.Qt.Key_F6)) - action.toggled.connect(self.__toggleCustomNxdataWindow) - self._displayCustomNxdataWindow = action - - def __toggleCustomNxdataWindow(self): - isVisible = self._displayCustomNxdataWindow.isChecked() - self.__customNxdataWindow.setVisible(isVisible) - - def __updateFileMenu(self): - files = self.__context.getRecentFiles() - self._openRecentAction.setEnabled(len(files) != 0) - menu = None - if len(files) != 0: - menu = qt.QMenu() - for filePath in files: - baseName = os.path.basename(filePath) - action = qt.QAction(baseName, self) - action.setToolTip(filePath) - action.triggered.connect(functools.partial(self.__openRecentFile, filePath)) - menu.addAction(action) - menu.addSeparator() - baseName = os.path.basename(filePath) - action = qt.QAction("Clear history", self) - action.setToolTip("Clear the history of the recent files") - action.triggered.connect(self.__clearRecentFile) - menu.addAction(action) - self._openRecentAction.setMenu(menu) - - def __clearRecentFile(self): - self.__context.clearRencentFiles() - - def __openRecentFile(self, filePath): - self.appendFile(filePath) - - def __updateOptionMenu(self): - """Update the state of the checked options as it is based on global - environment values.""" - - # plot backend - - action = self._plotBackendSelection - title = action.text().split(": ", 1)[0] - action.setText("%s: %s" % (title, silx.config.DEFAULT_PLOT_BACKEND)) - - action = self._usePlotWithMatplotlib - action.setChecked(silx.config.DEFAULT_PLOT_BACKEND in ["matplotlib", "mpl"]) - title = action.text().split(" (", 1)[0] - if not action.isChecked(): - title += " (applied after application restart)" - action.setText(title) - - action = self._usePlotWithOpengl - action.setChecked(silx.config.DEFAULT_PLOT_BACKEND in ["opengl", "gl"]) - title = action.text().split(" (", 1)[0] - if not action.isChecked(): - title += " (applied after application restart)" - action.setText(title) - - # plot orientation - - action = self._plotImageOrientation - if silx.config.DEFAULT_PLOT_IMAGE_Y_AXIS_ORIENTATION == "downward": - action.setIcon(self._iconDownward) - else: - action.setIcon(self._iconUpward) - action.setIconVisibleInMenu(True) - - action = self._useYAxisOrientationDownward - action.setChecked(silx.config.DEFAULT_PLOT_IMAGE_Y_AXIS_ORIENTATION == "downward") - title = action.text().split(" (", 1)[0] - if not action.isChecked(): - title += " (applied after application restart)" - action.setText(title) - - action = self._useYAxisOrientationUpward - action.setChecked(silx.config.DEFAULT_PLOT_IMAGE_Y_AXIS_ORIENTATION != "downward") - title = action.text().split(" (", 1)[0] - if not action.isChecked(): - title += " (applied after application restart)" - action.setText(title) - - def createMenus(self): - fileMenu = self.menuBar().addMenu("&File") - fileMenu.addAction(self._openAction) - fileMenu.addAction(self._openRecentAction) - fileMenu.addAction(self._closeAllAction) - fileMenu.addSeparator() - fileMenu.addAction(self._exitAction) - fileMenu.aboutToShow.connect(self.__updateFileMenu) - - optionMenu = self.menuBar().addMenu("&Options") - optionMenu.addAction(self._plotImageOrientation) - optionMenu.addAction(self._plotBackendSelection) - optionMenu.aboutToShow.connect(self.__updateOptionMenu) - - viewMenu = self.menuBar().addMenu("&Views") - viewMenu.addAction(self._displayCustomNxdataWindow) - - helpMenu = self.menuBar().addMenu("&Help") - helpMenu.addAction(self._aboutAction) - helpMenu.addAction(self._documentationAction) - - def open(self): - dialog = self.createFileDialog() - if self.__dialogState is None: - currentDirectory = os.getcwd() - dialog.setDirectory(currentDirectory) - else: - dialog.restoreState(self.__dialogState) - - result = dialog.exec_() - if not result: - return - - self.__dialogState = dialog.saveState() - - filenames = dialog.selectedFiles() - for filename in filenames: - self.appendFile(filename) - - def closeAll(self): - """Close all currently opened files""" - model = self.__treeview.findHdf5TreeModel() - model.clear() - - def createFileDialog(self): - dialog = qt.QFileDialog(self) - dialog.setWindowTitle("Open") - dialog.setModal(True) - - # NOTE: hdf5plugin have to be loaded before - extensions = collections.OrderedDict() - for description, ext in silx.io.supported_extensions().items(): - extensions[description] = " ".join(sorted(list(ext))) - - # Add extensions supported by fabio - extensions["NeXus layout from EDF files"] = "*.edf" - extensions["NeXus layout from TIFF image files"] = "*.tif *.tiff" - extensions["NeXus layout from CBF files"] = "*.cbf" - extensions["NeXus layout from MarCCD image files"] = "*.mccd" - - all_supported_extensions = set() - for name, exts in extensions.items(): - exts = exts.split(" ") - all_supported_extensions.update(exts) - all_supported_extensions = sorted(list(all_supported_extensions)) - - filters = [] - filters.append("All supported files (%s)" % " ".join(all_supported_extensions)) - for name, extension in extensions.items(): - filters.append("%s (%s)" % (name, extension)) - filters.append("All files (*)") - - dialog.setNameFilters(filters) - dialog.setFileMode(qt.QFileDialog.ExistingFiles) - return dialog - - def about(self): - from .About import About - About.about(self, "Silx viewer") - - def showDocumentation(self): - subpath = "index.html" - url = projecturl.getDocumentationUrl(subpath) - qt.QDesktopServices.openUrl(qt.QUrl(url)) - - def setContentSorted(self, sort): - """Set whether file content should be sorted or not. - - :param bool sort: - """ - sort = bool(sort) - if sort != self.isContentSorted(): - - # save expanded nodes - pathss = [] - root = qt.QModelIndex() - model = self.__treeview.model() - for i in range(model.rowCount(root)): - index = model.index(i, 0, root) - paths = self.__getPathFromExpandedNodes(self.__treeview, index) - pathss.append(paths) - - self.__treeview.setModel( - self.__treeModelSorted if sort else self.__treeModelSorted.sourceModel()) - self._sortContentAction.setChecked(self.isContentSorted()) - - # restore expanded nodes - model = self.__treeview.model() - for i in range(model.rowCount(root)): - index = model.index(i, 0, root) - paths = pathss.pop(0) - self.__expandNodesFromPaths(self.__treeview, index, paths) - - def isContentSorted(self): - """Returns whether the file content is sorted or not. - - :rtype: bool - """ - return self.__treeview.model() is self.__treeModelSorted - - def __forcePlotImageDownward(self): - silx.config.DEFAULT_PLOT_IMAGE_Y_AXIS_ORIENTATION = "downward" - - def __forcePlotImageUpward(self): - silx.config.DEFAULT_PLOT_IMAGE_Y_AXIS_ORIENTATION = "upward" - - def __forceMatplotlibBackend(self): - silx.config.DEFAULT_PLOT_BACKEND = "matplotlib" - - def __forceOpenglBackend(self): - silx.config.DEFAULT_PLOT_BACKEND = "opengl" - - def appendFile(self, filename): - if self.__displayIt is None: - # Store the file to display it (loading could be async) - self.__displayIt = filename - self.__treeview.findHdf5TreeModel().appendFile(filename) - - def displaySelectedData(self): - """Called to update the dataviewer with the selected data. - """ - selected = list(self.__treeview.selectedH5Nodes(ignoreBrokenLinks=False)) - if len(selected) == 1: - # Update the viewer for a single selection - data = selected[0] - self.__dataPanel.setData(data) - else: - _logger.debug("Too many data selected") - - def displayData(self, data): - """Called to update the dataviewer with a secific data. - """ - self.__dataPanel.setData(data) - - def displaySelectedCustomData(self): - selected = list(self.__customNxdata.selectedItems()) - if len(selected) == 1: - # Update the viewer for a single selection - item = selected[0] - self.__dataPanel.setCustomDataItem(item) - else: - _logger.debug("Too many items selected") - - def __customNxdataRemoved(self, item): - if self.__dataPanel.getCustomNxdataItem() is item: - self.__dataPanel.setCustomDataItem(None) - - def __customNxdataUpdated(self, item): - if self.__dataPanel.getCustomNxdataItem() is item: - self.__dataPanel.setCustomDataItem(item) - - def __makeSureCustomNxDataWindowIsVisible(self): - if not self.__customNxdataWindow.isVisible(): - self.__customNxdataWindow.setVisible(True) - self._displayCustomNxdataWindow.setChecked(True) - - def useAsNewCustomSignal(self, h5dataset): - self.__makeSureCustomNxDataWindowIsVisible() - model = self.__customNxdata.model() - model.createFromSignal(h5dataset) - - def useAsNewCustomNxdata(self, h5nxdata): - self.__makeSureCustomNxDataWindowIsVisible() - model = self.__customNxdata.model() - model.createFromNxdata(h5nxdata) - - def customContextMenu(self, event): - """Called to populate the context menu - - :param silx.gui.hdf5.Hdf5ContextMenuEvent event: Event - containing expected information to populate the context menu - """ - selectedObjects = event.source().selectedH5Nodes(ignoreBrokenLinks=False) - menu = event.menu() - - if not menu.isEmpty(): - menu.addSeparator() - - for obj in selectedObjects: - h5 = obj.h5py_object - - name = obj.name - if name.startswith("/"): - name = name[1:] - if name == "": - name = "the root" - - action = qt.QAction("Show %s" % name, event.source()) - action.triggered.connect(lambda: self.displayData(h5)) - menu.addAction(action) - - if silx.io.is_dataset(h5): - action = qt.QAction("Use as a new custom signal", event.source()) - action.triggered.connect(lambda: self.useAsNewCustomSignal(h5)) - menu.addAction(action) - - if silx.io.is_group(h5) and silx.io.nxdata.is_valid_nxdata(h5): - action = qt.QAction("Use as a new custom NXdata", event.source()) - action.triggered.connect(lambda: self.useAsNewCustomNxdata(h5)) - menu.addAction(action) - - if silx.io.is_file(h5): - action = qt.QAction("Close %s" % obj.local_filename, event.source()) - action.triggered.connect(lambda: self.__treeview.findHdf5TreeModel().removeH5pyObject(h5)) - menu.addAction(action) - action = qt.QAction("Synchronize %s" % obj.local_filename, event.source()) - action.triggered.connect(lambda: self.__synchronizeH5pyObject(h5)) - menu.addAction(action) diff --git a/silx/app/view/__init__.py b/silx/app/view/__init__.py deleted file mode 100644 index 229c44e..0000000 --- a/silx/app/view/__init__.py +++ /dev/null @@ -1,28 +0,0 @@ -# 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. -# -# ############################################################################*/ -"""Package containing source code of the `silx view` application""" - -__authors__ = ["V. Valls"] -__license__ = "MIT" -__date__ = "07/06/2018" diff --git a/silx/app/view/main.py b/silx/app/view/main.py deleted file mode 100644 index a1369c1..0000000 --- a/silx/app/view/main.py +++ /dev/null @@ -1,171 +0,0 @@ -# coding: utf-8 -# /*########################################################################## -# Copyright (C) 2016-2020 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 launcher of the `silx view` application""" - -__authors__ = ["V. Valls"] -__license__ = "MIT" -__date__ = "17/01/2019" - -import argparse -import logging -import os -import signal -import sys - - -_logger = logging.getLogger(__name__) -"""Module logger""" - - -def createParser(): - parser = argparse.ArgumentParser(description=__doc__) - parser.add_argument( - 'files', - nargs=argparse.ZERO_OR_MORE, - help='Data file to show (h5 file, edf files, spec files)') - parser.add_argument( - '--debug', - dest="debug", - action="store_true", - default=False, - help='Set logging system in debug mode') - parser.add_argument( - '--use-opengl-plot', - dest="use_opengl_plot", - action="store_true", - default=False, - help='Use OpenGL for plots (instead of matplotlib)') - parser.add_argument( - '-f', '--fresh', - dest="fresh_preferences", - action="store_true", - default=False, - help='Start the application using new fresh user preferences') - parser.add_argument( - '--hdf5-file-locking', - dest="hdf5_file_locking", - action="store_true", - default=False, - help='Start the application with HDF5 file locking enabled (it is disabled by default)') - return parser - - -def createWindow(parent, settings): - from .Viewer import Viewer - window = Viewer(parent=None, settings=settings) - return window - - -def mainQt(options): - """Part of the main depending on Qt""" - if options.debug: - logging.root.setLevel(logging.DEBUG) - - # - # Import most of the things here to be sure to use the right logging level - # - - # This needs to be done prior to load HDF5 - hdf5_file_locking = 'TRUE' if options.hdf5_file_locking else 'FALSE' - _logger.info('Set HDF5_USE_FILE_LOCKING=%s', hdf5_file_locking) - os.environ['HDF5_USE_FILE_LOCKING'] = hdf5_file_locking - - try: - # it should be loaded before h5py - import hdf5plugin # noqa - except ImportError: - _logger.debug("Backtrace", exc_info=True) - - import h5py - - import silx - import silx.utils.files - from silx.gui import qt - # Make sure matplotlib is configured - # Needed for Debian 8: compatibility between Qt4/Qt5 and old matplotlib - import silx.gui.utils.matplotlib # noqa - - app = qt.QApplication([]) - qt.QLocale.setDefault(qt.QLocale.c()) - - def sigintHandler(*args): - """Handler for the SIGINT signal.""" - qt.QApplication.quit() - - signal.signal(signal.SIGINT, sigintHandler) - sys.excepthook = qt.exceptionHandler - - timer = qt.QTimer() - timer.start(500) - # Application have to wake up Python interpreter, else SIGINT is not - # catched - timer.timeout.connect(lambda: None) - - settings = qt.QSettings(qt.QSettings.IniFormat, - qt.QSettings.UserScope, - "silx", - "silx-view", - None) - if options.fresh_preferences: - settings.clear() - - window = createWindow(parent=None, settings=settings) - window.setAttribute(qt.Qt.WA_DeleteOnClose, True) - - if options.use_opengl_plot: - # It have to be done after the settings (after the Viewer creation) - silx.config.DEFAULT_PLOT_BACKEND = "opengl" - - # NOTE: under Windows, cmd does not convert `*.tif` into existing files - options.files = silx.utils.files.expand_filenames(options.files) - - for filename in options.files: - # TODO: Would be nice to add a process widget and a cancel button - try: - window.appendFile(filename) - except IOError as e: - _logger.error(e.args[0]) - _logger.debug("Backtrace", exc_info=True) - - window.show() - result = app.exec_() - # remove ending warnings relative to QTimer - app.deleteLater() - return result - - -def main(argv): - """ - Main function to launch the viewer as an application - - :param argv: Command line arguments - :returns: exit status - """ - parser = createParser() - options = parser.parse_args(argv[1:]) - mainQt(options) - - -if __name__ == '__main__': - main(sys.argv) diff --git a/silx/app/view/setup.py b/silx/app/view/setup.py deleted file mode 100644 index fa076cb..0000000 --- a/silx/app/view/setup.py +++ /dev/null @@ -1,40 +0,0 @@ -# coding: utf-8 -# /*########################################################################## -# Copyright (C) 2016 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__ = "06/06/2018" - -from numpy.distutils.misc_util import Configuration - - -def configuration(parent_package='', top_path=None): - config = Configuration('view', parent_package, top_path) - config.add_subpackage('test') - return config - - -if __name__ == "__main__": - from numpy.distutils.core import setup - setup(configuration=configuration) diff --git a/silx/app/view/test/__init__.py b/silx/app/view/test/__init__.py deleted file mode 100644 index 8e64948..0000000 --- a/silx/app/view/test/__init__.py +++ /dev/null @@ -1,41 +0,0 @@ -# coding: utf-8 -# /*########################################################################## -# -# Copyright (c) 2016-2017 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__ = "07/06/2018" - -import unittest - -from silx.test.utils import test_options - - -def suite(): - test_suite = unittest.TestSuite() - if test_options.WITH_QT_TEST: - from . import test_launcher - from . import test_view - test_suite.addTest(test_view.suite()) - test_suite.addTest(test_launcher.suite()) - return test_suite diff --git a/silx/app/view/test/test_launcher.py b/silx/app/view/test/test_launcher.py deleted file mode 100644 index 5f03de9..0000000 --- a/silx/app/view/test/test_launcher.py +++ /dev/null @@ -1,151 +0,0 @@ -# coding: utf-8 -# /*########################################################################## -# -# Copyright (c) 2016-2017 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 testing silx.app.view""" - -__authors__ = ["V. Valls"] -__license__ = "MIT" -__date__ = "07/06/2018" - - -import os -import shutil -import sys -import tempfile -import unittest -import logging -import subprocess - -from silx.test.utils import test_options -from .. import main -from silx import __main__ as silx_main - -_logger = logging.getLogger(__name__) - - -@unittest.skipUnless(test_options.WITH_QT_TEST, test_options.WITH_QT_TEST_REASON) -class TestLauncher(unittest.TestCase): - """Test command line parsing""" - - def testHelp(self): - # option -h must cause a raise SystemExit or a return 0 - try: - parser = main.createParser() - parser.parse_args(["view", "--help"]) - result = 0 - except SystemExit as e: - result = e.args[0] - self.assertEqual(result, 0) - - def testWrongOption(self): - try: - parser = main.createParser() - parser.parse_args(["view", "--foo"]) - self.fail() - except SystemExit as e: - result = e.args[0] - self.assertNotEqual(result, 0) - - def testWrongFile(self): - try: - parser = main.createParser() - result = parser.parse_args(["view", "__file.not.found__"]) - result = 0 - except SystemExit as e: - result = e.args[0] - self.assertEqual(result, 0) - - def executeAsScript(self, filename, *args): - """Execute a command line. - - Log output as debug in case of bad return code. - """ - env = self.createTestEnv() - - with tempfile.TemporaryDirectory() as tmpdir: - # Copy file to temporary dir to avoid import from current dir. - script = os.path.join(tmpdir, 'launcher.py') - shutil.copyfile(filename, script) - command_line = [sys.executable, script] + list(args) - - _logger.info("Execute: %s", " ".join(command_line)) - p = subprocess.Popen(command_line, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - env=env) - out, err = p.communicate() - _logger.info("Return code: %d", p.returncode) - try: - out = out.decode('utf-8') - except UnicodeError: - pass - try: - err = err.decode('utf-8') - except UnicodeError: - pass - - if p.returncode != 0: - _logger.info("stdout:") - _logger.info("%s", out) - _logger.info("stderr:") - _logger.info("%s", err) - else: - _logger.debug("stdout:") - _logger.debug("%s", out) - _logger.debug("stderr:") - _logger.debug("%s", err) - self.assertEqual(p.returncode, 0) - - def createTestEnv(self): - """ - Returns an associated environment with a working project. - """ - env = dict((str(k), str(v)) for k, v in os.environ.items()) - env["PYTHONPATH"] = os.pathsep.join(sys.path) - return env - - def testExecuteViewHelp(self): - """Test if the main module is well connected. - - Uses subprocess to avoid to parasite the current environment. - """ - self.executeAsScript(main.__file__, "--help") - - def testExecuteSilxViewHelp(self): - """Test if the main module is well connected. - - Uses subprocess to avoid to parasite the current environment. - """ - self.executeAsScript(silx_main.__file__, "view", "--help") - - -def suite(): - test_suite = unittest.TestSuite() - loader = unittest.defaultTestLoader.loadTestsFromTestCase - test_suite.addTest(loader(TestLauncher)) - return test_suite - - -if __name__ == '__main__': - unittest.main(defaultTest='suite') diff --git a/silx/app/view/test/test_view.py b/silx/app/view/test/test_view.py deleted file mode 100644 index 7ea5a2c..0000000 --- a/silx/app/view/test/test_view.py +++ /dev/null @@ -1,394 +0,0 @@ -# coding: utf-8 -# /*########################################################################## -# -# Copyright (c) 2016-2020 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 testing silx.app.view""" - -__authors__ = ["V. Valls"] -__license__ = "MIT" -__date__ = "07/06/2018" - - -import unittest -import weakref -import numpy -import tempfile -import shutil -import os.path -import h5py - -from silx.gui import qt -from silx.app.view.Viewer import Viewer -from silx.app.view.About import About -from silx.app.view.DataPanel import DataPanel -from silx.app.view.CustomNxdataWidget import CustomNxdataWidget -from silx.gui.hdf5._utils import Hdf5DatasetMimeData -from silx.gui.utils.testutils import TestCaseQt -from silx.io import commonh5 - -_tmpDirectory = None - - -def setUpModule(): - global _tmpDirectory - _tmpDirectory = tempfile.mkdtemp(prefix=__name__) - - # create h5 data - filename = _tmpDirectory + "/data.h5" - f = h5py.File(filename, "w") - g = f.create_group("arrays") - g.create_dataset("scalar", data=10) - g.create_dataset("integers", data=numpy.array([10, 20, 30])) - f.close() - - # create h5 data - filename = _tmpDirectory + "/data2.h5" - f = h5py.File(filename, "w") - g = f.create_group("arrays") - g.create_dataset("scalar", data=20) - g.create_dataset("integers", data=numpy.array([10, 20, 30])) - f.close() - - -def tearDownModule(): - global _tmpDirectory - shutil.rmtree(_tmpDirectory) - _tmpDirectory = None - - -class TestViewer(TestCaseQt): - """Test for Viewer class""" - - def testConstruct(self): - widget = Viewer() - self.qWaitForWindowExposed(widget) - - def testDestroy(self): - widget = Viewer() - ref = weakref.ref(widget) - widget = None - self.qWaitForDestroy(ref) - - -class TestAbout(TestCaseQt): - """Test for About box class""" - - def testConstruct(self): - widget = About() - self.qWaitForWindowExposed(widget) - - def testLicense(self): - widget = About() - widget.getHtmlLicense() - self.qWaitForWindowExposed(widget) - - def testDestroy(self): - widget = About() - ref = weakref.ref(widget) - widget = None - self.qWaitForDestroy(ref) - - -class TestDataPanel(TestCaseQt): - - def testConstruct(self): - widget = DataPanel() - self.qWaitForWindowExposed(widget) - - def testDestroy(self): - widget = DataPanel() - ref = weakref.ref(widget) - widget = None - self.qWaitForDestroy(ref) - - def testHeaderLabelPaintEvent(self): - widget = DataPanel() - data = numpy.array([1, 2, 3, 4, 5]) - widget.setData(data) - # Expected to execute HeaderLabel.paintEvent - widget.setVisible(True) - self.qWaitForWindowExposed(widget) - - def testData(self): - widget = DataPanel() - data = numpy.array([1, 2, 3, 4, 5]) - widget.setData(data) - self.assertIs(widget.getData(), data) - self.assertIs(widget.getCustomNxdataItem(), None) - - def testDataNone(self): - widget = DataPanel() - widget.setData(None) - self.assertIs(widget.getData(), None) - self.assertIs(widget.getCustomNxdataItem(), None) - - def testCustomDataItem(self): - class CustomDataItemMock(object): - def getVirtualGroup(self): - return None - - def text(self): - return "" - - data = CustomDataItemMock() - widget = DataPanel() - widget.setCustomDataItem(data) - self.assertIs(widget.getData(), None) - self.assertIs(widget.getCustomNxdataItem(), data) - - def testCustomDataItemNone(self): - data = None - widget = DataPanel() - widget.setCustomDataItem(data) - self.assertIs(widget.getData(), None) - self.assertIs(widget.getCustomNxdataItem(), data) - - def testRemoveDatasetsFrom(self): - f = h5py.File(os.path.join(_tmpDirectory, "data.h5"), mode='r') - try: - widget = DataPanel() - widget.setData(f["arrays/scalar"]) - widget.removeDatasetsFrom(f) - self.assertIs(widget.getData(), None) - finally: - widget.setData(None) - f.close() - - def testReplaceDatasetsFrom(self): - f = h5py.File(os.path.join(_tmpDirectory, "data.h5"), mode='r') - f2 = h5py.File(os.path.join(_tmpDirectory, "data2.h5"), mode='r') - try: - widget = DataPanel() - widget.setData(f["arrays/scalar"]) - self.assertEqual(widget.getData()[()], 10) - widget.replaceDatasetsFrom(f, f2) - self.assertEqual(widget.getData()[()], 20) - finally: - widget.setData(None) - f.close() - f2.close() - - -class TestCustomNxdataWidget(TestCaseQt): - - def testConstruct(self): - widget = CustomNxdataWidget() - self.qWaitForWindowExposed(widget) - - def testDestroy(self): - widget = CustomNxdataWidget() - ref = weakref.ref(widget) - widget = None - self.qWaitForDestroy(ref) - - def testCreateNxdata(self): - widget = CustomNxdataWidget() - model = widget.model() - model.createNewNxdata() - model.createNewNxdata("Foo") - widget.setVisible(True) - self.qWaitForWindowExposed(widget) - - def testCreateNxdataFromDataset(self): - widget = CustomNxdataWidget() - model = widget.model() - signal = commonh5.Dataset("foo", data=numpy.array([[[5]]])) - model.createFromSignal(signal) - widget.setVisible(True) - self.qWaitForWindowExposed(widget) - - def testCreateNxdataFromNxdata(self): - widget = CustomNxdataWidget() - model = widget.model() - data = numpy.array([[[5]]]) - nxdata = commonh5.Group("foo") - nxdata.attrs["NX_class"] = "NXdata" - nxdata.attrs["signal"] = "signal" - nxdata.create_dataset("signal", data=data) - model.createFromNxdata(nxdata) - widget.setVisible(True) - self.qWaitForWindowExposed(widget) - - def testCreateBadNxdata(self): - widget = CustomNxdataWidget() - model = widget.model() - signal = commonh5.Dataset("foo", data=numpy.array([[[5]]])) - model.createFromSignal(signal) - axis = commonh5.Dataset("foo", data=numpy.array([[[5]]])) - nxdataIndex = model.index(0, 0) - item = model.itemFromIndex(nxdataIndex) - item.setAxesDatasets([axis]) - nxdata = item.getVirtualGroup() - self.assertIsNotNone(nxdata) - self.assertFalse(item.isValid()) - - def testRemoveDatasetsFrom(self): - f = h5py.File(os.path.join(_tmpDirectory, "data.h5"), mode='r') - try: - widget = CustomNxdataWidget() - model = widget.model() - dataset = f["arrays/integers"] - model.createFromSignal(dataset) - widget.removeDatasetsFrom(f) - finally: - model.clear() - f.close() - - def testReplaceDatasetsFrom(self): - f = h5py.File(os.path.join(_tmpDirectory, "data.h5"), mode='r') - f2 = h5py.File(os.path.join(_tmpDirectory, "data2.h5"), mode='r') - try: - widget = CustomNxdataWidget() - model = widget.model() - dataset = f["arrays/integers"] - model.createFromSignal(dataset) - widget.replaceDatasetsFrom(f, f2) - finally: - model.clear() - f.close() - f2.close() - - -class TestCustomNxdataWidgetInteraction(TestCaseQt): - """Test CustomNxdataWidget with user interaction""" - - def setUp(self): - TestCaseQt.setUp(self) - - self.widget = CustomNxdataWidget() - self.model = self.widget.model() - data = numpy.array([[[5]]]) - dataset = commonh5.Dataset("foo", data=data) - self.model.createFromSignal(dataset) - self.selectionModel = self.widget.selectionModel() - - def tearDown(self): - self.selectionModel = None - self.model.clear() - self.model = None - self.widget = None - TestCaseQt.tearDown(self) - - def testSelectedNxdata(self): - index = self.model.index(0, 0) - self.selectionModel.setCurrentIndex(index, qt.QItemSelectionModel.ClearAndSelect) - nxdata = self.widget.selectedNxdata() - self.assertEqual(len(nxdata), 1) - self.assertIsNot(nxdata[0], None) - - def testSelectedItems(self): - index = self.model.index(0, 0) - self.selectionModel.setCurrentIndex(index, qt.QItemSelectionModel.ClearAndSelect) - items = self.widget.selectedItems() - self.assertEqual(len(items), 1) - self.assertIsNot(items[0], None) - self.assertIsInstance(items[0], qt.QStandardItem) - - def testRowsAboutToBeRemoved(self): - self.model.removeRow(0) - self.qWaitForWindowExposed(self.widget) - - def testPaintItems(self): - self.widget.expandAll() - self.widget.setVisible(True) - self.qWaitForWindowExposed(self.widget) - - def testCreateDefaultContextMenu(self): - nxDataIndex = self.model.index(0, 0) - menu = self.widget.createDefaultContextMenu(nxDataIndex) - self.assertIsNot(menu, None) - self.assertIsInstance(menu, qt.QMenu) - - signalIndex = self.model.index(0, 0, nxDataIndex) - menu = self.widget.createDefaultContextMenu(signalIndex) - self.assertIsNot(menu, None) - self.assertIsInstance(menu, qt.QMenu) - - axesIndex = self.model.index(1, 0, nxDataIndex) - menu = self.widget.createDefaultContextMenu(axesIndex) - self.assertIsNot(menu, None) - self.assertIsInstance(menu, qt.QMenu) - - def testDropNewDataset(self): - dataset = commonh5.Dataset("foo", numpy.array([1, 2, 3, 4])) - mimedata = Hdf5DatasetMimeData(dataset=dataset) - self.model.dropMimeData(mimedata, qt.Qt.CopyAction, -1, -1, qt.QModelIndex()) - self.assertEqual(self.model.rowCount(qt.QModelIndex()), 2) - - def testDropNewNxdata(self): - data = numpy.array([[[5]]]) - nxdata = commonh5.Group("foo") - nxdata.attrs["NX_class"] = "NXdata" - nxdata.attrs["signal"] = "signal" - nxdata.create_dataset("signal", data=data) - mimedata = Hdf5DatasetMimeData(dataset=nxdata) - self.model.dropMimeData(mimedata, qt.Qt.CopyAction, -1, -1, qt.QModelIndex()) - self.assertEqual(self.model.rowCount(qt.QModelIndex()), 2) - - def testDropAxisDataset(self): - dataset = commonh5.Dataset("foo", numpy.array([1, 2, 3, 4])) - mimedata = Hdf5DatasetMimeData(dataset=dataset) - nxDataIndex = self.model.index(0, 0) - axesIndex = self.model.index(1, 0, nxDataIndex) - self.model.dropMimeData(mimedata, qt.Qt.CopyAction, -1, -1, axesIndex) - self.assertEqual(self.model.rowCount(qt.QModelIndex()), 1) - item = self.model.itemFromIndex(axesIndex) - self.assertIsNot(item.getDataset(), None) - - def testMimeData(self): - nxDataIndex = self.model.index(0, 0) - signalIndex = self.model.index(0, 0, nxDataIndex) - mimeData = self.model.mimeData([signalIndex]) - self.assertIsNot(mimeData, None) - self.assertIsInstance(mimeData, qt.QMimeData) - - def testRemoveNxdataItem(self): - nxdataIndex = self.model.index(0, 0) - item = self.model.itemFromIndex(nxdataIndex) - self.model.removeNxdataItem(item) - - def testAppendAxisToNxdataItem(self): - nxdataIndex = self.model.index(0, 0) - item = self.model.itemFromIndex(nxdataIndex) - self.model.appendAxisToNxdataItem(item) - - def testRemoveAxisItem(self): - nxdataIndex = self.model.index(0, 0) - axesIndex = self.model.index(1, 0, nxdataIndex) - item = self.model.itemFromIndex(axesIndex) - self.model.removeAxisItem(item) - - -def suite(): - test_suite = unittest.TestSuite() - loader = unittest.defaultTestLoader.loadTestsFromTestCase - test_suite.addTest(loader(TestViewer)) - test_suite.addTest(loader(TestAbout)) - test_suite.addTest(loader(TestDataPanel)) - test_suite.addTest(loader(TestCustomNxdataWidget)) - test_suite.addTest(loader(TestCustomNxdataWidgetInteraction)) - return test_suite - - -if __name__ == '__main__': - unittest.main(defaultTest='suite') diff --git a/silx/app/view/utils.py b/silx/app/view/utils.py deleted file mode 100644 index 80167c8..0000000 --- a/silx/app/view/utils.py +++ /dev/null @@ -1,45 +0,0 @@ -# coding: utf-8 -# /*########################################################################## -# Copyright (C) 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. -# -# ############################################################################*/ -"""Browse a data file with a GUI""" - -__authors__ = ["V. Valls"] -__license__ = "MIT" -__date__ = "28/05/2018" - - -_trueStrings = set(["yes", "true", "1"]) -_falseStrings = set(["no", "false", "0"]) - - -def stringToBool(string): - """Returns a boolean from a string. - - :raise ValueError: If the string do not contains a boolean information. - """ - lower = string.lower() - if lower in _trueStrings: - return True - if lower in _falseStrings: - return False - raise ValueError("'%s' is not a valid boolean" % string) |