diff options
Diffstat (limited to 'src/silx/app/view')
-rw-r--r-- | src/silx/app/view/About.py | 33 | ||||
-rw-r--r-- | src/silx/app/view/ApplicationContext.py | 40 | ||||
-rw-r--r-- | src/silx/app/view/CustomNxdataWidget.py | 13 | ||||
-rw-r--r-- | src/silx/app/view/DataPanel.py | 3 | ||||
-rw-r--r-- | src/silx/app/view/Viewer.py | 201 | ||||
-rw-r--r-- | src/silx/app/view/__init__.py | 1 | ||||
-rw-r--r-- | src/silx/app/view/main.py | 78 | ||||
-rw-r--r-- | src/silx/app/view/setup.py | 40 | ||||
-rw-r--r-- | src/silx/app/view/test/__init__.py | 1 | ||||
-rw-r--r-- | src/silx/app/view/test/test_launcher.py | 14 | ||||
-rw-r--r-- | src/silx/app/view/test/test_view.py | 23 | ||||
-rw-r--r-- | src/silx/app/view/utils.py | 45 |
12 files changed, 262 insertions, 230 deletions
diff --git a/src/silx/app/view/About.py b/src/silx/app/view/About.py index 85f1450..350337d 100644 --- a/src/silx/app/view/About.py +++ b/src/silx/app/view/About.py @@ -1,6 +1,5 @@ -# coding: utf-8 # /*########################################################################## -# Copyright (C) 2016-2021 European Synchrotron Radiation Facility +# Copyright (C) 2016-2022 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 @@ -25,7 +24,7 @@ __authors__ = ["V. Valls"] __license__ = "MIT" -__date__ = "05/07/2018" +__date__ = "18/01/2022" import os import sys @@ -116,10 +115,9 @@ class About(qt.QDialog): :rtype: str """ from silx._version import __date__ as date + year = date.split("/")[2] - info = dict( - year=year - ) + info = dict(year=year) textLicense = _LICENSE_TEMPLATE.format(**info) return textLicense @@ -192,6 +190,7 @@ class About(qt.QDialog): # 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 = [ @@ -202,7 +201,11 @@ class About(qt.QDialog): isAvailable = h5py.h5z.filter_avail(filterId) optionals.append(self.__formatOptionalFilters(name, isAvailable)) else: - optionals.append(self.__formatOptionalLibraries("hdf5plugin", "hdf5plugin" in sys.modules)) + optionals.append( + self.__formatOptionalLibraries( + "hdf5plugin", "hdf5plugin" in sys.modules + ) + ) # Access to the logo in SVG or PNG logo = icons.getQFile("silx:" + os.path.join("gui", "logo", "silx")) @@ -218,7 +221,7 @@ class About(qt.QDialog): qt_version=qt.qVersion(), python_version=sys.version.replace("\n", "<br />"), optional_lib="<br />".join(optionals), - silx_image_path=logo.fileName() + silx_image_path=logo.fileName(), ) self.__label.setText(message.format(**info)) @@ -226,14 +229,18 @@ class About(qt.QDialog): def __updateSize(self): """Force the size to a QMessageBox like size.""" - if qt.BINDING in ("PySide2", "PyQt5"): - screenSize = qt.QApplication.desktop().availableGeometry(qt.QCursor.pos()).size() + if qt.BINDING == "PyQt5": + screenSize = ( + qt.QApplication.desktop().availableGeometry(qt.QCursor.pos()).size() + ) else: # Qt6 - screenSize = qt.QApplication.instance().primaryScreen().availableGeometry().size() + screenSize = ( + qt.QApplication.instance().primaryScreen().availableGeometry().size() + ) hardLimit = min(screenSize.width() - 480, 1000) if screenSize.width() <= 1024: hardLimit = screenSize.width() - softLimit = min(screenSize.width() / 2, 420) + softLimit = min(screenSize.width() // 2, 420) layoutMinimumSize = self.layout().totalMinimumSize() width = layoutMinimumSize.width() @@ -243,7 +250,7 @@ class About(qt.QDialog): width = hardLimit height = layoutMinimumSize.height() - self.setFixedSize(width, height) + self.setFixedSize(int(width), int(height)) @staticmethod def about(parent, applicationName): diff --git a/src/silx/app/view/ApplicationContext.py b/src/silx/app/view/ApplicationContext.py index 324f3b8..157b8cc 100644 --- a/src/silx/app/view/ApplicationContext.py +++ b/src/silx/app/view/ApplicationContext.py @@ -1,6 +1,5 @@ -# coding: utf-8 # /*########################################################################## -# Copyright (C) 2016-2018 European Synchrotron Radiation Facility +# Copyright (C) 2016-2023 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 @@ -29,6 +28,7 @@ __date__ = "23/05/2018" import weakref import logging +from collections.abc import Sequence import silx from silx.gui.data.DataViews import DataViewHooks @@ -70,15 +70,20 @@ class ApplicationContext(DataViewHooks): if settings is None: return settings.beginGroup("library") + mplTightLayout = settings.value("mpl.tight_layout", False, bool) plotBackend = settings.value("plot.backend", "") plotImageYAxisOrientation = settings.value("plot-image.y-axis-orientation", "") settings.endGroup() # Use matplotlib backend by default - silx.config.DEFAULT_PLOT_BACKEND = \ - "opengl" if plotBackend == "opengl" else "matplotlib" + silx.config.DEFAULT_PLOT_BACKEND = ( + ("opengl", "matplotlib") if plotBackend == "opengl" else "matplotlib" + ) if plotImageYAxisOrientation != "": - silx.config.DEFAULT_PLOT_IMAGE_Y_AXIS_ORIENTATION = plotImageYAxisOrientation + silx.config.DEFAULT_PLOT_IMAGE_Y_AXIS_ORIENTATION = ( + plotImageYAxisOrientation + ) + silx.config._MPL_TIGHT_LAYOUT = mplTightLayout def restoreSettings(self): """Restore the settings of all the application""" @@ -122,8 +127,12 @@ class ApplicationContext(DataViewHooks): 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.setValue("plot.backend", self.getDefaultPlotBackend()) + settings.setValue( + "plot-image.y-axis-orientation", + silx.config.DEFAULT_PLOT_IMAGE_Y_AXIS_ORIENTATION, + ) + settings.setValue("mpl.tight_layout", silx.config._MPL_TIGHT_LAYOUT) settings.endGroup() settings.beginGroup("recent-files") @@ -163,8 +172,7 @@ class ApplicationContext(DataViewHooks): self.__recentFiles.pop() def clearRencentFiles(self): - """Clear the history of the rencent files. - """ + """Clear the history of the rencent files.""" self.__recentFiles[:] = [] def getColormap(self, view): @@ -193,3 +201,17 @@ class ApplicationContext(DataViewHooks): dialog.setModal(False) self.__defaultColormapDialog = dialog return self.__defaultColormapDialog + + @staticmethod + def getDefaultPlotBackend() -> str: + """Returns default plot backend as a str from current config""" + backend = silx.config.DEFAULT_PLOT_BACKEND + if isinstance(backend, str): + return backend + if ( + isinstance(backend, Sequence) + and len(backend) + and isinstance(backend[0], str) + ): + return backend[0] + return "matplotlib" # fallback diff --git a/src/silx/app/view/CustomNxdataWidget.py b/src/silx/app/view/CustomNxdataWidget.py index 8c6cd39..3ec62c0 100644 --- a/src/silx/app/view/CustomNxdataWidget.py +++ b/src/silx/app/view/CustomNxdataWidget.py @@ -1,4 +1,3 @@ -# coding: utf-8 # /*########################################################################## # Copyright (C) 2016-2021 European Synchrotron Radiation Facility # @@ -569,7 +568,7 @@ class _Model(qt.QStandardItemModel): """ if isinstance(item, _NxDataItem): parent = item.parent() - assert(parent is None) + assert parent is None model = item.model() model.removeRow(item.row()) else: @@ -694,7 +693,7 @@ class CustomNxDataToolBar(qt.QToolBar): def setCustomNxDataWidget(self, widget): """Set the linked CustomNxdataWidget to this toolbar.""" - assert(isinstance(widget, CustomNxdataWidget)) + assert isinstance(widget, CustomNxdataWidget) if self.__nxdataWidget is not None: selectionModel = self.__nxdataWidget.selectionModel() selectionModel.currentChanged.disconnect(self.__currentSelectionChanged) @@ -714,7 +713,9 @@ class CustomNxDataToolBar(qt.QToolBar): 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)) + self.__addNxDataAxisAction.setEnabled( + isinstance(item, _NxDataItem) or isinstance(item, _DatasetItemRow) + ) class _HashDropZones(qt.QStyledItemDelegate): @@ -848,7 +849,9 @@ class CustomNxdataWidget(qt.QTreeView): if isinstance(item, _NxDataItem): action = qt.QAction("Add a new axis", menu) - action.triggered.connect(lambda: weakself.model().appendAxisToNxdataItem(item)) + action.triggered.connect( + lambda: weakself.model().appendAxisToNxdataItem(item) + ) action.setIcon(icons.getQIcon("nxdata-axis-add")) action.setIconVisibleInMenu(True) menu.addAction(action) diff --git a/src/silx/app/view/DataPanel.py b/src/silx/app/view/DataPanel.py index 5d87381..592a520 100644 --- a/src/silx/app/view/DataPanel.py +++ b/src/silx/app/view/DataPanel.py @@ -1,4 +1,3 @@ -# coding: utf-8 # /*########################################################################## # Copyright (C) 2018 European Synchrotron Radiation Facility # @@ -38,7 +37,6 @@ _logger = logging.getLogger(__name__) class _HeaderLabel(qt.QLabel): - def __init__(self, parent=None): qt.QLabel.__init__(self, parent=parent) self.setFrameShape(qt.QFrame.StyledPanel) @@ -90,7 +88,6 @@ class _HeaderLabel(qt.QLabel): class DataPanel(qt.QWidget): - def __init__(self, parent=None, context=None): qt.QWidget.__init__(self, parent=parent) diff --git a/src/silx/app/view/Viewer.py b/src/silx/app/view/Viewer.py index 7e5e4c9..12426a1 100644 --- a/src/silx/app/view/Viewer.py +++ b/src/silx/app/view/Viewer.py @@ -1,6 +1,5 @@ -# coding: utf-8 # /*########################################################################## -# Copyright (C) 2016-2021 European Synchrotron Radiation Facility +# Copyright (C) 2016-2023 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 @@ -23,15 +22,19 @@ # ############################################################################*/ """Browse a data file with a GUI""" +from __future__ import annotations + __authors__ = ["V. Valls"] __license__ = "MIT" __date__ = "15/01/2019" import os -import collections import logging import functools +import traceback +from types import TracebackType +from typing import Optional import silx.io.nxdata from silx.gui import qt @@ -40,7 +43,7 @@ import silx.gui.hdf5 from .ApplicationContext import ApplicationContext from .CustomNxdataWidget import CustomNxdataWidget from .CustomNxdataWidget import CustomNxDataToolBar -from . import utils +from ..utils import parseutils from silx.gui.utils import projecturl from .DataPanel import DataPanel @@ -65,6 +68,8 @@ class Viewer(qt.QMainWindow): silxIcon = icons.getQIcon("silx") self.setWindowIcon(silxIcon) + self.__error = "" + self.__context = self.createApplicationContext(settings) self.__context.restoreLibrarySettings() @@ -87,7 +92,9 @@ class Viewer(qt.QMainWindow): treeModel.sigH5pyObjectRemoved.connect(self.__h5FileRemoved) treeModel.sigH5pyObjectSynchronized.connect(self.__h5FileSynchonized) treeModel.setDatasetDragEnabled(True) - self.__treeModelSorted = silx.gui.hdf5.NexusSortFilterProxyModel(self.__treeview) + 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) @@ -142,8 +149,8 @@ class Viewer(qt.QMainWindow): columns.insert(1, treeModel.DESCRIPTION_COLUMN) self.__treeview.header().setSections(columns) - self._iconUpward = icons.getQIcon('plot-yup') - self._iconDownward = icons.getQIcon('plot-ydown') + self._iconUpward = icons.getQIcon("plot-yup") + self._iconDownward = icons.getQIcon("plot-ydown") self.createActions() self.createMenus() @@ -162,23 +169,22 @@ class Viewer(qt.QMainWindow): action.setText("Refresh") action.setToolTip("Refresh all selected items") action.triggered.connect(self.__refreshSelected) - action.setShortcut(qt.QKeySequence(qt.Qt.Key_F5)) + action.setShortcuts( + [ + qt.QKeySequence(qt.Qt.Key_F5), + qt.QKeySequence(qt.Qt.CTRL | qt.Qt.Key_R), + ] + ) 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)) + action.setShortcut(qt.QKeySequence.Delete) treeView.addAction(action) self.__closeAction = action @@ -189,7 +195,7 @@ class Viewer(qt.QMainWindow): 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)) + action.setShortcut(qt.QKeySequence(qt.Qt.CTRL | qt.Qt.Key_Plus)) toolbar.addAction(action) treeView.addAction(action) self.__expandAllAction = action @@ -199,7 +205,7 @@ class Viewer(qt.QMainWindow): 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)) + action.setShortcut(qt.QKeySequence(qt.Qt.CTRL | qt.Qt.Key_Minus)) toolbar.addAction(action) treeView.addAction(action) self.__collapseAllAction = action @@ -254,20 +260,18 @@ class Viewer(qt.QMainWindow): qt.QApplication.restoreOverrideCursor() def __refreshSelected(self): - """Refresh all selected items - """ + """Refresh all selected items""" qt.QApplication.setOverrideCursor(qt.Qt.WaitCursor) selection = self.__treeview.selectionModel() indexes = selection.selectedIndexes() selectedItems = [] model = self.__treeview.model() - h5files = set([]) + h5files = [] 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(): @@ -275,15 +279,21 @@ class Viewer(qt.QMainWindow): rootRow = rootIndex.row() relativePath = self.__getRelativePath(model, rootIndex, index) selectedItems.append((rootRow, relativePath)) - h5files.add(h5.file) + h5 = model.data( + rootIndex, role=silx.gui.hdf5.Hdf5TreeModel.H5PY_OBJECT_ROLE + ) + item = model.data( + rootIndex, role=silx.gui.hdf5.Hdf5TreeModel.H5PY_ITEM_ROLE + ) + h5files.append((h5, item._openedPath)) if len(h5files) == 0: qt.QApplication.restoreOverrideCursor() return model = self.__treeview.findHdf5TreeModel() - for h5 in h5files: - self.__synchronizeH5pyObject(h5) + for h5, filename in h5files: + self.__synchronizeH5pyObject(h5, filename) model = self.__treeview.model() itemSelection = qt.QItemSelection() @@ -298,14 +308,15 @@ class Viewer(qt.QMainWindow): qt.QApplication.restoreOverrideCursor() - def __synchronizeH5pyObject(self, h5): + def __synchronizeH5pyObject(self, h5, filename: Optional[str] = None): 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 + if filename is None: + filename = f"{h5.file.filename}::{h5.name}" row = model.h5pyObjectRow(h5) index = self.__treeview.model().index(row, 0, qt.QModelIndex()) paths = self.__getPathFromExpandedNodes(self.__treeview, index) @@ -348,7 +359,7 @@ class Viewer(qt.QMainWindow): path = node._getCanonicalName() if rootPath is None: rootPath = path - path = path[len(rootPath):] + path = path[len(rootPath) :] paths.append(path) for child in range(model.rowCount(index)): @@ -453,9 +464,9 @@ class Viewer(qt.QMainWindow): layout.addWidget(customNxdataWidget) return widget - def __h5FileLoaded(self, loadedH5): + def __h5FileLoaded(self, loadedH5, filename): self.__context.pushRecentFile(loadedH5.file.filename) - if loadedH5.file.filename == self.__displayIt: + if filename == self.__displayIt: self.__displayIt = None self.displayData(loadedH5) @@ -519,11 +530,7 @@ class Viewer(qt.QMainWindow): 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 + isFullScreen = parseutils.to_bool(isFullScreen, False) settings.endGroup() settings.beginGroup("mainlayout") @@ -540,23 +547,14 @@ class Viewer(qt.QMainWindow): 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 + isVisible = parseutils.to_bool(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 + isSorted = parseutils.to_bool(isSorted, True) self.setContentSorted(isSorted) settings.endGroup() @@ -569,12 +567,13 @@ class Viewer(qt.QMainWindow): def createActions(self): action = qt.QAction("E&xit", self) - action.setShortcuts(qt.QKeySequence.Quit) + action.setShortcut(qt.QKeySequence.Quit) action.setStatusTip("Exit the application") action.triggered.connect(self.close) self._exitAction = action action = qt.QAction("&Open...", self) + action.setShortcut(qt.QKeySequence.Open) action.setStatusTip("Open a file") action.triggered.connect(self.open) self._openAction = action @@ -584,6 +583,7 @@ class Viewer(qt.QMainWindow): self._openRecentMenu = menu action = qt.QAction("Close All", self) + action.setShortcut(qt.QKeySequence.Close) action.setStatusTip("Close all opened files") action.triggered.connect(self.closeAll) self._closeAllAction = action @@ -625,9 +625,11 @@ class Viewer(qt.QMainWindow): # Plot image orientation self._plotImageOrientationMenu = qt.QMenu( - "Default plot image y-axis orientation", self) + "Default plot image y-axis orientation", self + ) self._plotImageOrientationMenu.setStatusTip( - "Select the default y-axis orientation used by plot displaying images") + "Select the default y-axis orientation used by plot displaying images" + ) group = qt.QActionGroup(self) group.setExclusive(True) @@ -650,10 +652,19 @@ class Viewer(qt.QMainWindow): self._plotImageOrientationMenu.addAction(action) self._useYAxisOrientationUpward = action + # mpl layout + + action = qt.QAction("Use MPL tight layout", self) + action.setCheckable(True) + action.triggered.connect(self.__forceMplTightLayout) + self._useMplTightLayout = 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.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) @@ -672,7 +683,9 @@ class Viewer(qt.QMainWindow): baseName = os.path.basename(filePath) action = qt.QAction(baseName, self) action.setToolTip(filePath) - action.triggered.connect(functools.partial(self.__openRecentFile, filePath)) + action.triggered.connect( + functools.partial(self.__openRecentFile, filePath) + ) self._openRecentMenu.addAction(action) self._openRecentMenu.addSeparator() baseName = os.path.basename(filePath) @@ -694,17 +707,18 @@ class Viewer(qt.QMainWindow): # plot backend title = self._plotBackendMenu.title().split(": ", 1)[0] - self._plotBackendMenu.setTitle("%s: %s" % (title, silx.config.DEFAULT_PLOT_BACKEND)) + backend = self.__context.getDefaultPlotBackend() + self._plotBackendMenu.setTitle(f"{title}: {backend}") action = self._usePlotWithMatplotlib - action.setChecked(silx.config.DEFAULT_PLOT_BACKEND in ["matplotlib", "mpl"]) + action.setChecked(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"]) + action.setChecked(backend in ["opengl", "gl"]) title = action.text().split(" (", 1)[0] if not action.isChecked(): title += " (applied after application restart)" @@ -719,19 +733,28 @@ class Viewer(qt.QMainWindow): menu.setIcon(self._iconUpward) action = self._useYAxisOrientationDownward - action.setChecked(silx.config.DEFAULT_PLOT_IMAGE_Y_AXIS_ORIENTATION == "downward") + 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") + 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) + # mpl + + action = self._useMplTightLayout + action.setChecked(silx.config._MPL_TIGHT_LAYOUT) + def createMenus(self): fileMenu = self.menuBar().addMenu("&File") fileMenu.addAction(self._openAction) @@ -744,6 +767,7 @@ class Viewer(qt.QMainWindow): optionMenu = self.menuBar().addMenu("&Options") optionMenu.addMenu(self._plotImageOrientationMenu) optionMenu.addMenu(self._plotBackendMenu) + optionMenu.addAction(self._useMplTightLayout) optionMenu.aboutToShow.connect(self.__updateOptionMenu) viewMenu = self.menuBar().addMenu("&Views") @@ -753,6 +777,17 @@ class Viewer(qt.QMainWindow): helpMenu.addAction(self._aboutAction) helpMenu.addAction(self._documentationAction) + self.__errorButton = qt.QToolButton(self) + self.__errorButton.setIcon( + self.style().standardIcon(qt.QStyle.SP_MessageBoxWarning) + ) + self.__errorButton.setToolTip( + "An error occured!\nClick to display last error\nor check messages in the console" + ) + self.__errorButton.setVisible(False) + self.__errorButton.clicked.connect(self.__errorButtonClicked) + self.menuBar().setCornerWidget(self.__errorButton) + def open(self): dialog = self.createFileDialog() if self.__dialogState is None: @@ -782,7 +817,7 @@ class Viewer(qt.QMainWindow): dialog.setModal(True) # NOTE: hdf5plugin have to be loaded before - extensions = collections.OrderedDict() + extensions = {} for description, ext in silx.io.supported_extensions().items(): extensions[description] = " ".join(sorted(list(ext))) @@ -810,6 +845,7 @@ class Viewer(qt.QMainWindow): def about(self): from .About import About + About.about(self, "Silx viewer") def showDocumentation(self): @@ -824,7 +860,6 @@ class Viewer(qt.QMainWindow): """ sort = bool(sort) if sort != self.isContentSorted(): - # save expanded nodes pathss = [] root = qt.QModelIndex() @@ -835,7 +870,8 @@ class Viewer(qt.QMainWindow): pathss.append(paths) self.__treeview.setModel( - self.__treeModelSorted if sort else self.__treeModelSorted.sourceModel()) + self.__treeModelSorted if sort else self.__treeModelSorted.sourceModel() + ) self._sortContentAction.setChecked(self.isContentSorted()) # restore expanded nodes @@ -862,7 +898,10 @@ class Viewer(qt.QMainWindow): silx.config.DEFAULT_PLOT_BACKEND = "matplotlib" def __forceOpenglBackend(self): - silx.config.DEFAULT_PLOT_BACKEND = "opengl" + silx.config.DEFAULT_PLOT_BACKEND = "opengl", "matplotlib" + + def __forceMplTightLayout(self): + silx.config._MPL_TIGHT_LAYOUT = self._useMplTightLayout.isChecked() def appendFile(self, filename): if self.__displayIt is None: @@ -871,8 +910,7 @@ class Viewer(qt.QMainWindow): self.__treeview.findHdf5TreeModel().appendFile(filename) def displaySelectedData(self): - """Called to update the dataviewer with the selected data. - """ + """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 @@ -882,8 +920,7 @@ class Viewer(qt.QMainWindow): _logger.debug("Too many data selected") def displayData(self, data): - """Called to update the dataviewer with a secific data. - """ + """Called to update the dataviewer with a secific data.""" self.__dataPanel.setData(data) def displaySelectedCustomData(self): @@ -955,8 +992,42 @@ class Viewer(qt.QMainWindow): 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)) + action.triggered.connect( + lambda: self.__treeview.findHdf5TreeModel().removeH5pyObject(h5) + ) menu.addAction(action) - action = qt.QAction("Synchronize %s" % obj.local_filename, event.source()) + action = qt.QAction( + "Synchronize %s" % obj.local_filename, event.source() + ) action.triggered.connect(lambda: self.__synchronizeH5pyObject(h5)) menu.addAction(action) + + def __errorButtonClicked(self): + button = qt.QMessageBox.warning( + self, + "Error", + self.getError(), + qt.QMessageBox.Reset | qt.QMessageBox.Close, + qt.QMessageBox.Close, + ) + if button == qt.QMessageBox.Reset: + self.setError("") + + def getError(self) -> str: + """Returns error information string""" + return self.__error + + def setError(self, error: str): + """Set error information string""" + if error == self.__error: + return + + self.__error = error + self.__errorButton.setVisible(error != "") + + def setErrorFromException( + self, type_: type[BaseException], value: BaseException, trace: TracebackType + ): + """Set information about the last exception that occured""" + formattedTrace = "\n".join(traceback.format_tb(trace)) + self.setError(f"{type_.__name__}:\n{value}\n\n{formattedTrace}") diff --git a/src/silx/app/view/__init__.py b/src/silx/app/view/__init__.py index 229c44e..97c64ef 100644 --- a/src/silx/app/view/__init__.py +++ b/src/silx/app/view/__init__.py @@ -1,4 +1,3 @@ -# coding: utf-8 # /*########################################################################## # Copyright (C) 2016-2018 European Synchrotron Radiation Facility # diff --git a/src/silx/app/view/main.py b/src/silx/app/view/main.py index dbc6a2b..f6c5274 100644 --- a/src/silx/app/view/main.py +++ b/src/silx/app/view/main.py @@ -1,6 +1,5 @@ -# coding: utf-8 # /*########################################################################## -# Copyright (C) 2016-2021 European Synchrotron Radiation Facility +# Copyright (C) 2016-2023 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 @@ -32,6 +31,8 @@ import logging import os import signal import sys +import traceback +from silx.app.utils import parseutils _logger = logging.getLogger(__name__) @@ -41,38 +42,53 @@ _logger = logging.getLogger(__name__) def createParser(): parser = argparse.ArgumentParser(description=__doc__) parser.add_argument( - 'files', + "files", nargs=argparse.ZERO_OR_MORE, - help='Data file to show (h5 file, edf files, spec files)') + help="Data file to show (h5 file, edf files, spec files)", + ) parser.add_argument( - '--debug', + "--slices", + dest="slices", + default=tuple(), + type=int, + nargs="+", + help="List of slice indices to open (Only for dataset)", + ) + parser.add_argument( + "--debug", dest="debug", action="store_true", default=False, - help='Set logging system in debug mode') + help="Set logging system in debug mode", + ) parser.add_argument( - '--use-opengl-plot', + "--use-opengl-plot", dest="use_opengl_plot", action="store_true", default=False, - help='Use OpenGL for plots (instead of matplotlib)') + help="Use OpenGL for plots (instead of matplotlib)", + ) parser.add_argument( - '-f', '--fresh', + "-f", + "--fresh", dest="fresh_preferences", action="store_true", default=False, - help='Start the application using new fresh user preferences') + help="Start the application using new fresh user preferences", + ) parser.add_argument( - '--hdf5-file-locking', + "--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)') + 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 @@ -92,7 +108,7 @@ def mainQt(options): except ImportError: _logger.debug("No resource module available") else: - if hasattr(resource, 'RLIMIT_NOFILE'): + if hasattr(resource, "RLIMIT_NOFILE"): try: hard_nofile = resource.getrlimit(resource.RLIMIT_NOFILE)[1] resource.setrlimit(resource.RLIMIT_NOFILE, (hard_nofile, hard_nofile)) @@ -102,9 +118,9 @@ def mainQt(options): _logger.debug("Set max opened files to %d", hard_nofile) # 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 + 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 @@ -115,8 +131,8 @@ def mainQt(options): 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 @@ -129,7 +145,6 @@ def mainQt(options): qt.QApplication.quit() signal.signal(signal.SIGINT, sigintHandler) - sys.excepthook = qt.exceptionHandler timer = qt.QTimer() timer.start(500) @@ -137,28 +152,33 @@ def mainQt(options): # catched timer.timeout.connect(lambda: None) - settings = qt.QSettings(qt.QSettings.IniFormat, - qt.QSettings.UserScope, - "silx", - "silx-view", - 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) + def exceptHook(type_, value, trace): + _logger.error("An error occured in silx view:") + _logger.error("%s %s %s", type_, value, "".join(traceback.format_tb(trace))) + try: + window.setErrorFromException(type_, value, trace) + except Exception: + pass + + sys.excepthook = exceptHook + 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: + for url in parseutils.filenames_to_dataurls(options.files, options.slices): # TODO: Would be nice to add a process widget and a cancel button try: - window.appendFile(filename) + window.appendFile(url.path()) except IOError as e: _logger.error(e.args[0]) _logger.debug("Backtrace", exc_info=True) @@ -182,5 +202,5 @@ def main(argv): mainQt(options) -if __name__ == '__main__': +if __name__ == "__main__": main(sys.argv) diff --git a/src/silx/app/view/setup.py b/src/silx/app/view/setup.py deleted file mode 100644 index fa076cb..0000000 --- a/src/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/src/silx/app/view/test/__init__.py b/src/silx/app/view/test/__init__.py index 7790ee5..1d8207b 100644 --- a/src/silx/app/view/test/__init__.py +++ b/src/silx/app/view/test/__init__.py @@ -1,4 +1,3 @@ -# coding: utf-8 # /*########################################################################## # # Copyright (c) 2016-2017 European Synchrotron Radiation Facility diff --git a/src/silx/app/view/test/test_launcher.py b/src/silx/app/view/test/test_launcher.py index 4f7aaa5..49b1032 100644 --- a/src/silx/app/view/test/test_launcher.py +++ b/src/silx/app/view/test/test_launcher.py @@ -1,4 +1,3 @@ -# coding: utf-8 # /*########################################################################## # # Copyright (c) 2016-2017 European Synchrotron Radiation Facility @@ -85,23 +84,22 @@ class TestLauncher(unittest.TestCase): with tempfile.TemporaryDirectory() as tmpdir: # Copy file to temporary dir to avoid import from current dir. - script = os.path.join(tmpdir, 'launcher.py') + 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) + 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') + out = out.decode("utf-8") except UnicodeError: pass try: - err = err.decode('utf-8') + err = err.decode("utf-8") except UnicodeError: pass diff --git a/src/silx/app/view/test/test_view.py b/src/silx/app/view/test/test_view.py index e236e42..1eb588b 100644 --- a/src/silx/app/view/test/test_view.py +++ b/src/silx/app/view/test/test_view.py @@ -1,4 +1,3 @@ -# coding: utf-8 # /*########################################################################## # # Copyright (c) 2016-2020 European Synchrotron Radiation Facility @@ -116,7 +115,6 @@ class TestAbout(TestCaseQt): @pytest.mark.usefixtures("qapp") @pytest.mark.usefixtures("data_class_attr") class TestDataPanel(TestCaseQt): - def testConstruct(self): widget = DataPanel() self.qWaitForWindowExposed(widget) @@ -170,7 +168,7 @@ class TestDataPanel(TestCaseQt): self.assertIs(widget.getCustomNxdataItem(), data) def testRemoveDatasetsFrom(self): - f = h5py.File(self.data_h5, mode='r') + f = h5py.File(self.data_h5, mode="r") try: widget = DataPanel() widget.setData(f["arrays/scalar"]) @@ -181,8 +179,8 @@ class TestDataPanel(TestCaseQt): f.close() def testReplaceDatasetsFrom(self): - f = h5py.File(self.data_h5, mode='r') - f2 = h5py.File(self.data2_h5, mode='r') + f = h5py.File(self.data_h5, mode="r") + f2 = h5py.File(self.data2_h5, mode="r") try: widget = DataPanel() widget.setData(f["arrays/scalar"]) @@ -198,7 +196,6 @@ class TestDataPanel(TestCaseQt): @pytest.mark.usefixtures("qapp") @pytest.mark.usefixtures("data_class_attr") class TestCustomNxdataWidget(TestCaseQt): - def testConstruct(self): widget = CustomNxdataWidget() self.qWaitForWindowExposed(widget) @@ -251,7 +248,7 @@ class TestCustomNxdataWidget(TestCaseQt): self.assertFalse(item.isValid()) def testRemoveDatasetsFrom(self): - f = h5py.File(self.data_h5, mode='r') + f = h5py.File(self.data_h5, mode="r") try: widget = CustomNxdataWidget() model = widget.model() @@ -263,8 +260,8 @@ class TestCustomNxdataWidget(TestCaseQt): f.close() def testReplaceDatasetsFrom(self): - f = h5py.File(self.data_h5, mode='r') - f2 = h5py.File(self.data2_h5, mode='r') + f = h5py.File(self.data_h5, mode="r") + f2 = h5py.File(self.data2_h5, mode="r") try: widget = CustomNxdataWidget() model = widget.model() @@ -300,14 +297,18 @@ class TestCustomNxdataWidgetInteraction(TestCaseQt): def testSelectedNxdata(self): index = self.model.index(0, 0) - self.selectionModel.setCurrentIndex(index, qt.QItemSelectionModel.ClearAndSelect) + 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) + self.selectionModel.setCurrentIndex( + index, qt.QItemSelectionModel.ClearAndSelect + ) items = self.widget.selectedItems() self.assertEqual(len(items), 1) self.assertIsNot(items[0], None) diff --git a/src/silx/app/view/utils.py b/src/silx/app/view/utils.py deleted file mode 100644 index 80167c8..0000000 --- a/src/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) |