summaryrefslogtreecommitdiff
path: root/silx/app
diff options
context:
space:
mode:
Diffstat (limited to 'silx/app')
-rw-r--r--silx/app/convert.py48
-rw-r--r--silx/app/test/test_convert.py16
-rw-r--r--silx/app/view/Viewer.py104
-rw-r--r--silx/app/view/main.py74
-rw-r--r--silx/app/view/test/test_view.py38
5 files changed, 143 insertions, 137 deletions
diff --git a/silx/app/convert.py b/silx/app/convert.py
index a8c2783..7e601ce 100644
--- a/silx/app/convert.py
+++ b/silx/app/convert.py
@@ -23,29 +23,23 @@
# ############################################################################*/
"""Convert silx supported data files into HDF5 files"""
+__authors__ = ["P. Knobel"]
+__license__ = "MIT"
+__date__ = "05/02/2019"
+
import ast
import os
import argparse
from glob import glob
import logging
-import numpy
import re
import time
+import numpy
+import six
import silx.io
from silx.io.specfile import is_specfile
-from silx.third_party import six
-
-try:
- from silx.io import fabioh5
-except ImportError:
- fabioh5 = None
-
-
-__authors__ = ["P. Knobel"]
-__license__ = "MIT"
-__date__ = "12/09/2017"
-
+from silx.io import fabioh5
_logger = logging.getLogger(__name__)
"""Module logger"""
@@ -306,20 +300,14 @@ def main(argv):
_logger.debug("Backtrace", exc_info=True)
hdf5plugin = None
+ import h5py
+
try:
- import h5py
from silx.io.convert import write_to_h5
except ImportError:
_logger.debug("Backtrace", exc_info=True)
- h5py = None
write_to_h5 = None
- if h5py is None:
- message = "Module 'h5py' is not installed but is mandatory."\
- + " You can install it using \"pip install h5py\"."
- _logger.error(message)
- return -1
-
if hdf5plugin is None:
message = "Module 'hdf5plugin' is not installed. It supports additional hdf5"\
+ " compressions. You can install it using \"pip install hdf5plugin\"."
@@ -455,7 +443,11 @@ def main(argv):
create_dataset_args["chunks"] = chunks
if options.compression is not None:
- create_dataset_args["compression"] = options.compression
+ try:
+ compression = int(options.compression)
+ except ValueError:
+ compression = options.compression
+ create_dataset_args["compression"] = compression
if options.compression_opts is not None:
create_dataset_args["compression_opts"] = options.compression_opts
@@ -470,18 +462,6 @@ def main(argv):
not contains_specfile(options.input_files) and
not options.add_root_group) or options.file_pattern is not None:
# File series -> stack of images
- if fabioh5 is None:
- # return a helpful error message if fabio is missing
- try:
- import fabio
- except ImportError:
- _logger.error("The fabio library is required to convert"
- " edf files. Please install it with 'pip "
- "install fabio` and try again.")
- else:
- # unexpected problem in silx.io.fabioh5
- raise
- return -1
input_group = fabioh5.File(file_series=options.input_files)
if hdf5_path != "/":
# we want to append only data and headers to an existing file
diff --git a/silx/app/test/test_convert.py b/silx/app/test/test_convert.py
index 97be3fd..bb1ae99 100644
--- a/silx/app/test/test_convert.py
+++ b/silx/app/test/test_convert.py
@@ -35,11 +35,7 @@ import tempfile
import unittest
import io
import gc
-
-try:
- import h5py
-except ImportError:
- h5py = None
+import h5py
import silx
from .. import convert
@@ -103,14 +99,6 @@ class TestConvertCommand(unittest.TestCase):
result = e.args[0]
self.assertEqual(result, 0)
- @testutils.test_logging(convert._logger.name, error=1)
- def testH5pyNotInstalled(self):
- with testutils.EnsureImportError("h5py"):
- result = convert.main(["convert", "foo.spec", "bar.edf"])
- # we explicitly return -1 if h5py is not imported
- self.assertNotEqual(result, 0)
-
- @unittest.skipIf(h5py is None, "h5py is required to test convert")
def testWrongOption(self):
# presence of a wrong option must cause a SystemExit or a return
# with a non-zero status
@@ -120,14 +108,12 @@ class TestConvertCommand(unittest.TestCase):
result = e.args[0]
self.assertNotEqual(result, 0)
- @unittest.skipIf(h5py is None, "h5py is required to test convert")
@testutils.test_logging(convert._logger.name, error=3)
# one error log per missing file + one "Aborted" error log
def testWrongFiles(self):
result = convert.main(["convert", "foo.spec", "bar.edf"])
self.assertNotEqual(result, 0)
- @unittest.skipIf(h5py is None, "h5py is required to test convert")
def testFile(self):
# create a writable temp directory
tempdir = tempfile.mkdtemp()
diff --git a/silx/app/view/Viewer.py b/silx/app/view/Viewer.py
index 88ff989..d543352 100644
--- a/silx/app/view/Viewer.py
+++ b/silx/app/view/Viewer.py
@@ -1,6 +1,6 @@
# coding: utf-8
# /*##########################################################################
-# Copyright (C) 2016-2018 European Synchrotron Radiation Facility
+# Copyright (C) 2016-2019 European Synchrotron Radiation Facility
#
# Permission is hereby granted, free of charge, to any person obtaining a copy
# of this software and associated documentation files (the "Software"), to deal
@@ -25,7 +25,7 @@
__authors__ = ["V. Valls"]
__license__ = "MIT"
-__date__ = "08/10/2018"
+__date__ = "15/01/2019"
import os
@@ -41,6 +41,7 @@ from .ApplicationContext import ApplicationContext
from .CustomNxdataWidget import CustomNxdataWidget
from .CustomNxdataWidget import CustomNxDataToolBar
from . import utils
+from silx.gui.utils import projecturl
from .DataPanel import DataPanel
@@ -61,6 +62,9 @@ class Viewer(qt.QMainWindow):
qt.QMainWindow.__init__(self, parent)
self.setWindowTitle("Silx viewer")
+ silxIcon = icons.getQIcon("silx")
+ self.setWindowIcon(silxIcon)
+
self.__context = ApplicationContext(self, settings)
self.__context.restoreLibrarySettings()
@@ -74,6 +78,7 @@ class Viewer(qt.QMainWindow):
rightPanel.setOrientation(qt.Qt.Vertical)
self.__splitter2 = rightPanel
+ self.__displayIt = None
self.__treeWindow = self.__createTreeWindow(self.__treeview)
# Custom the model to be able to manage the life cycle of the files
@@ -150,11 +155,26 @@ class Viewer(qt.QMainWindow):
action.setText("Refresh")
action.setToolTip("Refresh all selected items")
action.triggered.connect(self.__refreshSelected)
- action.setShortcut(qt.QKeySequence(qt.Qt.ControlModifier + qt.Qt.Key_Plus))
+ action.setShortcut(qt.QKeySequence(qt.Qt.Key_F5))
toolbar.addAction(action)
treeView.addAction(action)
self.__refreshAction = action
+ # Another shortcut for refresh
+ action = qt.QAction(toolbar)
+ action.setShortcut(qt.QKeySequence(qt.Qt.ControlModifier + qt.Qt.Key_R))
+ treeView.addAction(action)
+ action.triggered.connect(self.__refreshSelected)
+
+ action = qt.QAction(toolbar)
+ # action.setIcon(icons.getQIcon("view-refresh"))
+ action.setText("Close")
+ action.setToolTip("Close selected item")
+ action.triggered.connect(self.__removeSelected)
+ action.setShortcut(qt.QKeySequence(qt.Qt.Key_Delete))
+ treeView.addAction(action)
+ self.__closeAction = action
+
toolbar.addSeparator()
action = qt.QAction(toolbar)
@@ -185,6 +205,37 @@ class Viewer(qt.QMainWindow):
layout.addWidget(treeView)
return widget
+ def __removeSelected(self):
+ """Close selected items"""
+ qt.QApplication.setOverrideCursor(qt.Qt.WaitCursor)
+
+ selection = self.__treeview.selectionModel()
+ indexes = selection.selectedIndexes()
+ selectedItems = []
+ model = self.__treeview.model()
+ h5files = set([])
+ while len(indexes) > 0:
+ index = indexes.pop(0)
+ if index.column() != 0:
+ continue
+ h5 = model.data(index, role=silx.gui.hdf5.Hdf5TreeModel.H5PY_OBJECT_ROLE)
+ rootIndex = index
+ # Reach the root of the tree
+ while rootIndex.parent().isValid():
+ rootIndex = rootIndex.parent()
+ rootRow = rootIndex.row()
+ relativePath = self.__getRelativePath(model, rootIndex, index)
+ selectedItems.append((rootRow, relativePath))
+ h5files.add(h5.file)
+
+ if len(h5files) != 0:
+ model = self.__treeview.findHdf5TreeModel()
+ for h5 in h5files:
+ row = model.h5pyObjectRow(h5)
+ model.removeH5pyObject(h5)
+
+ qt.QApplication.restoreOverrideCursor()
+
def __refreshSelected(self):
"""Refresh all selected items
"""
@@ -387,6 +438,9 @@ class Viewer(qt.QMainWindow):
def __h5FileLoaded(self, loadedH5):
self.__context.pushRecentFile(loadedH5.file.filename)
+ if loadedH5.file.filename == self.__displayIt:
+ self.__displayIt = None
+ self.displayData(loadedH5)
def __h5FileRemoved(self, removedH5):
self.__dataPanel.removeDatasetsFrom(removedH5)
@@ -402,10 +456,11 @@ class Viewer(qt.QMainWindow):
self.__context.saveSettings()
# Clean up as much as possible Python objects
- model = self.__customNxdata.model()
- model.clear()
- model = self.__treeview.findHdf5TreeModel()
- model.clear()
+ self.displayData(None)
+ customModel = self.__customNxdata.model()
+ customModel.clear()
+ hdf5Model = self.__treeview.findHdf5TreeModel()
+ hdf5Model.clear()
def saveSettings(self, settings):
"""Save the window settings to this settings object
@@ -502,6 +557,11 @@ class Viewer(qt.QMainWindow):
action.triggered.connect(self.about)
self._aboutAction = action
+ action = qt.QAction("&Documentation", self)
+ action.setStatusTip("Show the Silx library's documentation")
+ action.triggered.connect(self.showDocumentation)
+ self._documentationAction = action
+
# Plot backend
action = qt.QAction("Plot rendering backend", self)
@@ -563,7 +623,7 @@ class Viewer(qt.QMainWindow):
action = qt.QAction("Show custom NXdata selector", self)
action.setStatusTip("Show a widget which allow to create plot by selecting data and axes")
action.setCheckable(True)
- action.setShortcut(qt.QKeySequence(qt.Qt.Key_F5))
+ action.setShortcut(qt.QKeySequence(qt.Qt.Key_F6))
action.toggled.connect(self.__toggleCustomNxdataWindow)
self._displayCustomNxdataWindow = action
@@ -662,6 +722,7 @@ class Viewer(qt.QMainWindow):
helpMenu = self.menuBar().addMenu("&Help")
helpMenu.addAction(self._aboutAction)
+ helpMenu.addAction(self._documentationAction)
def open(self):
dialog = self.createFileDialog()
@@ -691,18 +752,11 @@ class Viewer(qt.QMainWindow):
for description, ext in silx.io.supported_extensions().items():
extensions[description] = " ".join(sorted(list(ext)))
- try:
- # NOTE: hdf5plugin have to be loaded before
- import fabio
- except Exception:
- _logger.debug("Backtrace while loading fabio", exc_info=True)
- fabio = None
-
- if fabio is not None:
- extensions["NeXus layout from EDF files"] = "*.edf"
- extensions["NeXus layout from TIFF image files"] = "*.tif *.tiff"
- extensions["NeXus layout from CBF files"] = "*.cbf"
- extensions["NeXus layout from MarCCD image files"] = "*.mccd"
+ # Add extensions supported by fabio
+ extensions["NeXus layout from EDF files"] = "*.edf"
+ extensions["NeXus layout from TIFF image files"] = "*.tif *.tiff"
+ extensions["NeXus layout from CBF files"] = "*.cbf"
+ extensions["NeXus layout from MarCCD image files"] = "*.mccd"
all_supported_extensions = set()
for name, exts in extensions.items():
@@ -724,6 +778,11 @@ class Viewer(qt.QMainWindow):
from .About import About
About.about(self, "Silx viewer")
+ def showDocumentation(self):
+ subpath = "index.html"
+ url = projecturl.getDocumentationUrl(subpath)
+ qt.QDesktopServices.openUrl(qt.QUrl(url))
+
def __forcePlotImageDownward(self):
silx.config.DEFAULT_PLOT_IMAGE_Y_AXIS_ORIENTATION = "downward"
@@ -737,6 +796,9 @@ class Viewer(qt.QMainWindow):
silx.config.DEFAULT_PLOT_BACKEND = "opengl"
def appendFile(self, filename):
+ if self.__displayIt is None:
+ # Store the file to display it (loading could be async)
+ self.__displayIt = filename
self.__treeview.findHdf5TreeModel().appendFile(filename)
def displaySelectedData(self):
@@ -823,7 +885,7 @@ class Viewer(qt.QMainWindow):
menu.addAction(action)
if silx.io.is_file(h5):
- action = qt.QAction("Remove %s" % obj.local_filename, event.source())
+ action = qt.QAction("Close %s" % obj.local_filename, event.source())
action.triggered.connect(lambda: self.__treeview.findHdf5TreeModel().removeH5pyObject(h5))
menu.addAction(action)
action = qt.QAction("Synchronize %s" % obj.local_filename, event.source())
diff --git a/silx/app/view/main.py b/silx/app/view/main.py
index fc89a22..90b8b17 100644
--- a/silx/app/view/main.py
+++ b/silx/app/view/main.py
@@ -25,7 +25,7 @@
__authors__ = ["V. Valls"]
__license__ = "MIT"
-__date__ = "07/06/2018"
+__date__ = "17/01/2019"
import sys
import argparse
@@ -36,25 +36,6 @@ import signal
_logger = logging.getLogger(__name__)
"""Module logger"""
-if "silx.gui.qt" not in sys.modules:
- # Try first PyQt5 and not the priority imposed by silx.gui.qt.
- # To avoid problem with unittests we only do it if silx.gui.qt is not
- # yet loaded.
- # TODO: Can be removed for silx 0.8, as it should be the default binding
- # of the silx library.
- try:
- import PyQt5.QtCore
- except ImportError:
- pass
-
-import silx
-from silx.gui import qt
-
-
-def sigintHandler(*args):
- """Handler for the SIGINT signal."""
- qt.QApplication.quit()
-
def createParser():
parser = argparse.ArgumentParser(description=__doc__)
@@ -83,16 +64,8 @@ def createParser():
return parser
-def main(argv):
- """
- Main function to launch the viewer as an application
-
- :param argv: Command line arguments
- :returns: exit status
- """
- parser = createParser()
- options = parser.parse_args(argv[1:])
-
+def mainQt(options):
+ """Part of the main depending on Qt"""
if options.debug:
logging.root.setLevel(logging.DEBUG)
@@ -106,25 +79,22 @@ def main(argv):
except ImportError:
_logger.debug("Backtrace", exc_info=True)
- try:
- import h5py
- except ImportError:
- _logger.debug("Backtrace", exc_info=True)
- h5py = None
+ import h5py
- if h5py is None:
- message = "Module 'h5py' is not installed but is mandatory."\
- + " You can install it using \"pip install h5py\"."
- _logger.error(message)
- return -1
-
- #
- # Run the application
- #
+ 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
+ from silx.gui.plot import matplotlib
app = qt.QApplication([])
qt.QLocale.setDefault(qt.QLocale.c())
+ def sigintHandler(*args):
+ """Handler for the SIGINT signal."""
+ qt.QApplication.quit()
+
signal.signal(signal.SIGINT, sigintHandler)
sys.excepthook = qt.exceptionHandler
@@ -150,7 +120,11 @@ def main(argv):
# It have to be done after the settings (after the Viewer creation)
silx.config.DEFAULT_PLOT_BACKEND = "opengl"
+ # NOTE: under Windows, cmd does not convert `*.tif` into existing files
+ options.files = silx.utils.files.expand_filenames(options.files)
+
for filename in options.files:
+ # TODO: Would be nice to add a process widget and a cancel button
try:
window.appendFile(filename)
except IOError as e:
@@ -164,5 +138,17 @@ def main(argv):
return result
+def main(argv):
+ """
+ Main function to launch the viewer as an application
+
+ :param argv: Command line arguments
+ :returns: exit status
+ """
+ parser = createParser()
+ options = parser.parse_args(argv[1:])
+ mainQt(options)
+
+
if __name__ == '__main__':
main(sys.argv)
diff --git a/silx/app/view/test/test_view.py b/silx/app/view/test/test_view.py
index ebcd405..6601dce 100644
--- a/silx/app/view/test/test_view.py
+++ b/silx/app/view/test/test_view.py
@@ -35,10 +35,7 @@ import numpy
import tempfile
import shutil
import os.path
-try:
- import h5py
-except ImportError:
- h5py = None
+import h5py
from silx.gui import qt
from silx.app.view.Viewer import Viewer
@@ -56,22 +53,21 @@ def setUpModule():
global _tmpDirectory
_tmpDirectory = tempfile.mkdtemp(prefix=__name__)
- if h5py is not None:
- # create h5 data
- filename = _tmpDirectory + "/data.h5"
- f = h5py.File(filename, "w")
- g = f.create_group("arrays")
- g.create_dataset("scalar", data=10)
- g.create_dataset("integers", data=numpy.array([10, 20, 30]))
- f.close()
+ # create h5 data
+ filename = _tmpDirectory + "/data.h5"
+ f = h5py.File(filename, "w")
+ g = f.create_group("arrays")
+ g.create_dataset("scalar", data=10)
+ g.create_dataset("integers", data=numpy.array([10, 20, 30]))
+ f.close()
- # create h5 data
- filename = _tmpDirectory + "/data2.h5"
- f = h5py.File(filename, "w")
- g = f.create_group("arrays")
- g.create_dataset("scalar", data=20)
- g.create_dataset("integers", data=numpy.array([10, 20, 30]))
- f.close()
+ # create h5 data
+ filename = _tmpDirectory + "/data2.h5"
+ f = h5py.File(filename, "w")
+ g = f.create_group("arrays")
+ g.create_dataset("scalar", data=20)
+ g.create_dataset("integers", data=numpy.array([10, 20, 30]))
+ f.close()
def tearDownModule():
@@ -167,7 +163,6 @@ class TestDataPanel(TestCaseQt):
self.assertIs(widget.getData(), None)
self.assertIs(widget.getCustomNxdataItem(), data)
- @unittest.skipIf(h5py is None, "Could not import h5py")
def testRemoveDatasetsFrom(self):
f = h5py.File(os.path.join(_tmpDirectory, "data.h5"))
try:
@@ -179,7 +174,6 @@ class TestDataPanel(TestCaseQt):
widget.setData(None)
f.close()
- @unittest.skipIf(h5py is None, "Could not import h5py")
def testReplaceDatasetsFrom(self):
f = h5py.File(os.path.join(_tmpDirectory, "data.h5"))
f2 = h5py.File(os.path.join(_tmpDirectory, "data2.h5"))
@@ -248,7 +242,6 @@ class TestCustomNxdataWidget(TestCaseQt):
self.assertIsNotNone(nxdata)
self.assertFalse(item.isValid())
- @unittest.skipIf(h5py is None, "Could not import h5py")
def testRemoveDatasetsFrom(self):
f = h5py.File(os.path.join(_tmpDirectory, "data.h5"))
try:
@@ -261,7 +254,6 @@ class TestCustomNxdataWidget(TestCaseQt):
model.clear()
f.close()
- @unittest.skipIf(h5py is None, "Could not import h5py")
def testReplaceDatasetsFrom(self):
f = h5py.File(os.path.join(_tmpDirectory, "data.h5"))
f2 = h5py.File(os.path.join(_tmpDirectory, "data2.h5"))