summaryrefslogtreecommitdiff
path: root/silx/app
diff options
context:
space:
mode:
Diffstat (limited to 'silx/app')
-rw-r--r--silx/app/__init__.py29
-rw-r--r--silx/app/convert.py525
-rw-r--r--silx/app/setup.py41
-rw-r--r--silx/app/test/__init__.py39
-rw-r--r--silx/app/test/test_convert.py167
-rw-r--r--silx/app/test_.py159
-rw-r--r--silx/app/view/About.py257
-rw-r--r--silx/app/view/ApplicationContext.py194
-rw-r--r--silx/app/view/CustomNxdataWidget.py1008
-rw-r--r--silx/app/view/DataPanel.py192
-rw-r--r--silx/app/view/Viewer.py971
-rw-r--r--silx/app/view/__init__.py28
-rw-r--r--silx/app/view/main.py171
-rw-r--r--silx/app/view/setup.py40
-rw-r--r--silx/app/view/test/__init__.py41
-rw-r--r--silx/app/view/test/test_launcher.py151
-rw-r--r--silx/app/view/test/test_view.py394
-rw-r--r--silx/app/view/utils.py45
18 files changed, 0 insertions, 4452 deletions
diff --git a/silx/app/__init__.py b/silx/app/__init__.py
deleted file mode 100644
index 3af680c..0000000
--- a/silx/app/__init__.py
+++ /dev/null
@@ -1,29 +0,0 @@
-# coding: utf-8
-# /*##########################################################################
-#
-# Copyright (c) 2016-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.
-#
-# ###########################################################################*/
-"""This package contains the application provided by the launcher"""
-
-__authors__ = ["V. Valls"]
-__license__ = "MIT"
-__date__ = "30/03/2017"
diff --git a/silx/app/convert.py b/silx/app/convert.py
deleted file mode 100644
index 7e601ce..0000000
--- a/silx/app/convert.py
+++ /dev/null
@@ -1,525 +0,0 @@
-# coding: utf-8
-# /*##########################################################################
-# Copyright (C) 2017-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.
-#
-# ############################################################################*/
-"""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 re
-import time
-import numpy
-import six
-
-import silx.io
-from silx.io.specfile import is_specfile
-from silx.io import fabioh5
-
-_logger = logging.getLogger(__name__)
-"""Module logger"""
-
-
-def c_format_string_to_re(pattern_string):
- """
-
- :param pattern_string: C style format string with integer patterns
- (e.g. "%d", "%04d").
- Not supported: fixed length padded with whitespaces (e.g "%4d", "%-4d")
- :return: Equivalent regular expression (e.g. "\\d+", "\\d{4}")
- """
- # escape dots and backslashes
- pattern_string = pattern_string.replace("\\", "\\\\")
- pattern_string = pattern_string.replace(".", r"\.")
-
- # %d
- pattern_string = pattern_string.replace("%d", r"([-+]?\d+)")
-
- # %0nd
- for sub_pattern in re.findall(r"%0\d+d", pattern_string):
- n = int(re.search(r"%0(\d+)d", sub_pattern).group(1))
- if n == 1:
- re_sub_pattern = r"([+-]?\d)"
- else:
- re_sub_pattern = r"([\d+-]\d{%d})" % (n - 1)
- pattern_string = pattern_string.replace(sub_pattern, re_sub_pattern, 1)
-
- return pattern_string
-
-
-def drop_indices_before_begin(filenames, regex, begin):
- """
-
- :param List[str] filenames: list of filenames
- :param str regex: Regexp used to find indices in a filename
- :param str begin: Comma separated list of begin indices
- :return: List of filenames with only indices >= begin
- """
- begin_indices = list(map(int, begin.split(",")))
- output_filenames = []
- for fname in filenames:
- m = re.match(regex, fname)
- file_indices = list(map(int, m.groups()))
- if len(file_indices) != len(begin_indices):
- raise IOError("Number of indices found in filename "
- "does not match number of parsed end indices.")
- good_indices = True
- for i, fidx in enumerate(file_indices):
- if fidx < begin_indices[i]:
- good_indices = False
- if good_indices:
- output_filenames.append(fname)
- return output_filenames
-
-
-def drop_indices_after_end(filenames, regex, end):
- """
-
- :param List[str] filenames: list of filenames
- :param str regex: Regexp used to find indices in a filename
- :param str end: Comma separated list of end indices
- :return: List of filenames with only indices <= end
- """
- end_indices = list(map(int, end.split(",")))
- output_filenames = []
- for fname in filenames:
- m = re.match(regex, fname)
- file_indices = list(map(int, m.groups()))
- if len(file_indices) != len(end_indices):
- raise IOError("Number of indices found in filename "
- "does not match number of parsed end indices.")
- good_indices = True
- for i, fidx in enumerate(file_indices):
- if fidx > end_indices[i]:
- good_indices = False
- if good_indices:
- output_filenames.append(fname)
- return output_filenames
-
-
-def are_files_missing_in_series(filenames, regex):
- """Return True if any file is missing in a list of filenames
- that are supposed to follow a pattern.
-
- :param List[str] filenames: list of filenames
- :param str regex: Regexp used to find indices in a filename
- :return: boolean
- :raises AssertionError: if a filename does not match the regexp
- """
- previous_indices = None
- for fname in filenames:
- m = re.match(regex, fname)
- assert m is not None, \
- "regex %s does not match filename %s" % (fname, regex)
- new_indices = list(map(int, m.groups()))
- if previous_indices is not None:
- for old_idx, new_idx in zip(previous_indices, new_indices):
- if (new_idx - old_idx) > 1:
- _logger.error("Index increment > 1 in file series: "
- "previous idx %d, next idx %d",
- old_idx, new_idx)
- return True
- previous_indices = new_indices
- return False
-
-
-def are_all_specfile(filenames):
- """Return True if all files in a list are SPEC files.
- :param List[str] filenames: list of filenames
- """
- for fname in filenames:
- if not is_specfile(fname):
- return False
- return True
-
-
-def contains_specfile(filenames):
- """Return True if any file in a list are SPEC files.
- :param List[str] filenames: list of filenames
- """
- for fname in filenames:
- if is_specfile(fname):
- return True
- return False
-
-
-def main(argv):
- """
- Main function to launch the converter as an application
-
- :param argv: Command line arguments
- :returns: exit status
- """
- parser = argparse.ArgumentParser(description=__doc__)
- parser.add_argument(
- 'input_files',
- nargs="*",
- help='Input files (EDF, TIFF, SPEC...). When specifying multiple '
- 'files, you cannot specify both fabio images and SPEC files. '
- 'Multiple SPEC files will simply be concatenated, with one '
- 'entry per scan. Multiple image files will be merged into '
- 'a single entry with a stack of images.')
- # input_files and --filepattern are mutually exclusive
- parser.add_argument(
- '--file-pattern',
- help='File name pattern for loading a series of indexed image files '
- '(toto_%%04d.edf). This argument is incompatible with argument '
- 'input_files. If an output URI with a HDF5 path is provided, '
- 'only the content of the NXdetector group will be copied there. '
- 'If no HDF5 path, or just "/", is given, a complete NXdata '
- 'structure will be created.')
- parser.add_argument(
- '-o', '--output-uri',
- default=time.strftime("%Y%m%d-%H%M%S") + '.h5',
- help='Output file name (HDF5). An URI can be provided to write'
- ' the data into a specific group in the output file: '
- '/path/to/file::/path/to/group. '
- 'If not provided, the filename defaults to a timestamp:'
- ' YYYYmmdd-HHMMSS.h5')
- parser.add_argument(
- '-m', '--mode',
- default="w-",
- help='Write mode: "r+" (read/write, file must exist), '
- '"w" (write, existing file is lost), '
- '"w-" (write, fail if file exists) or '
- '"a" (read/write if exists, create otherwise)')
- parser.add_argument(
- '--begin',
- help='First file index, or first file indices to be considered. '
- 'This argument only makes sense when used together with '
- '--file-pattern. Provide as many start indices as there '
- 'are indices in the file pattern, separated by commas. '
- 'Examples: "--filepattern toto_%%d.edf --begin 100", '
- ' "--filepattern toto_%%d_%%04d_%%02d.edf --begin 100,2000,5".')
- parser.add_argument(
- '--end',
- help='Last file index, or last file indices to be considered. '
- 'The same rules as with argument --begin apply. '
- 'Example: "--filepattern toto_%%d_%%d.edf --end 199,1999"')
- parser.add_argument(
- '--add-root-group',
- action="store_true",
- help='This option causes each input file to be written to a '
- 'specific root group with the same name as the file. When '
- 'merging multiple input files, this can help preventing conflicts'
- ' when datasets have the same name (see --overwrite-data). '
- 'This option is ignored when using --file-pattern.')
- parser.add_argument(
- '--overwrite-data',
- action="store_true",
- help='If the output path exists and an input dataset has the same'
- ' name as an existing output dataset, overwrite the output '
- 'dataset (in modes "r+" or "a").')
- parser.add_argument(
- '--min-size',
- type=int,
- default=500,
- help='Minimum number of elements required to be in a dataset to '
- 'apply compression or chunking (default 500).')
- parser.add_argument(
- '--chunks',
- nargs="?",
- const="auto",
- help='Chunk shape. Provide an argument that evaluates as a python '
- 'tuple (e.g. "(1024, 768)"). If this option is provided without '
- 'specifying an argument, the h5py library will guess a chunk for '
- 'you. Note that if you specify an explicit chunking shape, it '
- 'will be applied identically to all datasets with a large enough '
- 'size (see --min-size). ')
- parser.add_argument(
- '--compression',
- nargs="?",
- const="gzip",
- help='Compression filter. By default, the datasets in the output '
- 'file are not compressed. If this option is specified without '
- 'argument, the GZIP compression is used. Additional compression '
- 'filters may be available, depending on your HDF5 installation.')
-
- def check_gzip_compression_opts(value):
- ivalue = int(value)
- if ivalue < 0 or ivalue > 9:
- raise argparse.ArgumentTypeError(
- "--compression-opts must be an int from 0 to 9")
- return ivalue
-
- parser.add_argument(
- '--compression-opts',
- type=check_gzip_compression_opts,
- help='Compression options. For "gzip", this may be an integer from '
- '0 to 9, with a default of 4. This is only supported for GZIP.')
- parser.add_argument(
- '--shuffle',
- action="store_true",
- help='Enables the byte shuffle filter. This may improve the compression '
- 'ratio for block oriented compressors like GZIP or LZF.')
- parser.add_argument(
- '--fletcher32',
- action="store_true",
- help='Adds a checksum to each chunk to detect data corruption.')
- parser.add_argument(
- '--debug',
- action="store_true",
- default=False,
- help='Set logging system in debug mode')
-
- options = parser.parse_args(argv[1:])
-
- if options.debug:
- logging.root.setLevel(logging.DEBUG)
-
- # Import after parsing --debug
- try:
- # it should be loaded before h5py
- import hdf5plugin # noqa
- except ImportError:
- _logger.debug("Backtrace", exc_info=True)
- hdf5plugin = None
-
- import h5py
-
- try:
- from silx.io.convert import write_to_h5
- except ImportError:
- _logger.debug("Backtrace", exc_info=True)
- write_to_h5 = None
-
- if hdf5plugin is None:
- message = "Module 'hdf5plugin' is not installed. It supports additional hdf5"\
- + " compressions. You can install it using \"pip install hdf5plugin\"."
- _logger.debug(message)
-
- # Process input arguments (mutually exclusive arguments)
- if bool(options.input_files) == bool(options.file_pattern is not None):
- if not options.input_files:
- message = "You must specify either input files (at least one), "
- message += "or a file pattern."
- else:
- message = "You cannot specify input files and a file pattern"
- message += " at the same time."
- _logger.error(message)
- return -1
- elif options.input_files:
- # some shells (windows) don't interpret wildcard characters (*, ?, [])
- old_input_list = list(options.input_files)
- options.input_files = []
- for fname in old_input_list:
- globbed_files = glob(fname)
- if not globbed_files:
- # no files found, keep the name as it is, to raise an error later
- options.input_files += [fname]
- else:
- # glob does not sort files, but the bash shell does
- options.input_files += sorted(globbed_files)
- else:
- # File series
- dirname = os.path.dirname(options.file_pattern)
- file_pattern_re = c_format_string_to_re(options.file_pattern) + "$"
- files_in_dir = glob(os.path.join(dirname, "*"))
- _logger.debug("""
- Processing file_pattern
- dirname: %s
- file_pattern_re: %s
- files_in_dir: %s
- """, dirname, file_pattern_re, files_in_dir)
-
- options.input_files = sorted(list(filter(lambda name: re.match(file_pattern_re, name),
- files_in_dir)))
- _logger.debug("options.input_files: %s", options.input_files)
-
- if options.begin is not None:
- options.input_files = drop_indices_before_begin(options.input_files,
- file_pattern_re,
- options.begin)
- _logger.debug("options.input_files after applying --begin: %s",
- options.input_files)
-
- if options.end is not None:
- options.input_files = drop_indices_after_end(options.input_files,
- file_pattern_re,
- options.end)
- _logger.debug("options.input_files after applying --end: %s",
- options.input_files)
-
- if are_files_missing_in_series(options.input_files,
- file_pattern_re):
- _logger.error("File missing in the file series. Aborting.")
- return -1
-
- if not options.input_files:
- _logger.error("No file matching --file-pattern found.")
- return -1
-
- # Test that the output path is writeable
- if "::" in options.output_uri:
- output_name, hdf5_path = options.output_uri.split("::")
- else:
- output_name, hdf5_path = options.output_uri, "/"
-
- if os.path.isfile(output_name):
- if options.mode == "w-":
- _logger.error("Output file %s exists and mode is 'w-' (default)."
- " Aborting. To append data to an existing file, "
- "use 'a' or 'r+'.",
- output_name)
- return -1
- elif not os.access(output_name, os.W_OK):
- _logger.error("Output file %s exists and is not writeable.",
- output_name)
- return -1
- elif options.mode == "w":
- _logger.info("Output file %s exists and mode is 'w'. "
- "Overwriting existing file.", output_name)
- elif options.mode in ["a", "r+"]:
- _logger.info("Appending data to existing file %s.",
- output_name)
- else:
- if options.mode == "r+":
- _logger.error("Output file %s does not exist and mode is 'r+'"
- " (append, file must exist). Aborting.",
- output_name)
- return -1
- else:
- _logger.info("Creating new output file %s.",
- output_name)
-
- # Test that all input files exist and are readable
- bad_input = False
- for fname in options.input_files:
- if not os.access(fname, os.R_OK):
- _logger.error("Cannot read input file %s.",
- fname)
- bad_input = True
- if bad_input:
- _logger.error("Aborting.")
- return -1
-
- # create_dataset special args
- create_dataset_args = {}
- if options.chunks is not None:
- if options.chunks.lower() in ["auto", "true"]:
- create_dataset_args["chunks"] = True
- else:
- try:
- chunks = ast.literal_eval(options.chunks)
- except (ValueError, SyntaxError):
- _logger.error("Invalid --chunks argument %s", options.chunks)
- return -1
- if not isinstance(chunks, (tuple, list)):
- _logger.error("--chunks argument str does not evaluate to a tuple")
- return -1
- else:
- nitems = numpy.prod(chunks)
- nbytes = nitems * 8
- if nbytes > 10**6:
- _logger.warning("Requested chunk size might be larger than"
- " the default 1MB chunk cache, for float64"
- " data. This can dramatically affect I/O "
- "performances.")
- create_dataset_args["chunks"] = chunks
-
- if options.compression is not None:
- 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
-
- if options.shuffle:
- create_dataset_args["shuffle"] = True
-
- if options.fletcher32:
- create_dataset_args["fletcher32"] = True
-
- if (len(options.input_files) > 1 and
- not contains_specfile(options.input_files) and
- not options.add_root_group) or options.file_pattern is not None:
- # File series -> stack of images
- input_group = fabioh5.File(file_series=options.input_files)
- if hdf5_path != "/":
- # we want to append only data and headers to an existing file
- input_group = input_group["/scan_0/instrument/detector_0"]
- with h5py.File(output_name, mode=options.mode) as h5f:
- write_to_h5(input_group, h5f,
- h5path=hdf5_path,
- overwrite_data=options.overwrite_data,
- create_dataset_args=create_dataset_args,
- min_size=options.min_size)
-
- elif len(options.input_files) == 1 or \
- are_all_specfile(options.input_files) or\
- options.add_root_group:
- # single file, or spec files
- h5paths_and_groups = []
- for input_name in options.input_files:
- hdf5_path_for_file = hdf5_path
- if options.add_root_group:
- hdf5_path_for_file = hdf5_path.rstrip("/") + "/" + os.path.basename(input_name)
- try:
- h5paths_and_groups.append((hdf5_path_for_file,
- silx.io.open(input_name)))
- except IOError:
- _logger.error("Cannot read file %s. If this is a file format "
- "supported by the fabio library, you can try to"
- " install fabio (`pip install fabio`)."
- " Aborting conversion.",
- input_name)
- return -1
-
- with h5py.File(output_name, mode=options.mode) as h5f:
- for hdf5_path_for_file, input_group in h5paths_and_groups:
- write_to_h5(input_group, h5f,
- h5path=hdf5_path_for_file,
- overwrite_data=options.overwrite_data,
- create_dataset_args=create_dataset_args,
- min_size=options.min_size)
-
- else:
- # multiple file, SPEC and fabio images mixed
- _logger.error("Multiple files with incompatible formats specified. "
- "You can provide multiple SPEC files or multiple image "
- "files, but not both.")
- return -1
-
- with h5py.File(output_name, mode="r+") as h5f:
- # append "silx convert" to the creator attribute, for NeXus files
- previous_creator = h5f.attrs.get("creator", u"")
- creator = "silx convert (v%s)" % silx.version
- # only if it not already there
- if creator not in previous_creator:
- if not previous_creator:
- new_creator = creator
- else:
- new_creator = previous_creator + "; " + creator
- h5f.attrs["creator"] = numpy.array(
- new_creator,
- dtype=h5py.special_dtype(vlen=six.text_type))
-
- return 0
diff --git a/silx/app/setup.py b/silx/app/setup.py
deleted file mode 100644
index 85c3662..0000000
--- a/silx/app/setup.py
+++ /dev/null
@@ -1,41 +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__ = "23/04/2018"
-
-from numpy.distutils.misc_util import Configuration
-
-
-def configuration(parent_package='', top_path=None):
- config = Configuration('app', parent_package, top_path)
- config.add_subpackage('test')
- config.add_subpackage('view')
- return config
-
-
-if __name__ == "__main__":
- from numpy.distutils.core import setup
- setup(configuration=configuration)
diff --git a/silx/app/test/__init__.py b/silx/app/test/__init__.py
deleted file mode 100644
index 7c91134..0000000
--- a/silx/app/test/__init__.py
+++ /dev/null
@@ -1,39 +0,0 @@
-# coding: utf-8
-# /*##########################################################################
-#
-# Copyright (c) 2016-2017 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"
-
-import unittest
-
-from ..view import test as test_view
-from . import test_convert
-
-
-def suite():
- test_suite = unittest.TestSuite()
- test_suite.addTest(test_view.suite())
- test_suite.addTest(test_convert.suite())
- return test_suite
diff --git a/silx/app/test/test_convert.py b/silx/app/test/test_convert.py
deleted file mode 100644
index 857f30c..0000000
--- a/silx/app/test/test_convert.py
+++ /dev/null
@@ -1,167 +0,0 @@
-# coding: utf-8
-# /*##########################################################################
-#
-# Copyright (c) 2016-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.
-#
-# ###########################################################################*/
-"""Module testing silx.app.convert"""
-
-__authors__ = ["P. Knobel"]
-__license__ = "MIT"
-__date__ = "17/01/2018"
-
-
-import os
-import sys
-import tempfile
-import unittest
-import io
-import gc
-import h5py
-
-import silx
-from .. import convert
-from silx.utils import testutils
-from silx.io.utils import h5py_read_dataset
-
-
-# content of a spec file
-sftext = """#F /tmp/sf.dat
-#E 1455180875
-#D Thu Feb 11 09:54:35 2016
-#C imaging User = opid17
-#O0 Pslit HGap MRTSlit UP MRTSlit DOWN
-#O1 Sslit1 VOff Sslit1 HOff Sslit1 VGap
-#o0 pshg mrtu mrtd
-#o2 ss1vo ss1ho ss1vg
-
-#J0 Seconds IA ion.mono Current
-#J1 xbpmc2 idgap1 Inorm
-
-#S 1 ascan ss1vo -4.55687 -0.556875 40 0.2
-#D Thu Feb 11 09:55:20 2016
-#T 0.2 (Seconds)
-#P0 180.005 -0.66875 0.87125
-#P1 14.74255 16.197579 12.238283
-#N 4
-#L MRTSlit UP second column 3rd_col
--1.23 5.89 8
-8.478100E+01 5 1.56
-3.14 2.73 -3.14
-1.2 2.3 3.4
-
-#S 1 aaaaaa
-#D Thu Feb 11 10:00:32 2016
-#@MCADEV 1
-#@MCA %16C
-#@CHANN 3 0 2 1
-#@CALIB 1 2 3
-#N 3
-#L uno duo
-1 2
-@A 0 1 2
-@A 10 9 8
-3 4
-@A 3.1 4 5
-@A 7 6 5
-5 6
-@A 6 7.7 8
-@A 4 3 2
-"""
-
-
-class TestConvertCommand(unittest.TestCase):
- """Test command line parsing"""
-
- def testHelp(self):
- # option -h must cause a `raise SystemExit` or a `return 0`
- try:
- result = convert.main(["convert", "--help"])
- except SystemExit as e:
- result = e.args[0]
- self.assertEqual(result, 0)
-
- def testWrongOption(self):
- # presence of a wrong option must cause a SystemExit or a return
- # with a non-zero status
- try:
- result = convert.main(["convert", "--foo"])
- except SystemExit as e:
- result = e.args[0]
- self.assertNotEqual(result, 0)
-
- @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)
-
- def testFile(self):
- # create a writable temp directory
- tempdir = tempfile.mkdtemp()
-
- # write a temporary SPEC file
- specname = os.path.join(tempdir, "input.dat")
- with io.open(specname, "wb") as fd:
- if sys.version_info < (3, ):
- fd.write(sftext)
- else:
- fd.write(bytes(sftext, 'ascii'))
-
- # convert it
- h5name = os.path.join(tempdir, "output.h5")
- assert not os.path.isfile(h5name)
- command_list = ["convert", "-m", "w",
- specname, "-o", h5name]
- result = convert.main(command_list)
-
- self.assertEqual(result, 0)
- self.assertTrue(os.path.isfile(h5name))
-
- with h5py.File(h5name, "r") as h5f:
- title12 = h5py_read_dataset(h5f["/1.2/title"])
- if sys.version_info < (3, ):
- title12 = title12.encode("utf-8")
- self.assertEqual(title12,
- "aaaaaa")
-
- creator = h5f.attrs.get("creator")
- self.assertIsNotNone(creator, "No creator attribute in NXroot group")
- if sys.version_info < (3, ):
- creator = creator.encode("utf-8")
- self.assertIn("silx convert (v%s)" % silx.version, creator)
-
- # delete input file
- gc.collect() # necessary to free spec file on Windows
- os.unlink(specname)
- os.unlink(h5name)
- os.rmdir(tempdir)
-
-
-def suite():
- test_suite = unittest.TestSuite()
- loader = unittest.defaultTestLoader.loadTestsFromTestCase
- test_suite.addTest(loader(TestConvertCommand))
- return test_suite
-
-
-if __name__ == '__main__':
- unittest.main(defaultTest='suite')
diff --git a/silx/app/test_.py b/silx/app/test_.py
deleted file mode 100644
index a8e58bf..0000000
--- a/silx/app/test_.py
+++ /dev/null
@@ -1,159 +0,0 @@
-# coding: utf-8
-# /*##########################################################################
-# Copyright (C) 2016-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.
-#
-# ############################################################################*/
-"""Launch unittests of the library"""
-
-__authors__ = ["V. Valls"]
-__license__ = "MIT"
-__date__ = "12/01/2018"
-
-import sys
-import argparse
-import logging
-import unittest
-
-
-class StreamHandlerUnittestReady(logging.StreamHandler):
- """The unittest class TestResult redefine sys.stdout/err to capture
- stdout/err from tests and to display them only when a test fail.
-
- This class allow to use unittest stdout-capture by using the last sys.stdout
- and not a cached one.
- """
-
- def emit(self, record):
- """
- :type record: logging.LogRecord
- """
- self.stream = sys.stderr
- super(StreamHandlerUnittestReady, self).emit(record)
-
- def flush(self):
- pass
-
-
-def createBasicHandler():
- """Create the handler using the basic configuration"""
- hdlr = StreamHandlerUnittestReady()
- fs = logging.BASIC_FORMAT
- dfs = None
- fmt = logging.Formatter(fs, dfs)
- hdlr.setFormatter(fmt)
- return hdlr
-
-
-# Use an handler compatible with unittests, else use_buffer is not working
-for h in logging.root.handlers:
- logging.root.removeHandler(h)
-logging.root.addHandler(createBasicHandler())
-logging.captureWarnings(True)
-
-_logger = logging.getLogger(__name__)
-"""Module logger"""
-
-
-class TextTestResultWithSkipList(unittest.TextTestResult):
- """Override default TextTestResult to display list of skipped tests at the
- end
- """
-
- def printErrors(self):
- unittest.TextTestResult.printErrors(self)
- # Print skipped tests at the end
- self.printErrorList("SKIPPED", self.skipped)
-
-
-def main(argv):
- """
- Main function to launch the unittests as an application
-
- :param argv: Command line arguments
- :returns: exit status
- """
- from silx.test import utils
-
- parser = argparse.ArgumentParser(description=__doc__)
- parser.add_argument("-v", "--verbose", default=0,
- action="count", dest="verbose",
- help="Increase verbosity. Option -v prints additional " +
- "INFO messages. Use -vv for full verbosity, " +
- "including debug messages and test help strings.")
- parser.add_argument("--qt-binding", dest="qt_binding", default=None,
- help="Force using a Qt binding: 'PyQt5' or 'PySide2'")
- utils.test_options.add_parser_argument(parser)
-
- options = parser.parse_args(argv[1:])
-
- test_verbosity = 1
- use_buffer = True
- if options.verbose == 1:
- logging.root.setLevel(logging.INFO)
- _logger.info("Set log level: INFO")
- test_verbosity = 2
- use_buffer = False
- elif options.verbose > 1:
- logging.root.setLevel(logging.DEBUG)
- _logger.info("Set log level: DEBUG")
- test_verbosity = 2
- use_buffer = False
-
- if options.qt_binding:
- binding = options.qt_binding.lower()
- if binding == "pyqt4":
- _logger.info("Force using PyQt4")
- import PyQt4.QtCore # noqa
- elif binding == "pyqt5":
- _logger.info("Force using PyQt5")
- import PyQt5.QtCore # noqa
- elif binding == "pyside":
- _logger.info("Force using PySide")
- import PySide.QtCore # noqa
- elif binding == "pyside2":
- _logger.info("Force using PySide2")
- import PySide2.QtCore # noqa
- else:
- raise ValueError("Qt binding '%s' is unknown" % options.qt_binding)
-
- # Configure test options
- utils.test_options.configure(options)
-
- # Run the tests
- runnerArgs = {}
- runnerArgs["verbosity"] = test_verbosity
- runnerArgs["buffer"] = use_buffer
- runner = unittest.TextTestRunner(**runnerArgs)
- runner.resultclass = TextTestResultWithSkipList
-
- # Display the result when using CTRL-C
- unittest.installHandler()
-
- import silx.test
- test_suite = unittest.TestSuite()
- test_suite.addTest(silx.test.suite())
- result = runner.run(test_suite)
-
- if result.wasSuccessful():
- exit_status = 0
- else:
- exit_status = 1
- return exit_status
diff --git a/silx/app/view/About.py b/silx/app/view/About.py
deleted file mode 100644
index a2b430f..0000000
--- a/silx/app/view/About.py
+++ /dev/null
@@ -1,257 +0,0 @@
-# coding: utf-8
-# /*##########################################################################
-# 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
-# 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.
-#
-# ############################################################################*/
-"""About box for Silx viewer"""
-
-__authors__ = ["V. Valls"]
-__license__ = "MIT"
-__date__ = "05/07/2018"
-
-import os
-import sys
-
-from silx.gui import qt
-from silx.gui import icons
-
-_LICENSE_TEMPLATE = """<p align="center">
-<b>Copyright (C) {year} European Synchrotron Radiation Facility</b>
-</p>
-
-<p align="justify">
-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:
-</p>
-
-<p align="justify">
-The above copyright notice and this permission notice shall be included in
-all copies or substantial portions of the Software.
-</p>
-
-<p align="justify">
-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.
-</p>
-"""
-
-
-class About(qt.QDialog):
- """
- Util dialog to display an common about box for all the silx GUIs.
- """
-
- def __init__(self, parent=None):
- """
- :param files_: List of HDF5 or Spec files (pathes or
- :class:`silx.io.spech5.SpecH5` or :class:`h5py.File`
- instances)
- """
- super(About, self).__init__(parent)
- self.__createLayout()
- self.setSizePolicy(qt.QSizePolicy.Fixed, qt.QSizePolicy.Fixed)
- self.setModal(True)
- self.setApplicationName(None)
-
- def __createLayout(self):
- layout = qt.QVBoxLayout(self)
- layout.setContentsMargins(24, 15, 24, 20)
- layout.setSpacing(8)
-
- self.__label = qt.QLabel(self)
- self.__label.setWordWrap(True)
- flags = self.__label.textInteractionFlags()
- flags = flags | qt.Qt.TextSelectableByKeyboard
- flags = flags | qt.Qt.TextSelectableByMouse
- self.__label.setTextInteractionFlags(flags)
- self.__label.setOpenExternalLinks(True)
- self.__label.setSizePolicy(qt.QSizePolicy.Minimum, qt.QSizePolicy.Preferred)
-
- licenseButton = qt.QPushButton(self)
- licenseButton.setText("License...")
- licenseButton.clicked.connect(self.__displayLicense)
- licenseButton.setAutoDefault(False)
-
- self.__options = qt.QDialogButtonBox()
- self.__options.addButton(licenseButton, qt.QDialogButtonBox.ActionRole)
- okButton = self.__options.addButton(qt.QDialogButtonBox.Ok)
- okButton.setDefault(True)
- okButton.clicked.connect(self.accept)
-
- layout.addWidget(self.__label)
- layout.addWidget(self.__options)
- layout.setStretch(0, 100)
- layout.setStretch(1, 0)
-
- def getHtmlLicense(self):
- """Returns the text license in HTML format.
-
- :rtype: str
- """
- from silx._version import __date__ as date
- year = date.split("/")[2]
- info = dict(
- year=year
- )
- textLicense = _LICENSE_TEMPLATE.format(**info)
- return textLicense
-
- def __displayLicense(self):
- """Displays the license used by silx."""
- text = self.getHtmlLicense()
- licenseDialog = qt.QMessageBox(self)
- licenseDialog.setWindowTitle("License")
- licenseDialog.setText(text)
- licenseDialog.exec_()
-
- def setApplicationName(self, name):
- self.__applicationName = name
- if name is None:
- self.setWindowTitle("About")
- else:
- self.setWindowTitle("About %s" % name)
- self.__updateText()
-
- @staticmethod
- def __formatOptionalLibraries(name, isAvailable):
- """Utils to format availability of features"""
- if isAvailable:
- template = '<b>%s</b> is <font color="green">loaded</font>'
- else:
- template = '<b>%s</b> is <font color="red">not loaded</font>'
- return template % name
-
- @staticmethod
- def __formatOptionalFilters(name, isAvailable):
- """Utils to format availability of features"""
- if isAvailable:
- template = '<b>%s</b> is <font color="green">available</font>'
- else:
- template = '<b>%s</b> is <font color="red">not available</font>'
- return template % name
-
- def __updateText(self):
- """Update the content of the dialog according to the settings."""
- import silx._version
-
- message = """<table>
- <tr><td width="50%" align="center" valign="middle">
- <img src="{silx_image_path}" width="100" />
- </td><td width="50%" align="center" valign="middle">
- <b>{application_name}</b>
- <br />
- <br />{silx_version}
- <br />
- <br /><a href="{project_url}">Upstream project on GitHub</a>
- </td></tr>
- </table>
- <dl>
- <dt><b>Silx version</b></dt><dd>{silx_version}</dd>
- <dt><b>Qt version</b></dt><dd>{qt_version}</dd>
- <dt><b>Qt binding</b></dt><dd>{qt_binding}</dd>
- <dt><b>Python version</b></dt><dd>{python_version}</dd>
- <dt><b>Optional libraries</b></dt><dd>{optional_lib}</dd>
- </dl>
- <p>
- Copyright (C) <a href="{esrf_url}">European Synchrotron Radiation Facility</a>
- </p>
- """
-
- optionals = []
- optionals.append(self.__formatOptionalLibraries("H5py", "h5py" in sys.modules))
- optionals.append(self.__formatOptionalLibraries("FabIO", "fabio" in sys.modules))
-
- try:
- import h5py.version
- if h5py.version.hdf5_version_tuple >= (1, 10, 2):
- # 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 = [
- ("HDF5 LZ4 filter", FILTER_LZ4),
- ("HDF5 Bitshuffle filter", FILTER_BITSHUFFLE),
- ]
- for name, filterId in filters:
- isAvailable = h5py.h5z.filter_avail(filterId)
- optionals.append(self.__formatOptionalFilters(name, isAvailable))
- else:
- optionals.append(self.__formatOptionalLibraries("hdf5plugin", "hdf5plugin" in sys.modules))
- except ImportError:
- pass
-
- # Access to the logo in SVG or PNG
- logo = icons.getQFile("silx:" + os.path.join("gui", "logo", "silx"))
-
- info = dict(
- application_name=self.__applicationName,
- esrf_url="http://www.esrf.eu",
- project_url="https://github.com/silx-kit/silx",
- silx_version=silx._version.version,
- qt_binding=qt.BINDING,
- qt_version=qt.qVersion(),
- python_version=sys.version.replace("\n", "<br />"),
- optional_lib="<br />".join(optionals),
- silx_image_path=logo.fileName()
- )
-
- self.__label.setText(message.format(**info))
- self.__updateSize()
-
- def __updateSize(self):
- """Force the size to a QMessageBox like size."""
- screenSize = qt.QApplication.desktop().availableGeometry(qt.QCursor.pos()).size()
- hardLimit = min(screenSize.width() - 480, 1000)
- if screenSize.width() <= 1024:
- hardLimit = screenSize.width()
- softLimit = min(screenSize.width() / 2, 420)
-
- layoutMinimumSize = self.layout().totalMinimumSize()
- width = layoutMinimumSize.width()
- if width > softLimit:
- width = softLimit
- if width > hardLimit:
- width = hardLimit
-
- height = layoutMinimumSize.height()
- self.setFixedSize(width, height)
-
- @staticmethod
- def about(parent, applicationName):
- """Displays a silx about box with title and text text.
-
- :param qt.QWidget parent: The parent widget
- :param str title: The title of the dialog
- :param str applicationName: The content of the dialog
- """
- dialog = About(parent)
- dialog.setApplicationName(applicationName)
- dialog.exec_()
diff --git a/silx/app/view/ApplicationContext.py b/silx/app/view/ApplicationContext.py
deleted file mode 100644
index 8693848..0000000
--- a/silx/app/view/ApplicationContext.py
+++ /dev/null
@@ -1,194 +0,0 @@
-# coding: utf-8
-# /*##########################################################################
-# Copyright (C) 2016-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__ = "23/05/2018"
-
-import weakref
-import logging
-
-import silx
-from silx.gui.data.DataViews import DataViewHooks
-from silx.gui.colors import Colormap
-from silx.gui.dialog.ColormapDialog import ColormapDialog
-
-
-_logger = logging.getLogger(__name__)
-
-
-class ApplicationContext(DataViewHooks):
- """
- Store the conmtext of the application
-
- It overwrites the DataViewHooks to custom the use of the DataViewer for
- the silx view application.
-
- - Create a single colormap shared with all the views
- - Create a single colormap dialog shared with all the views
- """
-
- def __init__(self, parent, settings=None):
- self.__parent = weakref.ref(parent)
- self.__defaultColormap = None
- self.__defaultColormapDialog = None
- self.__settings = settings
- self.__recentFiles = []
-
- def getSettings(self):
- """Returns actual application settings.
-
- :rtype: qt.QSettings
- """
- return self.__settings
-
- def restoreLibrarySettings(self):
- """Restore the library settings, which must be done early"""
- settings = self.__settings
- if settings is None:
- return
- settings.beginGroup("library")
- plotBackend = settings.value("plot.backend", "")
- plotImageYAxisOrientation = settings.value("plot-image.y-axis-orientation", "")
- settings.endGroup()
-
- if plotBackend != "":
- silx.config.DEFAULT_PLOT_BACKEND = plotBackend
- if plotImageYAxisOrientation != "":
- silx.config.DEFAULT_PLOT_IMAGE_Y_AXIS_ORIENTATION = plotImageYAxisOrientation
-
- def restoreSettings(self):
- """Restore the settings of all the application"""
- settings = self.__settings
- if settings is None:
- return
- parent = self.__parent()
- parent.restoreSettings(settings)
-
- settings.beginGroup("colormap")
- byteArray = settings.value("default", None)
- if byteArray is not None:
- try:
- colormap = Colormap()
- colormap.restoreState(byteArray)
- self.__defaultColormap = colormap
- except Exception:
- _logger.debug("Backtrace", exc_info=True)
- settings.endGroup()
-
- self.__recentFiles = []
- settings.beginGroup("recent-files")
- for index in range(1, 10 + 1):
- if not settings.contains("path%d" % index):
- break
- filePath = settings.value("path%d" % index)
- self.__recentFiles.append(filePath)
- settings.endGroup()
-
- def saveSettings(self):
- """Save the settings of all the application"""
- settings = self.__settings
- if settings is None:
- return
- parent = self.__parent()
- parent.saveSettings(settings)
-
- if self.__defaultColormap is not None:
- settings.beginGroup("colormap")
- settings.setValue("default", self.__defaultColormap.saveState())
- 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.endGroup()
-
- settings.beginGroup("recent-files")
- for index in range(0, 11):
- key = "path%d" % (index + 1)
- if index < len(self.__recentFiles):
- filePath = self.__recentFiles[index]
- settings.setValue(key, filePath)
- else:
- settings.remove(key)
- settings.endGroup()
-
- def getRecentFiles(self):
- """Returns the list of recently opened files.
-
- The list is limited to the last 10 entries. The newest file path is
- in first.
-
- :rtype: List[str]
- """
- return self.__recentFiles
-
- def pushRecentFile(self, filePath):
- """Push a new recent file to the list.
-
- If the file is duplicated in the list, all duplications are removed
- before inserting the new filePath.
-
- If the list becan bigger than 10 items, oldest paths are removed.
-
- :param filePath: File path to push
- """
- # Remove old occurencies
- self.__recentFiles[:] = (f for f in self.__recentFiles if f != filePath)
- self.__recentFiles.insert(0, filePath)
- while len(self.__recentFiles) > 10:
- self.__recentFiles.pop()
-
- def clearRencentFiles(self):
- """Clear the history of the rencent files.
- """
- self.__recentFiles[:] = []
-
- def getColormap(self, view):
- """Returns a default colormap.
-
- Override from DataViewHooks
-
- :rtype: Colormap
- """
- if self.__defaultColormap is None:
- self.__defaultColormap = Colormap(name="viridis")
- return self.__defaultColormap
-
- def getColormapDialog(self, view):
- """Returns a shared color dialog as default for all the views.
-
- Override from DataViewHooks
-
- :rtype: ColorDialog
- """
- if self.__defaultColormapDialog is None:
- parent = self.__parent()
- if parent is None:
- return None
- dialog = ColormapDialog(parent=parent)
- dialog.setModal(False)
- self.__defaultColormapDialog = dialog
- return self.__defaultColormapDialog
diff --git a/silx/app/view/CustomNxdataWidget.py b/silx/app/view/CustomNxdataWidget.py
deleted file mode 100644
index 72c9940..0000000
--- a/silx/app/view/CustomNxdataWidget.py
+++ /dev/null
@@ -1,1008 +0,0 @@
-# coding: utf-8
-# /*##########################################################################
-# Copyright (C) 2016-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.
-#
-# ############################################################################*/
-
-"""Widget to custom NXdata groups"""
-
-__authors__ = ["V. Valls"]
-__license__ = "MIT"
-__date__ = "15/06/2018"
-
-import logging
-import numpy
-import weakref
-
-from silx.gui import qt
-from silx.io import commonh5
-import silx.io.nxdata
-from silx.gui.hdf5._utils import Hdf5DatasetMimeData
-from silx.gui.data.TextFormatter import TextFormatter
-from silx.gui.hdf5.Hdf5Formatter import Hdf5Formatter
-from silx.gui import icons
-
-
-_logger = logging.getLogger(__name__)
-_formatter = TextFormatter()
-_hdf5Formatter = Hdf5Formatter(textFormatter=_formatter)
-
-
-class _RowItems(qt.QStandardItem):
- """Define the list of items used for a specific row."""
-
- def type(self):
- return qt.QStandardItem.UserType + 1
-
- def getRowItems(self):
- """Returns the list of items used for a specific row.
-
- The first item should be this class.
-
- :rtype: List[qt.QStandardItem]
- """
- raise NotImplementedError()
-
-
-class _DatasetItemRow(_RowItems):
- """Define a row which can contain a dataset."""
-
- def __init__(self, label="", dataset=None):
- """Constructor"""
- super(_DatasetItemRow, self).__init__(label)
- self.setEditable(False)
- self.setDropEnabled(False)
- self.setDragEnabled(False)
-
- self.__name = qt.QStandardItem()
- self.__name.setEditable(False)
- self.__name.setDropEnabled(True)
-
- self.__type = qt.QStandardItem()
- self.__type.setEditable(False)
- self.__type.setDropEnabled(False)
- self.__type.setDragEnabled(False)
-
- self.__shape = qt.QStandardItem()
- self.__shape.setEditable(False)
- self.__shape.setDropEnabled(False)
- self.__shape.setDragEnabled(False)
-
- self.setDataset(dataset)
-
- def getDefaultFormatter(self):
- """Get the formatter used to display dataset informations.
-
- :rtype: Hdf5Formatter
- """
- return _hdf5Formatter
-
- def setDataset(self, dataset):
- """Set the dataset stored in this item.
-
- :param Union[numpy.ndarray,h5py.Dataset,silx.io.commonh5.Dataset] dataset:
- The dataset to store.
- """
- self.__dataset = dataset
- if self.__dataset is not None:
- name = self.__dataset.name
-
- if silx.io.is_dataset(dataset):
- type_ = self.getDefaultFormatter().humanReadableType(dataset)
- shape = self.getDefaultFormatter().humanReadableShape(dataset)
-
- if dataset.shape is None:
- icon_name = "item-none"
- elif len(dataset.shape) < 4:
- icon_name = "item-%ddim" % len(dataset.shape)
- else:
- icon_name = "item-ndim"
- icon = icons.getQIcon(icon_name)
- else:
- type_ = ""
- shape = ""
- icon = qt.QIcon()
- else:
- name = ""
- type_ = ""
- shape = ""
- icon = qt.QIcon()
-
- self.__icon = icon
- self.__name.setText(name)
- self.__name.setDragEnabled(self.__dataset is not None)
- self.__name.setIcon(self.__icon)
- self.__type.setText(type_)
- self.__shape.setText(shape)
-
- parent = self.parent()
- if parent is not None:
- self.parent()._datasetUpdated()
-
- def getDataset(self):
- """Returns the dataset stored within the item."""
- return self.__dataset
-
- def getRowItems(self):
- """Returns the list of items used for a specific row.
-
- The first item should be this class.
-
- :rtype: List[qt.QStandardItem]
- """
- return [self, self.__name, self.__type, self.__shape]
-
-
-class _DatasetAxisItemRow(_DatasetItemRow):
- """Define a row describing an axis."""
-
- def __init__(self):
- """Constructor"""
- super(_DatasetAxisItemRow, self).__init__()
-
- def setAxisId(self, axisId):
- """Set the id of the axis (the first axis is 0)
-
- :param int axisId: Identifier of this axis.
- """
- self.__axisId = axisId
- label = "Axis %d" % (axisId + 1)
- self.setText(label)
-
- def getAxisId(self):
- """Returns the identifier of this axis.
-
- :rtype: int
- """
- return self.__axisId
-
-
-class _NxDataItem(qt.QStandardItem):
- """
- Define a custom NXdata.
- """
-
- def __init__(self):
- """Constructor"""
- qt.QStandardItem.__init__(self)
- self.__error = None
- self.__title = None
- self.__axes = []
- self.__virtual = None
-
- item = _DatasetItemRow("Signal", None)
- self.appendRow(item.getRowItems())
- self.__signal = item
-
- self.setEditable(False)
- self.setDragEnabled(False)
- self.setDropEnabled(False)
- self.__setError(None)
-
- def getRowItems(self):
- """Returns the list of items used for a specific row.
-
- The first item should be this class.
-
- :rtype: List[qt.QStandardItem]
- """
- row = [self]
- for _ in range(3):
- item = qt.QStandardItem("")
- item.setEditable(False)
- item.setDragEnabled(False)
- item.setDropEnabled(False)
- row.append(item)
- return row
-
- def _datasetUpdated(self):
- """Called when the NXdata contained of the item have changed.
-
- It invalidates the NXdata stored and send an event `sigNxdataUpdated`.
- """
- self.__virtual = None
- self.__setError(None)
- model = self.model()
- if model is not None:
- model.sigNxdataUpdated.emit(self.index())
-
- def createVirtualGroup(self):
- """Returns a new virtual Group using a NeXus NXdata structure to store
- data
-
- :rtype: silx.io.commonh5.Group
- """
- name = ""
- if self.__title is not None:
- name = self.__title
- virtual = commonh5.Group(name)
- virtual.attrs["NX_class"] = "NXdata"
-
- if self.__title is not None:
- virtual.attrs["title"] = self.__title
-
- if self.__signal is not None:
- signal = self.__signal.getDataset()
- if signal is not None:
- # Could be done using a link instead of a copy
- node = commonh5.DatasetProxy("signal", target=signal)
- virtual.attrs["signal"] = "signal"
- virtual.add_node(node)
-
- axesAttr = []
- for i, axis in enumerate(self.__axes):
- if axis is None:
- name = "."
- else:
- axis = axis.getDataset()
- if axis is None:
- name = "."
- else:
- name = "axis%d" % i
- node = commonh5.DatasetProxy(name, target=axis)
- virtual.add_node(node)
- axesAttr.append(name)
-
- if axesAttr != []:
- virtual.attrs["axes"] = numpy.array(axesAttr)
-
- validator = silx.io.nxdata.NXdata(virtual)
- if not validator.is_valid:
- message = "<html>"
- message += "This NXdata is not consistant"
- message += "<ul>"
- for issue in validator.issues:
- message += "<li>%s</li>" % issue
- message += "</ul>"
- message += "</html>"
- self.__setError(message)
- else:
- self.__setError(None)
- return virtual
-
- def isValid(self):
- """Returns true if the stored NXdata is valid
-
- :rtype: bool
- """
- return self.__error is None
-
- def getVirtualGroup(self):
- """Returns a cached virtual Group using a NeXus NXdata structure to
- store data.
-
- If the stored NXdata was invalidated, :meth:`createVirtualGroup` is
- internally called to update the cache.
-
- :rtype: silx.io.commonh5.Group
- """
- if self.__virtual is None:
- self.__virtual = self.createVirtualGroup()
- return self.__virtual
-
- def getTitle(self):
- """Returns the title of the NXdata
-
- :rtype: str
- """
- return self.text()
-
- def setTitle(self, title):
- """Set the title of the NXdata
-
- :param str title: The title of this NXdata
- """
- self.setText(title)
-
- def __setError(self, error):
- """Set the error message in case of the current state of the stored
- NXdata is not valid.
-
- :param str error: Message to display
- """
- self.__error = error
- style = qt.QApplication.style()
- if error is None:
- message = ""
- icon = style.standardIcon(qt.QStyle.SP_DirLinkIcon)
- else:
- message = error
- icon = style.standardIcon(qt.QStyle.SP_MessageBoxCritical)
- self.setIcon(icon)
- self.setToolTip(message)
-
- def getError(self):
- """Returns the error message in case the NXdata is not valid.
-
- :rtype: str"""
- return self.__error
-
- def setSignalDataset(self, dataset):
- """Set the dataset to use as signal with this NXdata.
-
- :param Union[numpy.ndarray,h5py.Dataset,silx.io.commonh5.Dataset] dataset:
- The dataset to use as signal.
- """
-
- self.__signal.setDataset(dataset)
- self._datasetUpdated()
-
- def getSignalDataset(self):
- """Returns the dataset used as signal.
-
- :rtype: Union[numpy.ndarray,h5py.Dataset,silx.io.commonh5.Dataset]
- """
- return self.__signal.getDataset()
-
- def setAxesDatasets(self, datasets):
- """Set all the available dataset used as axes.
-
- Axes will be created or removed from the GUI in order to provide the
- same amount of requested axes.
-
- A `None` element is an axes with no dataset.
-
- :param List[Union[numpy.ndarray,h5py.Dataset,silx.io.commonh5.Dataset,None]] datasets:
- List of dataset to use as axes.
- """
- for i, dataset in enumerate(datasets):
- if i < len(self.__axes):
- mustAppend = False
- item = self.__axes[i]
- else:
- mustAppend = True
- item = _DatasetAxisItemRow()
- item.setAxisId(i)
- item.setDataset(dataset)
- if mustAppend:
- self.__axes.append(item)
- self.appendRow(item.getRowItems())
-
- # Clean up extra axis
- for i in range(len(datasets), len(self.__axes)):
- item = self.__axes.pop(len(datasets))
- self.removeRow(item.row())
-
- self._datasetUpdated()
-
- def getAxesDatasets(self):
- """Returns available axes as dataset.
-
- A `None` element is an axes with no dataset.
-
- :rtype: List[Union[numpy.ndarray,h5py.Dataset,silx.io.commonh5.Dataset,None]]
- """
- datasets = []
- for axis in self.__axes:
- datasets.append(axis.getDataset())
- return datasets
-
-
-class _Model(qt.QStandardItemModel):
- """Model storing a list of custom NXdata items.
-
- Supports drag and drop of datasets.
- """
-
- sigNxdataUpdated = qt.Signal(qt.QModelIndex)
- """Emitted when stored NXdata was edited"""
-
- def __init__(self, parent=None):
- """Constructor"""
- qt.QStandardItemModel.__init__(self, parent)
- root = self.invisibleRootItem()
- root.setDropEnabled(True)
- root.setDragEnabled(False)
-
- def supportedDropActions(self):
- """Inherited method to redefine supported drop actions."""
- return qt.Qt.CopyAction | qt.Qt.MoveAction
-
- def mimeTypes(self):
- """Inherited method to redefine draggable mime types."""
- return [Hdf5DatasetMimeData.MIME_TYPE]
-
- def mimeData(self, indexes):
- """
- Returns an object that contains serialized items of data corresponding
- to the list of indexes specified.
-
- :param List[qt.QModelIndex] indexes: List of indexes
- :rtype: qt.QMimeData
- """
- if len(indexes) > 1:
- return None
- if len(indexes) == 0:
- return None
-
- qindex = indexes[0]
- qindex = self.index(qindex.row(), 0, parent=qindex.parent())
- item = self.itemFromIndex(qindex)
- if isinstance(item, _DatasetItemRow):
- dataset = item.getDataset()
- if dataset is None:
- return None
- else:
- mimeData = Hdf5DatasetMimeData(dataset=item.getDataset())
- else:
- mimeData = None
- return mimeData
-
- def dropMimeData(self, mimedata, action, row, column, parentIndex):
- """Inherited method to handle a drop operation to this model."""
- if action == qt.Qt.IgnoreAction:
- return True
-
- if mimedata.hasFormat(Hdf5DatasetMimeData.MIME_TYPE):
- if row != -1 or column != -1:
- # It is not a drop on a specific item
- return False
- item = self.itemFromIndex(parentIndex)
- if item is None or item is self.invisibleRootItem():
- # Drop at the end
- dataset = mimedata.dataset()
- if silx.io.is_dataset(dataset):
- self.createFromSignal(dataset)
- elif silx.io.is_group(dataset):
- nxdata = dataset
- try:
- self.createFromNxdata(nxdata)
- except ValueError:
- _logger.error("Error while dropping a group as an NXdata")
- _logger.debug("Backtrace", exc_info=True)
- return False
- else:
- _logger.error("Dropping a wrong object")
- return False
- else:
- item = item.parent().child(item.row(), 0)
- if not isinstance(item, _DatasetItemRow):
- # Dropped at a bad place
- return False
- dataset = mimedata.dataset()
- if silx.io.is_dataset(dataset):
- item.setDataset(dataset)
- else:
- _logger.error("Dropping a wrong object")
- return False
- return True
-
- return False
-
- def __getNxdataByTitle(self, title):
- """Returns an NXdata item by its title, else None.
-
- :rtype: Union[_NxDataItem,None]
- """
- for row in range(self.rowCount()):
- qindex = self.index(row, 0)
- item = self.itemFromIndex(qindex)
- if item.getTitle() == title:
- return item
- return None
-
- def findFreeNxdataTitle(self):
- """Returns an NXdata title which is not yet used.
-
- :rtype: str
- """
- for i in range(self.rowCount() + 1):
- name = "NXData #%d" % (i + 1)
- group = self.__getNxdataByTitle(name)
- if group is None:
- break
- return name
-
- def createNewNxdata(self, name=None):
- """Create a new NXdata item.
-
- :param Union[str,None] name: A title for the new NXdata
- """
- item = _NxDataItem()
- if name is None:
- name = self.findFreeNxdataTitle()
- item.setTitle(name)
- self.appendRow(item.getRowItems())
-
- def createFromSignal(self, dataset):
- """Create a new NXdata item from a signal dataset.
-
- This signal will also define an amount of axes according to its number
- of dimensions.
-
- :param Union[numpy.ndarray,h5py.Dataset,silx.io.commonh5.Dataset] dataset:
- A dataset uses as signal.
- """
-
- item = _NxDataItem()
- name = self.findFreeNxdataTitle()
- item.setTitle(name)
- item.setSignalDataset(dataset)
- item.setAxesDatasets([None] * len(dataset.shape))
- self.appendRow(item.getRowItems())
-
- def createFromNxdata(self, nxdata):
- """Create a new custom NXdata item from an existing NXdata group.
-
- If the NXdata is not valid, nothing is created, and an exception is
- returned.
-
- :param Union[h5py.Group,silx.io.commonh5.Group] nxdata: An h5py group
- following the NXData specification.
- :raise ValueError:If `nxdata` is not valid.
- """
- validator = silx.io.nxdata.NXdata(nxdata)
- if validator.is_valid:
- item = _NxDataItem()
- title = validator.title
- if title in [None or ""]:
- title = self.findFreeNxdataTitle()
- item.setTitle(title)
- item.setSignalDataset(validator.signal)
- item.setAxesDatasets(validator.axes)
- self.appendRow(item.getRowItems())
- else:
- raise ValueError("Not a valid NXdata")
-
- def removeNxdataItem(self, item):
- """Remove an NXdata item from this model.
-
- :param _NxDataItem item: An item
- """
- if isinstance(item, _NxDataItem):
- parent = item.parent()
- assert(parent is None)
- model = item.model()
- model.removeRow(item.row())
- else:
- _logger.error("Unexpected item")
-
- def appendAxisToNxdataItem(self, item):
- """Append a new axes to this item (or the NXdata item own by this item).
-
- :param Union[_NxDataItem,qt.QStandardItem] item: An item
- """
- if item is not None and not isinstance(item, _NxDataItem):
- item = item.parent()
- nxdataItem = item
- if isinstance(item, _NxDataItem):
- datasets = nxdataItem.getAxesDatasets()
- datasets.append(None)
- nxdataItem.setAxesDatasets(datasets)
- else:
- _logger.error("Unexpected item")
-
- def removeAxisItem(self, item):
- """Remove an axis item from this model.
-
- :param _DatasetAxisItemRow item: An axis item
- """
- if isinstance(item, _DatasetAxisItemRow):
- axisId = item.getAxisId()
- nxdataItem = item.parent()
- datasets = nxdataItem.getAxesDatasets()
- del datasets[axisId]
- nxdataItem.setAxesDatasets(datasets)
- else:
- _logger.error("Unexpected item")
-
-
-class CustomNxDataToolBar(qt.QToolBar):
- """A specialised toolbar to manage custom NXdata model and items."""
-
- def __init__(self, parent=None):
- """Constructor"""
- super(CustomNxDataToolBar, self).__init__(parent=parent)
- self.__nxdataWidget = None
- self.__initContent()
- # Initialize action state
- self.__currentSelectionChanged(qt.QModelIndex(), qt.QModelIndex())
-
- def __initContent(self):
- """Create all expected actions and set the content of this toolbar."""
- action = qt.QAction("Create a new custom NXdata", self)
- action.setIcon(icons.getQIcon("nxdata-create"))
- action.triggered.connect(self.__createNewNxdata)
- self.addAction(action)
- self.__addNxDataAction = action
-
- action = qt.QAction("Remove the selected NXdata", self)
- action.setIcon(icons.getQIcon("nxdata-remove"))
- action.triggered.connect(self.__removeSelectedNxdata)
- self.addAction(action)
- self.__removeNxDataAction = action
-
- self.addSeparator()
-
- action = qt.QAction("Create a new axis to the selected NXdata", self)
- action.setIcon(icons.getQIcon("nxdata-axis-add"))
- action.triggered.connect(self.__appendNewAxisToSelectedNxdata)
- self.addAction(action)
- self.__addNxDataAxisAction = action
-
- action = qt.QAction("Remove the selected NXdata axis", self)
- action.setIcon(icons.getQIcon("nxdata-axis-remove"))
- action.triggered.connect(self.__removeSelectedAxis)
- self.addAction(action)
- self.__removeNxDataAxisAction = action
-
- def __getSelectedItem(self):
- """Get the selected item from the linked CustomNxdataWidget.
-
- :rtype: qt.QStandardItem
- """
- selectionModel = self.__nxdataWidget.selectionModel()
- index = selectionModel.currentIndex()
- if not index.isValid():
- return
- model = self.__nxdataWidget.model()
- index = model.index(index.row(), 0, index.parent())
- item = model.itemFromIndex(index)
- return item
-
- def __createNewNxdata(self):
- """Create a new NXdata item to the linked CustomNxdataWidget."""
- if self.__nxdataWidget is None:
- return
- model = self.__nxdataWidget.model()
- model.createNewNxdata()
-
- def __removeSelectedNxdata(self):
- """Remove the NXdata item currently selected in the linked
- CustomNxdataWidget."""
- if self.__nxdataWidget is None:
- return
- model = self.__nxdataWidget.model()
- item = self.__getSelectedItem()
- model.removeNxdataItem(item)
-
- def __appendNewAxisToSelectedNxdata(self):
- """Append a new axis to the NXdata item currently selected in the
- linked CustomNxdataWidget."""
- if self.__nxdataWidget is None:
- return
- model = self.__nxdataWidget.model()
- item = self.__getSelectedItem()
- model.appendAxisToNxdataItem(item)
-
- def __removeSelectedAxis(self):
- """Remove the axis item currently selected in the linked
- CustomNxdataWidget."""
- if self.__nxdataWidget is None:
- return
- model = self.__nxdataWidget.model()
- item = self.__getSelectedItem()
- model.removeAxisItem(item)
-
- def setCustomNxDataWidget(self, widget):
- """Set the linked CustomNxdataWidget to this toolbar."""
- assert(isinstance(widget, CustomNxdataWidget))
- if self.__nxdataWidget is not None:
- selectionModel = self.__nxdataWidget.selectionModel()
- selectionModel.currentChanged.disconnect(self.__currentSelectionChanged)
- self.__nxdataWidget = widget
- if self.__nxdataWidget is not None:
- selectionModel = self.__nxdataWidget.selectionModel()
- selectionModel.currentChanged.connect(self.__currentSelectionChanged)
-
- def __currentSelectionChanged(self, current, previous):
- """Update the actions according to the linked CustomNxdataWidget
- item selection"""
- if not current.isValid():
- item = None
- else:
- model = self.__nxdataWidget.model()
- index = model.index(current.row(), 0, current.parent())
- 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))
-
-
-class _HashDropZones(qt.QStyledItemDelegate):
- """Delegate item displaying a drop zone when the item do not contains
- dataset."""
-
- def __init__(self, parent=None):
- """Constructor"""
- super(_HashDropZones, self).__init__(parent)
- pen = qt.QPen()
- pen.setColor(qt.QColor("#D0D0D0"))
- pen.setStyle(qt.Qt.DotLine)
- pen.setWidth(2)
- self.__dropPen = pen
-
- def paint(self, painter, option, index):
- """
- Paint the item
-
- :param qt.QPainter painter: A painter
- :param qt.QStyleOptionViewItem option: Options of the item to paint
- :param qt.QModelIndex index: Index of the item to paint
- """
- displayDropZone = False
- if index.isValid():
- model = index.model()
- rowIndex = model.index(index.row(), 0, index.parent())
- rowItem = model.itemFromIndex(rowIndex)
- if isinstance(rowItem, _DatasetItemRow):
- displayDropZone = rowItem.getDataset() is None
-
- if displayDropZone:
- painter.save()
-
- # Draw background if selected
- if option.state & qt.QStyle.State_Selected:
- colorGroup = qt.QPalette.Inactive
- if option.state & qt.QStyle.State_Active:
- colorGroup = qt.QPalette.Active
- if not option.state & qt.QStyle.State_Enabled:
- colorGroup = qt.QPalette.Disabled
- brush = option.palette.brush(colorGroup, qt.QPalette.Highlight)
- painter.fillRect(option.rect, brush)
-
- painter.setPen(self.__dropPen)
- painter.drawRect(option.rect.adjusted(3, 3, -3, -3))
- painter.restore()
- else:
- qt.QStyledItemDelegate.paint(self, painter, option, index)
-
-
-class CustomNxdataWidget(qt.QTreeView):
- """Widget providing a table displaying and allowing to custom virtual
- NXdata."""
-
- sigNxdataItemUpdated = qt.Signal(qt.QStandardItem)
- """Emitted when the NXdata from an NXdata item was edited"""
-
- sigNxdataItemRemoved = qt.Signal(qt.QStandardItem)
- """Emitted when an NXdata item was removed"""
-
- def __init__(self, parent=None):
- """Constructor"""
- qt.QTreeView.__init__(self, parent=None)
- self.__model = _Model(self)
- self.__model.setColumnCount(4)
- self.__model.setHorizontalHeaderLabels(["Name", "Dataset", "Type", "Shape"])
- self.setModel(self.__model)
-
- self.setItemDelegateForColumn(1, _HashDropZones(self))
-
- self.__model.sigNxdataUpdated.connect(self.__nxdataUpdate)
- self.__model.rowsAboutToBeRemoved.connect(self.__rowsAboutToBeRemoved)
- self.__model.rowsAboutToBeInserted.connect(self.__rowsAboutToBeInserted)
-
- header = self.header()
- if qt.qVersion() < "5.0":
- setResizeMode = header.setResizeMode
- else:
- setResizeMode = header.setSectionResizeMode
- setResizeMode(0, qt.QHeaderView.ResizeToContents)
- setResizeMode(1, qt.QHeaderView.Stretch)
- setResizeMode(2, qt.QHeaderView.ResizeToContents)
- setResizeMode(3, qt.QHeaderView.ResizeToContents)
-
- self.setSelectionMode(qt.QAbstractItemView.SingleSelection)
- self.setDropIndicatorShown(True)
- self.setDragDropOverwriteMode(True)
- self.setDragEnabled(True)
- self.viewport().setAcceptDrops(True)
-
- self.setContextMenuPolicy(qt.Qt.CustomContextMenu)
- self.customContextMenuRequested[qt.QPoint].connect(self.__executeContextMenu)
-
- def __rowsAboutToBeInserted(self, parentIndex, start, end):
- if qt.qVersion()[0:2] == "5.":
- # FIXME: workaround for https://github.com/silx-kit/silx/issues/1919
- # Uses of ResizeToContents looks to break nice update of cells with Qt5
- # This patch make the view blinking
- self.repaint()
-
- def __rowsAboutToBeRemoved(self, parentIndex, start, end):
- """Called when an item was removed from the model."""
- items = []
- model = self.model()
- for index in range(start, end):
- qindex = model.index(index, 0, parent=parentIndex)
- item = self.__model.itemFromIndex(qindex)
- if isinstance(item, _NxDataItem):
- items.append(item)
- for item in items:
- self.sigNxdataItemRemoved.emit(item)
-
- if qt.qVersion()[0:2] == "5.":
- # FIXME: workaround for https://github.com/silx-kit/silx/issues/1919
- # Uses of ResizeToContents looks to break nice update of cells with Qt5
- # This patch make the view blinking
- self.repaint()
-
- def __nxdataUpdate(self, index):
- """Called when a virtual NXdata was updated from the model."""
- model = self.model()
- item = model.itemFromIndex(index)
- self.sigNxdataItemUpdated.emit(item)
-
- def createDefaultContextMenu(self, index):
- """Create a default context menu at this position.
-
- :param qt.QModelIndex index: Index of the item
- """
- index = self.__model.index(index.row(), 0, parent=index.parent())
- item = self.__model.itemFromIndex(index)
-
- menu = qt.QMenu()
-
- weakself = weakref.proxy(self)
-
- if isinstance(item, _NxDataItem):
- action = qt.QAction("Add a new axis", menu)
- action.triggered.connect(lambda: weakself.model().appendAxisToNxdataItem(item))
- action.setIcon(icons.getQIcon("nxdata-axis-add"))
- action.setIconVisibleInMenu(True)
- menu.addAction(action)
- menu.addSeparator()
- action = qt.QAction("Remove this NXdata", menu)
- action.triggered.connect(lambda: weakself.model().removeNxdataItem(item))
- action.setIcon(icons.getQIcon("remove"))
- action.setIconVisibleInMenu(True)
- menu.addAction(action)
- else:
- if isinstance(item, _DatasetItemRow):
- if item.getDataset() is not None:
- action = qt.QAction("Remove this dataset", menu)
- action.triggered.connect(lambda: item.setDataset(None))
- menu.addAction(action)
-
- if isinstance(item, _DatasetAxisItemRow):
- menu.addSeparator()
- action = qt.QAction("Remove this axis", menu)
- action.triggered.connect(lambda: weakself.model().removeAxisItem(item))
- action.setIcon(icons.getQIcon("remove"))
- action.setIconVisibleInMenu(True)
- menu.addAction(action)
-
- return menu
-
- def __executeContextMenu(self, point):
- """Execute the context menu at this position."""
- index = self.indexAt(point)
- menu = self.createDefaultContextMenu(index)
- if menu is None or menu.isEmpty():
- return
- menu.exec_(qt.QCursor.pos())
-
- def removeDatasetsFrom(self, root):
- """
- Remove all datasets provided by this root
-
- :param root: The root file of datasets to remove
- """
- for row in range(self.__model.rowCount()):
- qindex = self.__model.index(row, 0)
- item = self.model().itemFromIndex(qindex)
-
- edited = False
- datasets = item.getAxesDatasets()
- for i, dataset in enumerate(datasets):
- if dataset is not None:
- # That's an approximation, IS can't be used as h5py generates
- # To objects for each requests to a node
- if dataset.file.filename == root.file.filename:
- datasets[i] = None
- edited = True
- if edited:
- item.setAxesDatasets(datasets)
-
- dataset = item.getSignalDataset()
- if dataset is not None:
- # That's an approximation, IS can't be used as h5py generates
- # To objects for each requests to a node
- if dataset.file.filename == root.file.filename:
- item.setSignalDataset(None)
-
- def replaceDatasetsFrom(self, removedRoot, loadedRoot):
- """
- Replace any dataset from any NXdata items using the same dataset name
- from another root.
-
- Usually used when a file was synchronized.
-
- :param removedRoot: The h5py root file which is replaced
- (which have to be removed)
- :param loadedRoot: The new h5py root file which have to be used
- instread.
- """
- for row in range(self.__model.rowCount()):
- qindex = self.__model.index(row, 0)
- item = self.model().itemFromIndex(qindex)
-
- edited = False
- datasets = item.getAxesDatasets()
- for i, dataset in enumerate(datasets):
- newDataset = self.__replaceDatasetRoot(dataset, removedRoot, loadedRoot)
- if dataset is not newDataset:
- datasets[i] = newDataset
- edited = True
- if edited:
- item.setAxesDatasets(datasets)
-
- dataset = item.getSignalDataset()
- newDataset = self.__replaceDatasetRoot(dataset, removedRoot, loadedRoot)
- if dataset is not newDataset:
- item.setSignalDataset(newDataset)
-
- def __replaceDatasetRoot(self, dataset, fromRoot, toRoot):
- """
- Replace the dataset by the same dataset name from another root.
- """
- if dataset is None:
- return None
-
- if dataset.file is None:
- # Not from the expected root
- return dataset
-
- # That's an approximation, IS can't be used as h5py generates
- # To objects for each requests to a node
- if dataset.file.filename == fromRoot.file.filename:
- # Try to find the same dataset name
- try:
- return toRoot[dataset.name]
- except Exception:
- _logger.debug("Backtrace", exc_info=True)
- return None
- else:
- # Not from the expected root
- return dataset
-
- def selectedItems(self):
- """Returns the list of selected items containing NXdata
-
- :rtype: List[qt.QStandardItem]
- """
- result = []
- for qindex in self.selectedIndexes():
- if qindex.column() != 0:
- continue
- if not qindex.isValid():
- continue
- item = self.__model.itemFromIndex(qindex)
- if not isinstance(item, _NxDataItem):
- continue
- result.append(item)
- return result
-
- def selectedNxdata(self):
- """Returns the list of selected NXdata
-
- :rtype: List[silx.io.commonh5.Group]
- """
- result = []
- for qindex in self.selectedIndexes():
- if qindex.column() != 0:
- continue
- if not qindex.isValid():
- continue
- item = self.__model.itemFromIndex(qindex)
- if not isinstance(item, _NxDataItem):
- continue
- result.append(item.getVirtualGroup())
- return result
diff --git a/silx/app/view/DataPanel.py b/silx/app/view/DataPanel.py
deleted file mode 100644
index 5d87381..0000000
--- a/silx/app/view/DataPanel.py
+++ /dev/null
@@ -1,192 +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__ = "12/10/2018"
-
-import logging
-import os.path
-
-from silx.gui import qt
-from silx.gui.data.DataViewerFrame import DataViewerFrame
-
-
-_logger = logging.getLogger(__name__)
-
-
-class _HeaderLabel(qt.QLabel):
-
- def __init__(self, parent=None):
- qt.QLabel.__init__(self, parent=parent)
- self.setFrameShape(qt.QFrame.StyledPanel)
-
- def sizeHint(self):
- return qt.QSize(10, 30)
-
- def minimumSizeHint(self):
- return qt.QSize(10, 30)
-
- def setData(self, filename, path):
- if filename == "" and path == "":
- text = ""
- elif filename == "":
- text = path
- else:
- text = "%s::%s" % (filename, path)
- self.setText(text)
- tooltip = ""
- template = "<li><b>%s</b>: %s</li>"
- tooltip += template % ("Directory", os.path.dirname(filename))
- tooltip += template % ("File name", os.path.basename(filename))
- tooltip += template % ("Data path", path)
- tooltip = "<ul>%s</ul>" % tooltip
- tooltip = "<html>%s</html>" % tooltip
- self.setToolTip(tooltip)
-
- def paintEvent(self, event):
- painter = qt.QPainter(self)
-
- opt = qt.QStyleOptionHeader()
- opt.orientation = qt.Qt.Horizontal
- opt.text = self.text()
- opt.textAlignment = self.alignment()
- opt.direction = self.layoutDirection()
- opt.fontMetrics = self.fontMetrics()
- opt.palette = self.palette()
- opt.state = qt.QStyle.State_Active
- opt.position = qt.QStyleOptionHeader.Beginning
- style = self.style()
-
- # Background
- margin = -1
- opt.rect = self.rect().adjusted(margin, margin, -margin, -margin)
- style.drawControl(qt.QStyle.CE_HeaderSection, opt, painter, None)
-
- # Frame border and text
- super(_HeaderLabel, self).paintEvent(event)
-
-
-class DataPanel(qt.QWidget):
-
- def __init__(self, parent=None, context=None):
- qt.QWidget.__init__(self, parent=parent)
-
- self.__customNxdataItem = None
-
- self.__dataTitle = _HeaderLabel(self)
- self.__dataTitle.setVisible(False)
-
- self.__dataViewer = DataViewerFrame(self)
- self.__dataViewer.setGlobalHooks(context)
-
- layout = qt.QVBoxLayout(self)
- layout.setSpacing(0)
- layout.setContentsMargins(0, 0, 0, 0)
- layout.addWidget(self.__dataTitle)
- layout.addWidget(self.__dataViewer)
-
- def getData(self):
- return self.__dataViewer.data()
-
- def getCustomNxdataItem(self):
- return self.__customNxdataItem
-
- def setData(self, data):
- self.__customNxdataItem = None
- self.__dataViewer.setData(data)
- self.__dataTitle.setVisible(data is not None)
- if data is not None:
- self.__dataTitle.setVisible(True)
- if hasattr(data, "name"):
- if hasattr(data, "file"):
- filename = str(data.file.filename)
- else:
- filename = ""
- path = data.name
- else:
- filename = ""
- path = ""
- self.__dataTitle.setData(filename, path)
-
- def setCustomDataItem(self, item):
- self.__customNxdataItem = item
- if item is not None:
- data = item.getVirtualGroup()
- else:
- data = None
- self.__dataViewer.setData(data)
- self.__dataTitle.setVisible(item is not None)
- if item is not None:
- text = item.text()
- self.__dataTitle.setText(text)
-
- def removeDatasetsFrom(self, root):
- """
- Remove all datasets provided by this root
-
- .. note:: This function do not update data stored inside
- customNxdataItem cause in the silx-view context this item is
- already updated on his own.
-
- :param root: The root file of datasets to remove
- """
- data = self.__dataViewer.data()
- if data is not None:
- if data.file is not None:
- # That's an approximation, IS can't be used as h5py generates
- # To objects for each requests to a node
- if data.file.filename == root.file.filename:
- self.__dataViewer.setData(None)
-
- def replaceDatasetsFrom(self, removedH5, loadedH5):
- """
- Replace any dataset from any NXdata items using the same dataset name
- from another root.
-
- Usually used when a file was synchronized.
-
- .. note:: This function do not update data stored inside
- customNxdataItem cause in the silx-view context this item is
- already updated on his own.
-
- :param removedRoot: The h5py root file which is replaced
- (which have to be removed)
- :param loadedRoot: The new h5py root file which have to be used
- instread.
- """
-
- data = self.__dataViewer.data()
- if data is not None:
- if data.file is not None:
- if data.file.filename == removedH5.file.filename:
- # Try to synchonize the viewed data
- try:
- # TODO: It have to update the data without changing the
- # view which is not so easy
- newData = loadedH5[data.name]
- self.__dataViewer.setData(newData)
- except Exception:
- _logger.debug("Backtrace", exc_info=True)
diff --git a/silx/app/view/Viewer.py b/silx/app/view/Viewer.py
deleted file mode 100644
index dd4d075..0000000
--- a/silx/app/view/Viewer.py
+++ /dev/null
@@ -1,971 +0,0 @@
-# coding: utf-8
-# /*##########################################################################
-# Copyright (C) 2016-2020 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__ = "15/01/2019"
-
-
-import os
-import collections
-import logging
-import functools
-
-import silx.io.nxdata
-from silx.gui import qt
-from silx.gui import icons
-import silx.gui.hdf5
-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
-
-
-_logger = logging.getLogger(__name__)
-
-
-class Viewer(qt.QMainWindow):
- """
- This window allows to browse a data file like images or HDF5 and it's
- content.
- """
-
- def __init__(self, parent=None, settings=None):
- """
- Constructor
- """
-
- qt.QMainWindow.__init__(self, parent)
- self.setWindowTitle("Silx viewer")
-
- silxIcon = icons.getQIcon("silx")
- self.setWindowIcon(silxIcon)
-
- self.__context = self.createApplicationContext(settings)
- self.__context.restoreLibrarySettings()
-
- self.__dialogState = None
- self.__customNxDataItem = None
- self.__treeview = silx.gui.hdf5.Hdf5TreeView(self)
- self.__treeview.setExpandsOnDoubleClick(False)
- """Silx HDF5 TreeView"""
-
- rightPanel = qt.QSplitter(self)
- 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
- treeModel = silx.gui.hdf5.Hdf5TreeModel(self.__treeview, ownFiles=False)
- treeModel.sigH5pyObjectLoaded.connect(self.__h5FileLoaded)
- treeModel.sigH5pyObjectRemoved.connect(self.__h5FileRemoved)
- treeModel.sigH5pyObjectSynchronized.connect(self.__h5FileSynchonized)
- treeModel.setDatasetDragEnabled(True)
- 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)
-
- self.__treeview.setModel(self.__treeModelSorted)
- rightPanel.addWidget(self.__treeWindow)
-
- self.__customNxdata = CustomNxdataWidget(self)
- self.__customNxdata.setSelectionBehavior(qt.QAbstractItemView.SelectRows)
- # optimise the rendering
- self.__customNxdata.setUniformRowHeights(True)
- self.__customNxdata.setIconSize(qt.QSize(16, 16))
- self.__customNxdata.setExpandsOnDoubleClick(False)
-
- self.__customNxdataWindow = self.__createCustomNxdataWindow(self.__customNxdata)
- self.__customNxdataWindow.setVisible(False)
- rightPanel.addWidget(self.__customNxdataWindow)
-
- rightPanel.setStretchFactor(1, 1)
- rightPanel.setCollapsible(0, False)
- rightPanel.setCollapsible(1, False)
-
- self.__dataPanel = DataPanel(self, self.__context)
-
- spliter = qt.QSplitter(self)
- spliter.addWidget(rightPanel)
- spliter.addWidget(self.__dataPanel)
- spliter.setStretchFactor(1, 1)
- spliter.setCollapsible(0, False)
- spliter.setCollapsible(1, False)
- self.__splitter = spliter
-
- main_panel = qt.QWidget(self)
- layout = qt.QVBoxLayout()
- layout.addWidget(spliter)
- layout.setStretchFactor(spliter, 1)
- main_panel.setLayout(layout)
-
- self.setCentralWidget(main_panel)
-
- self.__treeview.activated.connect(self.displaySelectedData)
- self.__customNxdata.activated.connect(self.displaySelectedCustomData)
- self.__customNxdata.sigNxdataItemRemoved.connect(self.__customNxdataRemoved)
- self.__customNxdata.sigNxdataItemUpdated.connect(self.__customNxdataUpdated)
- self.__treeview.addContextMenuCallback(self.customContextMenu)
-
- treeModel = self.__treeview.findHdf5TreeModel()
- columns = list(treeModel.COLUMN_IDS)
- columns.remove(treeModel.VALUE_COLUMN)
- columns.remove(treeModel.NODE_COLUMN)
- columns.remove(treeModel.DESCRIPTION_COLUMN)
- columns.insert(1, treeModel.DESCRIPTION_COLUMN)
- self.__treeview.header().setSections(columns)
-
- self._iconUpward = icons.getQIcon('plot-yup')
- self._iconDownward = icons.getQIcon('plot-ydown')
-
- self.createActions()
- self.createMenus()
- self.__context.restoreSettings()
-
- def createApplicationContext(self, settings):
- return ApplicationContext(self, settings)
-
- def __createTreeWindow(self, treeView):
- toolbar = qt.QToolBar(self)
- toolbar.setIconSize(qt.QSize(16, 16))
- toolbar.setStyleSheet("QToolBar { border: 0px }")
-
- action = qt.QAction(toolbar)
- action.setIcon(icons.getQIcon("view-refresh"))
- action.setText("Refresh")
- action.setToolTip("Refresh all selected items")
- action.triggered.connect(self.__refreshSelected)
- 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)
- action.setIcon(icons.getQIcon("tree-expand-all"))
- 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))
- toolbar.addAction(action)
- treeView.addAction(action)
- self.__expandAllAction = action
-
- action = qt.QAction(toolbar)
- action.setIcon(icons.getQIcon("tree-collapse-all"))
- 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))
- toolbar.addAction(action)
- treeView.addAction(action)
- self.__collapseAllAction = action
-
- action = qt.QAction("&Sort file content", toolbar)
- action.setIcon(icons.getQIcon("tree-sort"))
- action.setToolTip("Toggle sorting of file content")
- action.setCheckable(True)
- action.setChecked(True)
- action.triggered.connect(self.setContentSorted)
- toolbar.addAction(action)
- treeView.addAction(action)
- self._sortContentAction = action
-
- widget = qt.QWidget(self)
- layout = qt.QVBoxLayout(widget)
- layout.setContentsMargins(0, 0, 0, 0)
- layout.setSpacing(0)
- layout.addWidget(toolbar)
- 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
- """
- 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:
- qt.QApplication.restoreOverrideCursor()
- return
-
- model = self.__treeview.findHdf5TreeModel()
- for h5 in h5files:
- self.__synchronizeH5pyObject(h5)
-
- model = self.__treeview.model()
- itemSelection = qt.QItemSelection()
- for rootRow, relativePath in selectedItems:
- rootIndex = model.index(rootRow, 0, qt.QModelIndex())
- index = self.__indexFromPath(model, rootIndex, relativePath)
- if index is None:
- continue
- indexEnd = model.index(index.row(), model.columnCount() - 1, index.parent())
- itemSelection.select(index, indexEnd)
- selection.select(itemSelection, qt.QItemSelectionModel.ClearAndSelect)
-
- qt.QApplication.restoreOverrideCursor()
-
- def __synchronizeH5pyObject(self, h5):
- 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
- row = model.h5pyObjectRow(h5)
- index = self.__treeview.model().index(row, 0, qt.QModelIndex())
- paths = self.__getPathFromExpandedNodes(self.__treeview, index)
- model.removeH5pyObject(h5)
- model.insertFile(filename, row)
- index = self.__treeview.model().index(row, 0, qt.QModelIndex())
- self.__expandNodesFromPaths(self.__treeview, index, paths)
-
- def __getRelativePath(self, model, rootIndex, index):
- """Returns a relative path from an index to his rootIndex.
-
- If the path is empty the index is also the rootIndex.
- """
- path = ""
- while index.isValid():
- if index == rootIndex:
- return path
- name = model.data(index)
- if path == "":
- path = name
- else:
- path = name + "/" + path
- index = index.parent()
-
- # index is not a children of rootIndex
- raise ValueError("index is not a children of the rootIndex")
-
- def __getPathFromExpandedNodes(self, view, rootIndex):
- """Return relative path from the root index of the extended nodes"""
- model = view.model()
- rootPath = None
- paths = []
- indexes = [rootIndex]
- while len(indexes):
- index = indexes.pop(0)
- if not view.isExpanded(index):
- continue
-
- node = model.data(index, role=silx.gui.hdf5.Hdf5TreeModel.H5PY_ITEM_ROLE)
- path = node._getCanonicalName()
- if rootPath is None:
- rootPath = path
- path = path[len(rootPath):]
- paths.append(path)
-
- for child in range(model.rowCount(index)):
- childIndex = model.index(child, 0, index)
- indexes.append(childIndex)
- return paths
-
- def __indexFromPath(self, model, rootIndex, path):
- elements = path.split("/")
- if elements[0] == "":
- elements.pop(0)
- index = rootIndex
- while len(elements) != 0:
- element = elements.pop(0)
- found = False
- for child in range(model.rowCount(index)):
- childIndex = model.index(child, 0, index)
- name = model.data(childIndex)
- if element == name:
- index = childIndex
- found = True
- break
- if not found:
- return None
- return index
-
- def __expandNodesFromPaths(self, view, rootIndex, paths):
- model = view.model()
- for path in paths:
- index = self.__indexFromPath(model, rootIndex, path)
- if index is not None:
- view.setExpanded(index, True)
-
- def __expandAllSelected(self):
- """Expand all selected items of the tree.
-
- The depth is fixed to avoid infinite loop with recurssive links.
- """
- qt.QApplication.setOverrideCursor(qt.Qt.WaitCursor)
-
- selection = self.__treeview.selectionModel()
- indexes = selection.selectedIndexes()
- model = self.__treeview.model()
- while len(indexes) > 0:
- index = indexes.pop(0)
- if isinstance(index, tuple):
- index, depth = index
- else:
- depth = 0
- if index.column() != 0:
- continue
-
- if depth > 10:
- # Avoid infinite loop with recursive links
- break
-
- if model.hasChildren(index):
- self.__treeview.setExpanded(index, True)
- for row in range(model.rowCount(index)):
- childIndex = model.index(row, 0, index)
- indexes.append((childIndex, depth + 1))
- qt.QApplication.restoreOverrideCursor()
-
- def __collapseAllSelected(self):
- """Collapse all selected items of the tree.
-
- The depth is fixed to avoid infinite loop with recurssive links.
- """
- selection = self.__treeview.selectionModel()
- indexes = selection.selectedIndexes()
- model = self.__treeview.model()
- while len(indexes) > 0:
- index = indexes.pop(0)
- if isinstance(index, tuple):
- index, depth = index
- else:
- depth = 0
- if index.column() != 0:
- continue
-
- if depth > 10:
- # Avoid infinite loop with recursive links
- break
-
- if model.hasChildren(index):
- self.__treeview.setExpanded(index, False)
- for row in range(model.rowCount(index)):
- childIndex = model.index(row, 0, index)
- indexes.append((childIndex, depth + 1))
-
- def __createCustomNxdataWindow(self, customNxdataWidget):
- toolbar = CustomNxDataToolBar(self)
- toolbar.setCustomNxDataWidget(customNxdataWidget)
- toolbar.setIconSize(qt.QSize(16, 16))
- toolbar.setStyleSheet("QToolBar { border: 0px }")
-
- widget = qt.QWidget(self)
- layout = qt.QVBoxLayout(widget)
- layout.setContentsMargins(0, 0, 0, 0)
- layout.setSpacing(0)
- layout.addWidget(toolbar)
- layout.addWidget(customNxdataWidget)
- return widget
-
- 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)
- self.__customNxdata.removeDatasetsFrom(removedH5)
- removedH5.close()
-
- def __h5FileSynchonized(self, removedH5, loadedH5):
- self.__dataPanel.replaceDatasetsFrom(removedH5, loadedH5)
- self.__customNxdata.replaceDatasetsFrom(removedH5, loadedH5)
- removedH5.close()
-
- def closeEvent(self, event):
- self.__context.saveSettings()
-
- # Clean up as much as possible Python objects
- 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
-
- :param qt.QSettings settings: Initialized settings
- """
- isFullScreen = bool(self.windowState() & qt.Qt.WindowFullScreen)
- if isFullScreen:
- # show in normal to catch the normal geometry
- self.showNormal()
-
- settings.beginGroup("mainwindow")
- settings.setValue("size", self.size())
- settings.setValue("pos", self.pos())
- settings.setValue("full-screen", isFullScreen)
- settings.endGroup()
-
- settings.beginGroup("mainlayout")
- settings.setValue("spliter", self.__splitter.sizes())
- settings.setValue("spliter2", self.__splitter2.sizes())
- isVisible = self.__customNxdataWindow.isVisible()
- settings.setValue("custom-nxdata-window-visible", isVisible)
- settings.endGroup()
-
- settings.beginGroup("content")
- isSorted = self._sortContentAction.isChecked()
- settings.setValue("is-sorted", isSorted)
- settings.endGroup()
-
- if isFullScreen:
- self.showFullScreen()
-
- def restoreSettings(self, settings):
- """Restore the window settings using this settings object
-
- :param qt.QSettings settings: Initialized settings
- """
- settings.beginGroup("mainwindow")
- 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
- settings.endGroup()
-
- settings.beginGroup("mainlayout")
- try:
- data = settings.value("spliter")
- data = [int(d) for d in data]
- self.__splitter.setSizes(data)
- except Exception:
- _logger.debug("Backtrace", exc_info=True)
- try:
- data = settings.value("spliter2")
- data = [int(d) for d in data]
- self.__splitter2.setSizes(data)
- 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
- 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
- self.setContentSorted(isSorted)
- settings.endGroup()
-
- if not pos.isNull():
- self.move(pos)
- if not size.isNull():
- self.resize(size)
- if isFullScreen:
- self.showFullScreen()
-
- def createActions(self):
- action = qt.QAction("E&xit", self)
- action.setShortcuts(qt.QKeySequence.Quit)
- action.setStatusTip("Exit the application")
- action.triggered.connect(self.close)
- self._exitAction = action
-
- action = qt.QAction("&Open...", self)
- action.setStatusTip("Open a file")
- action.triggered.connect(self.open)
- self._openAction = action
-
- action = qt.QAction("Open Recent", self)
- action.setStatusTip("Open a recently openned file")
- action.triggered.connect(self.open)
- self._openRecentAction = action
-
- action = qt.QAction("Close All", self)
- action.setStatusTip("Close all opened files")
- action.triggered.connect(self.closeAll)
- self._closeAllAction = action
-
- action = qt.QAction("&About", self)
- action.setStatusTip("Show the application's About box")
- 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)
- action.setStatusTip("Select plot rendering backend")
- self._plotBackendSelection = action
-
- menu = qt.QMenu()
- action.setMenu(menu)
- group = qt.QActionGroup(self)
- group.setExclusive(True)
-
- action = qt.QAction("matplotlib", self)
- action.setStatusTip("Plot will be rendered using matplotlib")
- action.setCheckable(True)
- action.triggered.connect(self.__forceMatplotlibBackend)
- group.addAction(action)
- menu.addAction(action)
- self._usePlotWithMatplotlib = action
-
- action = qt.QAction("OpenGL", self)
- action.setStatusTip("Plot will be rendered using OpenGL")
- action.setCheckable(True)
- action.triggered.connect(self.__forceOpenglBackend)
- group.addAction(action)
- menu.addAction(action)
- self._usePlotWithOpengl = action
-
- # Plot image orientation
-
- action = qt.QAction("Default plot image y-axis orientation", self)
- action.setStatusTip("Select the default y-axis orientation used by plot displaying images")
- self._plotImageOrientation = action
-
- menu = qt.QMenu()
- action.setMenu(menu)
- group = qt.QActionGroup(self)
- group.setExclusive(True)
-
- action = qt.QAction("Downward, origin on top", self)
- action.setIcon(self._iconDownward)
- action.setStatusTip("Plot images will use a downward Y-axis orientation")
- action.setCheckable(True)
- action.triggered.connect(self.__forcePlotImageDownward)
- group.addAction(action)
- menu.addAction(action)
- self._useYAxisOrientationDownward = action
-
- action = qt.QAction("Upward, origin on bottom", self)
- action.setIcon(self._iconUpward)
- action.setStatusTip("Plot images will use a upward Y-axis orientation")
- action.setCheckable(True)
- action.triggered.connect(self.__forcePlotImageUpward)
- group.addAction(action)
- menu.addAction(action)
- self._useYAxisOrientationUpward = 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.setCheckable(True)
- action.setShortcut(qt.QKeySequence(qt.Qt.Key_F6))
- action.toggled.connect(self.__toggleCustomNxdataWindow)
- self._displayCustomNxdataWindow = action
-
- def __toggleCustomNxdataWindow(self):
- isVisible = self._displayCustomNxdataWindow.isChecked()
- self.__customNxdataWindow.setVisible(isVisible)
-
- def __updateFileMenu(self):
- files = self.__context.getRecentFiles()
- self._openRecentAction.setEnabled(len(files) != 0)
- menu = None
- if len(files) != 0:
- menu = qt.QMenu()
- for filePath in files:
- baseName = os.path.basename(filePath)
- action = qt.QAction(baseName, self)
- action.setToolTip(filePath)
- action.triggered.connect(functools.partial(self.__openRecentFile, filePath))
- menu.addAction(action)
- menu.addSeparator()
- baseName = os.path.basename(filePath)
- action = qt.QAction("Clear history", self)
- action.setToolTip("Clear the history of the recent files")
- action.triggered.connect(self.__clearRecentFile)
- menu.addAction(action)
- self._openRecentAction.setMenu(menu)
-
- def __clearRecentFile(self):
- self.__context.clearRencentFiles()
-
- def __openRecentFile(self, filePath):
- self.appendFile(filePath)
-
- def __updateOptionMenu(self):
- """Update the state of the checked options as it is based on global
- environment values."""
-
- # plot backend
-
- action = self._plotBackendSelection
- title = action.text().split(": ", 1)[0]
- action.setText("%s: %s" % (title, silx.config.DEFAULT_PLOT_BACKEND))
-
- action = self._usePlotWithMatplotlib
- action.setChecked(silx.config.DEFAULT_PLOT_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"])
- title = action.text().split(" (", 1)[0]
- if not action.isChecked():
- title += " (applied after application restart)"
- action.setText(title)
-
- # plot orientation
-
- action = self._plotImageOrientation
- if silx.config.DEFAULT_PLOT_IMAGE_Y_AXIS_ORIENTATION == "downward":
- action.setIcon(self._iconDownward)
- else:
- action.setIcon(self._iconUpward)
- action.setIconVisibleInMenu(True)
-
- action = self._useYAxisOrientationDownward
- 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")
- title = action.text().split(" (", 1)[0]
- if not action.isChecked():
- title += " (applied after application restart)"
- action.setText(title)
-
- def createMenus(self):
- fileMenu = self.menuBar().addMenu("&File")
- fileMenu.addAction(self._openAction)
- fileMenu.addAction(self._openRecentAction)
- fileMenu.addAction(self._closeAllAction)
- fileMenu.addSeparator()
- fileMenu.addAction(self._exitAction)
- fileMenu.aboutToShow.connect(self.__updateFileMenu)
-
- optionMenu = self.menuBar().addMenu("&Options")
- optionMenu.addAction(self._plotImageOrientation)
- optionMenu.addAction(self._plotBackendSelection)
- optionMenu.aboutToShow.connect(self.__updateOptionMenu)
-
- viewMenu = self.menuBar().addMenu("&Views")
- viewMenu.addAction(self._displayCustomNxdataWindow)
-
- helpMenu = self.menuBar().addMenu("&Help")
- helpMenu.addAction(self._aboutAction)
- helpMenu.addAction(self._documentationAction)
-
- def open(self):
- dialog = self.createFileDialog()
- if self.__dialogState is None:
- currentDirectory = os.getcwd()
- dialog.setDirectory(currentDirectory)
- else:
- dialog.restoreState(self.__dialogState)
-
- result = dialog.exec_()
- if not result:
- return
-
- self.__dialogState = dialog.saveState()
-
- filenames = dialog.selectedFiles()
- for filename in filenames:
- self.appendFile(filename)
-
- def closeAll(self):
- """Close all currently opened files"""
- model = self.__treeview.findHdf5TreeModel()
- model.clear()
-
- def createFileDialog(self):
- dialog = qt.QFileDialog(self)
- dialog.setWindowTitle("Open")
- dialog.setModal(True)
-
- # NOTE: hdf5plugin have to be loaded before
- extensions = collections.OrderedDict()
- for description, ext in silx.io.supported_extensions().items():
- extensions[description] = " ".join(sorted(list(ext)))
-
- # 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():
- exts = exts.split(" ")
- all_supported_extensions.update(exts)
- all_supported_extensions = sorted(list(all_supported_extensions))
-
- filters = []
- filters.append("All supported files (%s)" % " ".join(all_supported_extensions))
- for name, extension in extensions.items():
- filters.append("%s (%s)" % (name, extension))
- filters.append("All files (*)")
-
- dialog.setNameFilters(filters)
- dialog.setFileMode(qt.QFileDialog.ExistingFiles)
- return dialog
-
- def about(self):
- 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 setContentSorted(self, sort):
- """Set whether file content should be sorted or not.
-
- :param bool sort:
- """
- sort = bool(sort)
- if sort != self.isContentSorted():
-
- # save expanded nodes
- pathss = []
- root = qt.QModelIndex()
- model = self.__treeview.model()
- for i in range(model.rowCount(root)):
- index = model.index(i, 0, root)
- paths = self.__getPathFromExpandedNodes(self.__treeview, index)
- pathss.append(paths)
-
- self.__treeview.setModel(
- self.__treeModelSorted if sort else self.__treeModelSorted.sourceModel())
- self._sortContentAction.setChecked(self.isContentSorted())
-
- # restore expanded nodes
- model = self.__treeview.model()
- for i in range(model.rowCount(root)):
- index = model.index(i, 0, root)
- paths = pathss.pop(0)
- self.__expandNodesFromPaths(self.__treeview, index, paths)
-
- def isContentSorted(self):
- """Returns whether the file content is sorted or not.
-
- :rtype: bool
- """
- return self.__treeview.model() is self.__treeModelSorted
-
- def __forcePlotImageDownward(self):
- silx.config.DEFAULT_PLOT_IMAGE_Y_AXIS_ORIENTATION = "downward"
-
- def __forcePlotImageUpward(self):
- silx.config.DEFAULT_PLOT_IMAGE_Y_AXIS_ORIENTATION = "upward"
-
- def __forceMatplotlibBackend(self):
- silx.config.DEFAULT_PLOT_BACKEND = "matplotlib"
-
- def __forceOpenglBackend(self):
- 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):
- """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
- data = selected[0]
- self.__dataPanel.setData(data)
- else:
- _logger.debug("Too many data selected")
-
- def displayData(self, data):
- """Called to update the dataviewer with a secific data.
- """
- self.__dataPanel.setData(data)
-
- def displaySelectedCustomData(self):
- selected = list(self.__customNxdata.selectedItems())
- if len(selected) == 1:
- # Update the viewer for a single selection
- item = selected[0]
- self.__dataPanel.setCustomDataItem(item)
- else:
- _logger.debug("Too many items selected")
-
- def __customNxdataRemoved(self, item):
- if self.__dataPanel.getCustomNxdataItem() is item:
- self.__dataPanel.setCustomDataItem(None)
-
- def __customNxdataUpdated(self, item):
- if self.__dataPanel.getCustomNxdataItem() is item:
- self.__dataPanel.setCustomDataItem(item)
-
- def __makeSureCustomNxDataWindowIsVisible(self):
- if not self.__customNxdataWindow.isVisible():
- self.__customNxdataWindow.setVisible(True)
- self._displayCustomNxdataWindow.setChecked(True)
-
- def useAsNewCustomSignal(self, h5dataset):
- self.__makeSureCustomNxDataWindowIsVisible()
- model = self.__customNxdata.model()
- model.createFromSignal(h5dataset)
-
- def useAsNewCustomNxdata(self, h5nxdata):
- self.__makeSureCustomNxDataWindowIsVisible()
- model = self.__customNxdata.model()
- model.createFromNxdata(h5nxdata)
-
- def customContextMenu(self, event):
- """Called to populate the context menu
-
- :param silx.gui.hdf5.Hdf5ContextMenuEvent event: Event
- containing expected information to populate the context menu
- """
- selectedObjects = event.source().selectedH5Nodes(ignoreBrokenLinks=False)
- menu = event.menu()
-
- if not menu.isEmpty():
- menu.addSeparator()
-
- for obj in selectedObjects:
- h5 = obj.h5py_object
-
- name = obj.name
- if name.startswith("/"):
- name = name[1:]
- if name == "":
- name = "the root"
-
- action = qt.QAction("Show %s" % name, event.source())
- action.triggered.connect(lambda: self.displayData(h5))
- menu.addAction(action)
-
- if silx.io.is_dataset(h5):
- action = qt.QAction("Use as a new custom signal", event.source())
- action.triggered.connect(lambda: self.useAsNewCustomSignal(h5))
- menu.addAction(action)
-
- if silx.io.is_group(h5) and silx.io.nxdata.is_valid_nxdata(h5):
- action = qt.QAction("Use as a new custom NXdata", event.source())
- action.triggered.connect(lambda: self.useAsNewCustomNxdata(h5))
- menu.addAction(action)
-
- 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))
- menu.addAction(action)
- action = qt.QAction("Synchronize %s" % obj.local_filename, event.source())
- action.triggered.connect(lambda: self.__synchronizeH5pyObject(h5))
- menu.addAction(action)
diff --git a/silx/app/view/__init__.py b/silx/app/view/__init__.py
deleted file mode 100644
index 229c44e..0000000
--- a/silx/app/view/__init__.py
+++ /dev/null
@@ -1,28 +0,0 @@
-# coding: utf-8
-# /*##########################################################################
-# Copyright (C) 2016-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.
-#
-# ############################################################################*/
-"""Package containing source code of the `silx view` application"""
-
-__authors__ = ["V. Valls"]
-__license__ = "MIT"
-__date__ = "07/06/2018"
diff --git a/silx/app/view/main.py b/silx/app/view/main.py
deleted file mode 100644
index a1369c1..0000000
--- a/silx/app/view/main.py
+++ /dev/null
@@ -1,171 +0,0 @@
-# coding: utf-8
-# /*##########################################################################
-# Copyright (C) 2016-2020 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.
-#
-# ############################################################################*/
-"""Module containing launcher of the `silx view` application"""
-
-__authors__ = ["V. Valls"]
-__license__ = "MIT"
-__date__ = "17/01/2019"
-
-import argparse
-import logging
-import os
-import signal
-import sys
-
-
-_logger = logging.getLogger(__name__)
-"""Module logger"""
-
-
-def createParser():
- parser = argparse.ArgumentParser(description=__doc__)
- parser.add_argument(
- 'files',
- nargs=argparse.ZERO_OR_MORE,
- help='Data file to show (h5 file, edf files, spec files)')
- parser.add_argument(
- '--debug',
- dest="debug",
- action="store_true",
- default=False,
- help='Set logging system in debug mode')
- parser.add_argument(
- '--use-opengl-plot',
- dest="use_opengl_plot",
- action="store_true",
- default=False,
- help='Use OpenGL for plots (instead of matplotlib)')
- parser.add_argument(
- '-f', '--fresh',
- dest="fresh_preferences",
- action="store_true",
- default=False,
- help='Start the application using new fresh user preferences')
- parser.add_argument(
- '--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)')
- return parser
-
-
-def createWindow(parent, settings):
- from .Viewer import Viewer
- window = Viewer(parent=None, settings=settings)
- return window
-
-
-def mainQt(options):
- """Part of the main depending on Qt"""
- if options.debug:
- logging.root.setLevel(logging.DEBUG)
-
- #
- # Import most of the things here to be sure to use the right logging level
- #
-
- # 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
-
- try:
- # it should be loaded before h5py
- import hdf5plugin # noqa
- except ImportError:
- _logger.debug("Backtrace", exc_info=True)
-
- 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
-
- 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
-
- timer = qt.QTimer()
- timer.start(500)
- # Application have to wake up Python interpreter, else SIGINT is not
- # catched
- timer.timeout.connect(lambda: 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)
-
- 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:
- # TODO: Would be nice to add a process widget and a cancel button
- try:
- window.appendFile(filename)
- except IOError as e:
- _logger.error(e.args[0])
- _logger.debug("Backtrace", exc_info=True)
-
- window.show()
- result = app.exec_()
- # remove ending warnings relative to QTimer
- app.deleteLater()
- 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/setup.py b/silx/app/view/setup.py
deleted file mode 100644
index fa076cb..0000000
--- a/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/silx/app/view/test/__init__.py b/silx/app/view/test/__init__.py
deleted file mode 100644
index 8e64948..0000000
--- a/silx/app/view/test/__init__.py
+++ /dev/null
@@ -1,41 +0,0 @@
-# coding: utf-8
-# /*##########################################################################
-#
-# Copyright (c) 2016-2017 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__ = "07/06/2018"
-
-import unittest
-
-from silx.test.utils import test_options
-
-
-def suite():
- test_suite = unittest.TestSuite()
- if test_options.WITH_QT_TEST:
- from . import test_launcher
- from . import test_view
- test_suite.addTest(test_view.suite())
- test_suite.addTest(test_launcher.suite())
- return test_suite
diff --git a/silx/app/view/test/test_launcher.py b/silx/app/view/test/test_launcher.py
deleted file mode 100644
index 5f03de9..0000000
--- a/silx/app/view/test/test_launcher.py
+++ /dev/null
@@ -1,151 +0,0 @@
-# coding: utf-8
-# /*##########################################################################
-#
-# Copyright (c) 2016-2017 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.
-#
-# ###########################################################################*/
-"""Module testing silx.app.view"""
-
-__authors__ = ["V. Valls"]
-__license__ = "MIT"
-__date__ = "07/06/2018"
-
-
-import os
-import shutil
-import sys
-import tempfile
-import unittest
-import logging
-import subprocess
-
-from silx.test.utils import test_options
-from .. import main
-from silx import __main__ as silx_main
-
-_logger = logging.getLogger(__name__)
-
-
-@unittest.skipUnless(test_options.WITH_QT_TEST, test_options.WITH_QT_TEST_REASON)
-class TestLauncher(unittest.TestCase):
- """Test command line parsing"""
-
- def testHelp(self):
- # option -h must cause a raise SystemExit or a return 0
- try:
- parser = main.createParser()
- parser.parse_args(["view", "--help"])
- result = 0
- except SystemExit as e:
- result = e.args[0]
- self.assertEqual(result, 0)
-
- def testWrongOption(self):
- try:
- parser = main.createParser()
- parser.parse_args(["view", "--foo"])
- self.fail()
- except SystemExit as e:
- result = e.args[0]
- self.assertNotEqual(result, 0)
-
- def testWrongFile(self):
- try:
- parser = main.createParser()
- result = parser.parse_args(["view", "__file.not.found__"])
- result = 0
- except SystemExit as e:
- result = e.args[0]
- self.assertEqual(result, 0)
-
- def executeAsScript(self, filename, *args):
- """Execute a command line.
-
- Log output as debug in case of bad return code.
- """
- env = self.createTestEnv()
-
- with tempfile.TemporaryDirectory() as tmpdir:
- # Copy file to temporary dir to avoid import from current dir.
- 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)
- out, err = p.communicate()
- _logger.info("Return code: %d", p.returncode)
- try:
- out = out.decode('utf-8')
- except UnicodeError:
- pass
- try:
- err = err.decode('utf-8')
- except UnicodeError:
- pass
-
- if p.returncode != 0:
- _logger.info("stdout:")
- _logger.info("%s", out)
- _logger.info("stderr:")
- _logger.info("%s", err)
- else:
- _logger.debug("stdout:")
- _logger.debug("%s", out)
- _logger.debug("stderr:")
- _logger.debug("%s", err)
- self.assertEqual(p.returncode, 0)
-
- def createTestEnv(self):
- """
- Returns an associated environment with a working project.
- """
- env = dict((str(k), str(v)) for k, v in os.environ.items())
- env["PYTHONPATH"] = os.pathsep.join(sys.path)
- return env
-
- def testExecuteViewHelp(self):
- """Test if the main module is well connected.
-
- Uses subprocess to avoid to parasite the current environment.
- """
- self.executeAsScript(main.__file__, "--help")
-
- def testExecuteSilxViewHelp(self):
- """Test if the main module is well connected.
-
- Uses subprocess to avoid to parasite the current environment.
- """
- self.executeAsScript(silx_main.__file__, "view", "--help")
-
-
-def suite():
- test_suite = unittest.TestSuite()
- loader = unittest.defaultTestLoader.loadTestsFromTestCase
- test_suite.addTest(loader(TestLauncher))
- return test_suite
-
-
-if __name__ == '__main__':
- unittest.main(defaultTest='suite')
diff --git a/silx/app/view/test/test_view.py b/silx/app/view/test/test_view.py
deleted file mode 100644
index 7ea5a2c..0000000
--- a/silx/app/view/test/test_view.py
+++ /dev/null
@@ -1,394 +0,0 @@
-# coding: utf-8
-# /*##########################################################################
-#
-# Copyright (c) 2016-2020 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.
-#
-# ###########################################################################*/
-"""Module testing silx.app.view"""
-
-__authors__ = ["V. Valls"]
-__license__ = "MIT"
-__date__ = "07/06/2018"
-
-
-import unittest
-import weakref
-import numpy
-import tempfile
-import shutil
-import os.path
-import h5py
-
-from silx.gui import qt
-from silx.app.view.Viewer import Viewer
-from silx.app.view.About import About
-from silx.app.view.DataPanel import DataPanel
-from silx.app.view.CustomNxdataWidget import CustomNxdataWidget
-from silx.gui.hdf5._utils import Hdf5DatasetMimeData
-from silx.gui.utils.testutils import TestCaseQt
-from silx.io import commonh5
-
-_tmpDirectory = None
-
-
-def setUpModule():
- global _tmpDirectory
- _tmpDirectory = tempfile.mkdtemp(prefix=__name__)
-
- # 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()
-
-
-def tearDownModule():
- global _tmpDirectory
- shutil.rmtree(_tmpDirectory)
- _tmpDirectory = None
-
-
-class TestViewer(TestCaseQt):
- """Test for Viewer class"""
-
- def testConstruct(self):
- widget = Viewer()
- self.qWaitForWindowExposed(widget)
-
- def testDestroy(self):
- widget = Viewer()
- ref = weakref.ref(widget)
- widget = None
- self.qWaitForDestroy(ref)
-
-
-class TestAbout(TestCaseQt):
- """Test for About box class"""
-
- def testConstruct(self):
- widget = About()
- self.qWaitForWindowExposed(widget)
-
- def testLicense(self):
- widget = About()
- widget.getHtmlLicense()
- self.qWaitForWindowExposed(widget)
-
- def testDestroy(self):
- widget = About()
- ref = weakref.ref(widget)
- widget = None
- self.qWaitForDestroy(ref)
-
-
-class TestDataPanel(TestCaseQt):
-
- def testConstruct(self):
- widget = DataPanel()
- self.qWaitForWindowExposed(widget)
-
- def testDestroy(self):
- widget = DataPanel()
- ref = weakref.ref(widget)
- widget = None
- self.qWaitForDestroy(ref)
-
- def testHeaderLabelPaintEvent(self):
- widget = DataPanel()
- data = numpy.array([1, 2, 3, 4, 5])
- widget.setData(data)
- # Expected to execute HeaderLabel.paintEvent
- widget.setVisible(True)
- self.qWaitForWindowExposed(widget)
-
- def testData(self):
- widget = DataPanel()
- data = numpy.array([1, 2, 3, 4, 5])
- widget.setData(data)
- self.assertIs(widget.getData(), data)
- self.assertIs(widget.getCustomNxdataItem(), None)
-
- def testDataNone(self):
- widget = DataPanel()
- widget.setData(None)
- self.assertIs(widget.getData(), None)
- self.assertIs(widget.getCustomNxdataItem(), None)
-
- def testCustomDataItem(self):
- class CustomDataItemMock(object):
- def getVirtualGroup(self):
- return None
-
- def text(self):
- return ""
-
- data = CustomDataItemMock()
- widget = DataPanel()
- widget.setCustomDataItem(data)
- self.assertIs(widget.getData(), None)
- self.assertIs(widget.getCustomNxdataItem(), data)
-
- def testCustomDataItemNone(self):
- data = None
- widget = DataPanel()
- widget.setCustomDataItem(data)
- self.assertIs(widget.getData(), None)
- self.assertIs(widget.getCustomNxdataItem(), data)
-
- def testRemoveDatasetsFrom(self):
- f = h5py.File(os.path.join(_tmpDirectory, "data.h5"), mode='r')
- try:
- widget = DataPanel()
- widget.setData(f["arrays/scalar"])
- widget.removeDatasetsFrom(f)
- self.assertIs(widget.getData(), None)
- finally:
- widget.setData(None)
- f.close()
-
- def testReplaceDatasetsFrom(self):
- f = h5py.File(os.path.join(_tmpDirectory, "data.h5"), mode='r')
- f2 = h5py.File(os.path.join(_tmpDirectory, "data2.h5"), mode='r')
- try:
- widget = DataPanel()
- widget.setData(f["arrays/scalar"])
- self.assertEqual(widget.getData()[()], 10)
- widget.replaceDatasetsFrom(f, f2)
- self.assertEqual(widget.getData()[()], 20)
- finally:
- widget.setData(None)
- f.close()
- f2.close()
-
-
-class TestCustomNxdataWidget(TestCaseQt):
-
- def testConstruct(self):
- widget = CustomNxdataWidget()
- self.qWaitForWindowExposed(widget)
-
- def testDestroy(self):
- widget = CustomNxdataWidget()
- ref = weakref.ref(widget)
- widget = None
- self.qWaitForDestroy(ref)
-
- def testCreateNxdata(self):
- widget = CustomNxdataWidget()
- model = widget.model()
- model.createNewNxdata()
- model.createNewNxdata("Foo")
- widget.setVisible(True)
- self.qWaitForWindowExposed(widget)
-
- def testCreateNxdataFromDataset(self):
- widget = CustomNxdataWidget()
- model = widget.model()
- signal = commonh5.Dataset("foo", data=numpy.array([[[5]]]))
- model.createFromSignal(signal)
- widget.setVisible(True)
- self.qWaitForWindowExposed(widget)
-
- def testCreateNxdataFromNxdata(self):
- widget = CustomNxdataWidget()
- model = widget.model()
- data = numpy.array([[[5]]])
- nxdata = commonh5.Group("foo")
- nxdata.attrs["NX_class"] = "NXdata"
- nxdata.attrs["signal"] = "signal"
- nxdata.create_dataset("signal", data=data)
- model.createFromNxdata(nxdata)
- widget.setVisible(True)
- self.qWaitForWindowExposed(widget)
-
- def testCreateBadNxdata(self):
- widget = CustomNxdataWidget()
- model = widget.model()
- signal = commonh5.Dataset("foo", data=numpy.array([[[5]]]))
- model.createFromSignal(signal)
- axis = commonh5.Dataset("foo", data=numpy.array([[[5]]]))
- nxdataIndex = model.index(0, 0)
- item = model.itemFromIndex(nxdataIndex)
- item.setAxesDatasets([axis])
- nxdata = item.getVirtualGroup()
- self.assertIsNotNone(nxdata)
- self.assertFalse(item.isValid())
-
- def testRemoveDatasetsFrom(self):
- f = h5py.File(os.path.join(_tmpDirectory, "data.h5"), mode='r')
- try:
- widget = CustomNxdataWidget()
- model = widget.model()
- dataset = f["arrays/integers"]
- model.createFromSignal(dataset)
- widget.removeDatasetsFrom(f)
- finally:
- model.clear()
- f.close()
-
- def testReplaceDatasetsFrom(self):
- f = h5py.File(os.path.join(_tmpDirectory, "data.h5"), mode='r')
- f2 = h5py.File(os.path.join(_tmpDirectory, "data2.h5"), mode='r')
- try:
- widget = CustomNxdataWidget()
- model = widget.model()
- dataset = f["arrays/integers"]
- model.createFromSignal(dataset)
- widget.replaceDatasetsFrom(f, f2)
- finally:
- model.clear()
- f.close()
- f2.close()
-
-
-class TestCustomNxdataWidgetInteraction(TestCaseQt):
- """Test CustomNxdataWidget with user interaction"""
-
- def setUp(self):
- TestCaseQt.setUp(self)
-
- self.widget = CustomNxdataWidget()
- self.model = self.widget.model()
- data = numpy.array([[[5]]])
- dataset = commonh5.Dataset("foo", data=data)
- self.model.createFromSignal(dataset)
- self.selectionModel = self.widget.selectionModel()
-
- def tearDown(self):
- self.selectionModel = None
- self.model.clear()
- self.model = None
- self.widget = None
- TestCaseQt.tearDown(self)
-
- def testSelectedNxdata(self):
- index = self.model.index(0, 0)
- 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)
- items = self.widget.selectedItems()
- self.assertEqual(len(items), 1)
- self.assertIsNot(items[0], None)
- self.assertIsInstance(items[0], qt.QStandardItem)
-
- def testRowsAboutToBeRemoved(self):
- self.model.removeRow(0)
- self.qWaitForWindowExposed(self.widget)
-
- def testPaintItems(self):
- self.widget.expandAll()
- self.widget.setVisible(True)
- self.qWaitForWindowExposed(self.widget)
-
- def testCreateDefaultContextMenu(self):
- nxDataIndex = self.model.index(0, 0)
- menu = self.widget.createDefaultContextMenu(nxDataIndex)
- self.assertIsNot(menu, None)
- self.assertIsInstance(menu, qt.QMenu)
-
- signalIndex = self.model.index(0, 0, nxDataIndex)
- menu = self.widget.createDefaultContextMenu(signalIndex)
- self.assertIsNot(menu, None)
- self.assertIsInstance(menu, qt.QMenu)
-
- axesIndex = self.model.index(1, 0, nxDataIndex)
- menu = self.widget.createDefaultContextMenu(axesIndex)
- self.assertIsNot(menu, None)
- self.assertIsInstance(menu, qt.QMenu)
-
- def testDropNewDataset(self):
- dataset = commonh5.Dataset("foo", numpy.array([1, 2, 3, 4]))
- mimedata = Hdf5DatasetMimeData(dataset=dataset)
- self.model.dropMimeData(mimedata, qt.Qt.CopyAction, -1, -1, qt.QModelIndex())
- self.assertEqual(self.model.rowCount(qt.QModelIndex()), 2)
-
- def testDropNewNxdata(self):
- data = numpy.array([[[5]]])
- nxdata = commonh5.Group("foo")
- nxdata.attrs["NX_class"] = "NXdata"
- nxdata.attrs["signal"] = "signal"
- nxdata.create_dataset("signal", data=data)
- mimedata = Hdf5DatasetMimeData(dataset=nxdata)
- self.model.dropMimeData(mimedata, qt.Qt.CopyAction, -1, -1, qt.QModelIndex())
- self.assertEqual(self.model.rowCount(qt.QModelIndex()), 2)
-
- def testDropAxisDataset(self):
- dataset = commonh5.Dataset("foo", numpy.array([1, 2, 3, 4]))
- mimedata = Hdf5DatasetMimeData(dataset=dataset)
- nxDataIndex = self.model.index(0, 0)
- axesIndex = self.model.index(1, 0, nxDataIndex)
- self.model.dropMimeData(mimedata, qt.Qt.CopyAction, -1, -1, axesIndex)
- self.assertEqual(self.model.rowCount(qt.QModelIndex()), 1)
- item = self.model.itemFromIndex(axesIndex)
- self.assertIsNot(item.getDataset(), None)
-
- def testMimeData(self):
- nxDataIndex = self.model.index(0, 0)
- signalIndex = self.model.index(0, 0, nxDataIndex)
- mimeData = self.model.mimeData([signalIndex])
- self.assertIsNot(mimeData, None)
- self.assertIsInstance(mimeData, qt.QMimeData)
-
- def testRemoveNxdataItem(self):
- nxdataIndex = self.model.index(0, 0)
- item = self.model.itemFromIndex(nxdataIndex)
- self.model.removeNxdataItem(item)
-
- def testAppendAxisToNxdataItem(self):
- nxdataIndex = self.model.index(0, 0)
- item = self.model.itemFromIndex(nxdataIndex)
- self.model.appendAxisToNxdataItem(item)
-
- def testRemoveAxisItem(self):
- nxdataIndex = self.model.index(0, 0)
- axesIndex = self.model.index(1, 0, nxdataIndex)
- item = self.model.itemFromIndex(axesIndex)
- self.model.removeAxisItem(item)
-
-
-def suite():
- test_suite = unittest.TestSuite()
- loader = unittest.defaultTestLoader.loadTestsFromTestCase
- test_suite.addTest(loader(TestViewer))
- test_suite.addTest(loader(TestAbout))
- test_suite.addTest(loader(TestDataPanel))
- test_suite.addTest(loader(TestCustomNxdataWidget))
- test_suite.addTest(loader(TestCustomNxdataWidgetInteraction))
- return test_suite
-
-
-if __name__ == '__main__':
- unittest.main(defaultTest='suite')
diff --git a/silx/app/view/utils.py b/silx/app/view/utils.py
deleted file mode 100644
index 80167c8..0000000
--- a/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)