summaryrefslogtreecommitdiff
path: root/src/silx/app/view/Viewer.py
diff options
context:
space:
mode:
Diffstat (limited to 'src/silx/app/view/Viewer.py')
-rw-r--r--src/silx/app/view/Viewer.py201
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}")