diff options
Diffstat (limited to 'src/silx/app/view/Viewer.py')
-rw-r--r-- | src/silx/app/view/Viewer.py | 201 |
1 files changed, 136 insertions, 65 deletions
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}") |