summaryrefslogtreecommitdiff
path: root/src/silx/app/view
diff options
context:
space:
mode:
Diffstat (limited to 'src/silx/app/view')
-rw-r--r--src/silx/app/view/About.py33
-rw-r--r--src/silx/app/view/ApplicationContext.py40
-rw-r--r--src/silx/app/view/CustomNxdataWidget.py13
-rw-r--r--src/silx/app/view/DataPanel.py3
-rw-r--r--src/silx/app/view/Viewer.py201
-rw-r--r--src/silx/app/view/__init__.py1
-rw-r--r--src/silx/app/view/main.py78
-rw-r--r--src/silx/app/view/setup.py40
-rw-r--r--src/silx/app/view/test/__init__.py1
-rw-r--r--src/silx/app/view/test/test_launcher.py14
-rw-r--r--src/silx/app/view/test/test_view.py23
-rw-r--r--src/silx/app/view/utils.py45
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)