diff options
Diffstat (limited to 'silx')
1032 files changed, 0 insertions, 207848 deletions
diff --git a/silx/__init__.py b/silx/__init__.py deleted file mode 100644 index 2892572..0000000 --- a/silx/__init__.py +++ /dev/null @@ -1,61 +0,0 @@ -# coding: utf-8 -# /*########################################################################## -# -# Copyright (c) 2015-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. -# -# ###########################################################################*/ -"""The silx package contains the following main sub-packages: - -- silx.gui: Qt widgets for data visualization and data file browsing -- silx.image: Some processing functions for 2D images -- silx.io: Reading and writing data files (HDF5/NeXus, SPEC, ...) -- silx.math: Some processing functions for 1D, 2D, 3D, nD arrays -- silx.opencl: OpenCL-based data processing -- silx.sx: High-level silx functions suited for (I)Python console. -- silx.utils: Miscellaneous convenient functions - -See silx documentation: http://www.silx.org/doc/silx/latest/ -""" - -from __future__ import absolute_import, print_function, division - -__authors__ = ["Jérôme Kieffer"] -__license__ = "MIT" -__date__ = "26/04/2018" - -import os as _os -import logging as _logging -from ._config import Config as _Config - -config = _Config() -"""Global configuration shared with the whole library""" - -# Attach a do nothing logging handler for silx -_logging.getLogger(__name__).addHandler(_logging.NullHandler()) - - -project = _os.path.basename(_os.path.dirname(_os.path.abspath(__file__))) - -try: - from ._version import __date__ as date # noqa - from ._version import version, version_info, hexversion, strictversion # noqa -except ImportError: - raise RuntimeError("Do NOT use %s from its sources: build it and use the built version" % project) diff --git a/silx/__main__.py b/silx/__main__.py deleted file mode 100644 index f832a09..0000000 --- a/silx/__main__.py +++ /dev/null @@ -1,75 +0,0 @@ -#!/usr/bin/env python -# coding: utf-8 -# /*########################################################################## -# -# Copyright (c) 2017-2021 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 module describe silx applications which are available through -the silx launcher. - -Your environment should provide a command `silx`. You can reach help with -`silx --help`, and check the version with `silx --version`. -""" - -__authors__ = ["V. Valls", "P. Knobel"] -__license__ = "MIT" -__date__ = "07/06/2018" - - -import logging -logging.basicConfig() - -import multiprocessing -import sys -from silx.utils.launcher import Launcher -import silx._version - - -def main(): - """Main function of the launcher - - This function is referenced in the setup.py file, to create a - launcher script generated by setuptools. - - :rtype: int - :returns: The execution status - """ - multiprocessing.freeze_support() - - launcher = Launcher(prog="silx", version=silx._version.version) - launcher.add_command("view", - module_name="silx.app.view.main", - description="Browse a data file with a GUI") - launcher.add_command("convert", - module_name="silx.app.convert", - description="Convert and concatenate files into a HDF5 file") - launcher.add_command("test", - module_name="silx.app.test_", - description="Launch silx unittest") - status = launcher.execute(sys.argv) - return status - - -if __name__ == "__main__": - # executed when using python -m PROJECT_NAME - status = main() - sys.exit(status) diff --git a/silx/_config.py b/silx/_config.py deleted file mode 100644 index fb0e409..0000000 --- a/silx/_config.py +++ /dev/null @@ -1,148 +0,0 @@ -#!/usr/bin/env python -# coding: utf-8 -# /*########################################################################## -# -# Copyright (c) 2017-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. -# -# ###########################################################################*/ -"""This module contains library wide configuration. -""" - -__authors__ = ["V. Valls"] -__license__ = "MIT" -__date__ = "09/11/2018" - - -class Config(object): - """ - Class containing shared global configuration for the silx library. - - .. versionadded:: 0.8 - """ - - DEFAULT_PLOT_BACKEND = "matplotlib", "opengl" - """Default plot backend. - - It will be used as default backend for all the next created PlotWidget. - - This attribute can be set with: - - - 'matplotlib' (default) or 'mpl' - - 'opengl', 'gl' - - 'none' - - A :class:`silx.gui.plot.backend.BackendBase.BackendBase` class - - A callable returning backend class or binding name - - If multiple backends are provided, the first available one is used. - - .. versionadded:: 0.8 - """ - - DEFAULT_COLORMAP_NAME = 'gray' - """Default LUT for the plot widgets. - - The available list of names are available in the module - :module:`silx.gui.colors`. - - .. versionadded:: 0.8 - """ - - DEFAULT_PLOT_IMAGE_Y_AXIS_ORIENTATION = 'upward' - """Default Y-axis orientation for plot widget displaying images. - - This attribute can be set with: - - - 'upward' (default), which set the origin to the bottom with an upward - orientation. - - 'downward', which set the origin to the top with a backward orientation. - - It will have an influence on: - - - :class:`silx.gui.plot.StackWidget` - - :class:`silx.gui.plot.ComplexImageView` - - :class:`silx.gui.plot.Plot2D` - - :class:`silx.gui.plot.ImageView` - - .. versionadded:: 0.8 - """ - - DEFAULT_PLOT_CURVE_COLORS = ['#000000', # black - '#0000ff', # blue - '#ff0000', # red - '#00ff00', # green - '#ff66ff', # pink - '#ffff00', # yellow - '#a52a2a', # brown - '#00ffff', # cyan - '#ff00ff', # magenta - '#ff9900', # orange - '#6600ff', # violet - '#a0a0a4', # grey - '#000080', # darkBlue - '#800000', # darkRed - '#008000', # darkGreen - '#008080', # darkCyan - '#800080', # darkMagenta - '#808000', # darkYellow - '#660000'] # darkBrown - """Default list of colors for plot widget displaying curves. - - It will have an influence on: - - - :class:`silx.gui.plot.PlotWidget` - - .. versionadded:: 0.9 - """ - - DEFAULT_PLOT_CURVE_SYMBOL_MODE = False - """Whether to display curves with markers or not by default in PlotWidget. - - It will have an influence on PlotWidget curve items. - - .. versionadded:: 0.10 - """ - - DEFAULT_PLOT_SYMBOL = 'o' - """Default marker of the item. - - It will have an influence on PlotWidget items - - Supported symbols: - - - 'o', 'Circle' - - 'd', 'Diamond' - - 's', 'Square' - - '+', 'Plus' - - 'x', 'Cross' - - '.', 'Point' - - ',', 'Pixel' - - '', 'None' - - .. versionadded:: 0.10 - """ - - DEFAULT_PLOT_SYMBOL_SIZE = 6.0 - """Default marker size of the item. - - It will have an influence on PlotWidget items - - .. versionadded:: 0.10 - """ 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) diff --git a/silx/gui/__init__.py b/silx/gui/__init__.py deleted file mode 100644 index b796e20..0000000 --- a/silx/gui/__init__.py +++ /dev/null @@ -1,49 +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 provides a set of Qt widgets. - -It contains the following sub-packages and modules: - -- silx.gui.colors: Functions to handle colors and colormap -- silx.gui.console: IPython console widget -- silx.gui.data: - Widgets for displaying data arrays using table views and plot widgets -- silx.gui.dialog: Specific dialog widgets -- silx.gui.fit: Widgets for controlling curve fitting -- silx.gui.hdf5: Widgets for displaying content relative to HDF5 format -- silx.gui.icons: Functions to access embedded icons -- silx.gui.plot: Widgets for 1D and 2D plotting and related tools -- silx.gui.plot3d: Widgets for visualizing data in 3D based on OpenGL -- silx.gui.printer: Shared printer used by the library -- silx.gui.qt: Common wrapper over different Python Qt binding -- silx.gui.utils: Miscellaneous helpers for Qt -- silx.gui.widgets: Miscellaneous standalone widgets - -See silx documentation: http://www.silx.org/doc/silx/latest/ -""" - -__authors__ = ["T. Vincent"] -__license__ = "MIT" -__date__ = "23/05/2016" diff --git a/silx/gui/_glutils/Context.py b/silx/gui/_glutils/Context.py deleted file mode 100644 index c62dbb9..0000000 --- a/silx/gui/_glutils/Context.py +++ /dev/null @@ -1,75 +0,0 @@ -# coding: utf-8 -# /*########################################################################## -# -# Copyright (c) 2014-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. -# -# ###########################################################################*/ -"""Abstraction of OpenGL context. - -It defines a way to get current OpenGL context to support multiple -OpenGL contexts. -""" - -__authors__ = ["T. Vincent"] -__license__ = "MIT" -__date__ = "25/07/2016" - -import contextlib - - -class _DEFAULT_CONTEXT(object): - """The default value for OpenGL context""" - pass - -_context = _DEFAULT_CONTEXT -"""The current OpenGL context""" - - -def getCurrent(): - """Returns platform dependent object of current OpenGL context. - - This is useful to associate OpenGL resources with the context they are - created in. - - :return: Platform specific OpenGL context - """ - return _context - - -def setCurrent(context=_DEFAULT_CONTEXT): - """Set a platform dependent OpenGL context - - :param context: Platform dependent GL context - """ - global _context - _context = context - - -@contextlib.contextmanager -def current(context): - """Context manager setting the platform-dependent GL context - - :param context: Platform dependent GL context - """ - previous_context = getCurrent() - setCurrent(context) - yield - setCurrent(previous_context) diff --git a/silx/gui/_glutils/FramebufferTexture.py b/silx/gui/_glutils/FramebufferTexture.py deleted file mode 100644 index e065030..0000000 --- a/silx/gui/_glutils/FramebufferTexture.py +++ /dev/null @@ -1,165 +0,0 @@ -# coding: utf-8 -# /*########################################################################## -# -# Copyright (c) 2014-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. -# -# ###########################################################################*/ -"""Association of a texture and a framebuffer object for off-screen rendering. -""" - -__authors__ = ["T. Vincent"] -__license__ = "MIT" -__date__ = "25/07/2016" - - -import logging - -from . import gl -from .Texture import Texture - - -_logger = logging.getLogger(__name__) - - -class FramebufferTexture(object): - """Framebuffer with a texture. - - Aimed at off-screen rendering to texture. - - :param internalFormat: OpenGL texture internal format - :param shape: Shape (height, width) of the framebuffer and texture - :type shape: 2-tuple of int - :param stencilFormat: Stencil renderbuffer format - :param depthFormat: Depth renderbuffer format - :param kwargs: Extra arguments for :class:`Texture` constructor - """ - - _PACKED_FORMAT = gl.GL_DEPTH24_STENCIL8, gl.GL_DEPTH_STENCIL - - def __init__(self, - internalFormat, - shape, - stencilFormat=gl.GL_DEPTH24_STENCIL8, - depthFormat=gl.GL_DEPTH24_STENCIL8, - **kwargs): - - self._texture = Texture(internalFormat, shape=shape, **kwargs) - self._texture.prepare() - - self._previousFramebuffer = 0 # Used by with statement - - self._name = gl.glGenFramebuffers(1) - - with self: # Bind FBO - # Attachments - gl.glFramebufferTexture2D(gl.GL_FRAMEBUFFER, - gl.GL_COLOR_ATTACHMENT0, - gl.GL_TEXTURE_2D, - self._texture.name, - 0) - - height, width = self._texture.shape - - if stencilFormat is not None: - self._stencilId = gl.glGenRenderbuffers(1) - gl.glBindRenderbuffer(gl.GL_RENDERBUFFER, self._stencilId) - gl.glRenderbufferStorage(gl.GL_RENDERBUFFER, - stencilFormat, - width, height) - gl.glFramebufferRenderbuffer(gl.GL_FRAMEBUFFER, - gl.GL_STENCIL_ATTACHMENT, - gl.GL_RENDERBUFFER, - self._stencilId) - else: - self._stencilId = None - - if depthFormat is not None: - if self._stencilId and depthFormat in self._PACKED_FORMAT: - self._depthId = self._stencilId - else: - self._depthId = gl.glGenRenderbuffers(1) - gl.glBindRenderbuffer(gl.GL_RENDERBUFFER, self._depthId) - gl.glRenderbufferStorage(gl.GL_RENDERBUFFER, - depthFormat, - width, height) - gl.glFramebufferRenderbuffer(gl.GL_FRAMEBUFFER, - gl.GL_DEPTH_ATTACHMENT, - gl.GL_RENDERBUFFER, - self._depthId) - else: - self._depthId = None - - assert (gl.glCheckFramebufferStatus(gl.GL_FRAMEBUFFER) == - gl.GL_FRAMEBUFFER_COMPLETE) - - @property - def shape(self): - """Shape of the framebuffer (height, width)""" - return self._texture.shape - - @property - def texture(self): - """The texture this framebuffer is rendering to. - - The life-cycle of the texture is managed by this object""" - return self._texture - - @property - def name(self): - """OpenGL name of the framebuffer""" - if self._name is not None: - return self._name - else: - raise RuntimeError("No OpenGL framebuffer resource, \ - discard has already been called") - - def bind(self): - """Bind this framebuffer for rendering""" - gl.glBindFramebuffer(gl.GL_FRAMEBUFFER, self.name) - - # with statement - - def __enter__(self): - self._previousFramebuffer = gl.glGetInteger(gl.GL_FRAMEBUFFER_BINDING) - self.bind() - - def __exit__(self, exctype, excvalue, traceback): - gl.glBindFramebuffer(gl.GL_FRAMEBUFFER, self._previousFramebuffer) - self._previousFramebuffer = None - - def discard(self): - """Delete associated OpenGL resources including texture""" - if self._name is not None: - gl.glDeleteFramebuffers(self._name) - self._name = None - - if self._stencilId is not None: - gl.glDeleteRenderbuffers(self._stencilId) - if self._stencilId == self._depthId: - self._depthId = None - self._stencilId = None - if self._depthId is not None: - gl.glDeleteRenderbuffers(self._depthId) - self._depthId = None - - self._texture.discard() # Also discard the texture - else: - _logger.warning("Discard has already been called") diff --git a/silx/gui/_glutils/OpenGLWidget.py b/silx/gui/_glutils/OpenGLWidget.py deleted file mode 100644 index 5e3fcb8..0000000 --- a/silx/gui/_glutils/OpenGLWidget.py +++ /dev/null @@ -1,423 +0,0 @@ -# coding: utf-8 -# /*########################################################################## -# -# Copyright (c) 2017-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. -# -# ###########################################################################*/ -"""This package provides a compatibility layer for OpenGL widget. - -It provides a compatibility layer for Qt OpenGL widget used in silx -across Qt<=5.3 QtOpenGL.QGLWidget and QOpenGLWidget. -""" - -__authors__ = ["T. Vincent"] -__license__ = "MIT" -__date__ = "22/11/2019" - - -import logging -import sys - -from .. import qt -from ..utils.glutils import isOpenGLAvailable -from .._glutils import gl - - -_logger = logging.getLogger(__name__) - - -if not hasattr(qt, 'QOpenGLWidget') and not hasattr(qt, 'QGLWidget'): - OpenGLWidget = None - -else: - if hasattr(qt, 'QOpenGLWidget'): # PyQt>=5.4 - _logger.info('Using QOpenGLWidget') - _BaseOpenGLWidget = qt.QOpenGLWidget - - else: - _logger.info('Using QGLWidget') - _BaseOpenGLWidget = qt.QGLWidget - - class _OpenGLWidget(_BaseOpenGLWidget): - """Wrapper over QOpenGLWidget and QGLWidget""" - - sigOpenGLContextError = qt.Signal(str) - """Signal emitted when an OpenGL context error is detected at runtime. - - It provides the error reason as a str. - """ - - def __init__(self, parent, - alphaBufferSize=0, - depthBufferSize=24, - stencilBufferSize=8, - version=(2, 0), - f=qt.Qt.WindowFlags()): - # True if using QGLWidget, False if using QOpenGLWidget - self.__legacy = not hasattr(qt, 'QOpenGLWidget') - - self.__devicePixelRatio = 1.0 - self.__requestedOpenGLVersion = int(version[0]), int(version[1]) - self.__isValid = False - - if self.__legacy: # QGLWidget - format_ = qt.QGLFormat() - format_.setAlphaBufferSize(alphaBufferSize) - format_.setAlpha(alphaBufferSize != 0) - format_.setDepthBufferSize(depthBufferSize) - format_.setDepth(depthBufferSize != 0) - format_.setStencilBufferSize(stencilBufferSize) - format_.setStencil(stencilBufferSize != 0) - format_.setVersion(*self.__requestedOpenGLVersion) - format_.setDoubleBuffer(True) - - super(_OpenGLWidget, self).__init__(format_, parent, None, f) - - else: # QOpenGLWidget - super(_OpenGLWidget, self).__init__(parent, f) - - format_ = qt.QSurfaceFormat() - format_.setAlphaBufferSize(alphaBufferSize) - format_.setDepthBufferSize(depthBufferSize) - format_.setStencilBufferSize(stencilBufferSize) - format_.setVersion(*self.__requestedOpenGLVersion) - format_.setSwapBehavior(qt.QSurfaceFormat.DoubleBuffer) - self.setFormat(format_) - - # Enable receiving mouse move events when no buttons are pressed - self.setMouseTracking(True) - - def getDevicePixelRatio(self): - """Returns the ratio device-independent / device pixel size - - It should be either 1.0 or 2.0. - - :return: Scale factor between screen and Qt units - :rtype: float - """ - return self.__devicePixelRatio - - def getRequestedOpenGLVersion(self): - """Returns the requested OpenGL version. - - :return: (major, minor) - :rtype: 2-tuple of int""" - return self.__requestedOpenGLVersion - - def getOpenGLVersion(self): - """Returns the available OpenGL version. - - :return: (major, minor) - :rtype: 2-tuple of int""" - if self.__legacy: # QGLWidget - supportedVersion = 0, 0 - - # Go through all OpenGL version flags checking support - flags = self.format().openGLVersionFlags() - for version in ((1, 1), (1, 2), (1, 3), (1, 4), (1, 5), - (2, 0), (2, 1), - (3, 0), (3, 1), (3, 2), (3, 3), - (4, 0)): - versionFlag = getattr(qt.QGLFormat, - 'OpenGL_Version_%d_%d' % version) - if not versionFlag & flags: - break - supportedVersion = version - return supportedVersion - - else: # QOpenGLWidget - return self.format().version() - - # QOpenGLWidget methods - - def isValid(self): - """Returns True if OpenGL is available. - - This adds extra checks to Qt isValid method. - - :rtype: bool - """ - return self.__isValid and super(_OpenGLWidget, self).isValid() - - def defaultFramebufferObject(self): - """Returns the framebuffer object handle. - - See :meth:`QOpenGLWidget.defaultFramebufferObject` - """ - if self.__legacy: # QGLWidget - return 0 - else: # QOpenGLWidget - return super(_OpenGLWidget, self).defaultFramebufferObject() - - # *GL overridden methods - - def initializeGL(self): - parent = self.parent() - if parent is None: - _logger.error('_OpenGLWidget has no parent') - return - - # Check OpenGL version - if self.getOpenGLVersion() >= self.getRequestedOpenGLVersion(): - try: - gl.glGetError() # clear any previous error (if any) - version = gl.glGetString(gl.GL_VERSION) - except: - version = None - - if version: - self.__isValid = True - else: - errMsg = 'OpenGL not available' - if sys.platform.startswith('linux'): - errMsg += ': If connected remotely, ' \ - 'GLX forwarding might be disabled.' - _logger.error(errMsg) - self.sigOpenGLContextError.emit(errMsg) - self.__isValid = False - - else: - errMsg = 'OpenGL %d.%d not available' % \ - self.getRequestedOpenGLVersion() - _logger.error('OpenGL widget disabled: %s', errMsg) - self.sigOpenGLContextError.emit(errMsg) - self.__isValid = False - - if self.isValid(): - parent.initializeGL() - - def paintGL(self): - parent = self.parent() - if parent is None: - _logger.error('_OpenGLWidget has no parent') - return - - if qt.BINDING in ('PyQt5', 'PySide2'): - devicePixelRatio = self.window().windowHandle().devicePixelRatio() - - if devicePixelRatio != self.getDevicePixelRatio(): - # Update devicePixelRatio and call resizeOpenGL - # as resizeGL is not always called. - self.__devicePixelRatio = devicePixelRatio - self.makeCurrent() - parent.resizeGL(self.width(), self.height()) - - if self.isValid(): - parent.paintGL() - - def resizeGL(self, width, height): - parent = self.parent() - if parent is None: - _logger.error('_OpenGLWidget has no parent') - return - - if self.isValid(): - # Call parent resizeGL with device-independent pixel unit - # This works over both QGLWidget and QOpenGLWidget - parent.resizeGL(self.width(), self.height()) - - -class OpenGLWidget(qt.QWidget): - """OpenGL widget wrapper over QGLWidget and QOpenGLWidget - - This wrapper API implements a subset of QOpenGLWidget API. - The constructor takes a different set of arguments. - Methods returning object like :meth:`context` returns either - QGL* or QOpenGL* objects. - - :param parent: Parent widget see :class:`QWidget` - :param int alphaBufferSize: - Size in bits of the alpha channel (default: 0). - Set to 0 to disable alpha channel. - :param int depthBufferSize: - Size in bits of the depth buffer (default: 24). - Set to 0 to disable depth buffer. - :param int stencilBufferSize: - Size in bits of the stencil buffer (default: 8). - Set to 0 to disable stencil buffer - :param version: Requested OpenGL version (default: (2, 0)). - :type version: 2-tuple of int - :param f: see :class:`QWidget` - """ - - def __init__(self, parent=None, - alphaBufferSize=0, - depthBufferSize=24, - stencilBufferSize=8, - version=(2, 0), - f=qt.Qt.WindowFlags()): - super(OpenGLWidget, self).__init__(parent, f) - - layout = qt.QHBoxLayout(self) - layout.setContentsMargins(0, 0, 0, 0) - self.setLayout(layout) - - self.__context = None - - _check = isOpenGLAvailable(version=version, runtimeCheck=False) - if _OpenGLWidget is None or not _check: - _logger.error('OpenGL-based widget disabled: %s', _check.error) - self.__openGLWidget = None - label = self._createErrorQLabel(_check.error) - self.layout().addWidget(label) - - else: - self.__openGLWidget = _OpenGLWidget( - parent=self, - alphaBufferSize=alphaBufferSize, - depthBufferSize=depthBufferSize, - stencilBufferSize=stencilBufferSize, - version=version, - f=f) - # Async connection need, otherwise issue when hiding OpenGL - # widget while doing the rendering.. - self.__openGLWidget.sigOpenGLContextError.connect( - self._handleOpenGLInitError, qt.Qt.QueuedConnection) - self.layout().addWidget(self.__openGLWidget) - - @staticmethod - def _createErrorQLabel(error): - """Create QLabel displaying error message in place of OpenGL widget - - :param str error: The error message to display""" - label = qt.QLabel() - label.setText('OpenGL-based widget disabled:\n%s' % error) - label.setAlignment(qt.Qt.AlignCenter) - label.setWordWrap(True) - return label - - def _handleOpenGLInitError(self, error): - """Handle runtime errors in OpenGL widget""" - if self.__openGLWidget is not None: - self.__openGLWidget.setVisible(False) - self.__openGLWidget.setParent(None) - self.__openGLWidget = None - - label = self._createErrorQLabel(error) - self.layout().addWidget(label) - - # Additional API - - def getDevicePixelRatio(self): - """Returns the ratio device-independent / device pixel size - - It should be either 1.0 or 2.0. - - :return: Scale factor between screen and Qt units - :rtype: float - """ - if self.__openGLWidget is None: - return 1. - else: - return self.__openGLWidget.getDevicePixelRatio() - - def getDotsPerInch(self): - """Returns current screen resolution as device pixels per inch. - - :rtype: float - """ - screen = self.window().windowHandle().screen() - if screen is not None: - # TODO check if this is correct on different OS/screen - # OK on macOS10.12/qt5.13.2 - dpi = screen.physicalDotsPerInch() * self.getDevicePixelRatio() - else: # Fallback - dpi = 96. * self.getDevicePixelRatio() - return dpi - - def getOpenGLVersion(self): - """Returns the available OpenGL version. - - :return: (major, minor) - :rtype: 2-tuple of int""" - if self.__openGLWidget is None: - return 0, 0 - else: - return self.__openGLWidget.getOpenGLVersion() - - # QOpenGLWidget API - - def isValid(self): - """Returns True if OpenGL with the requested version is available. - - :rtype: bool - """ - if self.__openGLWidget is None: - return False - else: - return self.__openGLWidget.isValid() - - def context(self): - """Return Qt OpenGL context object or None. - - See :meth:`QOpenGLWidget.context` and :meth:`QGLWidget.context` - """ - if self.__openGLWidget is None: - return None - else: - # Keep a reference on QOpenGLContext to make - # else PyQt5 keeps creating a new one. - self.__context = self.__openGLWidget.context() - return self.__context - - def defaultFramebufferObject(self): - """Returns the framebuffer object handle. - - See :meth:`QOpenGLWidget.defaultFramebufferObject` - """ - if self.__openGLWidget is None: - return 0 - else: - return self.__openGLWidget.defaultFramebufferObject() - - def makeCurrent(self): - """Make the underlying OpenGL widget's context current. - - See :meth:`QOpenGLWidget.makeCurrent` - """ - if self.__openGLWidget is not None: - self.__openGLWidget.makeCurrent() - - def update(self): - """Async update of the OpenGL widget. - - See :meth:`QOpenGLWidget.update` - """ - if self.__openGLWidget is not None: - self.__openGLWidget.update() - - # QOpenGLWidget API to override - - def initializeGL(self): - """Override to implement OpenGL initialization.""" - pass - - def paintGL(self): - """Override to implement OpenGL rendering.""" - pass - - def resizeGL(self, width, height): - """Override to implement resize of OpenGL framebuffer. - - :param int width: Width in device-independent pixels - :param int height: Height in device-independent pixels - """ - pass diff --git a/silx/gui/_glutils/Program.py b/silx/gui/_glutils/Program.py deleted file mode 100644 index 87eec5f..0000000 --- a/silx/gui/_glutils/Program.py +++ /dev/null @@ -1,202 +0,0 @@ -# coding: utf-8 -# /*########################################################################## -# -# Copyright (c) 2014-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. -# -# ###########################################################################*/ -"""This module provides a class to handle shader program compilation.""" - -__authors__ = ["T. Vincent"] -__license__ = "MIT" -__date__ = "25/07/2016" - - -import logging -import weakref - -import numpy - -from . import Context, gl - -_logger = logging.getLogger(__name__) - - -class Program(object): - """Wrap OpenGL shader program. - - The program is compiled lazily (i.e., at first program :meth:`use`). - When the program is compiled, it stores attributes and uniforms locations. - So, attributes and uniforms must be used after :meth:`use`. - - This object supports multiple OpenGL contexts. - - :param str vertexShader: The source of the vertex shader. - :param str fragmentShader: The source of the fragment shader. - :param str attrib0: - Attribute's name to bind to position 0 (default: 'position'). - On certain platform, this attribute MUST be active and with an - array attached to it in order for the rendering to occur.... - """ - - def __init__(self, vertexShader, fragmentShader, - attrib0='position'): - self._vertexShader = vertexShader - self._fragmentShader = fragmentShader - self._attrib0 = attrib0 - self._programs = weakref.WeakKeyDictionary() - - @staticmethod - def _compileGL(vertexShader, fragmentShader, attrib0): - program = gl.glCreateProgram() - - gl.glBindAttribLocation(program, 0, attrib0.encode('ascii')) - - vertex = gl.glCreateShader(gl.GL_VERTEX_SHADER) - gl.glShaderSource(vertex, vertexShader) - gl.glCompileShader(vertex) - if gl.glGetShaderiv(vertex, gl.GL_COMPILE_STATUS) != gl.GL_TRUE: - raise RuntimeError(gl.glGetShaderInfoLog(vertex)) - gl.glAttachShader(program, vertex) - gl.glDeleteShader(vertex) - - fragment = gl.glCreateShader(gl.GL_FRAGMENT_SHADER) - gl.glShaderSource(fragment, fragmentShader) - gl.glCompileShader(fragment) - if gl.glGetShaderiv(fragment, - gl.GL_COMPILE_STATUS) != gl.GL_TRUE: - raise RuntimeError(gl.glGetShaderInfoLog(fragment)) - gl.glAttachShader(program, fragment) - gl.glDeleteShader(fragment) - - gl.glLinkProgram(program) - if gl.glGetProgramiv(program, gl.GL_LINK_STATUS) != gl.GL_TRUE: - raise RuntimeError(gl.glGetProgramInfoLog(program)) - - attributes = {} - for index in range(gl.glGetProgramiv(program, - gl.GL_ACTIVE_ATTRIBUTES)): - name = gl.glGetActiveAttrib(program, index)[0] - namestr = name.decode('ascii') - attributes[namestr] = gl.glGetAttribLocation(program, name) - - uniforms = {} - for index in range(gl.glGetProgramiv(program, gl.GL_ACTIVE_UNIFORMS)): - name = gl.glGetActiveUniform(program, index)[0] - namestr = name.decode('ascii') - uniforms[namestr] = gl.glGetUniformLocation(program, name) - - return program, attributes, uniforms - - def _getProgramInfo(self): - glcontext = Context.getCurrent() - if glcontext not in self._programs: - raise RuntimeError( - "Program was not compiled for current OpenGL context.") - return self._programs[glcontext] - - @property - def attributes(self): - """Vertex attributes names and locations as a dict of {str: int}. - - WARNING: - Read-only usage. - To use only with a valid OpenGL context and after :meth:`use` - has been called for this context. - """ - return self._getProgramInfo()[1] - - @property - def uniforms(self): - """Program uniforms names and locations as a dict of {str: int}. - - WARNING: - Read-only usage. - To use only with a valid OpenGL context and after :meth:`use` - has been called for this context. - """ - return self._getProgramInfo()[2] - - @property - def program(self): - """OpenGL id of the program. - - WARNING: - To use only with a valid OpenGL context and after :meth:`use` - has been called for this context. - """ - return self._getProgramInfo()[0] - - # def discard(self): - # pass # Not implemented yet - - def use(self): - """Make use of the program, compiling it if necessary""" - glcontext = Context.getCurrent() - - if glcontext not in self._programs: - self._programs[glcontext] = self._compileGL( - self._vertexShader, - self._fragmentShader, - self._attrib0) - - if _logger.getEffectiveLevel() <= logging.DEBUG: - gl.glValidateProgram(self.program) - if gl.glGetProgramiv( - self.program, gl.GL_VALIDATE_STATUS) != gl.GL_TRUE: - _logger.debug('Cannot validate program: %s', - gl.glGetProgramInfoLog(self.program)) - - gl.glUseProgram(self.program) - - def setUniformMatrix(self, name, value, transpose=True, safe=False): - """Wrap glUniformMatrix[2|3|4]fv - - :param str name: The name of the uniform. - :param value: The 2D matrix (or the array of matrices, 3D). - Matrices are 2x2, 3x3 or 4x4. - :type value: numpy.ndarray with 2 or 3 dimensions of float32 - :param bool transpose: Whether to transpose (True, default) or not. - :param bool safe: False: raise an error if no uniform with this name; - True: silently ignores it. - - :raises KeyError: if no uniform corresponds to name. - """ - assert value.dtype == numpy.float32 - - shape = value.shape - assert len(shape) in (2, 3) - assert shape[-1] in (2, 3, 4) - assert shape[-1] == shape[-2] # As in OpenGL|ES 2.0 - - location = self.uniforms.get(name) - if location is not None: - count = 1 if len(shape) == 2 else shape[0] - transpose = gl.GL_TRUE if transpose else gl.GL_FALSE - - if shape[-1] == 2: - gl.glUniformMatrix2fv(location, count, transpose, value) - elif shape[-1] == 3: - gl.glUniformMatrix3fv(location, count, transpose, value) - elif shape[-1] == 4: - gl.glUniformMatrix4fv(location, count, transpose, value) - - elif not safe: - raise KeyError('No uniform: %s' % name) diff --git a/silx/gui/_glutils/Texture.py b/silx/gui/_glutils/Texture.py deleted file mode 100644 index c72135a..0000000 --- a/silx/gui/_glutils/Texture.py +++ /dev/null @@ -1,352 +0,0 @@ -# coding: utf-8 -# /*########################################################################## -# -# Copyright (c) 2014-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. -# -# ###########################################################################*/ -"""This module provides a class wrapping OpenGL 2D and 3D texture.""" - -__authors__ = ["T. Vincent"] -__license__ = "MIT" -__date__ = "04/10/2016" - - -try: - from collections import abc -except ImportError: # Python2 support - import collections as abc - -from ctypes import c_void_p -import logging - -import numpy - -from . import gl, utils - - -_logger = logging.getLogger(__name__) - - -class Texture(object): - """Base class to wrap OpenGL 2D and 3D texture - - :param internalFormat: OpenGL texture internal format - :param data: The data to copy to the texture or None for an empty texture - :type data: numpy.ndarray or None - :param format_: Input data format if different from internalFormat - :param shape: If data is None, shape of the texture - (height, width) or (depth, height, width) - :type shape: List[int] - :param int texUnit: The texture unit to use - :param minFilter: OpenGL texture minimization filter (default: GL_NEAREST) - :param magFilter: OpenGL texture magnification filter (default: GL_LINEAR) - :param wrap: Texture wrap mode for dimensions: (t, s) or (r, t, s) - If a single value is provided, it used for all dimensions. - :type wrap: OpenGL wrap mode or 2 or 3-tuple of wrap mode - """ - - def __init__(self, internalFormat, data=None, format_=None, - shape=None, texUnit=0, - minFilter=None, magFilter=None, wrap=None): - - self._internalFormat = internalFormat - if format_ is None: - format_ = self.internalFormat - - if data is None: - assert shape is not None - else: - assert shape is None - data = numpy.array(data, copy=False, order='C') - if format_ != gl.GL_RED: - shape = data.shape[:-1] # Last dimension is channels - else: - shape = data.shape - - self._deferredUpdates = [(format_, data, None)] - - assert len(shape) in (2, 3) - self._shape = tuple(shape) - self._ndim = len(shape) - - self.texUnit = texUnit - - self._texParameterUpdates = {} # Store texture params to update - - self._minFilter = minFilter if minFilter is not None else gl.GL_NEAREST - self._texParameterUpdates[gl.GL_TEXTURE_MIN_FILTER] = self._minFilter - - self._magFilter = magFilter if magFilter is not None else gl.GL_LINEAR - self._texParameterUpdates[gl.GL_TEXTURE_MAG_FILTER] = self._magFilter - - self._name = None # Store texture ID - - if wrap is not None: - if not isinstance(wrap, abc.Iterable): - wrap = [wrap] * self.ndim - - assert len(wrap) == self.ndim - - self._texParameterUpdates[gl.GL_TEXTURE_WRAP_S] = wrap[-1] - self._texParameterUpdates[gl.GL_TEXTURE_WRAP_T] = wrap[-2] - if self.ndim == 3: - self._texParameterUpdates[gl.GL_TEXTURE_WRAP_R] = wrap[0] - - @property - def target(self): - """OpenGL target type of this texture""" - return gl.GL_TEXTURE_2D if self.ndim == 2 else gl.GL_TEXTURE_3D - - @property - def ndim(self): - """The number of dimensions: 2 or 3""" - return self._ndim - - @property - def internalFormat(self): - """Texture internal format""" - return self._internalFormat - - @property - def shape(self): - """Shape of the texture: (height, width) or (depth, height, width)""" - return self._shape - - @property - def name(self): - """OpenGL texture name. - - It is None if not initialized or already discarded. - """ - return self._name - - @property - def minFilter(self): - """Minifying function parameter (GL_TEXTURE_MIN_FILTER)""" - return self._minFilter - - @minFilter.setter - def minFilter(self, minFilter): - if minFilter != self.minFilter: - self._minFilter = minFilter - self._texParameterUpdates[gl.GL_TEXTURE_MIN_FILTER] = minFilter - - @property - def magFilter(self): - """Magnification function parameter (GL_TEXTURE_MAG_FILTER)""" - return self._magFilter - - @magFilter.setter - def magFilter(self, magFilter): - if magFilter != self.magFilter: - self._magFilter = magFilter - self._texParameterUpdates[gl.GL_TEXTURE_MAG_FILTER] = magFilter - - def _isPrepareRequired(self) -> bool: - """Returns True if OpenGL texture needs to be updated. - - :rtype: bool - """ - return (self._name is None or - self._texParameterUpdates or - self._deferredUpdates) - - def _prepareAndBind(self, texUnit=None): - """Synchronizes the OpenGL texture""" - if self._name is None: - self._name = gl.glGenTextures(1) - - self._bind(texUnit) - - # Synchronizes texture parameters - for pname, param in self._texParameterUpdates.items(): - gl.glTexParameter(self.target, pname, param) - self._texParameterUpdates = {} - - # Copy data to texture - for format_, data, offset in self._deferredUpdates: - gl.glPixelStorei(gl.GL_UNPACK_ALIGNMENT, 1) - - # This are the defaults, useless to set if not modified - # gl.glPixelStorei(gl.GL_UNPACK_ROW_LENGTH, 0) - # gl.glPixelStorei(gl.GL_UNPACK_SKIP_PIXELS, 0) - # gl.glPixelStorei(gl.GL_UNPACK_SKIP_ROWS, 0) - # gl.glPixelStorei(gl.GL_UNPACK_IMAGE_HEIGHT, 0) - # gl.glPixelStorei(gl.GL_UNPACK_SKIP_IMAGES, 0) - - if data is None: - data = c_void_p(0) - type_ = gl.GL_UNSIGNED_BYTE - else: - type_ = utils.numpyToGLType(data.dtype) - - if offset is None: # Initialize texture - if self.ndim == 2: - _logger.debug( - 'Creating 2D texture shape: (%d, %d),' - ' internal format: %s, format: %s, type: %s', - self.shape[0], self.shape[1], - str(self.internalFormat), str(format_), str(type_)) - - gl.glTexImage2D( - gl.GL_TEXTURE_2D, - 0, - self.internalFormat, - self.shape[1], - self.shape[0], - 0, - format_, - type_, - data) - - else: - _logger.debug( - 'Creating 3D texture shape: (%d, %d, %d),' - ' internal format: %s, format: %s, type: %s', - self.shape[0], self.shape[1], self.shape[2], - str(self.internalFormat), str(format_), str(type_)) - - gl.glTexImage3D( - gl.GL_TEXTURE_3D, - 0, - self.internalFormat, - self.shape[2], - self.shape[1], - self.shape[0], - 0, - format_, - type_, - data) - - else: # Update already existing texture - if self.ndim == 2: - gl.glTexSubImage2D(gl.GL_TEXTURE_2D, - 0, - offset[1], - offset[0], - data.shape[1], - data.shape[0], - format_, - type_, - data) - - else: - gl.glTexSubImage3D(gl.GL_TEXTURE_3D, - 0, - offset[2], - offset[1], - offset[0], - data.shape[2], - data.shape[1], - data.shape[0], - format_, - type_, - data) - - self._deferredUpdates = [] - - def _bind(self, texUnit=None): - """Bind the texture to a texture unit. - - :param int texUnit: The texture unit to use - """ - if texUnit is None: - texUnit = self.texUnit - gl.glActiveTexture(gl.GL_TEXTURE0 + texUnit) - gl.glBindTexture(self.target, self.name) - - def _unbind(self, texUnit=None): - """Reset texture binding to a texture unit. - - :param int texUnit: The texture unit to use - """ - if texUnit is None: - texUnit = self.texUnit - gl.glActiveTexture(gl.GL_TEXTURE0 + texUnit) - gl.glBindTexture(self.target, 0) - - def prepare(self): - """Synchronizes the OpenGL texture. - - This method must be called with a current OpenGL context. - """ - if self._isPrepareRequired(): - self._prepareAndBind() - self._unbind() - - def bind(self, texUnit=None): - """Bind the texture to a texture unit. - - The OpenGL texture is updated if needed. - - This method must be called with a current OpenGL context. - - :param int texUnit: The texture unit to use - """ - if self._isPrepareRequired(): - self._prepareAndBind(texUnit) - else: - self._bind(texUnit) - - def discard(self): - """Delete associated OpenGL texture. - - This method must be called with a current OpenGL context. - """ - if self._name is not None: - gl.glDeleteTextures(self._name) - self._name = None - else: - _logger.warning("Texture not initialized or already discarded") - - # with statement - - def __enter__(self): - self.bind() - - def __exit__(self, exc_type, exc_val, exc_tb): - self._unbind() - - def update(self, format_, data, offset=(0, 0, 0), copy=True): - """Update the content of the texture. - - Texture is not resized, so data must fit into texture with the - given offset. - - This update is performed lazily during next call to - :meth:`prepare` or :meth:`bind`. - Data MUST not be changed until then. - - :param format_: The OpenGL format of the data - :param data: The data to use to update the texture - :param List[int] offset: Offset in the texture where to copy the data - :param bool copy: - True (default) to copy data, False to use as is (do not modify) - """ - data = numpy.array(data, copy=copy, order='C') - offset = tuple(offset) - - assert data.ndim == self.ndim - assert len(offset) >= self.ndim - for i in range(self.ndim): - assert offset[i] + data.shape[i] <= self.shape[i] - - self._deferredUpdates.append((format_, data, offset)) diff --git a/silx/gui/_glutils/VertexBuffer.py b/silx/gui/_glutils/VertexBuffer.py deleted file mode 100644 index b74b748..0000000 --- a/silx/gui/_glutils/VertexBuffer.py +++ /dev/null @@ -1,266 +0,0 @@ -# coding: utf-8 -# /*########################################################################## -# -# Copyright (c) 2014-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. -# -# ###########################################################################*/ -"""This module provides a class managing an OpenGL vertex buffer.""" - -__authors__ = ["T. Vincent"] -__license__ = "MIT" -__date__ = "10/01/2017" - - -import logging -from ctypes import c_void_p -import numpy - -from . import gl -from .utils import numpyToGLType, sizeofGLType - - -_logger = logging.getLogger(__name__) - - -class VertexBuffer(object): - """Object handling an OpenGL vertex buffer object - - :param data: Data used to fill the vertex buffer - :type data: numpy.ndarray or None - :param int size: Size in bytes of the buffer or None for data size - :param usage: OpenGL vertex buffer expected usage pattern: - GL_STREAM_DRAW, GL_STATIC_DRAW (default) or GL_DYNAMIC_DRAW - :param target: Target buffer: - GL_ARRAY_BUFFER (default) or GL_ELEMENT_ARRAY_BUFFER - """ - # OpenGL|ES 2.0 subset: - _USAGES = gl.GL_STREAM_DRAW, gl.GL_STATIC_DRAW, gl.GL_DYNAMIC_DRAW - _TARGETS = gl.GL_ARRAY_BUFFER, gl.GL_ELEMENT_ARRAY_BUFFER - - def __init__(self, - data=None, - size=None, - usage=None, - target=None): - if usage is None: - usage = gl.GL_STATIC_DRAW - assert usage in self._USAGES - - if target is None: - target = gl.GL_ARRAY_BUFFER - assert target in self._TARGETS - - self._target = target - self._usage = usage - - self._name = gl.glGenBuffers(1) - self.bind() - - if data is None: - assert size is not None - self._size = size - gl.glBufferData(self._target, - self._size, - c_void_p(0), - self._usage) - else: - data = numpy.array(data, copy=False, order='C') - if size is not None: - assert size <= data.nbytes - - self._size = size or data.nbytes - gl.glBufferData(self._target, - self._size, - data, - self._usage) - - gl.glBindBuffer(self._target, 0) - - @property - def target(self): - """The target buffer of the vertex buffer""" - return self._target - - @property - def usage(self): - """The expected usage of the vertex buffer""" - return self._usage - - @property - def name(self): - """OpenGL Vertex Buffer object name (int)""" - if self._name is not None: - return self._name - else: - raise RuntimeError("No OpenGL buffer resource, \ - discard has already been called") - - @property - def size(self): - """Size in bytes of the Vertex Buffer Object (int)""" - if self._size is not None: - return self._size - else: - raise RuntimeError("No OpenGL buffer resource, \ - discard has already been called") - - def bind(self): - """Bind the vertex buffer""" - gl.glBindBuffer(self._target, self.name) - - def update(self, data, offset=0, size=None): - """Update vertex buffer content. - - :param numpy.ndarray data: The data to put in the vertex buffer - :param int offset: Offset in bytes in the buffer where to put the data - :param int size: If provided, size of data to copy - """ - data = numpy.array(data, copy=False, order='C') - if size is None: - size = data.nbytes - assert offset + size <= self.size - with self: - gl.glBufferSubData(self._target, offset, size, data) - - def discard(self): - """Delete the vertex buffer""" - if self._name is not None: - gl.glDeleteBuffers(self._name) - self._name = None - self._size = None - else: - _logger.warning("Discard has already been called") - - # with statement - - def __enter__(self): - self.bind() - - def __exit__(self, exctype, excvalue, traceback): - gl.glBindBuffer(self._target, 0) - - -class VertexBufferAttrib(object): - """Describes data stored in a vertex buffer - - Convenient class to store info for glVertexAttribPointer calls - - :param VertexBuffer vbo: The vertex buffer storing the data - :param int type_: The OpenGL type of the data - :param int size: The number of data elements stored in the VBO - :param int dimension: The number of `type_` element(s) in [1, 4] - :param int offset: Start offset of data in the vertex buffer - :param int stride: Data stride in the vertex buffer - """ - - _GL_TYPES = gl.GL_UNSIGNED_BYTE, gl.GL_FLOAT, gl.GL_INT - - def __init__(self, - vbo, - type_, - size, - dimension=1, - offset=0, - stride=0, - normalization=False): - self.vbo = vbo - assert type_ in self._GL_TYPES - self.type_ = type_ - self.size = size - assert 1 <= dimension <= 4 - self.dimension = dimension - self.offset = offset - self.stride = stride - self.normalization = bool(normalization) - - @property - def itemsize(self): - """Size in bytes of a vertex buffer element (int)""" - return self.dimension * sizeofGLType(self.type_) - - itemSize = itemsize # Backward compatibility - - def setVertexAttrib(self, attribute): - """Call glVertexAttribPointer with objects information""" - normalization = gl.GL_TRUE if self.normalization else gl.GL_FALSE - with self.vbo: - gl.glVertexAttribPointer(attribute, - self.dimension, - self.type_, - normalization, - self.stride, - c_void_p(self.offset)) - - def copy(self): - return VertexBufferAttrib(self.vbo, - self.type_, - self.size, - self.dimension, - self.offset, - self.stride, - self.normalization) - - -def vertexBuffer(arrays, prefix=None, suffix=None, usage=None): - """Create a single vertex buffer from multiple 1D or 2D numpy arrays. - - It is possible to reserve memory before and after each array in the VBO - - :param arrays: Arrays of data to store - :type arrays: Iterable of numpy.ndarray - :param prefix: If given, number of elements to reserve before each array - :type prefix: Iterable of int or None - :param suffix: If given, number of elements to reserve after each array - :type suffix: Iterable of int or None - :param int usage: vertex buffer expected usage or None for default - :returns: List of VertexBufferAttrib objects sharing the same vertex buffer - """ - info = [] - vbosize = 0 - - if prefix is None: - prefix = (0,) * len(arrays) - if suffix is None: - suffix = (0,) * len(arrays) - - for data, pre, post in zip(arrays, prefix, suffix): - data = numpy.array(data, copy=False, order='C') - shape = data.shape - assert len(shape) <= 2 - type_ = numpyToGLType(data.dtype) - size = shape[0] + pre + post - dimension = 1 if len(shape) == 1 else shape[1] - sizeinbytes = size * dimension * sizeofGLType(type_) - sizeinbytes = 4 * ((sizeinbytes + 3) >> 2) # 4 bytes alignment - copyoffset = vbosize + pre * dimension * sizeofGLType(type_) - info.append((data, type_, size, dimension, - vbosize, sizeinbytes, copyoffset)) - vbosize += sizeinbytes - - vbo = VertexBuffer(size=vbosize, usage=usage) - - result = [] - for data, type_, size, dimension, offset, sizeinbytes, copyoffset in info: - copysize = data.shape[0] * dimension * sizeofGLType(type_) - vbo.update(data, offset=copyoffset, size=copysize) - result.append( - VertexBufferAttrib(vbo, type_, size, dimension, offset, 0)) - return result diff --git a/silx/gui/_glutils/__init__.py b/silx/gui/_glutils/__init__.py deleted file mode 100644 index e88affd..0000000 --- a/silx/gui/_glutils/__init__.py +++ /dev/null @@ -1,43 +0,0 @@ -# coding: utf-8 -# /*########################################################################## -# -# Copyright (c) 2014-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. -# -# ###########################################################################*/ -"""This package provides utility functions to handle OpenGL resources. - -The :mod:`gl` module provides a wrapper to OpenGL based on PyOpenGL. -""" - -__authors__ = ["T. Vincent"] -__license__ = "MIT" -__date__ = "25/07/2016" - - -# OpenGL convenient functions -from .OpenGLWidget import OpenGLWidget # noqa -from . import Context # noqa -from .FramebufferTexture import FramebufferTexture # noqa -from .Program import Program # noqa -from .Texture import Texture # noqa -from .VertexBuffer import VertexBuffer, VertexBufferAttrib, vertexBuffer # noqa -from .utils import sizeofGLType, isSupportedGLType, numpyToGLType # noqa -from .utils import segmentTrianglesIntersection # noqa diff --git a/silx/gui/_glutils/font.py b/silx/gui/_glutils/font.py deleted file mode 100644 index 6a4c489..0000000 --- a/silx/gui/_glutils/font.py +++ /dev/null @@ -1,163 +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. -# -# ###########################################################################*/ -"""Text rasterisation feature leveraging Qt font and text layout support.""" - -__authors__ = ["T. Vincent"] -__license__ = "MIT" -__date__ = "13/10/2016" - - -import logging -import numpy - -from ..utils.image import convertQImageToArray -from .. import qt - -_logger = logging.getLogger(__name__) - - -def getDefaultFontFamily(): - """Returns the default font family of the application""" - return qt.QApplication.instance().font().family() - - -# Font weights -ULTRA_LIGHT = 0 -"""Lightest characters: Minimum font weight""" - -LIGHT = 25 -"""Light characters""" - -NORMAL = 50 -"""Normal characters""" - -SEMI_BOLD = 63 -"""Between normal and bold characters""" - -BOLD = 74 -"""Thicker characters""" - -BLACK = 87 -"""Really thick characters""" - -ULTRA_BLACK = 99 -"""Thickest characters: Maximum font weight""" - - -def rasterText(text, font, - size=-1, - weight=-1, - italic=False, - devicePixelRatio=1.0): - """Raster text using Qt. - - It supports multiple lines. - - :param str text: The text to raster - :param font: Font name or QFont to use - :type font: str or :class:`QFont` - :param int size: - Font size in points - Used only if font is given as name. - :param int weight: - Font weight in [0, 99], see QFont.Weight. - Used only if font is given as name. - :param bool italic: - True for italic font (default: False). - Used only if font is given as name. - :param float devicePixelRatio: - The current ratio between device and device-independent pixel - (default: 1.0) - :return: Corresponding image in gray scale and baseline offset from top - :rtype: (HxW numpy.ndarray of uint8, int) - """ - if not text: - _logger.info("Trying to raster empty text, replaced by white space") - text = ' ' # Replace empty text by white space to produce an image - - if (devicePixelRatio != 1.0 and - not hasattr(qt.QImage, 'setDevicePixelRatio')): # Qt 4 - _logger.error('devicePixelRatio not supported') - devicePixelRatio = 1.0 - - if not isinstance(font, qt.QFont): - font = qt.QFont(font, size, weight, italic) - - # get text size - image = qt.QImage(1, 1, qt.QImage.Format_RGB888) - painter = qt.QPainter() - painter.begin(image) - painter.setPen(qt.Qt.white) - painter.setFont(font) - bounds = painter.boundingRect( - qt.QRect(0, 0, 4096, 4096), qt.Qt.TextExpandTabs, text) - painter.end() - - metrics = qt.QFontMetrics(font) - - # This does not provide the correct text bbox on macOS - # size = metrics.size(qt.Qt.TextExpandTabs, text) - # bounds = metrics.boundingRect( - # qt.QRect(0, 0, size.width(), size.height()), - # qt.Qt.TextExpandTabs, - # text) - - # Add extra border and handle devicePixelRatio - width = bounds.width() * devicePixelRatio + 2 - # align line size to 32 bits to ease conversion to numpy array - width = 4 * ((width + 3) // 4) - image = qt.QImage(int(width), - int(bounds.height() * devicePixelRatio + 2), - qt.QImage.Format_RGB888) - if (devicePixelRatio != 1.0 and - hasattr(image, 'setDevicePixelRatio')): # Qt 5 - image.setDevicePixelRatio(devicePixelRatio) - - # TODO if Qt5 use Format_Grayscale8 instead - image.fill(0) - - # Raster text - painter = qt.QPainter() - painter.begin(image) - painter.setPen(qt.Qt.white) - painter.setFont(font) - painter.drawText(bounds, qt.Qt.TextExpandTabs, text) - painter.end() - - array = convertQImageToArray(image) - - # RGB to R - array = numpy.ascontiguousarray(array[:, :, 0]) - - # Remove leading and trailing empty columns but one on each side - column_cumsum = numpy.cumsum(numpy.sum(array, axis=0)) - array = array[:, column_cumsum.argmin():column_cumsum.argmax() + 2] - - # Remove leading and trailing empty rows but one on each side - row_cumsum = numpy.cumsum(numpy.sum(array, axis=1)) - min_row = row_cumsum.argmin() - array = array[min_row:row_cumsum.argmax() + 2, :] - - return array, metrics.ascent() - min_row diff --git a/silx/gui/_glutils/gl.py b/silx/gui/_glutils/gl.py deleted file mode 100644 index 608d9ce..0000000 --- a/silx/gui/_glutils/gl.py +++ /dev/null @@ -1,168 +0,0 @@ -# coding: utf-8 -# /*########################################################################## -# -# Copyright (c) 2014-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. -# -# ###########################################################################*/ -"""This module loads PyOpenGL and provides a namespace for OpenGL.""" - -__authors__ = ["T. Vincent"] -__license__ = "MIT" -__date__ = "25/07/2016" - - -from contextlib import contextmanager as _contextmanager -from ctypes import c_uint -import logging - -_logger = logging.getLogger(__name__) - -import OpenGL -# Set the following to true for debugging -if _logger.getEffectiveLevel() <= logging.DEBUG: - _logger.debug('Enabling PyOpenGL debug flags') - OpenGL.ERROR_LOGGING = True - OpenGL.ERROR_CHECKING = True - OpenGL.ERROR_ON_COPY = True -else: - OpenGL.ERROR_LOGGING = False - OpenGL.ERROR_CHECKING = False - OpenGL.ERROR_ON_COPY = False - -import OpenGL.GL as _GL -from OpenGL.GL import * # noqa - -# Extentions core in OpenGL 3 -from OpenGL.GL.ARB import framebuffer_object as _FBO -from OpenGL.GL.ARB.framebuffer_object import * # noqa -from OpenGL.GL.ARB.texture_rg import GL_R32F, GL_R16F # noqa -from OpenGL.GL.ARB.texture_rg import GL_R16, GL_R8 # noqa - -# PyOpenGL 3.0.1 does not define it -try: - GLchar -except NameError: - from ctypes import c_char - GLchar = c_char - - -def testGL(): - """Test if required OpenGL version and extensions are available. - - This MUST be run with an active OpenGL context. - """ - version = glGetString(GL_VERSION).split()[0] # get version number - major, minor = int(version[0]), int(version[2]) - if major < 2 or (major == 2 and minor < 1): - raise RuntimeError( - "Requires at least OpenGL version 2.1, running with %s" % version) - - from OpenGL.GL.ARB.framebuffer_object import glInitFramebufferObjectARB - from OpenGL.GL.ARB.texture_rg import glInitTextureRgARB - - if not glInitFramebufferObjectARB(): - raise RuntimeError( - "OpenGL GL_ARB_framebuffer_object extension required !") - - if not glInitTextureRgARB(): - raise RuntimeError("OpenGL GL_ARB_texture_rg extension required !") - - -# Additional setup -if hasattr(glget, 'addGLGetConstant'): - glget.addGLGetConstant(GL_FRAMEBUFFER_BINDING, (1,)) - - -@_contextmanager -def enabled(capacity, enable=True): - """Context manager enabling an OpenGL capacity. - - This is not checking the current state of the capacity. - - :param capacity: The OpenGL capacity enum to enable/disable - :param bool enable: - True (default) to enable during context, False to disable - """ - if bool(enable) == glGetBoolean(capacity): - # Already in the right state: noop - yield - elif enable: - glEnable(capacity) - yield - glDisable(capacity) - else: - glDisable(capacity) - yield - glEnable(capacity) - - -def disabled(capacity, disable=True): - """Context manager disabling an OpenGL capacity. - - This is not checking the current state of the capacity. - - :param capacity: The OpenGL capacity enum to disable/enable - :param bool disable: - True (default) to disable during context, False to enable - """ - return enabled(capacity, not disable) - - -# Additional OpenGL wrapping - -def glGetActiveAttrib(program, index): - """Wrap PyOpenGL glGetActiveAttrib""" - bufsize = glGetProgramiv(program, GL_ACTIVE_ATTRIBUTE_MAX_LENGTH) - length = GLsizei() - size = GLint() - type_ = GLenum() - name = (GLchar * bufsize)() - - _GL.glGetActiveAttrib(program, index, bufsize, length, size, type_, name) - return name.value, size.value, type_.value - - -def glDeleteRenderbuffers(buffers): - if not hasattr(buffers, '__len__'): # Support single int argument - buffers = [buffers] - length = len(buffers) - _FBO.glDeleteRenderbuffers(length, (c_uint * length)(*buffers)) - - -def glDeleteFramebuffers(buffers): - if not hasattr(buffers, '__len__'): # Support single int argument - buffers = [buffers] - length = len(buffers) - _FBO.glDeleteFramebuffers(length, (c_uint * length)(*buffers)) - - -def glDeleteBuffers(buffers): - if not hasattr(buffers, '__len__'): # Support single int argument - buffers = [buffers] - length = len(buffers) - _GL.glDeleteBuffers(length, (c_uint * length)(*buffers)) - - -def glDeleteTextures(textures): - if not hasattr(textures, '__len__'): # Support single int argument - textures = [textures] - length = len(textures) - _GL.glDeleteTextures((c_uint * length)(*textures)) diff --git a/silx/gui/_glutils/utils.py b/silx/gui/_glutils/utils.py deleted file mode 100644 index d5627ef..0000000 --- a/silx/gui/_glutils/utils.py +++ /dev/null @@ -1,121 +0,0 @@ -# coding: utf-8 -# /*########################################################################## -# -# Copyright (c) 2014-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. -# -# ###########################################################################*/ -"""This module provides conversion functions between OpenGL and numpy types. -""" - -__authors__ = ["T. Vincent"] -__license__ = "MIT" -__date__ = "10/01/2017" - -import numpy - -from OpenGL.constants import BYTE_SIZES as _BYTE_SIZES -from OpenGL.constants import ARRAY_TO_GL_TYPE_MAPPING as _ARRAY_TO_GL_TYPE_MAPPING - - -def sizeofGLType(type_): - """Returns the size in bytes of an element of type `type_`""" - return _BYTE_SIZES[type_] - - -def isSupportedGLType(type_): - """Test if a numpy type or dtype can be converted to a GL type.""" - return numpy.dtype(type_).char in _ARRAY_TO_GL_TYPE_MAPPING - - -def numpyToGLType(type_): - """Returns the GL type corresponding the provided numpy type or dtype.""" - return _ARRAY_TO_GL_TYPE_MAPPING[numpy.dtype(type_).char] - - -def segmentTrianglesIntersection(segment, triangles): - """Check for segment/triangles intersection. - - This is based on signed tetrahedron volume comparison. - - See A. Kensler, A., Shirley, P. - Optimizing Ray-Triangle Intersection via Automated Search. - Symposium on Interactive Ray Tracing, vol. 0, p33-38 (2006) - - :param numpy.ndarray segment: - Segment end points as a 2x3 array of coordinates - :param numpy.ndarray triangles: - Nx3x3 array of triangles - :return: (triangle indices, segment parameter, barycentric coord) - Indices of intersected triangles, "depth" along the segment - of the intersection point and barycentric coordinates of intersection - point in the triangle. - :rtype: List[numpy.ndarray] - """ - # TODO triangles from vertices + indices - # TODO early rejection? e.g., check segment bbox vs triangle bbox - segment = numpy.asarray(segment) - assert segment.ndim == 2 - assert segment.shape == (2, 3) - - triangles = numpy.asarray(triangles) - assert triangles.ndim == 3 - assert triangles.shape[1] == 3 - - # Test line/triangles intersection - d = segment[1] - segment[0] - t0s0 = segment[0] - triangles[:, 0, :] - edge01 = triangles[:, 1, :] - triangles[:, 0, :] - edge02 = triangles[:, 2, :] - triangles[:, 0, :] - - dCrossEdge02 = numpy.cross(d, edge02) - t0s0CrossEdge01 = numpy.cross(t0s0, edge01) - volume = numpy.sum(dCrossEdge02 * edge01, axis=1) - del edge01 - subVolumes = numpy.empty((len(triangles), 3), dtype=triangles.dtype) - subVolumes[:, 1] = numpy.sum(dCrossEdge02 * t0s0, axis=1) - del dCrossEdge02 - subVolumes[:, 2] = numpy.sum(t0s0CrossEdge01 * d, axis=1) - subVolumes[:, 0] = volume - subVolumes[:, 1] - subVolumes[:, 2] - intersect = numpy.logical_or( - numpy.all(subVolumes >= 0., axis=1), # All positive - numpy.all(subVolumes <= 0., axis=1)) # All negative - intersect = numpy.where(intersect)[0] # Indices of intersected triangles - - # Get barycentric coordinates - barycentric = subVolumes[intersect] / volume[intersect].reshape(-1, 1) - del subVolumes - - # Test segment/triangles intersection - volAlpha = numpy.sum(t0s0CrossEdge01[intersect] * edge02[intersect], axis=1) - t = volAlpha / volume[intersect] # segment parameter of intersected triangles - del t0s0CrossEdge01 - del edge02 - del volAlpha - del volume - - inSegmentMask = numpy.logical_and(t >= 0., t <= 1.) - intersect = intersect[inSegmentMask] - t = t[inSegmentMask] - barycentric = barycentric[inSegmentMask] - - # Sort intersecting triangles by t - indices = numpy.argsort(t) - return intersect[indices], t[indices], barycentric[indices] diff --git a/silx/gui/colors.py b/silx/gui/colors.py deleted file mode 100755 index db837b5..0000000 --- a/silx/gui/colors.py +++ /dev/null @@ -1,1326 +0,0 @@ -# coding: utf-8 -# /*########################################################################## -# -# Copyright (c) 2015-2021 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 module provides API to manage colors. -""" - -from __future__ import absolute_import - -__authors__ = ["T. Vincent", "H.Payno"] -__license__ = "MIT" -__date__ = "29/01/2019" - -import numpy -import logging -import collections -import warnings - -from silx.gui import qt -from silx.gui.utils import blockSignals -from silx.math.combo import min_max -from silx.math import colormap as _colormap -from silx.utils.exceptions import NotEditableError -from silx.utils import deprecation -from silx.resources import resource_filename as _resource_filename - - -_logger = logging.getLogger(__name__) - -try: - import silx.gui.utils.matplotlib # noqa Initalize matplotlib - from matplotlib import cm as _matplotlib_cm - from matplotlib.pyplot import colormaps as _matplotlib_colormaps -except ImportError: - _logger.info("matplotlib not available, only embedded colormaps available") - _matplotlib_cm = None - _matplotlib_colormaps = None - - -_COLORDICT = {} -"""Dictionary of common colors.""" - -_COLORDICT['b'] = _COLORDICT['blue'] = '#0000ff' -_COLORDICT['r'] = _COLORDICT['red'] = '#ff0000' -_COLORDICT['g'] = _COLORDICT['green'] = '#00ff00' -_COLORDICT['k'] = _COLORDICT['black'] = '#000000' -_COLORDICT['w'] = _COLORDICT['white'] = '#ffffff' -_COLORDICT['pink'] = '#ff66ff' -_COLORDICT['brown'] = '#a52a2a' -_COLORDICT['orange'] = '#ff9900' -_COLORDICT['violet'] = '#6600ff' -_COLORDICT['gray'] = _COLORDICT['grey'] = '#a0a0a4' -# _COLORDICT['darkGray'] = _COLORDICT['darkGrey'] = '#808080' -# _COLORDICT['lightGray'] = _COLORDICT['lightGrey'] = '#c0c0c0' -_COLORDICT['y'] = _COLORDICT['yellow'] = '#ffff00' -_COLORDICT['m'] = _COLORDICT['magenta'] = '#ff00ff' -_COLORDICT['c'] = _COLORDICT['cyan'] = '#00ffff' -_COLORDICT['darkBlue'] = '#000080' -_COLORDICT['darkRed'] = '#800000' -_COLORDICT['darkGreen'] = '#008000' -_COLORDICT['darkBrown'] = '#660000' -_COLORDICT['darkCyan'] = '#008080' -_COLORDICT['darkYellow'] = '#808000' -_COLORDICT['darkMagenta'] = '#800080' -_COLORDICT['transparent'] = '#00000000' - - -# FIXME: It could be nice to expose a functional API instead of that attribute -COLORDICT = _COLORDICT - - -_LUT_DESCRIPTION = collections.namedtuple("_LUT_DESCRIPTION", ["source", "cursor_color", "preferred"]) -"""Description of a LUT for internal purpose.""" - - -_AVAILABLE_LUTS = collections.OrderedDict([ - ('gray', _LUT_DESCRIPTION('builtin', 'pink', True)), - ('reversed gray', _LUT_DESCRIPTION('builtin', 'pink', True)), - ('red', _LUT_DESCRIPTION('builtin', 'green', True)), - ('green', _LUT_DESCRIPTION('builtin', 'pink', True)), - ('blue', _LUT_DESCRIPTION('builtin', 'yellow', True)), - ('viridis', _LUT_DESCRIPTION('resource', 'pink', True)), - ('cividis', _LUT_DESCRIPTION('resource', 'pink', True)), - ('magma', _LUT_DESCRIPTION('resource', 'green', True)), - ('inferno', _LUT_DESCRIPTION('resource', 'green', True)), - ('plasma', _LUT_DESCRIPTION('resource', 'green', True)), - ('temperature', _LUT_DESCRIPTION('builtin', 'pink', True)), - ('jet', _LUT_DESCRIPTION('matplotlib', 'pink', True)), - ('hsv', _LUT_DESCRIPTION('matplotlib', 'black', True)), -]) -"""Description for internal porpose of all the default LUT provided by the library.""" - - -DEFAULT_MIN_LIN = 0 -"""Default min value if in linear normalization""" -DEFAULT_MAX_LIN = 1 -"""Default max value if in linear normalization""" - - -def rgba(color, colorDict=None): - """Convert color code '#RRGGBB' and '#RRGGBBAA' to a tuple (R, G, B, A) - of floats. - - It also supports RGB(A) from uint8 in [0, 255], float in [0, 1], and - QColor as color argument. - - :param str color: The color to convert - :param dict colorDict: A dictionary of color name conversion to color code - :returns: RGBA colors as floats in [0., 1.] - :rtype: tuple - """ - if colorDict is None: - colorDict = _COLORDICT - - if hasattr(color, 'getRgbF'): # QColor support - color = color.getRgbF() - - values = numpy.asarray(color).ravel() - - if values.dtype.kind in 'iuf': # integer or float - # Color is an array - assert len(values) in (3, 4) - - # Convert from integers in [0, 255] to float in [0, 1] - if values.dtype.kind in 'iu': - values = values / 255. - - # Clip to [0, 1] - values[values < 0.] = 0. - values[values > 1.] = 1. - - if len(values) == 3: - return values[0], values[1], values[2], 1. - else: - return tuple(values) - - # We assume color is a string - if not color.startswith('#'): - color = colorDict[color] - - assert len(color) in (7, 9) and color[0] == '#' - r = int(color[1:3], 16) / 255. - g = int(color[3:5], 16) / 255. - b = int(color[5:7], 16) / 255. - a = int(color[7:9], 16) / 255. if len(color) == 9 else 1. - return r, g, b, a - - -def greyed(color, colorDict=None): - """Convert color code '#RRGGBB' and '#RRGGBBAA' to a grey color - (R, G, B, A). - - It also supports RGB(A) from uint8 in [0, 255], float in [0, 1], and - QColor as color argument. - - :param str color: The color to convert - :param dict colorDict: A dictionary of color name conversion to color code - :returns: RGBA colors as floats in [0., 1.] - :rtype: tuple - """ - r, g, b, a = rgba(color=color, colorDict=colorDict) - g = 0.21 * r + 0.72 * g + 0.07 * b - return g, g, g, a - - -def asQColor(color): - """Convert color code '#RRGGBB' and '#RRGGBBAA' to a `qt.QColor`. - - It also supports RGB(A) from uint8 in [0, 255], float in [0, 1], and - QColor as color argument. - - :param str color: The color to convert - :rtype: qt.QColor - """ - color = rgba(color) - return qt.QColor.fromRgbF(*color) - - -def cursorColorForColormap(colormapName): - """Get a color suitable for overlay over a colormap. - - :param str colormapName: The name of the colormap. - :return: Name of the color. - :rtype: str - """ - description = _AVAILABLE_LUTS.get(colormapName, None) - if description is not None: - color = description.cursor_color - if color is not None: - return color - return 'black' - - -# Colormap loader - -_COLORMAP_CACHE = {} -"""Cache already used colormaps as name: color LUT""" - - -def _arrayToRgba8888(colors): - """Convert colors from a numpy array using float (0..1) int or uint - (0..255) to uint8 RGBA. - - :param numpy.ndarray colors: Array of float int or uint colors to convert - :return: colors as uint8 - :rtype: numpy.ndarray - """ - assert len(colors.shape) == 2 - assert colors.shape[1] in (3, 4) - - if colors.dtype == numpy.uint8: - pass - elif colors.dtype.kind == 'f': - # Each bin is [N, N+1[ except the last one: [255, 256] - colors = numpy.clip(colors.astype(numpy.float64) * 256, 0., 255.) - colors = colors.astype(numpy.uint8) - elif colors.dtype.kind in 'iu': - colors = numpy.clip(colors, 0, 255) - colors = colors.astype(numpy.uint8) - - if colors.shape[1] == 3: - tmp = numpy.empty((len(colors), 4), dtype=numpy.uint8) - tmp[:, 0:3] = colors - tmp[:, 3] = 255 - colors = tmp - - return colors - - -def _createColormapLut(name): - """Returns the color LUT corresponding to a colormap name - - :param str name: Name of the colormap to load - :returns: Corresponding table of colors - :rtype: numpy.ndarray - :raise ValueError: If no colormap corresponds to name - """ - description = _AVAILABLE_LUTS.get(name) - use_mpl = False - if description is not None: - if description.source == "builtin": - # Build colormap LUT - lut = numpy.zeros((256, 4), dtype=numpy.uint8) - lut[:, 3] = 255 - - if name == 'gray': - lut[:, :3] = numpy.arange(256, dtype=numpy.uint8).reshape(-1, 1) - elif name == 'reversed gray': - lut[:, :3] = numpy.arange(255, -1, -1, dtype=numpy.uint8).reshape(-1, 1) - elif name == 'red': - lut[:, 0] = numpy.arange(256, dtype=numpy.uint8) - elif name == 'green': - lut[:, 1] = numpy.arange(256, dtype=numpy.uint8) - elif name == 'blue': - lut[:, 2] = numpy.arange(256, dtype=numpy.uint8) - elif name == 'temperature': - # Red - lut[128:192, 0] = numpy.arange(2, 255, 4, dtype=numpy.uint8) - lut[192:, 0] = 255 - # Green - lut[:64, 1] = numpy.arange(0, 255, 4, dtype=numpy.uint8) - lut[64:192, 1] = 255 - lut[192:, 1] = numpy.arange(252, -1, -4, dtype=numpy.uint8) - # Blue - lut[:64, 2] = 255 - lut[64:128, 2] = numpy.arange(254, 0, -4, dtype=numpy.uint8) - else: - raise RuntimeError("Built-in colormap not implemented") - return lut - - elif description.source == "resource": - # Load colormap LUT - colors = numpy.load(_resource_filename("gui/colormaps/%s.npy" % name)) - # Convert to uint8 and add alpha channel - lut = _arrayToRgba8888(colors) - return lut - - elif description.source == "matplotlib": - use_mpl = True - - else: - raise RuntimeError("Internal LUT source '%s' unsupported" % description.source) - - # Here it expect a matplotlib LUTs - - if use_mpl: - # matplotlib is mandatory - if _matplotlib_cm is None: - raise ValueError("The colormap '%s' expect matplotlib, but matplotlib is not installed" % name) - - if _matplotlib_cm is not None: # Try to load with matplotlib - colormap = _matplotlib_cm.get_cmap(name) - lut = colormap(numpy.linspace(0, 1, colormap.N, endpoint=True)) - lut = _arrayToRgba8888(lut) - return lut - - raise ValueError("Unknown colormap '%s'" % name) - - -def _getColormap(name): - """Returns the color LUT corresponding to a colormap name - - :param str name: Name of the colormap to load - :returns: Corresponding table of colors - :rtype: numpy.ndarray - :raise ValueError: If no colormap corresponds to name - """ - name = str(name) - if name not in _COLORMAP_CACHE: - lut = _createColormapLut(name) - _COLORMAP_CACHE[name] = lut - return _COLORMAP_CACHE[name] - - -# Normalizations - -class _NormalizationMixIn: - """Colormap normalization mix-in class""" - - DEFAULT_RANGE = 0, 1 - """Fallback for (vmin, vmax)""" - - def isValid(self, value): - """Check if a value is in the valid range for this normalization. - - Override in subclass. - - :param Union[float,numpy.ndarray] value: - :rtype: Union[bool,numpy.ndarray] - """ - if isinstance(value, collections.abc.Iterable): - return numpy.ones_like(value, dtype=numpy.bool_) - else: - return True - - def autoscale(self, data, mode): - """Returns range for given data and autoscale mode. - - :param Union[None,numpy.ndarray] data: - :param str mode: Autoscale mode, see :class:`Colormap` - :returns: Range as (min, max) - :rtype: Tuple[float,float] - """ - data = None if data is None else numpy.array(data, copy=False) - if data is None or data.size == 0: - return self.DEFAULT_RANGE - - if mode == Colormap.MINMAX: - vmin, vmax = self.autoscaleMinMax(data) - elif mode == Colormap.STDDEV3: - dmin, dmax = self.autoscaleMinMax(data) - stdmin, stdmax = self.autoscaleMean3Std(data) - if dmin is None: - vmin = stdmin - elif stdmin is None: - vmin = dmin - else: - vmin = max(dmin, stdmin) - - if dmax is None: - vmax = stdmax - elif stdmax is None: - vmax = dmax - else: - vmax = min(dmax, stdmax) - - else: - raise ValueError('Unsupported mode: %s' % mode) - - # Check returned range and handle fallbacks - if vmin is None or not numpy.isfinite(vmin): - vmin = self.DEFAULT_RANGE[0] - if vmax is None or not numpy.isfinite(vmax): - vmax = self.DEFAULT_RANGE[1] - if vmax < vmin: - vmax = vmin - return float(vmin), float(vmax) - - def autoscaleMinMax(self, data): - """Autoscale using min/max - - :param numpy.ndarray data: - :returns: (vmin, vmax) - :rtype: Tuple[float,float] - """ - data = data[self.isValid(data)] - if data.size == 0: - return None, None - result = min_max(data, min_positive=False, finite=True) - return result.minimum, result.maximum - - def autoscaleMean3Std(self, data): - """Autoscale using mean+/-3std - - This implementation only works for normalization that do NOT - use the data range. - Override this method for normalization using the range. - - :param numpy.ndarray data: - :returns: (vmin, vmax) - :rtype: Tuple[float,float] - """ - # Use [0, 1] as data range for normalization not using range - normdata = self.apply(data, 0., 1.) - if normdata.dtype.kind == 'f': # Replaces inf by NaN - normdata[numpy.isfinite(normdata) == False] = numpy.nan - if normdata.size == 0: # Fallback - return None, None - - with warnings.catch_warnings(): - warnings.simplefilter('ignore', category=RuntimeWarning) - # Ignore nanmean "Mean of empty slice" warning and - # nanstd "Degrees of freedom <= 0 for slice" warning - mean, std = numpy.nanmean(normdata), numpy.nanstd(normdata) - - return self.revert(mean - 3 * std, 0., 1.), self.revert(mean + 3 * std, 0., 1.) - - -class _LinearNormalizationMixIn(_NormalizationMixIn): - """Colormap normalization mix-in class specific to autoscale taken from initial range""" - - def autoscaleMean3Std(self, data): - """Autoscale using mean+/-3std - - Do the autoscale on the data itself, not the normalized data. - - :param numpy.ndarray data: - :returns: (vmin, vmax) - :rtype: Tuple[float,float] - """ - if data.dtype.kind == 'f': # Replaces inf by NaN - data = numpy.array(data, copy=True) # Work on a copy - data[numpy.isfinite(data) == False] = numpy.nan - if data.size == 0: # Fallback - return None, None - with warnings.catch_warnings(): - warnings.simplefilter('ignore', category=RuntimeWarning) - # Ignore nanmean "Mean of empty slice" warning and - # nanstd "Degrees of freedom <= 0 for slice" warning - mean, std = numpy.nanmean(data), numpy.nanstd(data) - return mean - 3 * std, mean + 3 * std - - -class _LinearNormalization(_colormap.LinearNormalization, _LinearNormalizationMixIn): - """Linear normalization""" - def __init__(self): - _colormap.LinearNormalization.__init__(self) - _LinearNormalizationMixIn.__init__(self) - - -class _LogarithmicNormalization(_colormap.LogarithmicNormalization, _NormalizationMixIn): - """Logarithm normalization""" - - DEFAULT_RANGE = 1, 10 - - def __init__(self): - _colormap.LogarithmicNormalization.__init__(self) - _NormalizationMixIn.__init__(self) - - def isValid(self, value): - return value > 0. - - def autoscaleMinMax(self, data): - result = min_max(data, min_positive=True, finite=True) - return result.min_positive, result.maximum - - -class _SqrtNormalization(_colormap.SqrtNormalization, _NormalizationMixIn): - """Square root normalization""" - - DEFAULT_RANGE = 0, 1 - - def __init__(self): - _colormap.SqrtNormalization.__init__(self) - _NormalizationMixIn.__init__(self) - - def isValid(self, value): - return value >= 0. - - -class _GammaNormalization(_colormap.PowerNormalization, _LinearNormalizationMixIn): - """Gamma correction normalization: - - Linear normalization to [0, 1] followed by power normalization. - - :param gamma: Gamma correction factor - """ - def __init__(self, gamma): - _colormap.PowerNormalization.__init__(self, gamma) - _LinearNormalizationMixIn.__init__(self) - - -class _ArcsinhNormalization(_colormap.ArcsinhNormalization, _NormalizationMixIn): - """Inverse hyperbolic sine normalization""" - - def __init__(self): - _colormap.ArcsinhNormalization.__init__(self) - _NormalizationMixIn.__init__(self) - - -class Colormap(qt.QObject): - """Description of a colormap - - If no `name` nor `colors` are provided, a default gray LUT is used. - - :param str name: Name of the colormap - :param tuple colors: optional, custom colormap. - Nx3 or Nx4 numpy array of RGB(A) colors, - either uint8 or float in [0, 1]. - If 'name' is None, then this array is used as the colormap. - :param str normalization: Normalization: 'linear' (default) or 'log' - :param vmin: Lower bound of the colormap or None for autoscale (default) - :type vmin: Union[None, float] - :param vmax: Upper bounds of the colormap or None for autoscale (default) - :type vmax: Union[None, float] - """ - - LINEAR = 'linear' - """constant for linear normalization""" - - LOGARITHM = 'log' - """constant for logarithmic normalization""" - - SQRT = 'sqrt' - """constant for square root normalization""" - - GAMMA = 'gamma' - """Constant for gamma correction normalization""" - - ARCSINH = 'arcsinh' - """constant for inverse hyperbolic sine normalization""" - - _BASIC_NORMALIZATIONS = { - LINEAR: _LinearNormalization(), - LOGARITHM: _LogarithmicNormalization(), - SQRT: _SqrtNormalization(), - ARCSINH: _ArcsinhNormalization(), - } - """Normalizations without parameters""" - - NORMALIZATIONS = LINEAR, LOGARITHM, SQRT, GAMMA, ARCSINH - """Tuple of managed normalizations""" - - MINMAX = 'minmax' - """constant for autoscale using min/max data range""" - - STDDEV3 = 'stddev3' - """constant for autoscale using mean +/- 3*std(data) - with a clamp on min/max of the data""" - - AUTOSCALE_MODES = (MINMAX, STDDEV3) - """Tuple of managed auto scale algorithms""" - - sigChanged = qt.Signal() - """Signal emitted when the colormap has changed.""" - - _DEFAULT_NAN_COLOR = 255, 255, 255, 0 - - def __init__(self, name=None, colors=None, normalization=LINEAR, vmin=None, vmax=None, autoscaleMode=MINMAX): - qt.QObject.__init__(self) - self._editable = True - self.__gamma = 2.0 - # Default NaN color: fully transparent white - self.__nanColor = numpy.array(self._DEFAULT_NAN_COLOR, dtype=numpy.uint8) - - assert normalization in Colormap.NORMALIZATIONS - assert autoscaleMode in Colormap.AUTOSCALE_MODES - - if normalization is Colormap.LOGARITHM: - if (vmin is not None and vmin < 0) or (vmax is not None and vmax < 0): - m = "Unsuported vmin (%s) and/or vmax (%s) given for a log scale." - m += ' Autoscale will be performed.' - m = m % (vmin, vmax) - _logger.warning(m) - vmin = None - vmax = None - - self._name = None - self._colors = None - - if colors is not None and name is not None: - deprecation.deprecated_warning("Argument", - name="silx.gui.plot.Colors", - reason="name and colors can't be used at the same time", - since_version="0.10.0", - skip_backtrace_count=1) - - colors = None - - if name is not None: - self.setName(name) # And resets colormap LUT - elif colors is not None: - self.setColormapLUT(colors) - else: - # Default colormap is grey - self.setName("gray") - - self._normalization = str(normalization) - self._autoscaleMode = str(autoscaleMode) - self._vmin = float(vmin) if vmin is not None else None - self._vmax = float(vmax) if vmax is not None else None - - def setFromColormap(self, other): - """Set this colormap using information from the `other` colormap. - - :param ~silx.gui.colors.Colormap other: Colormap to use as reference. - """ - if not self.isEditable(): - raise NotEditableError('Colormap is not editable') - if self == other: - return - with blockSignals(self): - name = other.getName() - if name is not None: - self.setName(name) - else: - self.setColormapLUT(other.getColormapLUT()) - self.setNaNColor(other.getNaNColor()) - self.setNormalization(other.getNormalization()) - self.setGammaNormalizationParameter( - other.getGammaNormalizationParameter()) - self.setAutoscaleMode(other.getAutoscaleMode()) - self.setVRange(*other.getVRange()) - self.setEditable(other.isEditable()) - self.sigChanged.emit() - - def getNColors(self, nbColors=None): - """Returns N colors computed by sampling the colormap regularly. - - :param nbColors: - The number of colors in the returned array or None for the default value. - The default value is the size of the colormap LUT. - :type nbColors: int or None - :return: 2D array of uint8 of shape (nbColors, 4) - :rtype: numpy.ndarray - """ - # Handle default value for nbColors - if nbColors is None: - return numpy.array(self._colors, copy=True) - else: - nbColors = int(nbColors) - colormap = self.copy() - colormap.setNormalization(Colormap.LINEAR) - colormap.setVRange(vmin=0, vmax=nbColors - 1) - colors = colormap.applyToData( - numpy.arange(nbColors, dtype=numpy.int32)) - return colors - - def getName(self): - """Return the name of the colormap - :rtype: str - """ - return self._name - - def setName(self, name): - """Set the name of the colormap to use. - - :param str name: The name of the colormap. - At least the following names are supported: 'gray', - 'reversed gray', 'temperature', 'red', 'green', 'blue', 'jet', - 'viridis', 'magma', 'inferno', 'plasma'. - """ - name = str(name) - if self._name == name: - return - if self.isEditable() is False: - raise NotEditableError('Colormap is not editable') - if name not in self.getSupportedColormaps(): - raise ValueError("Colormap name '%s' is not supported" % name) - self._name = name - self._colors = _getColormap(self._name) - self.sigChanged.emit() - - def getColormapLUT(self, copy=True): - """Return the list of colors for the colormap or None if not set. - - This returns None if the colormap was set with :meth:`setName`. - Use :meth:`getNColors` to get the colormap LUT for any colormap. - - :param bool copy: If true a copy of the numpy array is provided - :return: the list of colors for the colormap or None if not set - :rtype: numpy.ndarray or None - """ - if self._name is None: - return numpy.array(self._colors, copy=copy) - else: - return None - - def setColormapLUT(self, colors): - """Set the colors of the colormap. - - :param numpy.ndarray colors: the colors of the LUT. - If float, it is converted from [0, 1] to uint8 range. - Otherwise it is casted to uint8. - - .. warning: this will set the value of name to None - """ - if self.isEditable() is False: - raise NotEditableError('Colormap is not editable') - assert colors is not None - - colors = numpy.array(colors, copy=False) - if colors.shape == (): - raise TypeError("An array is expected for 'colors' argument. '%s' was found." % type(colors)) - assert len(colors) != 0 - assert colors.ndim >= 2 - colors.shape = -1, colors.shape[-1] - self._colors = _arrayToRgba8888(colors) - self._name = None - self.sigChanged.emit() - - def getNaNColor(self): - """Returns the color to use for Not-A-Number floating point value. - - :rtype: QColor - """ - return qt.QColor(*self.__nanColor) - - def setNaNColor(self, color): - """Set the color to use for Not-A-Number floating point value. - - :param color: RGB(A) color to use for NaN values - :type color: QColor, str, tuple of uint8 or float in [0., 1.] - """ - color = (numpy.array(rgba(color)) * 255).astype(numpy.uint8) - if not numpy.array_equal(self.__nanColor, color): - self.__nanColor = color - self.sigChanged.emit() - - def getNormalization(self): - """Return the normalization of the colormap. - - See :meth:`setNormalization` for returned values. - - :return: the normalization of the colormap - :rtype: str - """ - return self._normalization - - def setNormalization(self, norm): - """Set the colormap normalization. - - Accepted normalizations: 'log', 'linear', 'sqrt' - - :param str norm: the norm to set - """ - assert norm in self.NORMALIZATIONS - if self.isEditable() is False: - raise NotEditableError('Colormap is not editable') - self._normalization = str(norm) - self.sigChanged.emit() - - def setGammaNormalizationParameter(self, gamma: float) -> None: - """Set the gamma correction parameter. - - Only used for gamma correction normalization. - - :param float gamma: - :raise ValueError: If gamma is not valid - """ - if gamma < 0. or not numpy.isfinite(gamma): - raise ValueError("Gamma value not supported") - if gamma != self.__gamma: - self.__gamma = gamma - self.sigChanged.emit() - - def getGammaNormalizationParameter(self) -> float: - """Returns the gamma correction parameter value. - - :rtype: float - """ - return self.__gamma - - def getAutoscaleMode(self): - """Return the autoscale mode of the colormap ('minmax' or 'stddev3') - - :rtype: str - """ - return self._autoscaleMode - - def setAutoscaleMode(self, mode): - """Set the autoscale mode: either 'minmax' or 'stddev3' - - :param str mode: the mode to set - """ - if self.isEditable() is False: - raise NotEditableError('Colormap is not editable') - assert mode in self.AUTOSCALE_MODES - if mode != self._autoscaleMode: - self._autoscaleMode = mode - self.sigChanged.emit() - - def isAutoscale(self): - """Return True if both min and max are in autoscale mode""" - return self._vmin is None and self._vmax is None - - def getVMin(self): - """Return the lower bound of the colormap - - :return: the lower bound of the colormap - :rtype: float or None - """ - return self._vmin - - def setVMin(self, vmin): - """Set the minimal value of the colormap - - :param float vmin: Lower bound of the colormap or None for autoscale - (default) - value) - """ - if self.isEditable() is False: - raise NotEditableError('Colormap is not editable') - if vmin is not None: - if self._vmax is not None and vmin > self._vmax: - err = "Can't set vmin because vmin >= vmax. " \ - "vmin = %s, vmax = %s" % (vmin, self._vmax) - raise ValueError(err) - - self._vmin = vmin - self.sigChanged.emit() - - def getVMax(self): - """Return the upper bounds of the colormap or None - - :return: the upper bounds of the colormap or None - :rtype: float or None - """ - return self._vmax - - def setVMax(self, vmax): - """Set the maximal value of the colormap - - :param float vmax: Upper bounds of the colormap or None for autoscale - (default) - """ - if self.isEditable() is False: - raise NotEditableError('Colormap is not editable') - if vmax is not None: - if self._vmin is not None and vmax < self._vmin: - err = "Can't set vmax because vmax <= vmin. " \ - "vmin = %s, vmax = %s" % (self._vmin, vmax) - raise ValueError(err) - - self._vmax = vmax - self.sigChanged.emit() - - def isEditable(self): - """ Return if the colormap is editable or not - - :return: editable state of the colormap - :rtype: bool - """ - return self._editable - - def setEditable(self, editable): - """ - Set the editable state of the colormap - - :param bool editable: is the colormap editable - """ - assert type(editable) is bool - self._editable = editable - self.sigChanged.emit() - - def _getNormalizer(self): - """Returns normalizer object""" - normalization = self.getNormalization() - if normalization == self.GAMMA: - return _GammaNormalization(self.getGammaNormalizationParameter()) - else: - return self._BASIC_NORMALIZATIONS[normalization] - - def _computeAutoscaleRange(self, data): - """Compute the data range which will be used in autoscale mode. - - :param numpy.ndarray data: The data for which to compute the range - :return: (vmin, vmax) range - """ - return self._getNormalizer().autoscale( - data, mode=self.getAutoscaleMode()) - - def getColormapRange(self, data=None): - """Return (vmin, vmax) the range of the colormap for the given data or item. - - :param Union[numpy.ndarray,~silx.gui.plot.items.ColormapMixIn] data: - The data or item to use for autoscale bounds. - :return: (vmin, vmax) corresponding to the colormap applied to data if provided. - :rtype: tuple - """ - vmin = self._vmin - vmax = self._vmax - assert vmin is None or vmax is None or vmin <= vmax # TODO handle this in setters - - normalizer = self._getNormalizer() - - # Handle invalid bounds as autoscale - if vmin is not None and not normalizer.isValid(vmin): - _logger.info( - 'Invalid vmin, switching to autoscale for lower bound') - vmin = None - if vmax is not None and not normalizer.isValid(vmax): - _logger.info( - 'Invalid vmax, switching to autoscale for upper bound') - vmax = None - - if vmin is None or vmax is None: # Handle autoscale - from .plot.items.core import ColormapMixIn # avoid cyclic import - if isinstance(data, ColormapMixIn): - min_, max_ = data._getColormapAutoscaleRange(self) - # Make sure min_, max_ are not None - min_ = normalizer.DEFAULT_RANGE[0] if min_ is None else min_ - max_ = normalizer.DEFAULT_RANGE[1] if max_ is None else max_ - else: - min_, max_ = normalizer.autoscale( - data, mode=self.getAutoscaleMode()) - - if vmin is None: # Set vmin respecting provided vmax - vmin = min_ if vmax is None else min(min_, vmax) - - if vmax is None: - vmax = max(max_, vmin) # Handle max_ <= 0 for log scale - - return vmin, vmax - - def getVRange(self): - """Get the bounds of the colormap - - :rtype: Tuple(Union[float,None],Union[float,None]) - :returns: A tuple of 2 values for min and max. Or None instead of float - for autoscale - """ - return self.getVMin(), self.getVMax() - - def setVRange(self, vmin, vmax): - """Set the bounds of the colormap - - :param vmin: Lower bound of the colormap or None for autoscale - (default) - :param vmax: Upper bounds of the colormap or None for autoscale - (default) - """ - if self.isEditable() is False: - raise NotEditableError('Colormap is not editable') - if vmin is not None and vmax is not None: - if vmin > vmax: - err = "Can't set vmin and vmax because vmin >= vmax " \ - "vmin = %s, vmax = %s" % (vmin, vmax) - raise ValueError(err) - - if self._vmin == vmin and self._vmax == vmax: - return - - self._vmin = vmin - self._vmax = vmax - self.sigChanged.emit() - - def __getitem__(self, item): - if item == 'autoscale': - return self.isAutoscale() - elif item == 'name': - return self.getName() - elif item == 'normalization': - return self.getNormalization() - elif item == 'vmin': - return self.getVMin() - elif item == 'vmax': - return self.getVMax() - elif item == 'colors': - return self.getColormapLUT() - elif item == 'autoscaleMode': - return self.getAutoscaleMode() - else: - raise KeyError(item) - - def _toDict(self): - """Return the equivalent colormap as a dictionary - (old colormap representation) - - :return: the representation of the Colormap as a dictionary - :rtype: dict - """ - return { - 'name': self._name, - 'colors': self.getColormapLUT(), - 'vmin': self._vmin, - 'vmax': self._vmax, - 'autoscale': self.isAutoscale(), - 'normalization': self.getNormalization(), - 'autoscaleMode': self.getAutoscaleMode(), - } - - def _setFromDict(self, dic): - """Set values to the colormap from a dictionary - - :param dict dic: the colormap as a dictionary - """ - if self.isEditable() is False: - raise NotEditableError('Colormap is not editable') - name = dic['name'] if 'name' in dic else None - colors = dic['colors'] if 'colors' in dic else None - if name is not None and colors is not None: - if isinstance(colors, int): - # Filter out argument which was supported but never used - _logger.info("Unused 'colors' from colormap dictionary filterer.") - colors = None - vmin = dic['vmin'] if 'vmin' in dic else None - vmax = dic['vmax'] if 'vmax' in dic else None - if 'normalization' in dic: - normalization = dic['normalization'] - else: - warn = 'Normalization not given in the dictionary, ' - warn += 'set by default to ' + Colormap.LINEAR - _logger.warning(warn) - normalization = Colormap.LINEAR - - if name is None and colors is None: - err = 'The colormap should have a name defined or a tuple of colors' - raise ValueError(err) - if normalization not in Colormap.NORMALIZATIONS: - err = 'Given normalization is not recognized (%s)' % normalization - raise ValueError(err) - - autoscaleMode = dic.get('autoscaleMode', Colormap.MINMAX) - if autoscaleMode not in Colormap.AUTOSCALE_MODES: - err = 'Given autoscale mode is not recognized (%s)' % autoscaleMode - raise ValueError(err) - - # If autoscale, then set boundaries to None - if dic.get('autoscale', False): - vmin, vmax = None, None - - if name is not None: - self.setName(name) - else: - self.setColormapLUT(colors) - self._vmin = vmin - self._vmax = vmax - self._autoscale = True if (vmin is None and vmax is None) else False - self._normalization = normalization - self._autoscaleMode = autoscaleMode - - self.sigChanged.emit() - - @staticmethod - def _fromDict(dic): - colormap = Colormap() - colormap._setFromDict(dic) - return colormap - - def copy(self): - """Return a copy of the Colormap. - - :rtype: silx.gui.colors.Colormap - """ - colormap = Colormap(name=self._name, - colors=self.getColormapLUT(), - vmin=self._vmin, - vmax=self._vmax, - normalization=self.getNormalization(), - autoscaleMode=self.getAutoscaleMode()) - colormap.setNaNColor(self.getNaNColor()) - colormap.setGammaNormalizationParameter( - self.getGammaNormalizationParameter()) - colormap.setEditable(self.isEditable()) - return colormap - - def applyToData(self, data, reference=None): - """Apply the colormap to the data - - :param Union[numpy.ndarray,~silx.gui.plot.item.ColormapMixIn] data: - The data to convert or the item for which to apply the colormap. - :param Union[numpy.ndarray,~silx.gui.plot.item.ColormapMixIn,None] reference: - The data or item to use as reference to compute autoscale - """ - if reference is None: - reference = data - vmin, vmax = self.getColormapRange(reference) - - if hasattr(data, "getColormappedData"): # Use item's data - data = data.getColormappedData(copy=False) - - return _colormap.cmap( - data, - self._colors, - vmin, - vmax, - self._getNormalizer(), - self.__nanColor) - - @staticmethod - def getSupportedColormaps(): - """Get the supported colormap names as a tuple of str. - - The list should at least contain and start by: - - ('gray', 'reversed gray', 'temperature', 'red', 'green', 'blue', - 'viridis', 'magma', 'inferno', 'plasma') - - :rtype: tuple - """ - colormaps = set() - if _matplotlib_colormaps is not None: - colormaps.update(_matplotlib_colormaps()) - colormaps.update(_AVAILABLE_LUTS.keys()) - - colormaps = tuple(cmap for cmap in sorted(colormaps) - if cmap not in _AVAILABLE_LUTS.keys()) - - return tuple(_AVAILABLE_LUTS.keys()) + colormaps - - def __str__(self): - return str(self._toDict()) - - def __eq__(self, other): - """Compare colormap values and not pointers""" - if other is None: - return False - if not isinstance(other, Colormap): - return False - if self.getNormalization() != other.getNormalization(): - return False - if self.getNormalization() == self.GAMMA: - delta = self.getGammaNormalizationParameter() - other.getGammaNormalizationParameter() - if abs(delta) > 0.001: - return False - return (self.getName() == other.getName() and - self.getAutoscaleMode() == other.getAutoscaleMode() and - self.getVMin() == other.getVMin() and - self.getVMax() == other.getVMax() and - numpy.array_equal(self.getColormapLUT(), other.getColormapLUT()) - ) - - _SERIAL_VERSION = 3 - - def restoreState(self, byteArray): - """ - Read the colormap state from a QByteArray. - - :param qt.QByteArray byteArray: Stream containing the state - :return: True if the restoration sussseed - :rtype: bool - """ - if self.isEditable() is False: - raise NotEditableError('Colormap is not editable') - stream = qt.QDataStream(byteArray, qt.QIODevice.ReadOnly) - - className = stream.readQString() - if className != self.__class__.__name__: - _logger.warning("Classname mismatch. Found %s." % className) - return False - - version = stream.readUInt32() - if version not in numpy.arange(1, self._SERIAL_VERSION+1): - _logger.warning("Serial version mismatch. Found %d." % version) - return False - - name = stream.readQString() - isNull = stream.readBool() - if not isNull: - vmin = stream.readQVariant() - else: - vmin = None - isNull = stream.readBool() - if not isNull: - vmax = stream.readQVariant() - else: - vmax = None - - normalization = stream.readQString() - if normalization == Colormap.GAMMA: - gamma = stream.readFloat() - else: - gamma = None - - if version == 1: - autoscaleMode = Colormap.MINMAX - else: - autoscaleMode = stream.readQString() - - if version <= 2: - nanColor = self._DEFAULT_NAN_COLOR - else: - nanColor = stream.readInt32(), stream.readInt32(), stream.readInt32(), stream.readInt32() - - # emit change event only once - old = self.blockSignals(True) - try: - self.setName(name) - self.setNormalization(normalization) - self.setAutoscaleMode(autoscaleMode) - self.setVRange(vmin, vmax) - if gamma is not None: - self.setGammaNormalizationParameter(gamma) - self.setNaNColor(nanColor) - finally: - self.blockSignals(old) - self.sigChanged.emit() - return True - - def saveState(self): - """ - Save state of the colomap into a QDataStream. - - :rtype: qt.QByteArray - """ - data = qt.QByteArray() - stream = qt.QDataStream(data, qt.QIODevice.WriteOnly) - - stream.writeQString(self.__class__.__name__) - stream.writeUInt32(self._SERIAL_VERSION) - stream.writeQString(self.getName()) - stream.writeBool(self.getVMin() is None) - if self.getVMin() is not None: - stream.writeQVariant(self.getVMin()) - stream.writeBool(self.getVMax() is None) - if self.getVMax() is not None: - stream.writeQVariant(self.getVMax()) - stream.writeQString(self.getNormalization()) - if self.getNormalization() == Colormap.GAMMA: - stream.writeFloat(self.getGammaNormalizationParameter()) - stream.writeQString(self.getAutoscaleMode()) - nanColor = self.getNaNColor() - stream.writeInt32(nanColor.red()) - stream.writeInt32(nanColor.green()) - stream.writeInt32(nanColor.blue()) - stream.writeInt32(nanColor.alpha()) - - return data - - -_PREFERRED_COLORMAPS = None -""" -Tuple of preferred colormap names accessed with :meth:`preferredColormaps`. -""" - - -def preferredColormaps(): - """Returns the name of the preferred colormaps. - - This list is used by widgets allowing to change the colormap - like the :class:`ColormapDialog` as a subset of colormap choices. - - :rtype: tuple of str - """ - global _PREFERRED_COLORMAPS - if _PREFERRED_COLORMAPS is None: - # Initialize preferred colormaps - default_preferred = [] - for name, info in _AVAILABLE_LUTS.items(): - if (info.preferred and - (info.source != 'matplotlib' or _matplotlib_cm is not None)): - default_preferred.append(name) - setPreferredColormaps(default_preferred) - return tuple(_PREFERRED_COLORMAPS) - - -def setPreferredColormaps(colormaps): - """Set the list of preferred colormap names. - - Warning: If a colormap name is not available - it will be removed from the list. - - :param colormaps: Not empty list of colormap names - :type colormaps: iterable of str - :raise ValueError: if the list of available preferred colormaps is empty. - """ - supportedColormaps = Colormap.getSupportedColormaps() - colormaps = [cmap for cmap in colormaps if cmap in supportedColormaps] - if len(colormaps) == 0: - raise ValueError("Cannot set preferred colormaps to an empty list") - - global _PREFERRED_COLORMAPS - _PREFERRED_COLORMAPS = colormaps - - -def registerLUT(name, colors, cursor_color='black', preferred=True): - """Register a custom LUT to be used with `Colormap` objects. - - It can override existing LUT names. - - :param str name: Name of the LUT as defined to configure colormaps - :param numpy.ndarray colors: The custom LUT to register. - Nx3 or Nx4 numpy array of RGB(A) colors, - either uint8 or float in [0, 1]. - :param bool preferred: If true, this LUT will be displayed as part of the - preferred colormaps in dialogs. - :param str cursor_color: Color used to display overlay over images using - colormap with this LUT. - """ - description = _LUT_DESCRIPTION('user', cursor_color, preferred=preferred) - colors = _arrayToRgba8888(colors) - _AVAILABLE_LUTS[name] = description - - if preferred: - # Invalidate the preferred cache - global _PREFERRED_COLORMAPS - if _PREFERRED_COLORMAPS is not None: - if name not in _PREFERRED_COLORMAPS: - _PREFERRED_COLORMAPS.append(name) - else: - # The cache is not yet loaded, it's fine - pass - - # Register the cache as the LUT was already loaded - _COLORMAP_CACHE[name] = colors diff --git a/silx/gui/console.py b/silx/gui/console.py deleted file mode 100644 index 5dc6336..0000000 --- a/silx/gui/console.py +++ /dev/null @@ -1,202 +0,0 @@ -# coding: utf-8 -# /*########################################################################## -# -# Copyright (c) 2004-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. -# -# ###########################################################################*/ -"""This module provides an IPython console widget. - -You can push variables - any python object - to the -console's interactive namespace. This provides users with an advanced way -of interacting with your program. For instance, if your program has a -:class:`PlotWidget` or a :class:`PlotWindow`, you can push a reference to -these widgets to allow your users to add curves, save data to files… by using -the widgets' methods from the console. - -.. note:: - - This module has a dependency on - `qtconsole <https://pypi.org/project/qtconsole/>`_. - An ``ImportError`` will be raised if it is - imported while the dependencies are not satisfied. - -Basic usage example:: - - from silx.gui import qt - from silx.gui.console import IPythonWidget - - app = qt.QApplication([]) - - hello_button = qt.QPushButton("Hello World!", None) - hello_button.show() - - console = IPythonWidget() - console.show() - console.pushVariables({"the_button": hello_button}) - - app.exec_() - -This program will display a console widget and a push button in two separate -windows. You will be able to interact with the button from the console, -for example change its text:: - - >>> the_button.setText("Spam spam") - -An IPython interactive console is a powerful tool that enables you to work -with data and plot it. -See `this tutorial <https://plot.ly/python/ipython-notebook-tutorial/>`_ -for more information on some of the rich features of IPython. -""" -__authors__ = ["Tim Rae", "V.A. Sole", "P. Knobel"] -__license__ = "MIT" -__date__ = "24/05/2016" - -import logging - -from . import qt - -_logger = logging.getLogger(__name__) - - -# This widget cannot be used inside an interactive IPython shell. -# It would raise MultipleInstanceError("Multiple incompatible subclass -# instances of InProcessInteractiveShell are being created"). -try: - __IPYTHON__ -except NameError: - pass # Not in IPython -else: - msg = "Module " + __name__ + " cannot be used within an IPython shell" - raise ImportError(msg) - -try: - from qtconsole.rich_jupyter_widget import RichJupyterWidget as \ - _RichJupyterWidget -except ImportError: - try: - from qtconsole.rich_ipython_widget import RichJupyterWidget as \ - _RichJupyterWidget - except ImportError: - from qtconsole.rich_ipython_widget import RichIPythonWidget as \ - _RichJupyterWidget - -from qtconsole.inprocess import QtInProcessKernelManager - -try: - from ipykernel import version_info as _ipykernel_version_info -except ImportError: - _ipykernel_version_info = None - - -class IPythonWidget(_RichJupyterWidget): - """Live IPython console widget. - - .. image:: img/IPythonWidget.png - - :param custom_banner: Custom welcome message to be printed at the top of - the console. - """ - - def __init__(self, parent=None, custom_banner=None, *args, **kwargs): - if parent is not None: - kwargs["parent"] = parent - super(IPythonWidget, self).__init__(*args, **kwargs) - if custom_banner is not None: - self.banner = custom_banner - self.setWindowTitle(self.banner) - self.kernel_manager = kernel_manager = QtInProcessKernelManager() - kernel_manager.start_kernel() - - # Monkey-patch to workaround issue: - # https://github.com/ipython/ipykernel/issues/370 - if (_ipykernel_version_info is not None and - _ipykernel_version_info[0] > 4 and - _ipykernel_version_info[:3] <= (5, 1, 0)): - def _abort_queues(*args, **kwargs): - pass - kernel_manager.kernel._abort_queues = _abort_queues - - self.kernel_client = kernel_client = self._kernel_manager.client() - kernel_client.start_channels() - - def stop(): - kernel_client.stop_channels() - kernel_manager.shutdown_kernel() - self.exit_requested.connect(stop) - - def sizeHint(self): - """Return a reasonable default size for usage in :class:`PlotWindow`""" - return qt.QSize(500, 300) - - def pushVariables(self, variable_dict): - """ Given a dictionary containing name / value pairs, push those - variables to the IPython console widget. - - :param variable_dict: Dictionary of variables to be pushed to the - console's interactive namespace (```{variable_name: object, …}```) - """ - self.kernel_manager.kernel.shell.push(variable_dict) - - -class IPythonDockWidget(qt.QDockWidget): - """Dock Widget including a :class:`IPythonWidget` inside - a vertical layout. - - .. image:: img/IPythonDockWidget.png - - :param available_vars: Dictionary of variables to be pushed to the - console's interactive namespace: ``{"variable_name": object, …}`` - :param custom_banner: Custom welcome message to be printed at the top of - the console - :param title: Dock widget title - :param parent: Parent :class:`qt.QMainWindow` containing this - :class:`qt.QDockWidget` - """ - def __init__(self, parent=None, available_vars=None, custom_banner=None, - title="Console"): - super(IPythonDockWidget, self).__init__(title, parent) - - self.ipyconsole = IPythonWidget(custom_banner=custom_banner) - - self.layout().setContentsMargins(0, 0, 0, 0) - self.setWidget(self.ipyconsole) - - if available_vars is not None: - self.ipyconsole.pushVariables(available_vars) - - def showEvent(self, event): - """Make sure this widget is raised when it is shown - (when it is first created as a tab in PlotWindow or when it is shown - again after hiding). - """ - self.raise_() - - -def main(): - """Run a Qt app with an IPython console""" - app = qt.QApplication([]) - widget = IPythonDockWidget() - widget.show() - app.exec_() - - -if __name__ == '__main__': - main() diff --git a/silx/gui/data/ArrayTableModel.py b/silx/gui/data/ArrayTableModel.py deleted file mode 100644 index b7bd9c4..0000000 --- a/silx/gui/data/ArrayTableModel.py +++ /dev/null @@ -1,670 +0,0 @@ -# coding: utf-8 -# /*########################################################################## -# -# Copyright (c) 2016-2021 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 module defines a data model for displaying and editing arrays of any -number of dimensions in a table view. -""" -from __future__ import division -import numpy -import logging -from silx.gui import qt -from silx.gui.data.TextFormatter import TextFormatter - -__authors__ = ["V.A. Sole"] -__license__ = "MIT" -__date__ = "27/09/2017" - - -_logger = logging.getLogger(__name__) - - -def _is_array(data): - """Return True if object implements all necessary attributes to be used - as a numpy array. - - :param object data: Array-like object (numpy array, h5py dataset...) - :return: boolean - """ - # add more required attribute if necessary - for attr in ("shape", "dtype"): - if not hasattr(data, attr): - return False - return True - - -class ArrayTableModel(qt.QAbstractTableModel): - """This data model provides access to 2D slices in a N-dimensional - array. - - A slice for a 3-D array is characterized by a perspective (the number of - the axis orthogonal to the slice) and an index at which the slice - intersects the orthogonal axis. - - In the n-D case, only slices parallel to the last two axes are handled. A - slice is therefore characterized by a list of indices locating the - slice on all the :math:`n - 2` orthogonal axes. - - :param parent: Parent QObject - :param data: Numpy array, or object implementing a similar interface - (e.g. h5py dataset) - :param str fmt: Format string for representing numerical values. - Default is ``"%g"``. - :param sequence[int] perspective: See documentation - of :meth:`setPerspective`. - """ - - MAX_NUMBER_OF_SECTIONS = 10e6 - """Maximum number of displayed rows and columns""" - - def __init__(self, parent=None, data=None, perspective=None): - qt.QAbstractTableModel.__init__(self, parent) - - self._array = None - """n-dimensional numpy array""" - - self._bgcolors = None - """(n+1)-dimensional numpy array containing RGB(A) color data - for the background color - """ - - self._fgcolors = None - """(n+1)-dimensional numpy array containing RGB(A) color data - for the foreground color - """ - - self._formatter = None - """Formatter for text representation of data""" - - formatter = TextFormatter(self) - formatter.setUseQuoteForText(False) - self.setFormatter(formatter) - - self._index = None - """This attribute stores the slice index, as a list of indices - where the frame intersects orthogonal axis.""" - - self._perspective = None - """Sequence of dimensions orthogonal to the frame to be viewed. - For an array with ``n`` dimensions, this is a sequence of ``n-2`` - integers. the first dimension is numbered ``0``. - By default, the data frames use the last two dimensions as their axes - and therefore the perspective is a sequence of the first ``n-2`` - dimensions. - For example, for a 5-D array, the default perspective is ``(0, 1, 2)`` - and the default frames axes are ``(3, 4)``.""" - - # set _data and _perspective - self.setArrayData(data, perspective=perspective) - - def _getRowDim(self): - """The row axis is the first axis parallel to the frames - (lowest dimension number) - - Return None for 0-D (scalar) or 1-D arrays - """ - n_dimensions = len(self._array.shape) - if n_dimensions < 2: - # scalar or 1D array: no row index - return None - # take all dimensions and remove the orthogonal ones - frame_axes = set(range(0, n_dimensions)) - set(self._perspective) - # sanity check - assert len(frame_axes) == 2 - return min(frame_axes) - - def _getColumnDim(self): - """The column axis is the second (highest dimension) axis parallel - to the frames - - Return None for 0-D (scalar) - """ - n_dimensions = len(self._array.shape) - if n_dimensions < 1: - # scalar: no column index - return None - frame_axes = set(range(0, n_dimensions)) - set(self._perspective) - # sanity check - assert (len(frame_axes) == 2) if n_dimensions > 1 else (len(frame_axes) == 1) - return max(frame_axes) - - def _getIndexTuple(self, table_row, table_col): - """Return the n-dimensional index of a value in the original array, - based on its row and column indices in the table view - - :param table_row: Row index (0-based) of a table cell - :param table_col: Column index (0-based) of a table cell - :return: Tuple of indices of the element in the numpy array - """ - row_dim = self._getRowDim() - col_dim = self._getColumnDim() - - # get indices on all orthogonal axes - selection = list(self._index) - # insert indices on parallel axes - if row_dim is not None: - selection.insert(row_dim, table_row) - if col_dim is not None: - selection.insert(col_dim, table_col) - return tuple(selection) - - # Methods to be implemented to subclass QAbstractTableModel - def rowCount(self, parent_idx=None): - """QAbstractTableModel method - Return number of rows to be displayed in table""" - row_dim = self._getRowDim() - if row_dim is None: - # 0-D and 1-D arrays - return 1 - return min(self._array.shape[row_dim], self.MAX_NUMBER_OF_SECTIONS) - - def columnCount(self, parent_idx=None): - """QAbstractTableModel method - Return number of columns to be displayed in table""" - col_dim = self._getColumnDim() - if col_dim is None: - # 0-D array - return 1 - return min(self._array.shape[col_dim], self.MAX_NUMBER_OF_SECTIONS) - - def __isClipped(self, orientation=qt.Qt.Vertical) -> bool: - """Returns whether or not array is clipped in a given orientation""" - if orientation == qt.Qt.Vertical: - dim = self._getRowDim() - else: - dim = self._getColumnDim() - return (dim is not None and - self._array.shape[dim] > self.MAX_NUMBER_OF_SECTIONS) - - def __isClippedIndex(self, index) -> bool: - """Returns whether or not index's cell represents clipped data.""" - if not index.isValid(): - return False - if index.row() == self.MAX_NUMBER_OF_SECTIONS - 2: - return self.__isClipped(qt.Qt.Vertical) - if index.column() == self.MAX_NUMBER_OF_SECTIONS - 2: - return self.__isClipped(qt.Qt.Horizontal) - return False - - def __clippedData(self, role=qt.Qt.DisplayRole): - """Return data for cells representing clipped data""" - if role == qt.Qt.DisplayRole: - return "..." - elif role == qt.Qt.ToolTipRole: - return "Dataset is too large: display is clipped" - else: - return None - - def data(self, index, role=qt.Qt.DisplayRole): - """QAbstractTableModel method to access data values - in the format ready to be displayed""" - if index.isValid(): - if self.__isClippedIndex(index): # Special displayed for clipped data - return self.__clippedData(role) - - row, column = index.row(), index.column() - - # When clipped, display last data of the array in last column of the table - if (self.__isClipped(qt.Qt.Vertical) and - row == self.MAX_NUMBER_OF_SECTIONS - 1): - row = self._array.shape[self._getRowDim()] - 1 - if (self.__isClipped(qt.Qt.Horizontal) and - column == self.MAX_NUMBER_OF_SECTIONS - 1): - column = self._array.shape[self._getColumnDim()] - 1 - - selection = self._getIndexTuple(row, column) - - if role == qt.Qt.DisplayRole: - return self._formatter.toString(self._array[selection], self._array.dtype) - - if role == qt.Qt.BackgroundRole and self._bgcolors is not None: - r, g, b = self._bgcolors[selection][0:3] - if self._bgcolors.shape[-1] == 3: - return qt.QColor(r, g, b) - if self._bgcolors.shape[-1] == 4: - a = self._bgcolors[selection][3] - return qt.QColor(r, g, b, a) - - if role == qt.Qt.ForegroundRole: - if self._fgcolors is not None: - r, g, b = self._fgcolors[selection][0:3] - if self._fgcolors.shape[-1] == 3: - return qt.QColor(r, g, b) - if self._fgcolors.shape[-1] == 4: - a = self._fgcolors[selection][3] - return qt.QColor(r, g, b, a) - - # no fg color given, use black or white - # based on luminosity threshold - elif self._bgcolors is not None: - r, g, b = self._bgcolors[selection][0:3] - lum = 0.21 * r + 0.72 * g + 0.07 * b - if lum < 128: - return qt.QColor(qt.Qt.white) - else: - return qt.QColor(qt.Qt.black) - - def headerData(self, section, orientation, role=qt.Qt.DisplayRole): - """QAbstractTableModel method - Return the 0-based row or column index, for display in the - horizontal and vertical headers""" - if self.__isClipped(orientation): # Header is clipped - if section == self.MAX_NUMBER_OF_SECTIONS - 2: - # Represent clipped data - return self.__clippedData(role) - - elif section == self.MAX_NUMBER_OF_SECTIONS - 1: - # Display last index from data not table - if role == qt.Qt.DisplayRole: - if orientation == qt.Qt.Vertical: - dim = self._getRowDim() - else: - dim = self._getColumnDim() - return str(self._array.shape[dim] - 1) - else: - return None - - if role == qt.Qt.DisplayRole: - return "%d" % section - return None - - def flags(self, index): - """QAbstractTableModel method to inform the view whether data - is editable or not.""" - if not self._editable or self.__isClippedIndex(index): - return qt.QAbstractTableModel.flags(self, index) - return qt.QAbstractTableModel.flags(self, index) | qt.Qt.ItemIsEditable - - def setData(self, index, value, role=None): - """QAbstractTableModel method to handle editing data. - Cast the new value into the same format as the array before editing - the array value.""" - if index.isValid() and role == qt.Qt.EditRole: - try: - # cast value to same type as array - v = numpy.array(value, dtype=self._array.dtype).item() - except ValueError: - return False - - selection = self._getIndexTuple(index.row(), - index.column()) - self._array[selection] = v - self.dataChanged.emit(index, index) - return True - else: - return False - - # Public methods - def setArrayData(self, data, copy=True, - perspective=None, editable=False): - """Set the data array and the viewing perspective. - - You can set ``copy=False`` if you need more performances, when dealing - with a large numpy array. In this case, a simple reference to the data - is used to access the data, rather than a copy of the array. - - .. warning:: - - Any change to the data model will affect your original data - array, when using a reference rather than a copy.. - - :param data: n-dimensional numpy array, or any object that can be - converted to a numpy array using ``numpy.array(data)`` (e.g. - a nested sequence). - :param bool copy: If *True* (default), a copy of the array is stored - and the original array is not modified if the table is edited. - If *False*, then the behavior depends on the data type: - if possible (if the original array is a proper numpy array) - a reference to the original array is used. - :param perspective: See documentation of :meth:`setPerspective`. - If None, the default perspective is the list of the first ``n-2`` - dimensions, to view frames parallel to the last two axes. - :param bool editable: Flag to enable editing data. Default *False*. - """ - if qt.qVersion() > "4.6": - self.beginResetModel() - else: - self.reset() - - if data is None: - # empty array - self._array = numpy.array([]) - elif copy: - # copy requested (default) - self._array = numpy.array(data, copy=True) - if hasattr(data, "dtype"): - # Avoid to lose the monkey-patched h5py dtype - self._array.dtype = data.dtype - elif not _is_array(data): - raise TypeError("data is not a proper array. Try setting" + - " copy=True to convert it into a numpy array" + - " (this will cause the data to be copied!)") - # # copy not requested, but necessary - # _logger.warning( - # "data is not an array-like object. " + - # "Data must be copied.") - # self._array = numpy.array(data, copy=True) - else: - # Copy explicitly disabled & data implements required attributes. - # We can use a reference. - self._array = data - - # reset colors to None if new data shape is inconsistent - valid_color_shapes = (self._array.shape + (3,), - self._array.shape + (4,)) - if self._bgcolors is not None: - if self._bgcolors.shape not in valid_color_shapes: - self._bgcolors = None - if self._fgcolors is not None: - if self._fgcolors.shape not in valid_color_shapes: - self._fgcolors = None - - self.setEditable(editable) - - self._index = [0 for _i in range((len(self._array.shape) - 2))] - self._perspective = tuple(perspective) if perspective is not None else\ - tuple(range(0, len(self._array.shape) - 2)) - - if qt.qVersion() > "4.6": - self.endResetModel() - - def setArrayColors(self, bgcolors=None, fgcolors=None): - """Set the colors for all table cells by passing an array - of RGB or RGBA values (integers between 0 and 255). - - The shape of the colors array must be consistent with the data shape. - - If the data array is n-dimensional, the colors array must be - (n+1)-dimensional, with the first n-dimensions identical to the data - array dimensions, and the last dimension length-3 (RGB) or - length-4 (RGBA). - - :param bgcolors: RGB or RGBA colors array, defining the background color - for each cell in the table. - :param fgcolors: RGB or RGBA colors array, defining the foreground color - (text color) for each cell in the table. - """ - # array must be RGB or RGBA - valid_shapes = (self._array.shape + (3,), self._array.shape + (4,)) - errmsg = "Inconsistent shape for color array, should be %s or %s" % valid_shapes - - if bgcolors is not None: - if not _is_array(bgcolors): - bgcolors = numpy.array(bgcolors) - assert bgcolors.shape in valid_shapes, errmsg - - self._bgcolors = bgcolors - - if fgcolors is not None: - if not _is_array(fgcolors): - fgcolors = numpy.array(fgcolors) - assert fgcolors.shape in valid_shapes, errmsg - - self._fgcolors = fgcolors - - def setEditable(self, editable): - """Set flags to make the data editable. - - .. warning:: - - If the data is a reference to a h5py dataset open in read-only - mode, setting *editable=True* will fail and print a warning. - - .. warning:: - - Making the data editable means that the underlying data structure - in this data model will be modified. - If the data is a reference to a public object (open with - ``copy=False``), this could have side effects. If it is a - reference to an HDF5 dataset, this means the file will be - modified. - - :param bool editable: Flag to enable editing data. - :return: True if setting desired flag succeeded, False if it failed. - """ - self._editable = editable - if hasattr(self._array, "file"): - if hasattr(self._array.file, "mode"): - if editable and self._array.file.mode == "r": - _logger.warning( - "Data is a HDF5 dataset open in read-only " + - "mode. Editing must be disabled.") - self._editable = False - return False - return True - - def getData(self, copy=True): - """Return a copy of the data array, or a reference to it - if *copy=False* is passed as parameter. - - In case the shape was modified, to convert 0-D or 1-D data - into 2-D data, the original shape is restored in the returned data. - - :param bool copy: If *True* (default), return a copy of the data. If - *False*, return a reference. - :return: numpy array of data, or reference to original data object - if *copy=False* - """ - data = self._array if not copy else numpy.array(self._array, copy=True) - return data - - def setFrameIndex(self, index): - """Set the active slice index. - - This method is only relevant to arrays with at least 3 dimensions. - - :param index: Index of the active slice in the array. - In the general n-D case, this is a sequence of :math:`n - 2` - indices where the slice intersects the respective orthogonal axes. - :raise IndexError: If any index in the index sequence is out of bound - on its respective axis. - """ - shape = self._array.shape - if len(shape) < 3: - # index is ignored - return - - if qt.qVersion() > "4.6": - self.beginResetModel() - else: - self.reset() - - if len(shape) == 3: - len_ = shape[self._perspective[0]] - # accept integers as index in the case of 3-D arrays - if not hasattr(index, "__len__"): - self._index = [index] - else: - self._index = index - if not 0 <= self._index[0] < len_: - raise ValueError("Index must be a positive integer " + - "lower than %d" % len_) - else: - # general n-D case - for i_, idx in enumerate(index): - if not 0 <= idx < shape[self._perspective[i_]]: - raise IndexError("Invalid index %d " % idx + - "not in range 0-%d" % (shape[i_] - 1)) - self._index = index - - if qt.qVersion() > "4.6": - self.endResetModel() - - def setFormatter(self, formatter): - """Set the formatter object to be used to display data from the model - - :param TextFormatter formatter: Formatter to use - """ - if formatter is self._formatter: - return - - if qt.qVersion() > "4.6": - self.beginResetModel() - - if self._formatter is not None: - self._formatter.formatChanged.disconnect(self.__formatChanged) - - self._formatter = formatter - if self._formatter is not None: - self._formatter.formatChanged.connect(self.__formatChanged) - - if qt.qVersion() > "4.6": - self.endResetModel() - else: - self.reset() - - def getFormatter(self): - """Returns the text formatter used. - - :rtype: TextFormatter - """ - return self._formatter - - def __formatChanged(self): - """Called when the format changed. - """ - self.reset() - - def setPerspective(self, perspective): - """Set the perspective by defining a sequence listing all axes - orthogonal to the frame or 2-D slice to be visualized. - - Alternatively, you can use :meth:`setFrameAxes` for the complementary - approach of specifying the two axes parallel to the frame. - - In the 1-D or 2-D case, this parameter is irrelevant. - - In the 3-D case, if the unit vectors describing - your axes are :math:`\vec{x}, \vec{y}, \vec{z}`, a perspective of 0 - means you slices are parallel to :math:`\vec{y}\vec{z}`, 1 means they - are parallel to :math:`\vec{x}\vec{z}` and 2 means they - are parallel to :math:`\vec{x}\vec{y}`. - - In the n-D case, this parameter is a sequence of :math:`n-2` axes - numbers. - For instance if you want to display 2-D frames whose axes are the - second and third dimensions of a 5-D array, set the perspective to - ``(0, 3, 4)``. - - :param perspective: Sequence of dimensions/axes orthogonal to the - frames. - :raise: IndexError if any value in perspective is higher than the - number of dimensions minus one (first dimension is 0), or - if the number of values is different from the number of dimensions - minus two. - """ - n_dimensions = len(self._array.shape) - if n_dimensions < 3: - _logger.warning( - "perspective is not relevant for 1D and 2D arrays") - return - - if not hasattr(perspective, "__len__"): - # we can tolerate an integer for 3-D array - if n_dimensions == 3: - perspective = [perspective] - else: - raise ValueError("perspective must be a sequence of integers") - - # ensure unicity of dimensions in perspective - perspective = tuple(set(perspective)) - - if len(perspective) != n_dimensions - 2 or\ - min(perspective) < 0 or max(perspective) >= n_dimensions: - raise IndexError( - "Invalid perspective " + str(perspective) + - " for %d-D array " % n_dimensions + - "with shape " + str(self._array.shape)) - - if qt.qVersion() > "4.6": - self.beginResetModel() - else: - self.reset() - - self._perspective = perspective - - # reset index - self._index = [0 for _i in range(n_dimensions - 2)] - - if qt.qVersion() > "4.6": - self.endResetModel() - - def setFrameAxes(self, row_axis, col_axis): - """Set the perspective by specifying the two axes parallel to the frame - to be visualised. - - The complementary approach of defining the orthogonal axes can be used - with :meth:`setPerspective`. - - :param int row_axis: Index (0-based) of the first dimension used as a frame - axis - :param int col_axis: Index (0-based) of the 2nd dimension used as a frame - axis - :raise: IndexError if axes are invalid - """ - if row_axis > col_axis: - _logger.warning("The dimension of the row axis must be lower " + - "than the dimension of the column axis. Swapping.") - row_axis, col_axis = min(row_axis, col_axis), max(row_axis, col_axis) - - n_dimensions = len(self._array.shape) - if n_dimensions < 3: - _logger.warning( - "Frame axes cannot be changed for 1D and 2D arrays") - return - - perspective = tuple(set(range(0, n_dimensions)) - {row_axis, col_axis}) - - if len(perspective) != n_dimensions - 2 or\ - min(perspective) < 0 or max(perspective) >= n_dimensions: - raise IndexError( - "Invalid perspective " + str(perspective) + - " for %d-D array " % n_dimensions + - "with shape " + str(self._array.shape)) - - if qt.qVersion() > "4.6": - self.beginResetModel() - else: - self.reset() - - self._perspective = perspective - # reset index - self._index = [0 for _i in range(n_dimensions - 2)] - - if qt.qVersion() > "4.6": - self.endResetModel() - - -if __name__ == "__main__": - app = qt.QApplication([]) - w = qt.QTableView() - d = numpy.random.normal(0, 1, (5, 1000, 1000)) - for i in range(5): - d[i, :, :] += i * 10 - m = ArrayTableModel(data=d) - w.setModel(m) - m.setFrameIndex(3) - # m.setArrayData(numpy.ones((100,))) - w.show() - app.exec_() diff --git a/silx/gui/data/ArrayTableWidget.py b/silx/gui/data/ArrayTableWidget.py deleted file mode 100644 index cb8e915..0000000 --- a/silx/gui/data/ArrayTableWidget.py +++ /dev/null @@ -1,492 +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. -# -# ###########################################################################*/ -"""This module defines a widget designed to display data arrays with any -number of dimensions as 2D frames (images, slices) in a table view. -The dimensions not displayed in the table can be browsed using improved -sliders. - -The widget uses a TableView that relies on a custom abstract item -model: :class:`silx.gui.data.ArrayTableModel`. -""" -from __future__ import division -import sys - -from silx.gui import qt -from silx.gui.widgets.TableWidget import TableView -from .ArrayTableModel import ArrayTableModel -from silx.gui.widgets.FrameBrowser import HorizontalSliderWithBrowser - -__authors__ = ["V.A. Sole", "P. Knobel"] -__license__ = "MIT" -__date__ = "24/01/2017" - - -class AxesSelector(qt.QWidget): - """Widget with two combo-boxes to select two dimensions among - all possible dimensions of an n-dimensional array. - - The first combobox contains values from :math:`0` to :math:`n-2`. - - The choices in the 2nd CB depend on the value selected in the first one. - If the value selected in the first CB is :math:`m`, the second one lets you - select values from :math:`m+1` to :math:`n-1`. - - The two axes can be used to select the row axis and the column axis t - display a slice of the array data in a table view. - """ - sigDimensionsChanged = qt.Signal(int, int) - """Signal emitted whenever one of the comboboxes is changed. - The signal carries the two selected dimensions.""" - - def __init__(self, parent=None, n=None): - qt.QWidget.__init__(self, parent) - self.layout = qt.QHBoxLayout(self) - self.layout.setContentsMargins(0, 2, 0, 2) - self.layout.setSpacing(10) - - self.rowsCB = qt.QComboBox(self) - self.columnsCB = qt.QComboBox(self) - - self.layout.addWidget(qt.QLabel("Rows dimension", self)) - self.layout.addWidget(self.rowsCB) - self.layout.addWidget(qt.QLabel(" ", self)) - self.layout.addWidget(qt.QLabel("Columns dimension", self)) - self.layout.addWidget(self.columnsCB) - self.layout.addStretch(1) - - self._slotsAreConnected = False - if n is not None: - self.setNDimensions(n) - - def setNDimensions(self, n): - """Initialize combo-boxes depending on number of dimensions of array. - Initially, the rows dimension is the second-to-last one, and the - columns dimension is the last one. - - Link the CBs together. MAke them emit a signal when their value is - changed. - - :param int n: Number of dimensions of array - """ - # remember the number of dimensions and the rows dimension - self.n = n - self._rowsDim = n - 2 - - # ensure slots are disconnected before (re)initializing widget - if self._slotsAreConnected: - self.rowsCB.currentIndexChanged.disconnect(self._rowDimChanged) - self.columnsCB.currentIndexChanged.disconnect(self._colDimChanged) - - self._clear() - self.rowsCB.addItems([str(i) for i in range(n - 1)]) - self.rowsCB.setCurrentIndex(n - 2) - if n >= 1: - self.columnsCB.addItem(str(n - 1)) - self.columnsCB.setCurrentIndex(0) - - # reconnect slots - self.rowsCB.currentIndexChanged.connect(self._rowDimChanged) - self.columnsCB.currentIndexChanged.connect(self._colDimChanged) - self._slotsAreConnected = True - - # emit new dimensions - if n > 2: - self.sigDimensionsChanged.emit(n - 2, n - 1) - - def setDimensions(self, row_dim, col_dim): - """Set the rows and columns dimensions. - - The rows dimension must be lower than the columns dimension. - - :param int row_dim: Rows dimension - :param int col_dim: Columns dimension - """ - if row_dim >= col_dim: - raise IndexError("Row dimension must be lower than column dimension") - if not (0 <= row_dim < self.n - 1): - raise IndexError("Row dimension must be between 0 and %d" % (self.n - 2)) - if not (row_dim < col_dim <= self.n - 1): - raise IndexError("Col dimension must be between %d and %d" % (row_dim + 1, self.n - 1)) - - # set the rows dimension; this triggers an update of columnsCB - self.rowsCB.setCurrentIndex(row_dim) - # columnsCB first item is "row_dim + 1". So index of "col_dim" is - # col_dim - (row_dim + 1) - self.columnsCB.setCurrentIndex(col_dim - row_dim - 1) - - def getDimensions(self): - """Return a 2-tuple of the rows dimension and the columns dimension. - - :return: 2-tuple of axes numbers (row_dimension, col_dimension) - """ - return self._getRowDim(), self._getColDim() - - def _clear(self): - """Empty the combo-boxes""" - self.rowsCB.clear() - self.columnsCB.clear() - - def _getRowDim(self): - """Get rows dimension, selected in :attr:`rowsCB` - """ - # rows combobox contains elements "0", ..."n-2", - # so the selected dim is always equal to the index - return self.rowsCB.currentIndex() - - def _getColDim(self): - """Get columns dimension, selected in :attr:`columnsCB`""" - # columns combobox contains elements "row_dim+1", "row_dim+2", ..., "n-1" - # so the selected dim is equal to row_dim + 1 + index - return self._rowsDim + 1 + self.columnsCB.currentIndex() - - def _rowDimChanged(self): - """Update columns combobox when the rows dimension is changed. - - Emit :attr:`sigDimensionsChanged`""" - old_col_dim = self._getColDim() - new_row_dim = self._getRowDim() - - # clear cols CB - self.columnsCB.currentIndexChanged.disconnect(self._colDimChanged) - self.columnsCB.clear() - # refill cols CB - for i in range(new_row_dim + 1, self.n): - self.columnsCB.addItem(str(i)) - - # keep previous col dimension if possible - new_col_cb_idx = old_col_dim - (new_row_dim + 1) - if new_col_cb_idx < 0: - # if row_dim is now greater than the previous col_dim, - # we select a new col_dim = row_dim + 1 (first element in cols CB) - new_col_cb_idx = 0 - self.columnsCB.setCurrentIndex(new_col_cb_idx) - - # reconnect slot - self.columnsCB.currentIndexChanged.connect(self._colDimChanged) - - self._rowsDim = new_row_dim - - self.sigDimensionsChanged.emit(self._getRowDim(), self._getColDim()) - - def _colDimChanged(self): - """Emit :attr:`sigDimensionsChanged`""" - self.sigDimensionsChanged.emit(self._getRowDim(), self._getColDim()) - - -def _get_shape(array_like): - """Return shape of an array like object. - - In case the object is a nested sequence (list of lists, tuples...), - the size of each dimension is assumed to be uniform, and is deduced from - the length of the first sequence. - - :param array_like: Array like object: numpy array, hdf5 dataset, - multi-dimensional sequence - :return: Shape of array, as a tuple of integers - """ - if hasattr(array_like, "shape"): - return array_like.shape - - shape = [] - subsequence = array_like - while hasattr(subsequence, "__len__"): - shape.append(len(subsequence)) - subsequence = subsequence[0] - - return tuple(shape) - - -class ArrayTableWidget(qt.QWidget): - """This widget is designed to display data of 2D frames (images, slices) - in a table view. The widget can load any n-dimensional array, and display - any 2-D frame/slice in the array. - - The index of the dimensions orthogonal to the displayed frame can be set - interactively using a browser widget (sliders, buttons and text entries). - - To set the data, use :meth:`setArrayData`. - To select the perspective, use :meth:`setPerspective` or - use :meth:`setFrameAxes`. - To select the frame, use :meth:`setFrameIndex`. - - .. image:: img/ArrayTableWidget.png - """ - def __init__(self, parent=None): - """ - - :param parent: parent QWidget - :param labels: list of labels for each dimension of the array - """ - qt.QWidget.__init__(self, parent) - self.mainLayout = qt.QVBoxLayout(self) - self.mainLayout.setContentsMargins(0, 0, 0, 0) - self.mainLayout.setSpacing(0) - - self.browserContainer = qt.QWidget(self) - self.browserLayout = qt.QGridLayout(self.browserContainer) - self.browserLayout.setContentsMargins(0, 0, 0, 0) - self.browserLayout.setSpacing(0) - - self._dimensionLabelsText = [] - """List of text labels sorted in the increasing order of the dimension - they apply to.""" - self._browserLabels = [] - """List of QLabel widgets.""" - self._browserWidgets = [] - """List of HorizontalSliderWithBrowser widgets.""" - - self.axesSelector = AxesSelector(self) - - self.view = TableView(self) - - self.mainLayout.addWidget(self.browserContainer) - self.mainLayout.addWidget(self.axesSelector) - self.mainLayout.addWidget(self.view) - - self.model = ArrayTableModel(self) - self.view.setModel(self.model) - - def setArrayData(self, data, labels=None, copy=True, editable=False): - """Set the data array. Update frame browsers and labels. - - :param data: Numpy array or similar object (e.g. nested sequence, - h5py dataset...) - :param labels: list of labels for each dimension of the array, or - boolean ``True`` to use default labels ("dimension 0", - "dimension 1", ...). `None` to disable labels (default). - :param bool copy: If *True*, store a copy of *data* in the model. If - *False*, store a reference to *data* if possible (only possible if - *data* is a proper numpy array or an object that implements the - same methods). - :param bool editable: Flag to enable editing data. Default is *False* - """ - self._data_shape = _get_shape(data) - - n_widgets = len(self._browserWidgets) - n_dimensions = len(self._data_shape) - - # Reset text of labels - self._dimensionLabelsText = [] - for i in range(n_dimensions): - if labels in [True, 1]: - label_text = "Dimension %d" % i - elif labels is None or i >= len(labels): - label_text = "" - else: - label_text = labels[i] - self._dimensionLabelsText.append(label_text) - - # not enough widgets, create new ones (we need n_dim - 2) - for i in range(n_widgets, n_dimensions - 2): - browser = HorizontalSliderWithBrowser(self.browserContainer) - self.browserLayout.addWidget(browser, i, 1) - self._browserWidgets.append(browser) - browser.valueChanged.connect(self._browserSlot) - browser.setEnabled(False) - browser.hide() - - label = qt.QLabel(self.browserContainer) - self._browserLabels.append(label) - self.browserLayout.addWidget(label, i, 0) - label.hide() - - n_widgets = len(self._browserWidgets) - for i in range(n_widgets): - label = self._browserLabels[i] - browser = self._browserWidgets[i] - - if (i + 2) < n_dimensions: - label.setText(self._dimensionLabelsText[i]) - browser.setRange(0, self._data_shape[i] - 1) - browser.setEnabled(True) - browser.show() - if labels is not None: - label.show() - else: - label.hide() - else: - browser.setEnabled(False) - browser.hide() - label.hide() - - # set model - self.model.setArrayData(data, copy=copy, editable=editable) - # some linux distributions need this call - self.view.setModel(self.model) - if editable: - self.view.enableCut() - self.view.enablePaste() - - # initialize & connect axesSelector - self.axesSelector.setNDimensions(n_dimensions) - self.axesSelector.sigDimensionsChanged.connect(self.setFrameAxes) - - def setArrayColors(self, bgcolors=None, fgcolors=None): - """Set the colors for all table cells by passing an array - of RGB or RGBA values (integers between 0 and 255). - - The shape of the colors array must be consistent with the data shape. - - If the data array is n-dimensional, the colors array must be - (n+1)-dimensional, with the first n-dimensions identical to the data - array dimensions, and the last dimension length-3 (RGB) or - length-4 (RGBA). - - :param bgcolors: RGB or RGBA colors array, defining the background color - for each cell in the table. - :param fgcolors: RGB or RGBA colors array, defining the foreground color - (text color) for each cell in the table. - """ - self.model.setArrayColors(bgcolors, fgcolors) - - def displayAxesSelector(self, isVisible): - """Allow to display or hide the axes selector. - - :param bool isVisible: True to display the axes selector. - """ - self.axesSelector.setVisible(isVisible) - - def setFrameIndex(self, index): - """Set the active slice/image index in the n-dimensional array. - - A frame is a 2D array extracted from an array. This frame is - necessarily parallel to 2 axes, and orthogonal to all other axes. - - The index of a frame is a sequence of indices along the orthogonal - axes, where the frame intersects the respective axis. The indices - are listed in the same order as the corresponding dimensions of the - data array. - - For example, it the data array has 5 dimensions, and we are - considering frames whose parallel axes are the 2nd and 4th dimensions - of the array, the frame index will be a sequence of length 3 - corresponding to the indices where the frame intersects the 1st, 3rd - and 5th axes. - - :param index: Sequence of indices defining the active data slice in - a n-dimensional array. The sequence length is :math:`n-2` - :raise: IndexError if any index in the index sequence is out of bound - on its respective axis. - """ - self.model.setFrameIndex(index) - - def _resetBrowsers(self, perspective): - """Adjust limits for browsers based on the perspective and the - size of the corresponding dimensions. Reset the index to 0. - Update the dimension in the labels. - - :param perspective: Sequence of axes/dimensions numbers (0-based) - defining the axes orthogonal to the frame. - """ - # for 3D arrays we can accept an int rather than a 1-tuple - if not hasattr(perspective, "__len__"): - perspective = [perspective] - - # perspective must be sorted - perspective = sorted(perspective) - - n_dimensions = len(self._data_shape) - for i in range(n_dimensions - 2): - browser = self._browserWidgets[i] - label = self._browserLabels[i] - browser.setRange(0, self._data_shape[perspective[i]] - 1) - browser.setValue(0) - label.setText(self._dimensionLabelsText[perspective[i]]) - - def setPerspective(self, perspective): - """Set the *perspective* by specifying which axes are orthogonal - to the frame. - - For the opposite approach (defining parallel axes), use - :meth:`setFrameAxes` instead. - - :param perspective: Sequence of unique axes numbers (0-based) defining - the orthogonal axes. For a n-dimensional array, the sequence - length is :math:`n-2`. The order is of the sequence is not taken - into account (the dimensions are displayed in increasing order - in the widget). - """ - self.model.setPerspective(perspective) - self._resetBrowsers(perspective) - - def setFrameAxes(self, row_axis, col_axis): - """Set the *perspective* by specifying which axes are parallel - to the frame. - - For the opposite approach (defining orthogonal axes), use - :meth:`setPerspective` instead. - - :param int row_axis: Index (0-based) of the first dimension used as a frame - axis - :param int col_axis: Index (0-based) of the 2nd dimension used as a frame - axis - """ - self.model.setFrameAxes(row_axis, col_axis) - n_dimensions = len(self._data_shape) - perspective = tuple(set(range(0, n_dimensions)) - {row_axis, col_axis}) - self._resetBrowsers(perspective) - - def _browserSlot(self, value): - index = [] - for browser in self._browserWidgets: - if browser.isEnabled(): - index.append(browser.value()) - self.setFrameIndex(index) - self.view.reset() - - def getData(self, copy=True): - """Return a copy of the data array, or a reference to it if - *copy=False* is passed as parameter. - - :param bool copy: If *True* (default), return a copy of the data. If - *False*, return a reference. - :return: Numpy array of data, or reference to original data object - if *copy=False* - """ - return self.model.getData(copy=copy) - - -def main(): - import numpy - a = qt.QApplication([]) - d = numpy.random.normal(0, 1, (4, 5, 1000, 1000)) - for j in range(4): - for i in range(5): - d[j, i, :, :] += i + 10 * j - w = ArrayTableWidget() - if "2" in sys.argv: - print("sending a single image") - w.setArrayData(d[0, 0]) - elif "3" in sys.argv: - print("sending 5 images") - w.setArrayData(d[0]) - else: - print("sending 4 * 5 images ") - w.setArrayData(d, labels=True) - w.show() - a.exec_() - -if __name__ == "__main__": - main() diff --git a/silx/gui/data/DataViewer.py b/silx/gui/data/DataViewer.py deleted file mode 100644 index 2e51439..0000000 --- a/silx/gui/data/DataViewer.py +++ /dev/null @@ -1,593 +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. -# -# ###########################################################################*/ -"""This module defines a widget designed to display data using the most adapted -view from the ones provided by silx. -""" -from __future__ import division - -import logging -import os.path -import collections -from silx.gui import qt -from silx.gui.data import DataViews -from silx.gui.data.DataViews import _normalizeData -from silx.gui.utils import blockSignals -from silx.gui.data.NumpyAxesSelector import NumpyAxesSelector - - -__authors__ = ["V. Valls"] -__license__ = "MIT" -__date__ = "12/02/2019" - - -_logger = logging.getLogger(__name__) - - -DataSelection = collections.namedtuple("DataSelection", - ["filename", "datapath", - "slice", "permutation"]) - - -class DataViewer(qt.QFrame): - """Widget to display any kind of data - - .. image:: img/DataViewer.png - - The method :meth:`setData` allows to set any data to the widget. Mostly - `numpy.array` and `h5py.Dataset` are supported with adapted views. Other - data types are displayed using a text viewer. - - A default view is automatically selected when a data is set. The method - :meth:`setDisplayMode` allows to change the view. To have a graphical tool - to select the view, prefer using the widget :class:`DataViewerFrame`. - - The dimension of the input data and the expected dimension of the selected - view can differ. For example you can display an image (2D) from 4D - data. In this case a :class:`NumpyAxesSelector` is displayed to allow the - user to select the axis mapping and the slicing of other axes. - - .. code-block:: python - - import numpy - data = numpy.random.rand(500,500) - viewer = DataViewer() - viewer.setData(data) - viewer.setVisible(True) - """ - - displayedViewChanged = qt.Signal(object) - """Emitted when the displayed view changes""" - - dataChanged = qt.Signal() - """Emitted when the data changes""" - - currentAvailableViewsChanged = qt.Signal() - """Emitted when the current available views (which support the current - data) change""" - - def __init__(self, parent=None): - """Constructor - - :param QWidget parent: The parent of the widget - """ - super(DataViewer, self).__init__(parent) - - self.__stack = qt.QStackedWidget(self) - self.__numpySelection = NumpyAxesSelector(self) - self.__numpySelection.selectedAxisChanged.connect(self.__numpyAxisChanged) - self.__numpySelection.selectionChanged.connect(self.__numpySelectionChanged) - self.__numpySelection.customAxisChanged.connect(self.__numpyCustomAxisChanged) - - self.setLayout(qt.QVBoxLayout(self)) - self.layout().addWidget(self.__stack, 1) - - group = qt.QGroupBox(self) - group.setLayout(qt.QVBoxLayout()) - group.layout().addWidget(self.__numpySelection) - group.setTitle("Axis selection") - self.__axisSelection = group - - self.layout().addWidget(self.__axisSelection) - - self.__currentAvailableViews = [] - self.__currentView = None - self.__data = None - self.__info = None - self.__useAxisSelection = False - self.__userSelectedView = None - self.__hooks = None - - self.__views = [] - self.__index = {} - """store stack index for each views""" - - self._initializeViews() - - def _initializeViews(self): - """Inisialize the available views""" - views = self.createDefaultViews(self.__stack) - self.__views = list(views) - self.setDisplayMode(DataViews.EMPTY_MODE) - - def setGlobalHooks(self, hooks): - """Set a data view hooks for all the views - - :param DataViewHooks context: The hooks to use - """ - self.__hooks = hooks - for v in self.__views: - v.setHooks(hooks) - - def createDefaultViews(self, parent=None): - """Create and returns available views which can be displayed by default - by the data viewer. It is called internally by the widget. It can be - overwriten to provide a different set of viewers. - - :param QWidget parent: QWidget parent of the views - :rtype: List[silx.gui.data.DataViews.DataView] - """ - viewClasses = [ - DataViews._EmptyView, - DataViews._Hdf5View, - DataViews._NXdataView, - DataViews._Plot1dView, - DataViews._ImageView, - DataViews._Plot3dView, - DataViews._RawView, - DataViews._StackView, - DataViews._Plot2dRecordView, - ] - views = [] - for viewClass in viewClasses: - try: - view = viewClass(parent) - views.append(view) - except Exception: - _logger.warning("%s instantiation failed. View is ignored" % viewClass.__name__) - _logger.debug("Backtrace", exc_info=True) - - return views - - def clear(self): - """Clear the widget""" - self.setData(None) - - def normalizeData(self, data): - """Returns a normalized data if the embed a numpy or a dataset. - Else returns the data.""" - return _normalizeData(data) - - def __getStackIndex(self, view): - """Get the stack index containing the view. - - :param silx.gui.data.DataViews.DataView view: The view - """ - if view not in self.__index: - widget = view.getWidget() - index = self.__stack.addWidget(widget) - self.__index[view] = index - else: - index = self.__index[view] - return index - - def __clearCurrentView(self): - """Clear the current selected view""" - view = self.__currentView - if view is not None: - view.clear() - - def __numpyCustomAxisChanged(self, name, value): - view = self.__currentView - if view is not None: - view.setCustomAxisValue(name, value) - - def __updateNumpySelectionAxis(self): - """ - Update the numpy-selector according to the needed axis names - """ - with blockSignals(self.__numpySelection): - previousPermutation = self.__numpySelection.permutation() - previousSelection = self.__numpySelection.selection() - - self.__numpySelection.clear() - - info = self._getInfo() - axisNames = self.__currentView.axesNames(self.__data, info) - if (info.isArray and info.size != 0 and - self.__data is not None and axisNames is not None): - self.__useAxisSelection = True - self.__numpySelection.setAxisNames(axisNames) - self.__numpySelection.setCustomAxis( - self.__currentView.customAxisNames()) - data = self.normalizeData(self.__data) - self.__numpySelection.setData(data) - - # Try to restore previous permutation and selection - try: - self.__numpySelection.setSelection( - previousSelection, previousPermutation) - except ValueError as e: - _logger.info("Not restoring selection because: %s", e) - - if hasattr(data, "shape"): - isVisible = not (len(axisNames) == 1 and len(data.shape) == 1) - else: - isVisible = True - self.__axisSelection.setVisible(isVisible) - else: - self.__useAxisSelection = False - self.__axisSelection.setVisible(False) - - def __updateDataInView(self): - """ - Update the views using the current data - """ - if self.__useAxisSelection: - self.__displayedData = self.__numpySelection.selectedData() - - permutation = self.__numpySelection.permutation() - normal = tuple(range(len(permutation))) - if permutation == normal: - permutation = None - slicing = self.__numpySelection.selection() - normal = tuple([slice(None)] * len(slicing)) - if slicing == normal: - slicing = None - else: - self.__displayedData = self.__data - permutation = None - slicing = None - - try: - filename = os.path.abspath(self.__data.file.filename) - except: - filename = None - - try: - datapath = self.__data.name - except: - datapath = None - - # FIXME: maybe use DataUrl, with added support of permutation - self.__displayedSelection = DataSelection(filename, datapath, slicing, permutation) - - # TODO: would be good to avoid that, it should be synchonous - qt.QTimer.singleShot(10, self.__setDataInView) - - def __setDataInView(self): - self.__currentView.setData(self.__displayedData) - self.__currentView.setDataSelection(self.__displayedSelection) - - def setDisplayedView(self, view): - """Set the displayed view. - - Change the displayed view according to the view itself. - - :param silx.gui.data.DataViews.DataView view: The DataView to use to display the data - """ - self.__userSelectedView = view - self._setDisplayedView(view) - - def _setDisplayedView(self, view): - """Internal set of the displayed view. - - Change the displayed view according to the view itself. - - :param silx.gui.data.DataViews.DataView view: The DataView to use to display the data - """ - if self.__currentView is view: - return - self.__clearCurrentView() - self.__currentView = view - self.__updateNumpySelectionAxis() - self.__updateDataInView() - stackIndex = self.__getStackIndex(self.__currentView) - if self.__currentView is not None: - self.__currentView.select() - self.__stack.setCurrentIndex(stackIndex) - self.displayedViewChanged.emit(view) - - def getViewFromModeId(self, modeId): - """Returns the first available view which have the requested modeId. - Return None if modeId does not correspond to an existing view. - - :param int modeId: Requested mode id - :rtype: silx.gui.data.DataViews.DataView - """ - for view in self.__views: - if view.modeId() == modeId: - return view - return None - - def setDisplayMode(self, modeId): - """Set the displayed view using display mode. - - Change the displayed view according to the requested mode. - - :param int modeId: Display mode, one of - - - `DataViews.EMPTY_MODE`: display nothing - - `DataViews.PLOT1D_MODE`: display the data as a curve - - `DataViews.IMAGE_MODE`: display the data as an image - - `DataViews.PLOT3D_MODE`: display the data as an isosurface - - `DataViews.RAW_MODE`: display the data as a table - - `DataViews.STACK_MODE`: display the data as a stack of images - - `DataViews.HDF5_MODE`: display the data as a table of HDF5 info - - `DataViews.NXDATA_MODE`: display the data as NXdata - """ - try: - view = self.getViewFromModeId(modeId) - except KeyError: - raise ValueError("Display mode %s is unknown" % modeId) - self._setDisplayedView(view) - - def displayedView(self): - """Returns the current displayed view. - - :rtype: silx.gui.data.DataViews.DataView - """ - return self.__currentView - - def addView(self, view): - """Allow to add a view to the dataview. - - If the current data support this view, it will be displayed. - - :param DataView view: A dataview - """ - if self.__hooks is not None: - view.setHooks(self.__hooks) - self.__views.append(view) - # TODO It can be skipped if the view do not support the data - self.__updateAvailableViews() - - def removeView(self, view): - """Allow to remove a view which was available from the dataview. - - If the view was displayed, the widget will be updated. - - :param DataView view: A dataview - """ - self.__views.remove(view) - self.__stack.removeWidget(view.getWidget()) - # invalidate the full index. It will be updated as expected - self.__index = {} - - if self.__userSelectedView is view: - self.__userSelectedView = None - - if view is self.__currentView: - self.__updateView() - else: - # TODO It can be skipped if the view is not part of the - # available views - self.__updateAvailableViews() - - def __updateAvailableViews(self): - """ - Update available views from the current data. - """ - data = self.__data - info = self._getInfo() - # sort available views according to priority - views = [] - for v in self.__views: - views.extend(v.getMatchingViews(data, info)) - views = [(v.getCachedDataPriority(data, info), v) for v in views] - views = filter(lambda t: t[0] > DataViews.DataView.UNSUPPORTED, views) - views = sorted(views, reverse=True) - views = [v[1] for v in views] - - # store available views - self.__setCurrentAvailableViews(views) - - def __updateView(self): - """Display the data using the widget which fit the best""" - data = self.__data - - # update available views for this data - self.__updateAvailableViews() - available = self.__currentAvailableViews - - # display the view with the most priority (the default view) - view = self.getDefaultViewFromAvailableViews(data, available) - self.__clearCurrentView() - try: - self._setDisplayedView(view) - except Exception as e: - # in case there is a problem to read the data, try to use a safe - # view - view = self.getSafeViewFromAvailableViews(data, available) - self._setDisplayedView(view) - raise e - - def getSafeViewFromAvailableViews(self, data, available): - """Returns a view which is sure to display something without failing - on rendering. - - :param object data: data which will be displayed - :param List[view] available: List of available views, from highest - priority to lowest. - :rtype: DataView - """ - hdf5View = self.getViewFromModeId(DataViews.HDF5_MODE) - if hdf5View in available: - return hdf5View - return self.getViewFromModeId(DataViews.EMPTY_MODE) - - def getDefaultViewFromAvailableViews(self, data, available): - """Returns the default view which will be used according to available - views. - - :param object data: data which will be displayed - :param List[view] available: List of available views, from highest - priority to lowest. - :rtype: DataView - """ - if len(available) > 0: - # returns the view with the highest priority - if self.__userSelectedView in available: - return self.__userSelectedView - self.__userSelectedView = None - view = available[0] - else: - # else returns the empty view - view = self.getViewFromModeId(DataViews.EMPTY_MODE) - return view - - def __setCurrentAvailableViews(self, availableViews): - """Set the current available viewa - - :param List[DataView] availableViews: Current available viewa - """ - self.__currentAvailableViews = availableViews - self.currentAvailableViewsChanged.emit() - - def currentAvailableViews(self): - """Returns the list of available views for the current data - - :rtype: List[DataView] - """ - return self.__currentAvailableViews - - def getReachableViews(self): - """Returns the list of reachable views from the registred available - views. - - :rtype: List[DataView] - """ - views = [] - for v in self.availableViews(): - views.extend(v.getReachableViews()) - return views - - def availableViews(self): - """Returns the list of registered views - - :rtype: List[DataView] - """ - return self.__views - - def setData(self, data): - """Set the data to view. - - It mostly can be a h5py.Dataset or a numpy.ndarray. Other kind of - objects will be displayed as text rendering. - - :param numpy.ndarray data: The data. - """ - self.__data = data - self._invalidateInfo() - self.__displayedData = None - self.__displayedSelection = None - self.__updateView() - self.__updateNumpySelectionAxis() - self.__updateDataInView() - self.dataChanged.emit() - - def __numpyAxisChanged(self): - """ - Called when axis selection of the numpy-selector changed - """ - self.__clearCurrentView() - - def __numpySelectionChanged(self): - """ - Called when data selection of the numpy-selector changed - """ - self.__updateDataInView() - - def data(self): - """Returns the data""" - return self.__data - - def _invalidateInfo(self): - """Invalidate DataInfo cache.""" - self.__info = None - - def _getInfo(self): - """Returns the DataInfo of the current selected data. - - This value is cached. - - :rtype: DataInfo - """ - if self.__info is None: - self.__info = DataViews.DataInfo(self.__data) - return self.__info - - def displayMode(self): - """Returns the current display mode""" - return self.__currentView.modeId() - - def replaceView(self, modeId, newView): - """Replace one of the builtin data views with a custom view. - Return True in case of success, False in case of failure. - - .. note:: - - This method must be called just after instantiation, before - the viewer is used. - - :param int modeId: Unique mode ID identifying the DataView to - be replaced. One of: - - - `DataViews.EMPTY_MODE` - - `DataViews.PLOT1D_MODE` - - `DataViews.IMAGE_MODE` - - `DataViews.PLOT2D_MODE` - - `DataViews.COMPLEX_IMAGE_MODE` - - `DataViews.PLOT3D_MODE` - - `DataViews.RAW_MODE` - - `DataViews.STACK_MODE` - - `DataViews.HDF5_MODE` - - `DataViews.NXDATA_MODE` - - `DataViews.NXDATA_INVALID_MODE` - - `DataViews.NXDATA_SCALAR_MODE` - - `DataViews.NXDATA_CURVE_MODE` - - `DataViews.NXDATA_XYVSCATTER_MODE` - - `DataViews.NXDATA_IMAGE_MODE` - - `DataViews.NXDATA_STACK_MODE` - - :param DataViews.DataView newView: New data view - :return: True if replacement was successful, else False - """ - assert isinstance(newView, DataViews.DataView) - isReplaced = False - for idx, view in enumerate(self.__views): - if view.modeId() == modeId: - if self.__hooks is not None: - newView.setHooks(self.__hooks) - self.__views[idx] = newView - isReplaced = True - break - elif isinstance(view, DataViews.CompositeDataView): - isReplaced = view.replaceView(modeId, newView) - if isReplaced: - break - - if isReplaced: - self.__updateAvailableViews() - return isReplaced diff --git a/silx/gui/data/DataViewerFrame.py b/silx/gui/data/DataViewerFrame.py deleted file mode 100644 index 9bfb95b..0000000 --- a/silx/gui/data/DataViewerFrame.py +++ /dev/null @@ -1,217 +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 module contains a DataViewer with a view selector. -""" - -__authors__ = ["V. Valls"] -__license__ = "MIT" -__date__ = "12/02/2019" - -from silx.gui import qt -from .DataViewer import DataViewer -from .DataViewerSelector import DataViewerSelector - - -class DataViewerFrame(qt.QWidget): - """ - A :class:`DataViewer` with a view selector. - - .. image:: img/DataViewerFrame.png - - This widget provides the same API as :class:`DataViewer`. Therefore, for more - documentation, take a look at the documentation of the class - :class:`DataViewer`. - - .. code-block:: python - - import numpy - data = numpy.random.rand(500,500) - viewer = DataViewerFrame() - viewer.setData(data) - viewer.setVisible(True) - - """ - - displayedViewChanged = qt.Signal(object) - """Emitted when the displayed view changes""" - - dataChanged = qt.Signal() - """Emitted when the data changes""" - - def __init__(self, parent=None): - """ - Constructor - - :param qt.QWidget parent: - """ - super(DataViewerFrame, self).__init__(parent) - - class _DataViewer(DataViewer): - """Overwrite methods to avoid to create views while the instance - is not created. `initializeViews` have to be called manually.""" - - def _initializeViews(self): - pass - - def initializeViews(self): - """Avoid to create views while the instance is not created.""" - super(_DataViewer, self)._initializeViews() - - def _createDefaultViews(self, parent): - """Expose the original `createDefaultViews` function""" - return super(_DataViewer, self).createDefaultViews() - - def createDefaultViews(self, parent=None): - """Allow the DataViewerFrame to override this function""" - return self.parent().createDefaultViews(parent) - - self.__dataViewer = _DataViewer(self) - # initialize views when `self.__dataViewer` is set - self.__dataViewer.initializeViews() - self.__dataViewer.setFrameShape(qt.QFrame.StyledPanel) - self.__dataViewer.setFrameShadow(qt.QFrame.Sunken) - self.__dataViewerSelector = DataViewerSelector(self, self.__dataViewer) - self.__dataViewerSelector.setFlat(True) - - layout = qt.QVBoxLayout(self) - layout.setContentsMargins(0, 0, 0, 0) - layout.setSpacing(0) - layout.addWidget(self.__dataViewer, 1) - layout.addWidget(self.__dataViewerSelector) - self.setLayout(layout) - - self.__dataViewer.dataChanged.connect(self.__dataChanged) - self.__dataViewer.displayedViewChanged.connect(self.__displayedViewChanged) - - def __dataChanged(self): - """Called when the data is changed""" - self.dataChanged.emit() - - def __displayedViewChanged(self, view): - """Called when the displayed view changes""" - self.displayedViewChanged.emit(view) - - def setGlobalHooks(self, hooks): - """Set a data view hooks for all the views - - :param DataViewHooks context: The hooks to use - """ - self.__dataViewer.setGlobalHooks(hooks) - - def getReachableViews(self): - return self.__dataViewer.getReachableViews() - - def availableViews(self): - """Returns the list of registered views - - :rtype: List[DataView] - """ - return self.__dataViewer.availableViews() - - def currentAvailableViews(self): - """Returns the list of available views for the current data - - :rtype: List[DataView] - """ - return self.__dataViewer.currentAvailableViews() - - def createDefaultViews(self, parent=None): - """Create and returns available views which can be displayed by default - by the data viewer. It is called internally by the widget. It can be - overwriten to provide a different set of viewers. - - :param QWidget parent: QWidget parent of the views - :rtype: List[silx.gui.data.DataViews.DataView] - """ - return self.__dataViewer._createDefaultViews(parent) - - def addView(self, view): - """Allow to add a view to the dataview. - - If the current data support this view, it will be displayed. - - :param DataView view: A dataview - """ - return self.__dataViewer.addView(view) - - def removeView(self, view): - """Allow to remove a view which was available from the dataview. - - If the view was displayed, the widget will be updated. - - :param DataView view: A dataview - """ - return self.__dataViewer.removeView(view) - - def setData(self, data): - """Set the data to view. - - It mostly can be a h5py.Dataset or a numpy.ndarray. Other kind of - objects will be displayed as text rendering. - - :param numpy.ndarray data: The data. - """ - self.__dataViewer.setData(data) - - def data(self): - """Returns the data""" - return self.__dataViewer.data() - - def setDisplayedView(self, view): - self.__dataViewer.setDisplayedView(view) - - def displayedView(self): - return self.__dataViewer.displayedView() - - def displayMode(self): - return self.__dataViewer.displayMode() - - def setDisplayMode(self, modeId): - """Set the displayed view using display mode. - - Change the displayed view according to the requested mode. - - :param int modeId: Display mode, one of - - - `EMPTY_MODE`: display nothing - - `PLOT1D_MODE`: display the data as a curve - - `PLOT2D_MODE`: display the data as an image - - `TEXT_MODE`: display the data as a text - - `ARRAY_MODE`: display the data as a table - """ - return self.__dataViewer.setDisplayMode(modeId) - - def getViewFromModeId(self, modeId): - """See :meth:`DataViewer.getViewFromModeId`""" - return self.__dataViewer.getViewFromModeId(modeId) - - def replaceView(self, modeId, newView): - """Replace one of the builtin data views with a custom view. - See :meth:`DataViewer.replaceView` for more documentation. - - :param DataViews.DataView newView: New data view - :return: True if replacement was successful, else False - """ - return self.__dataViewer.replaceView(modeId, newView) diff --git a/silx/gui/data/DataViewerSelector.py b/silx/gui/data/DataViewerSelector.py deleted file mode 100644 index a1e9947..0000000 --- a/silx/gui/data/DataViewerSelector.py +++ /dev/null @@ -1,175 +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. -# -# ###########################################################################*/ -"""This module defines a widget to be able to select the available view -of the DataViewer. -""" -from __future__ import division - -__authors__ = ["V. Valls"] -__license__ = "MIT" -__date__ = "12/02/2019" - -import weakref -import functools -from silx.gui import qt -import silx.utils.weakref - - -class DataViewerSelector(qt.QWidget): - """Widget to be able to select a custom view from the DataViewer""" - - def __init__(self, parent=None, dataViewer=None): - """Constructor - - :param QWidget parent: The parent of the widget - :param DataViewer dataViewer: The connected `DataViewer` - """ - super(DataViewerSelector, self).__init__(parent) - - self.__group = None - self.__buttons = {} - self.__buttonLayout = None - self.__buttonDummy = None - self.__dataViewer = None - - # Create the fixed layout - self.setLayout(qt.QHBoxLayout()) - layout = self.layout() - layout.setContentsMargins(0, 0, 0, 0) - self.__buttonLayout = qt.QHBoxLayout() - self.__buttonLayout.setContentsMargins(0, 0, 0, 0) - layout.addLayout(self.__buttonLayout) - layout.addStretch(1) - - if dataViewer is not None: - self.setDataViewer(dataViewer) - - def __updateButtons(self): - if self.__group is not None: - self.__group.deleteLater() - - # Clean up - for _, b in self.__buttons.items(): - b.deleteLater() - if self.__buttonDummy is not None: - self.__buttonDummy.deleteLater() - self.__buttonDummy = None - self.__buttons = {} - self.__buttonDummy = None - - self.__group = qt.QButtonGroup(self) - if self.__dataViewer is None: - return - - iconSize = qt.QSize(16, 16) - - for view in self.__dataViewer.getReachableViews(): - label = view.label() - icon = view.icon() - button = qt.QPushButton(label) - button.setIcon(icon) - button.setIconSize(iconSize) - button.setCheckable(True) - # the weak objects are needed to be able to destroy the widget safely - weakView = weakref.ref(view) - weakMethod = silx.utils.weakref.WeakMethodProxy(self.__setDisplayedView) - callback = functools.partial(weakMethod, weakView) - button.clicked.connect(callback) - self.__buttonLayout.addWidget(button) - self.__group.addButton(button) - self.__buttons[view] = button - - button = qt.QPushButton("Dummy") - button.setCheckable(True) - button.setVisible(False) - self.__buttonLayout.addWidget(button) - self.__group.addButton(button) - self.__buttonDummy = button - - self.__updateButtonsVisibility() - self.__displayedViewChanged(self.__dataViewer.displayedView()) - - def setDataViewer(self, dataViewer): - """Define the dataviewer connected to this status bar - - :param DataViewer dataViewer: The connected `DataViewer` - """ - if self.__dataViewer is dataViewer: - return - if self.__dataViewer is not None: - self.__dataViewer.dataChanged.disconnect(self.__updateButtonsVisibility) - self.__dataViewer.displayedViewChanged.disconnect(self.__displayedViewChanged) - self.__dataViewer = dataViewer - if self.__dataViewer is not None: - self.__dataViewer.dataChanged.connect(self.__updateButtonsVisibility) - self.__dataViewer.displayedViewChanged.connect(self.__displayedViewChanged) - self.__updateButtons() - - def setFlat(self, isFlat): - """Set the flat state of all the buttons. - - :param bool isFlat: True to display the buttons flatten. - """ - for b in self.__buttons.values(): - b.setFlat(isFlat) - self.__buttonDummy.setFlat(isFlat) - - def __displayedViewChanged(self, view): - """Called on displayed view changes""" - selectedButton = self.__buttons.get(view, self.__buttonDummy) - selectedButton.setChecked(True) - - def __setDisplayedView(self, refView, clickEvent=None): - """Display a data using the requested view - - :param DataView view: Requested view - :param clickEvent: Event sent by the clicked event - """ - if self.__dataViewer is None: - return - view = refView() - if view is None: - return - self.__dataViewer.setDisplayedView(view) - - def __checkAvailableButtons(self): - views = set(self.__dataViewer.getReachableViews()) - if views == set(self.__buttons.keys()): - return - # Recreate all the buttons - # TODO: We dont have to create everything again - # We expect the views stay quite stable - self.__updateButtons() - - def __updateButtonsVisibility(self): - """Called on data changed""" - if self.__dataViewer is None: - for b in self.__buttons.values(): - b.setVisible(False) - else: - self.__checkAvailableButtons() - availableViews = set(self.__dataViewer.currentAvailableViews()) - for view, button in self.__buttons.items(): - button.setVisible(view in availableViews) diff --git a/silx/gui/data/DataViews.py b/silx/gui/data/DataViews.py deleted file mode 100644 index b18a813..0000000 --- a/silx/gui/data/DataViews.py +++ /dev/null @@ -1,2059 +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 |