diff options
author | Picca Frédéric-Emmanuel <picca@debian.org> | 2022-11-03 10:02:44 +0100 |
---|---|---|
committer | Picca Frédéric-Emmanuel <picca@debian.org> | 2022-11-03 10:02:44 +0100 |
commit | 1c380bfeff1e13a9f7d506460336659502ca052d (patch) | |
tree | 48081d47748d4563eeaa76662287eb19638c8591 /src/silx/gui | |
parent | 4e774db12d5ebe7a20eded6dd434a289e27999e5 (diff) |
New upstream version 1.1.0+dfsg
Diffstat (limited to 'src/silx/gui')
332 files changed, 2070 insertions, 1582 deletions
diff --git a/src/silx/gui/__init__.py b/src/silx/gui/__init__.py index b796e20..31bb38e 100644 --- a/src/silx/gui/__init__.py +++ b/src/silx/gui/__init__.py @@ -1,4 +1,3 @@ -# coding: utf-8 # /*########################################################################## # # Copyright (c) 2016-2018 European Synchrotron Radiation Facility diff --git a/src/silx/gui/_glutils/Context.py b/src/silx/gui/_glutils/Context.py index c62dbb9..d2ddaa3 100644 --- a/src/silx/gui/_glutils/Context.py +++ b/src/silx/gui/_glutils/Context.py @@ -1,4 +1,3 @@ -# coding: utf-8 # /*########################################################################## # # Copyright (c) 2014-2019 European Synchrotron Radiation Facility diff --git a/src/silx/gui/_glutils/FramebufferTexture.py b/src/silx/gui/_glutils/FramebufferTexture.py index d12a6e0..75db264 100644 --- a/src/silx/gui/_glutils/FramebufferTexture.py +++ b/src/silx/gui/_glutils/FramebufferTexture.py @@ -1,4 +1,3 @@ -# coding: utf-8 # /*########################################################################## # # Copyright (c) 2014-2020 European Synchrotron Radiation Facility diff --git a/src/silx/gui/_glutils/OpenGLWidget.py b/src/silx/gui/_glutils/OpenGLWidget.py index 2ca4649..d35bb73 100644 --- a/src/silx/gui/_glutils/OpenGLWidget.py +++ b/src/silx/gui/_glutils/OpenGLWidget.py @@ -1,4 +1,3 @@ -# coding: utf-8 # /*########################################################################## # # Copyright (c) 2017-2021 European Synchrotron Radiation Facility @@ -45,7 +44,7 @@ _logger = logging.getLogger(__name__) if not hasattr(qt, 'QOpenGLWidget') and not hasattr(qt, 'QGLWidget'): - OpenGLWidget = None + _OpenGLWidget = None else: if hasattr(qt, 'QOpenGLWidget'): # PyQt>=5.4 @@ -70,7 +69,7 @@ else: depthBufferSize=24, stencilBufferSize=8, version=(2, 0), - f=qt.Qt.WindowFlags()): + f=qt.Qt.Widget): # True if using QGLWidget, False if using QOpenGLWidget self.__legacy = not hasattr(qt, 'QOpenGLWidget') @@ -262,7 +261,7 @@ class OpenGLWidget(qt.QWidget): depthBufferSize=24, stencilBufferSize=8, version=(2, 0), - f=qt.Qt.WindowFlags()): + f=qt.Qt.Widget): super(OpenGLWidget, self).__init__(parent, f) layout = qt.QHBoxLayout(self) diff --git a/src/silx/gui/_glutils/Program.py b/src/silx/gui/_glutils/Program.py index 87eec5f..d61c07d 100644 --- a/src/silx/gui/_glutils/Program.py +++ b/src/silx/gui/_glutils/Program.py @@ -1,4 +1,3 @@ -# coding: utf-8 # /*########################################################################## # # Copyright (c) 2014-2019 European Synchrotron Radiation Facility diff --git a/src/silx/gui/_glutils/Texture.py b/src/silx/gui/_glutils/Texture.py index c72135a..76bdcd8 100644 --- a/src/silx/gui/_glutils/Texture.py +++ b/src/silx/gui/_glutils/Texture.py @@ -1,4 +1,3 @@ -# coding: utf-8 # /*########################################################################## # # Copyright (c) 2014-2020 European Synchrotron Radiation Facility diff --git a/src/silx/gui/_glutils/VertexBuffer.py b/src/silx/gui/_glutils/VertexBuffer.py index b74b748..65fff86 100644 --- a/src/silx/gui/_glutils/VertexBuffer.py +++ b/src/silx/gui/_glutils/VertexBuffer.py @@ -1,4 +1,3 @@ -# coding: utf-8 # /*########################################################################## # # Copyright (c) 2014-2017 European Synchrotron Radiation Facility diff --git a/src/silx/gui/_glutils/__init__.py b/src/silx/gui/_glutils/__init__.py index e88affd..a7a4bee 100644 --- a/src/silx/gui/_glutils/__init__.py +++ b/src/silx/gui/_glutils/__init__.py @@ -1,4 +1,3 @@ -# coding: utf-8 # /*########################################################################## # # Copyright (c) 2014-2019 European Synchrotron Radiation Facility diff --git a/src/silx/gui/_glutils/font.py b/src/silx/gui/_glutils/font.py index 3ea474d..bee9745 100644 --- a/src/silx/gui/_glutils/font.py +++ b/src/silx/gui/_glutils/font.py @@ -1,7 +1,6 @@ -# coding: utf-8 # /*########################################################################## # -# Copyright (c) 2016-2021 European Synchrotron Radiation Facility +# Copyright (c) 2016-2022 European Synchrotron Radiation Facility # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal @@ -32,8 +31,13 @@ __date__ = "13/10/2016" import logging import numpy -from ..utils.image import convertQImageToArray from .. import qt +from ..utils.image import convertQImageToArray + +try: + from ..utils.matplotlib import rasterMathText +except ImportError: + rasterMathText = None _logger = logging.getLogger(__name__) @@ -66,11 +70,7 @@ ULTRA_BLACK = 99 """Thickest characters: Maximum font weight""" -def rasterText(text, font, - size=-1, - weight=-1, - italic=False, - devicePixelRatio=1.0): +def rasterTextQt(text, font, size=-1, weight=-1, italic=False, devicePixelRatio=1.0): """Raster text using Qt. It supports multiple lines. @@ -95,7 +95,7 @@ def rasterText(text, font, """ 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 + text = " " # Replace empty text by white space to produce an image if not isinstance(font, qt.QFont): font = qt.QFont(font, size, weight, italic) @@ -107,7 +107,8 @@ def rasterText(text, font, painter.setPen(qt.Qt.white) painter.setFont(font) bounds = painter.boundingRect( - qt.QRect(0, 0, 4096, 4096), qt.Qt.TextExpandTabs, text) + qt.QRect(0, 0, 4096, 4096), qt.Qt.TextExpandTabs, text + ) painter.end() metrics = qt.QFontMetrics(font) @@ -123,9 +124,9 @@ def rasterText(text, font, 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) + image = qt.QImage( + int(width), int(bounds.height() * devicePixelRatio + 2), qt.QImage.Format_RGB888 + ) image.setDevicePixelRatio(devicePixelRatio) # TODO if Qt5 use Format_Grayscale8 instead @@ -144,13 +145,45 @@ def rasterText(text, font, # 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 columns/rows but one on each side + filled_rows = numpy.nonzero(numpy.sum(array, axis=1))[0] + filled_columns = numpy.nonzero(numpy.sum(array, axis=0))[0] + if len(filled_rows) == 0 or len(filled_columns) == 0: + return array, metrics.ascent() - # 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, :] + min_row = max(0, filled_rows[0] - 1) + array = array[ + min_row : filled_rows[-1] + 2, + max(0, filled_columns[0] - 1) : filled_columns[-1] + 2, + ] return array, metrics.ascent() - min_row + + +def rasterText(text, font, size=-1, weight=-1, italic=False, devicePixelRatio=1.0): + """Raster text using Qt or matplotlib if there may be math syntax. + + 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 rasterMathText is not None and text.count("$") >= 2: + return rasterMathText(text, font, size, weight, italic, devicePixelRatio) + else: + return rasterTextQt(text, font, size, weight, italic, devicePixelRatio) diff --git a/src/silx/gui/_glutils/gl.py b/src/silx/gui/_glutils/gl.py index 608d9ce..d33cf49 100644 --- a/src/silx/gui/_glutils/gl.py +++ b/src/silx/gui/_glutils/gl.py @@ -1,4 +1,3 @@ -# coding: utf-8 # /*########################################################################## # # Copyright (c) 2014-2017 European Synchrotron Radiation Facility @@ -64,7 +63,7 @@ except NameError: GLchar = c_char -def testGL(): +def testGL() -> bool: """Test if required OpenGL version and extensions are available. This MUST be run with an active OpenGL context. @@ -72,18 +71,20 @@ def testGL(): 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) + _logger.error("OpenGL version >=2.1 required, running with %s" % version) + return False 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 !") + _logger.error("OpenGL GL_ARB_framebuffer_object extension required!") + return False if not glInitTextureRgARB(): - raise RuntimeError("OpenGL GL_ARB_texture_rg extension required !") + _logger.error("OpenGL GL_ARB_texture_rg extension required!") + return False + return True # Additional setup diff --git a/src/silx/gui/_glutils/utils.py b/src/silx/gui/_glutils/utils.py index 5886599..49b431a 100644 --- a/src/silx/gui/_glutils/utils.py +++ b/src/silx/gui/_glutils/utils.py @@ -1,4 +1,3 @@ -# coding: utf-8 # /*########################################################################## # # Copyright (c) 2014-2021 European Synchrotron Radiation Facility diff --git a/src/silx/gui/colors.py b/src/silx/gui/colors.py index 12046cf..4a5f278 100755 --- a/src/silx/gui/colors.py +++ b/src/silx/gui/colors.py @@ -1,4 +1,3 @@ -# coding: utf-8 # /*########################################################################## # # Copyright (c) 2015-2021 European Synchrotron Radiation Facility @@ -25,8 +24,6 @@ """This module provides API to manage colors. """ -from __future__ import absolute_import - __authors__ = ["T. Vincent", "H.Payno"] __license__ = "MIT" __date__ = "29/01/2019" diff --git a/src/silx/gui/console.py b/src/silx/gui/console.py index 953b6a1..c66d44a 100644 --- a/src/silx/gui/console.py +++ b/src/silx/gui/console.py @@ -1,7 +1,6 @@ -# coding: utf-8 # /*########################################################################## # -# Copyright (c) 2004-2021 European Synchrotron Radiation Facility +# Copyright (c) 2004-2022 European Synchrotron Radiation Facility # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal @@ -182,13 +181,6 @@ class IPythonDockWidget(qt.QDockWidget): 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""" diff --git a/src/silx/gui/data/ArrayTableModel.py b/src/silx/gui/data/ArrayTableModel.py index 23b0bb2..00cc235 100644 --- a/src/silx/gui/data/ArrayTableModel.py +++ b/src/silx/gui/data/ArrayTableModel.py @@ -1,7 +1,6 @@ -# coding: utf-8 # /*########################################################################## # -# Copyright (c) 2016-2021 European Synchrotron Radiation Facility +# Copyright (c) 2016-2022 European Synchrotron Radiation Facility # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal @@ -26,7 +25,6 @@ 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 @@ -34,7 +32,7 @@ from silx.gui.data.TextFormatter import TextFormatter __authors__ = ["V.A. Sole"] __license__ = "MIT" -__date__ = "27/09/2017" +__date__ = "18/01/2022" _logger = logging.getLogger(__name__) @@ -75,7 +73,7 @@ class ArrayTableModel(qt.QAbstractTableModel): of :meth:`setPerspective`. """ - MAX_NUMBER_OF_SECTIONS = 10e6 + MAX_NUMBER_OF_SECTIONS = 10000000 """Maximum number of displayed rows and columns""" def __init__(self, parent=None, data=None, perspective=None): @@ -235,7 +233,7 @@ class ArrayTableModel(qt.QAbstractTableModel): selection = self._getIndexTuple(row, column) - if role == qt.Qt.DisplayRole: + if role == qt.Qt.DisplayRole or role == qt.Qt.EditRole: return self._formatter.toString(self._array[selection], self._array.dtype) if role == qt.Qt.BackgroundRole and self._bgcolors is not None: diff --git a/src/silx/gui/data/ArrayTableWidget.py b/src/silx/gui/data/ArrayTableWidget.py index baef5f4..2f7762d 100644 --- a/src/silx/gui/data/ArrayTableWidget.py +++ b/src/silx/gui/data/ArrayTableWidget.py @@ -1,4 +1,3 @@ -# coding: utf-8 # /*########################################################################## # # Copyright (c) 2016-2021 European Synchrotron Radiation Facility @@ -30,7 +29,6 @@ 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 diff --git a/src/silx/gui/data/DataViewer.py b/src/silx/gui/data/DataViewer.py index 2e51439..2c93c65 100644 --- a/src/silx/gui/data/DataViewer.py +++ b/src/silx/gui/data/DataViewer.py @@ -1,7 +1,6 @@ -# coding: utf-8 # /*########################################################################## # -# Copyright (c) 2016-2019 European Synchrotron Radiation Facility +# Copyright (c) 2016-2022 European Synchrotron Radiation Facility # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal @@ -25,7 +24,6 @@ """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 @@ -83,6 +81,14 @@ class DataViewer(qt.QFrame): dataChanged = qt.Signal() """Emitted when the data changes""" + selectionChanged = qt.Signal(object, object) + """Emitted when the data selection changes. + + It provides: + - the slicing as a tuple of slice or None. + - the permutation as a tuple of int or None. + """ + currentAvailableViewsChanged = qt.Signal() """Emitted when the current available views (which support the current data) change""" @@ -118,6 +124,7 @@ class DataViewer(qt.QFrame): self.__useAxisSelection = False self.__userSelectedView = None self.__hooks = None + self.__previousSelection = DataSelection(None, None, None, None) self.__views = [] self.__index = {} @@ -279,6 +286,13 @@ class DataViewer(qt.QFrame): def __setDataInView(self): self.__currentView.setData(self.__displayedData) self.__currentView.setDataSelection(self.__displayedSelection) + # Emit signal only when selection has changed + if (self.__previousSelection.slice != self.__displayedSelection.slice or + self.__previousSelection.permutation != self.__displayedSelection.permutation + ): + self.selectionChanged.emit( + self.__displayedSelection.slice, self.__displayedSelection.permutation) + self.__previousSelection = self.__displayedSelection def setDisplayedView(self, view): """Set the displayed view. diff --git a/src/silx/gui/data/DataViewerFrame.py b/src/silx/gui/data/DataViewerFrame.py index 9bfb95b..912ca1c 100644 --- a/src/silx/gui/data/DataViewerFrame.py +++ b/src/silx/gui/data/DataViewerFrame.py @@ -1,7 +1,6 @@ -# coding: utf-8 # /*########################################################################## # -# Copyright (c) 2016-2018 European Synchrotron Radiation Facility +# Copyright (c) 2016-2022 European Synchrotron Radiation Facility # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal @@ -60,6 +59,14 @@ class DataViewerFrame(qt.QWidget): dataChanged = qt.Signal() """Emitted when the data changes""" + selectionChanged = qt.Signal(object, object) + """Emitted when the data selection changes. + + It provides: + - the slicing as a tuple of slice or None. + - the permutation as a tuple of int or None. + """ + def __init__(self, parent=None): """ Constructor @@ -104,6 +111,7 @@ class DataViewerFrame(qt.QWidget): self.__dataViewer.dataChanged.connect(self.__dataChanged) self.__dataViewer.displayedViewChanged.connect(self.__displayedViewChanged) + self.__dataViewer.selectionChanged.connect(self.__selectionChanged) def __dataChanged(self): """Called when the data is changed""" @@ -113,6 +121,10 @@ class DataViewerFrame(qt.QWidget): """Called when the displayed view changes""" self.displayedViewChanged.emit(view) + def __selectionChanged(self, slices, permutation): + """Called when the data selection has changed""" + self.selectionChanged.emit(slices, permutation) + def setGlobalHooks(self, hooks): """Set a data view hooks for all the views diff --git a/src/silx/gui/data/DataViewerSelector.py b/src/silx/gui/data/DataViewerSelector.py index a1e9947..d67908e 100644 --- a/src/silx/gui/data/DataViewerSelector.py +++ b/src/silx/gui/data/DataViewerSelector.py @@ -1,4 +1,3 @@ -# coding: utf-8 # /*########################################################################## # # Copyright (c) 2016-2017 European Synchrotron Radiation Facility @@ -25,7 +24,6 @@ """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" diff --git a/src/silx/gui/data/DataViews.py b/src/silx/gui/data/DataViews.py index b18a813..0a4569f 100644 --- a/src/silx/gui/data/DataViews.py +++ b/src/silx/gui/data/DataViews.py @@ -1,7 +1,6 @@ -# coding: utf-8 # /*########################################################################## # -# Copyright (c) 2016-2020 European Synchrotron Radiation Facility +# Copyright (c) 2016-2022 European Synchrotron Radiation Facility # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal @@ -876,7 +875,9 @@ class _Plot1dView(DataView): def createWidget(self, parent): from silx.gui import plot - return plot.Plot1D(parent=parent) + widget = plot.Plot1D(parent=parent) + widget.setGraphGrid(True) + return widget def clear(self): self.getWidget().clear() @@ -1759,6 +1760,8 @@ class _NXdataImageView(_NXdataBaseDataView): y_axis, x_axis = nxd.axes[img_slicing] y_label, x_label = nxd.axes_names[img_slicing] y_scale, x_scale = nxd.plot_style.axes_scale_types[img_slicing] + x_units = get_attr_as_unicode(x_axis, 'units') if x_axis else None + y_units = get_attr_as_unicode(y_axis, 'units') if y_axis else None self.getWidget().setImageData( [nxd.signal] + nxd.auxiliary_signals, @@ -1766,7 +1769,9 @@ class _NXdataImageView(_NXdataBaseDataView): signals_names=[nxd.signal_name] + nxd.auxiliary_signals_names, xlabel=x_label, ylabel=y_label, title=nxd.title, isRgba=isRgba, - xscale=x_scale, yscale=y_scale) + xscale=x_scale, yscale=y_scale, + keep_ratio=(x_units == y_units), + ) def getDataPriority(self, data, info): data = self.normalizeData(data) @@ -1804,13 +1809,17 @@ class _NXdataComplexImageView(_NXdataBaseDataView): img_slicing = slice(-2, None) y_axis, x_axis = nxd.axes[img_slicing] y_label, x_label = nxd.axes_names[img_slicing] + x_units = get_attr_as_unicode(x_axis, 'units') if x_axis else None + y_units = get_attr_as_unicode(y_axis, 'units') if y_axis else None self.getWidget().setImageData( [nxd.signal] + nxd.auxiliary_signals, x_axis=x_axis, y_axis=y_axis, signals_names=[nxd.signal_name] + nxd.auxiliary_signals_names, xlabel=x_label, ylabel=y_label, - title=nxd.title) + title=nxd.title, + keep_ratio=(x_units == y_units), + ) def axesNames(self, data, info): # disabled (used by default axis selector widget in Hdf5Viewer) diff --git a/src/silx/gui/data/Hdf5TableView.py b/src/silx/gui/data/Hdf5TableView.py index 9d65a84..f3fbb69 100644 --- a/src/silx/gui/data/Hdf5TableView.py +++ b/src/silx/gui/data/Hdf5TableView.py @@ -1,4 +1,3 @@ -# coding: utf-8 # /*########################################################################## # # Copyright (c) 2017-2021 European Synchrotron Radiation Facility @@ -26,7 +25,6 @@ This module define model and widget to display 1D slices from numpy array using compound data types or hdf5 databases. """ -from __future__ import division __authors__ = ["V. Valls"] __license__ = "MIT" @@ -450,8 +448,9 @@ class Hdf5TableModel(HierarchicalTableView.HierarchicalTableModel): return firstExtSource if firstExtSource[0] == ".": - firstExtSource.pop(0) - return os.path.join(os.path.dirname(filename), firstExtSource) + return filename + firstExtSource[1:] + else: + return os.path.join(os.path.dirname(filename), firstExtSource) self.__data.addHeaderRow(headerLabel="External sources") self.__data.addHeaderValueRow("Type", extType) diff --git a/src/silx/gui/data/HexaTableView.py b/src/silx/gui/data/HexaTableView.py index 9e00a7b..30f62f0 100644 --- a/src/silx/gui/data/HexaTableView.py +++ b/src/silx/gui/data/HexaTableView.py @@ -1,4 +1,3 @@ -# coding: utf-8 # /*########################################################################## # # Copyright (c) 2017-2021 European Synchrotron Radiation Facility @@ -26,7 +25,6 @@ This module defines model and widget to display raw data using an hexadecimal viewer. """ -from __future__ import division import collections diff --git a/src/silx/gui/data/NXdataWidgets.py b/src/silx/gui/data/NXdataWidgets.py index 54ea287..b9e34d2 100644 --- a/src/silx/gui/data/NXdataWidgets.py +++ b/src/silx/gui/data/NXdataWidgets.py @@ -1,7 +1,6 @@ -# coding: utf-8 # /*########################################################################## # -# Copyright (c) 2017-2021 European Synchrotron Radiation Facility +# Copyright (c) 2017-2022 European Synchrotron Radiation Facility # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal @@ -76,6 +75,7 @@ class ArrayCurvePlot(qt.QWidget): self.__values = None self._plot = Plot1D(self) + self._plot.setGraphGrid(True) self._selector = NumpyAxesSelector(self) self._selector.setNamedAxesSelectorVisibility(False) @@ -412,7 +412,8 @@ class ArrayImagePlot(qt.QWidget): signals_names=None, xlabel=None, ylabel=None, title=None, isRgba=False, - xscale=None, yscale=None): + xscale=None, yscale=None, + keep_ratio: bool=True): """ :param signals: list of n-D datasets, whose last 2 dimensions are used as the @@ -430,6 +431,7 @@ class ArrayImagePlot(qt.QWidget): :param isRgba: True if data is a 3D RGBA image :param str xscale: Scale of X axis in (None, 'linear', 'log') :param str yscale: Scale of Y axis in (None, 'linear', 'log') + :param keep_ratio: Toggle plot keep aspect ratio """ self._selector.selectionChanged.disconnect(self._updateImage) self._auxSigSlider.valueChanged.disconnect(self._sliderIdxChanged) @@ -465,6 +467,7 @@ class ArrayImagePlot(qt.QWidget): self._axis_scales = xscale, yscale self._updateImage() + self._plot.setKeepDataAspectRatio(keep_ratio) self._plot.resetZoom() self._selector.selectionChanged.connect(self._updateImage) @@ -629,7 +632,8 @@ class ArrayComplexImagePlot(qt.QWidget): x_axis=None, y_axis=None, signals_names=None, xlabel=None, ylabel=None, - title=None): + title=None, + keep_ratio: bool=True): """ :param signals: list of n-D datasets, whose last 2 dimensions are used as the @@ -644,6 +648,7 @@ class ArrayComplexImagePlot(qt.QWidget): :param xlabel: Label for X axis :param ylabel: Label for Y axis :param title: Graph title + :param keep_ratio: Toggle plot keep aspect ratio """ self._selector.selectionChanged.disconnect(self._updateImage) self._auxSigSlider.valueChanged.disconnect(self._sliderIdxChanged) @@ -673,6 +678,7 @@ class ArrayComplexImagePlot(qt.QWidget): self._auxSigSlider.setValue(0) self._updateImage() + self._plot.setKeepDataAspectRatio(keep_ratio) self._plot.getPlot().resetZoom() self._selector.selectionChanged.connect(self._updateImage) diff --git a/src/silx/gui/data/NumpyAxesSelector.py b/src/silx/gui/data/NumpyAxesSelector.py index e6da0d4..50b8dcd 100644 --- a/src/silx/gui/data/NumpyAxesSelector.py +++ b/src/silx/gui/data/NumpyAxesSelector.py @@ -1,4 +1,3 @@ -# coding: utf-8 # /*########################################################################## # # Copyright (c) 2016-2019 European Synchrotron Radiation Facility @@ -25,7 +24,6 @@ """This module defines a widget able to convert a numpy array from n-dimensions to a numpy array with less dimensions. """ -from __future__ import division __authors__ = ["V. Valls"] __license__ = "MIT" diff --git a/src/silx/gui/data/RecordTableView.py b/src/silx/gui/data/RecordTableView.py index ea73c62..9079ba6 100644 --- a/src/silx/gui/data/RecordTableView.py +++ b/src/silx/gui/data/RecordTableView.py @@ -1,4 +1,3 @@ -# coding: utf-8 # /*########################################################################## # # Copyright (c) 2017-2021 European Synchrotron Radiation Facility @@ -26,7 +25,6 @@ This module define model and widget to display 1D slices from numpy array using compound data types or hdf5 databases. """ -from __future__ import division import itertools import numpy diff --git a/src/silx/gui/data/TextFormatter.py b/src/silx/gui/data/TextFormatter.py index b6baca4..d409381 100644 --- a/src/silx/gui/data/TextFormatter.py +++ b/src/silx/gui/data/TextFormatter.py @@ -1,4 +1,3 @@ -# coding: utf-8 # /*########################################################################## # # Copyright (c) 2017-2021 European Synchrotron Radiation Facility diff --git a/src/silx/gui/data/_VolumeWindow.py b/src/silx/gui/data/_VolumeWindow.py index 03b6876..fa2730c 100644 --- a/src/silx/gui/data/_VolumeWindow.py +++ b/src/silx/gui/data/_VolumeWindow.py @@ -1,4 +1,3 @@ -# coding: utf-8 # /*########################################################################## # # Copyright (c) 2019 European Synchrotron Radiation Facility diff --git a/src/silx/gui/data/__init__.py b/src/silx/gui/data/__init__.py index 560062d..59d32f1 100644 --- a/src/silx/gui/data/__init__.py +++ b/src/silx/gui/data/__init__.py @@ -1,4 +1,3 @@ -# coding: utf-8 # /*########################################################################## # # Copyright (c) 2016-2017 European Synchrotron Radiation Facility diff --git a/src/silx/gui/data/setup.py b/src/silx/gui/data/setup.py deleted file mode 100644 index 23ccbdd..0000000 --- a/src/silx/gui/data/setup.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__ = ["P. Knobel"] -__license__ = "MIT" -__date__ = "16/01/2017" - - -from numpy.distutils.misc_util import Configuration - - -def configuration(parent_package='', top_path=None): - config = Configuration('data', parent_package, top_path) - config.add_subpackage('test') - return config - - -if __name__ == "__main__": - from numpy.distutils.core import setup - setup(configuration=configuration) diff --git a/src/silx/gui/data/test/__init__.py b/src/silx/gui/data/test/__init__.py index 7790ee5..1d8207b 100644 --- a/src/silx/gui/data/test/__init__.py +++ b/src/silx/gui/data/test/__init__.py @@ -1,4 +1,3 @@ -# coding: utf-8 # /*########################################################################## # # Copyright (c) 2016-2017 European Synchrotron Radiation Facility diff --git a/src/silx/gui/data/test/test_arraywidget.py b/src/silx/gui/data/test/test_arraywidget.py index c84a34f..024383d 100644 --- a/src/silx/gui/data/test/test_arraywidget.py +++ b/src/silx/gui/data/test/test_arraywidget.py @@ -1,4 +1,3 @@ -# coding: utf-8 # /*########################################################################## # # Copyright (c) 2016-2021 European Synchrotron Radiation Facility diff --git a/src/silx/gui/data/test/test_dataviewer.py b/src/silx/gui/data/test/test_dataviewer.py index 30b76ce..80f47b7 100644 --- a/src/silx/gui/data/test/test_dataviewer.py +++ b/src/silx/gui/data/test/test_dataviewer.py @@ -1,7 +1,6 @@ -# coding: utf-8 # /*########################################################################## # -# Copyright (c) 2016-2020 European Synchrotron Radiation Facility +# Copyright (c) 2016-2022 European Synchrotron Radiation Facility # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal @@ -192,18 +191,36 @@ class _TestAbstractDataViewer(TestCaseQt): listener.clear() def test_change_display_mode(self): + listener = SignalListener() data = numpy.arange(10 ** 4) data.shape = [10] * 4 widget = self.create_widget() + widget.selectionChanged.connect(listener) widget.setData(data) + widget.setDisplayMode(DataViews.PLOT1D_MODE) self.assertEqual(widget.displayedView().modeId(), DataViews.PLOT1D_MODE) + self.qWait(200) + assert listener.arguments() == [((0, 0, 0, slice(None)), None)] + listener.clear() + widget.setDisplayMode(DataViews.IMAGE_MODE) self.assertEqual(widget.displayedView().modeId(), DataViews.IMAGE_MODE) + self.qWait(200) + assert listener.arguments() == [((0, 0, slice(None), slice(None)), None)] + listener.clear() + widget.setDisplayMode(DataViews.RAW_MODE) self.assertEqual(widget.displayedView().modeId(), DataViews.RAW_MODE) + self.qWait(200) + # Changing from 2D to 2D view: Selection didn't changed + assert listener.callCount() == 0 + widget.setDisplayMode(DataViews.EMPTY_MODE) self.assertEqual(widget.displayedView().modeId(), DataViews.EMPTY_MODE) + self.qWait(200) + assert listener.arguments() == [(None, None)] + listener.clear() def test_create_default_views(self): widget = self.create_widget() diff --git a/src/silx/gui/data/test/test_numpyaxesselector.py b/src/silx/gui/data/test/test_numpyaxesselector.py index 37b8d3e..4a53149 100644 --- a/src/silx/gui/data/test/test_numpyaxesselector.py +++ b/src/silx/gui/data/test/test_numpyaxesselector.py @@ -1,4 +1,3 @@ -# coding: utf-8 # /*########################################################################## # # Copyright (c) 2016-2019 European Synchrotron Radiation Facility diff --git a/src/silx/gui/data/test/test_textformatter.py b/src/silx/gui/data/test/test_textformatter.py index af41def..b82cc7a 100644 --- a/src/silx/gui/data/test/test_textformatter.py +++ b/src/silx/gui/data/test/test_textformatter.py @@ -1,4 +1,3 @@ -# coding: utf-8 # /*########################################################################## # # Copyright (c) 2016-2021 European Synchrotron Radiation Facility diff --git a/src/silx/gui/dialog/AbstractDataFileDialog.py b/src/silx/gui/dialog/AbstractDataFileDialog.py index 5272f48..f656bb2 100644 --- a/src/silx/gui/dialog/AbstractDataFileDialog.py +++ b/src/silx/gui/dialog/AbstractDataFileDialog.py @@ -1,7 +1,6 @@ -# coding: utf-8 # /*########################################################################## # -# Copyright (c) 2016-2021 European Synchrotron Radiation Facility +# Copyright (c) 2016-2022 European Synchrotron Radiation Facility # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal @@ -35,7 +34,6 @@ import sys import os import logging import functools -from distutils.version import LooseVersion import numpy @@ -149,13 +147,13 @@ class _SideBar(qt.QListView): :rtype: List[str] """ urls = [] - version = LooseVersion(qt.qVersion()) + version = tuple(map(int, qt.qVersion().split('.')[:3])) feed_sidebar = True if not DEFAULT_SIDEBAR_URL: _logger.debug("Skip default sidebar URLs (from setted variable)") feed_sidebar = False - elif version < LooseVersion("5.11.2") and qt.BINDING == "PyQt5" and sys.platform in ["linux", "linux2"]: + elif version < (5, 11, 2) and qt.BINDING == "PyQt5" and sys.platform in ["linux", "linux2"]: # Avoid segfault on PyQt5 + gtk _logger.debug("Skip default sidebar URLs (avoid PyQt5 segfault)") feed_sidebar = False @@ -429,7 +427,7 @@ class _Browser(qt.QStackedWidget): self.__detailView.header().restoreState(headerData) viewMode = stream.readInt32() - self.setViewMode(viewMode) + self.setViewMode(qt.QFileDialog.ViewMode(viewMode)) return True def saveState(self): @@ -444,7 +442,10 @@ class _Browser(qt.QStackedWidget): stream.writeQString(nameId) stream.writeInt32(self.__serialVersion) stream.writeQVariant(self.__detailView.header().saveState()) - stream.writeInt32(self.viewMode()) + viewMode = self.viewMode() + if qt.BINDING == 'PyQt6': # No auto conversion to int + viewMode = viewMode.value + stream.writeInt32(viewMode) return data @@ -1695,7 +1696,7 @@ class AbstractDataFileDialog(qt.QDialog): if workingDirectory is not None: self.setDirectory(workingDirectory) result &= self.__browser.restoreState(browserData) - self.setViewMode(viewMode) + self.setViewMode(qt.QFileDialog.ViewMode(viewMode)) colormap = self.colormap() if colormap is not None: result &= self.colormap().restoreState(colormapData) @@ -1721,7 +1722,10 @@ class AbstractDataFileDialog(qt.QDialog): stream.writeQStringList(strings) stream.writeQString(u"%s" % self.directory()) stream.writeQVariant(self.__browser.saveState()) - stream.writeInt32(self.viewMode()) + viewMode = self.viewMode() + if qt.BINDING == 'PyQt6': # No auto conversion to int + viewMode = viewMode.value + stream.writeInt32(viewMode) colormap = self.colormap() if colormap is not None: stream.writeQVariant(self.colormap().saveState()) diff --git a/src/silx/gui/dialog/ColormapDialog.py b/src/silx/gui/dialog/ColormapDialog.py index 2506e2a..f3f38b5 100644 --- a/src/silx/gui/dialog/ColormapDialog.py +++ b/src/silx/gui/dialog/ColormapDialog.py @@ -1,7 +1,6 @@ -# coding: utf-8 # /*########################################################################## # -# Copyright (c) 2004-2021 European Synchrotron Radiation Facility +# Copyright (c) 2004-2022 European Synchrotron Radiation Facility # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal @@ -81,7 +80,7 @@ from silx.gui.plot import items from silx.gui import icons from silx.gui.qt import inspect as qtinspect from silx.gui.widgets.ColormapNameComboBox import ColormapNameComboBox -from silx.gui.widgets.WaitingPushButton import WaitingPushButton +from silx.gui.widgets.FormGridLayout import FormGridLayout from silx.math.histogram import Histogramnd from silx.utils import deprecation from silx.gui.plot.items.roi import RectangleROI @@ -128,6 +127,20 @@ class _BoundaryWidget(qt.QWidget): self.setLayout(qt.QHBoxLayout()) self.layout().setContentsMargins(0, 0, 0, 0) self._numVal = FloatEdit(parent=self, value=value) + + self._iconAuto = icons.getQIcon('scale-auto') + self._iconFixed = icons.getQIcon('scale-fixed') + + self._autoToggleAction = qt.QAction(self) + self._autoToggleAction.setText("Auto scale") + self._autoToggleAction.setToolTip("Toggle auto scale") + self._autoToggleAction.setCheckable(True) + self._autoToggleAction.setIcon(self._iconFixed) + self._autoToggleAction.setChecked(False) + self._autoToggleAction.toggled.connect(self._autoToggled) + + self._numVal.addAction(self._autoToggleAction, qt.QLineEdit.LeadingPosition) + self.layout().addWidget(self._numVal) self._autoCB = qt.QCheckBox('auto', parent=self) self.layout().addWidget(self._autoCB) @@ -174,7 +187,7 @@ class _BoundaryWidget(qt.QWidget): return self._numVal.value() def _autoToggled(self, enabled): - self._numVal.setEnabled(not enabled) + self._updateAutoScaleState(enabled) self._updateDisplayedText() self.sigAutoScaleChanged.emit(enabled) @@ -198,14 +211,27 @@ class _BoundaryWidget(qt.QWidget): if not self.__textWasEdited: self._numVal.setValue(value) self.__realValue = value - self._numVal.setEnabled(not isAuto) + self._updateAutoScaleState(isAuto) + + def _updateAutoScaleState(self, isAutoScale): + self._numVal.setReadOnly(isAutoScale) + palette = qt.QPalette() + if isAutoScale: + color = palette.color(qt.QPalette.Disabled, qt.QPalette.Base) + icon = self._iconAuto + else: + color = palette.color(qt.QPalette.Normal, qt.QPalette.Base) + icon = self._iconFixed + palette.setColor(qt.QPalette.Base, color) + self._numVal.setPalette(palette) + self._autoToggleAction.setIcon(icon) class _AutoscaleModeComboBox(qt.QComboBox): DATA = { Colormap.MINMAX: ("Min/max", "Use the data min/max"), - Colormap.STDDEV3: ("Mean ± 3 × stddev", "Use the data mean ± 3 × standard deviation"), + Colormap.STDDEV3: ("Mean±3std", "Use the data mean ± 3 × standard deviation"), } def __init__(self, parent: qt.QWidget): @@ -248,80 +274,31 @@ class _AutoscaleModeComboBox(qt.QComboBox): self.setCurrentIndex(self.count() - 1) -class _AutoScaleButtons(qt.QWidget): +class _AutoScaleButton(qt.QPushButton): autoRangeChanged = qt.Signal(object) def __init__(self, parent=None): - qt.QWidget.__init__(self, parent=parent) - layout = qt.QHBoxLayout(self) - layout.setContentsMargins(0, 0, 0, 0) - layout.setSpacing(0) - - self.setFocusPolicy(qt.Qt.NoFocus) - - self._bothAuto = qt.QPushButton(self) - self._bothAuto.setText("Autoscale") - self._bothAuto.setToolTip("Enable/disable the autoscale for both min and max") - self._bothAuto.setCheckable(True) - self._bothAuto.toggled[bool].connect(self.__bothToggled) - self._bothAuto.setFocusPolicy(qt.Qt.TabFocus) - - self._minAuto = qt.QCheckBox(self) - self._minAuto.setText("") - self._minAuto.setToolTip("Enable/disable the autoscale for min") - self._minAuto.toggled[bool].connect(self.__minToggled) - self._minAuto.setFocusPolicy(qt.Qt.TabFocus) - - self._maxAuto = qt.QCheckBox(self) - self._maxAuto.setText("") - self._maxAuto.setToolTip("Enable/disable the autoscale for max") - self._maxAuto.toggled[bool].connect(self.__maxToggled) - self._maxAuto.setFocusPolicy(qt.Qt.TabFocus) - - layout.addStretch(1) - layout.addWidget(self._minAuto) - layout.addSpacing(20) - layout.addWidget(self._bothAuto) - layout.addSpacing(20) - layout.addWidget(self._maxAuto) - layout.addStretch(1) - - def __bothToggled(self, checked): + qt.QPushButton.__init__(self, parent=parent) + self.setText("Autoscale") + self.setToolTip("Enable/disable the autoscale for both min and max") + self.setCheckable(True) + self.toggled[bool].connect(self.__toggled) + self.setFocusPolicy(qt.Qt.TabFocus) + + def __toggled(self, checked): autoRange = checked, checked self.setAutoRange(autoRange) self.autoRangeChanged.emit(autoRange) - def __minToggled(self, checked): - autoRange = self.getAutoRange() - self.setAutoRange(autoRange) - self.autoRangeChanged.emit(autoRange) - - def __maxToggled(self, checked): - autoRange = self.getAutoRange() - self.setAutoRange(autoRange) - self.autoRangeChanged.emit(autoRange) - def setAutoRangeFromColormap(self, colormap): vRange = colormap.getVRange() autoRange = vRange[0] is None, vRange[1] is None self.setAutoRange(autoRange) def setAutoRange(self, autoRange): - if autoRange[0] == autoRange[1]: - with utils.blockSignals(self._bothAuto): - self._bothAuto.setChecked(autoRange[0]) - else: - with utils.blockSignals(self._bothAuto): - self._bothAuto.setChecked(False) - with utils.blockSignals(self._minAuto): - self._minAuto.setChecked(autoRange[0]) - with utils.blockSignals(self._maxAuto): - self._maxAuto.setChecked(autoRange[1]) - - def getAutoRange(self): - return self._minAuto.isChecked(), self._maxAuto.isChecked() - + with utils.blockSignals(self): + self.setChecked(autoRange[0] if autoRange[0] == autoRange[1] else False) @enum.unique class _DataInPlotMode(enum.Enum): @@ -333,7 +310,7 @@ class _DataInPlotMode(enum.Enum): class _ColormapHistogram(qt.QWidget): """Display the colormap and the data as a plot.""" - sigRangeMoving = qt.Signal(object, object) + sigRangeMoving = qt.Signal(object, object, object) """Emitted when a mouse interaction moves the location of the colormap range in the plot. @@ -341,15 +318,17 @@ class _ColormapHistogram(qt.QWidget): - vmin: A float value if this range was moved, else None - vmax: A float value if this range was moved, else None + - gammaPos: A float value if this range was moved, else None """ - sigRangeMoved = qt.Signal(object, object) + sigRangeMoved = qt.Signal(object, object, object) """Emitted when a mouse interaction stop. This signal contains 2 elements: - vmin: A float value if this range was moved, else None - vmax: A float value if this range was moved, else None + - gammaPos: A float value if this range was moved, else None """ def __init__(self, parent): @@ -361,7 +340,7 @@ class _ColormapHistogram(qt.QWidget): self._histogramData = {} """Histogram displayed in the plot""" - self._dragging = False, False + self._dragging = False, False, False """True, if the min or the max handle is dragging""" self._dataRange = {} @@ -528,7 +507,8 @@ class _ColormapHistogram(qt.QWidget): def _initPlot(self): """Init the plot to display the range and the values""" self._plot = PlotWidget(self) - self._plot.setDataMargins(0.125, 0.125, 0.125, 0.125) + self._plot.setAxesDisplayed(False) + self._plot.setDataMargins(0.125, 0.125, 0.01, 0.01) self._plot.getXAxis().setLabel("Data Values") self._plot.getYAxis().setLabel("") self._plot.setInteractiveMode('select', zoomOnWheel=False) @@ -600,20 +580,26 @@ class _ColormapHistogram(qt.QWidget): if kind == 'markerMoving': value = event['xdata'] if event['label'] == 'Min': - self._dragging = True, False + self._dragging = True, False, False self._finiteRange = value, self._finiteRange[1] - self._last = value, None + self._last = value, None, None + self._updateGammaPosition() self.sigRangeMoving.emit(*self._last) elif event['label'] == 'Max': - self._dragging = False, True + self._dragging = False, True, False self._finiteRange = self._finiteRange[0], value - self._last = None, value + self._last = None, value, None + self._updateGammaPosition() + self.sigRangeMoving.emit(*self._last) + elif event['label'] == 'Gamma': + self._dragging = False, False, True + self._last = None, None, value self.sigRangeMoving.emit(*self._last) self._updateLutItem(self._finiteRange) elif kind == 'markerMoved': self.sigRangeMoved.emit(*self._last) self._plot.resetZoom() - self._dragging = False, False + self._dragging = False, False, False else: pass @@ -635,11 +621,12 @@ class _ColormapHistogram(qt.QWidget): draggable=isDraggable, color="blue", constraint=self._plotMinMarkerConstraint) + self._updateGammaPosition() if posMax is not None and not self._dragging[1]: self._plot.addXMarker( posMax, legend='Max', - text='Max', + text='\n\nMax', draggable=isDraggable, color="blue", constraint=self._plotMaxMarkerConstraint) @@ -647,6 +634,42 @@ class _ColormapHistogram(qt.QWidget): self._updateLutItem((posMin, posMax)) self._plot.resetZoom() + def _updateGammaPosition(self): + colormap = self.getColormap() + posMin, posMax = self._getDisplayableRange() + + if colormap is None: + gamma = None + else: + if colormap.getNormalization() == Colormap.GAMMA: + gamma = colormap.getGammaNormalizationParameter() + else: + gamma = None + + if gamma is not None: + if not self._dragging[2]: + posRange = posMax - posMin + if posRange > 0: + gammaPos = posMin + posRange * 0.5**(1/gamma) + else: + gammaPos = posMin + marker = self._plot._getMarker( + self._plot.addXMarker( + gammaPos, + legend='Gamma', + text='\nGamma', + draggable=True, + color="blue", + constraint=self._plotGammaMarkerConstraint, + ) + ) + marker.setZValue(2) + else: + try: + self._plot.removeMarker('Gamma') + except Exception: + pass + def _updateLutItem(self, vRange): colormap = self.getColormap() if colormap is None: @@ -718,6 +741,15 @@ class _ColormapHistogram(qt.QWidget): return x, y return max(x, vmin), y + def _plotGammaMarkerConstraint(self, x, y): + """Constraint of the gamma marker""" + vmin, vmax = self.getFiniteRange() + if vmin is not None: + x = max(x, vmin) + if vmax is not None: + x = min(x, vmax) + return x, y + def _setDataInPlotMode(self, mode): if self._dataInPlotMode == mode: return @@ -829,6 +861,9 @@ class ColormapDialog(qt.QDialog): self._item = None """Weak ref to an external item""" + self._colormapped = None + """Weak ref to reduce data update""" + self._colormapChange = utils.LockReentrant() """Used as a semaphore to avoid editing the colormap object when we are only attempt to display it. @@ -873,7 +908,7 @@ class ColormapDialog(qt.QDialog): self._gammaSpinBox = qt.QDoubleSpinBox(parent=self) self._gammaSpinBox.setEnabled(False) - self._gammaSpinBox.setRange(0., 1000.) + self._gammaSpinBox.setRange(0.01, 100.) self._gammaSpinBox.setDecimals(4) if hasattr(qt.QDoubleSpinBox, "setStepType"): # Introduced in Qt 5.12 @@ -891,13 +926,15 @@ class ColormapDialog(qt.QDialog): self._minValue = _BoundaryWidget(parent=self, value=1.0) self._minValue.sigAutoScaleChanged.connect(self._minAutoscaleUpdated) self._minValue.sigValueChanged.connect(self._minValueUpdated) + self._minValue.setMinimumWidth(140) # Max row self._maxValue = _BoundaryWidget(parent=self, value=10.0) self._maxValue.sigAutoScaleChanged.connect(self._maxAutoscaleUpdated) self._maxValue.sigValueChanged.connect(self._maxValueUpdated) + self._maxValue.setMinimumWidth(140) - self._autoButtons = _AutoScaleButtons(self) + self._autoButtons = _AutoScaleButton(self) self._autoButtons.autoRangeChanged.connect(self._autoRangeButtonsUpdated) rangeLayout = qt.QGridLayout() @@ -909,15 +946,20 @@ class ColormapDialog(qt.QDialog): labelMax = qt.QLabel("Max", self) labelMax.setAlignment(qt.Qt.AlignHCenter) labelMax.setFont(miniFont) - rangeLayout.addWidget(labelMin, 0, 0) - rangeLayout.addWidget(labelMax, 0, 1) - rangeLayout.addWidget(self._minValue, 1, 0) - rangeLayout.addWidget(self._maxValue, 1, 1) - rangeLayout.addWidget(self._autoButtons, 2, 0, 1, -1, qt.Qt.AlignCenter) + rangeLayout.addWidget(labelMin, 0, 1) + rangeLayout.addWidget(labelMax, 0, 3) + rangeLayout.addWidget(self._minValue, 1, 1) + rangeLayout.addWidget(self._maxValue, 1, 3) + rangeLayout.setColumnStretch(0, 1) + rangeLayout.setColumnStretch(1, 2) + rangeLayout.setColumnStretch(2, 1) + rangeLayout.setColumnStretch(3, 2) + rangeLayout.setColumnStretch(4, 1) self._histoWidget = _ColormapHistogram(self) self._histoWidget.sigRangeMoving.connect(self._histogramRangeMoving) self._histoWidget.sigRangeMoved.connect(self._histogramRangeMoved) + self._histoWidget.setSizePolicy(qt.QSizePolicy.Expanding, qt.QSizePolicy.Expanding) # Scale to buttons self._visibleAreaButton = qt.QPushButton(self) @@ -930,12 +972,12 @@ class ColormapDialog(qt.QDialog): # Place-holder for selected area ROI manager self._roiForColormapManager = None - self._selectedAreaButton = WaitingPushButton(self) + self._selectedAreaButton = qt.QPushButton(self) + self._selectedAreaButton.setCheckable(True) self._selectedAreaButton.setEnabled(False) self._selectedAreaButton.setText("Selection") self._selectedAreaButton.setIcon(icons.getQIcon("add-shape-rectangle")) self._selectedAreaButton.setCheckable(True) - self._selectedAreaButton.setDisabledWhenWaiting(False) self._selectedAreaButton.toggled.connect( self._handleScaleToSelectionToggled, type=qt.Qt.QueuedConnection) @@ -966,31 +1008,38 @@ class ColormapDialog(qt.QDialog): self.setModal(self.isModal()) - formLayout = qt.QFormLayout(self) - formLayout.setContentsMargins(10, 10, 10, 10) - formLayout.addRow('Colormap:', self._comboBoxColormap) - formLayout.addRow('Normalization:', self._comboBoxNormalization) - formLayout.addRow('Gamma:', self._gammaSpinBox) - formLayout.addRow(self._histoWidget) - formLayout.addRow(rangeLayout) - label = qt.QLabel('Mode:', self) - self._autoscaleModeLabel = label - label.setToolTip("Mode for autoscale. Algorithm used to find range in auto scale.") - formLayout.addItem(qt.QSpacerItem(1, 1, qt.QSizePolicy.Fixed, qt.QSizePolicy.Fixed)) - formLayout.addRow(label, autoScaleCombo) - layout = qt.QHBoxLayout() layout.setContentsMargins(0, 0, 0, 0) layout.addWidget(self._visibleAreaButton) layout.addWidget(self._selectedAreaButton) - self._scaleToAreaGroup = qt.QGroupBox('Scale to:', self) + layout.addStretch() + self._scaleToAreaGroup = qt.QWidget(self) self._scaleToAreaGroup.setLayout(layout) self._scaleToAreaGroup.setVisible(False) - formLayout.addRow(self._scaleToAreaGroup) + layoutScale = qt.QHBoxLayout() + layoutScale.setContentsMargins(0, 0, 0, 0) + layoutScale.addWidget(self._autoButtons) + layoutScale.addWidget(self._autoScaleCombo) + layoutScale.addStretch() + + + formLayout = FormGridLayout(self) + formLayout.setContentsMargins(10, 10, 10, 10) + + formLayout.addRow('Colormap:', self._comboBoxColormap) + formLayout.addRow('Normalization:', self._comboBoxNormalization) + formLayout.addRow('Gamma:', self._gammaSpinBox) + + formLayout.addItem(qt.QSpacerItem(1, 1, qt.QSizePolicy.Fixed, qt.QSizePolicy.Fixed)) + formLayout.addRow(self._histoWidget) + formLayout.addRow(rangeLayout) + formLayout.addItem(qt.QSpacerItem(1, 1, qt.QSizePolicy.Fixed, qt.QSizePolicy.Fixed)) + formLayout.addRow('Scale:', layoutScale) + formLayout.addRow("Fixed scale on:", self._scaleToAreaGroup) formLayout.addRow(self._buttonsModal) formLayout.addRow(self._buttonsNonModal) - formLayout.setSizeConstraint(qt.QLayout.SetMinimumSize) + formLayout.setSizeConstraint(qt.QLayout.SetMinAndMaxSize) self.setTabOrder(self._comboBoxColormap, self._comboBoxNormalization) self.setTabOrder(self._comboBoxNormalization, self._gammaSpinBox) @@ -1003,7 +1052,6 @@ class ColormapDialog(qt.QDialog): self.setTabOrder(self._selectedAreaButton, self._buttonsModal) self.setTabOrder(self._buttonsModal, self._buttonsNonModal) - self.setFixedSize(self.sizeHint()) self._applyColormap() def _invalidateColormap(self): @@ -1041,10 +1089,14 @@ class ColormapDialog(qt.QDialog): super(ColormapDialog, self).closeEvent(event) def hideEvent(self, event): + if self._selectedAreaButton.isChecked(): + self._selectedAreaButton.setChecked(False) self.visibleChanged.emit(False) super(ColormapDialog, self).hideEvent(event) def close(self): + if self._selectedAreaButton.isChecked(): + self._selectedAreaButton.setChecked(False) self.accept() qt.QDialog.close(self) @@ -1196,10 +1248,21 @@ class ColormapDialog(qt.QDialog): the data range or the histogram of the data using :meth:`setDataRange` and :meth:`setHistogram` """ - # While event from items are not supported, we can't ignore dup items - # old = self._getItem() - # if old is item: - # return + old = self._getItem() + if old is item: + # While event from items are not supported, we can't ignore dup items + if item is not None: + array = item.getColormappedData(copy=False) + else: + array = None + colormapped = self._colormapped + if colormapped is not None: + oldArray = colormapped() + else: + oldArray = None + if oldArray is array: + return + self._data = None self._itemHolder = None try: @@ -1252,7 +1315,12 @@ class ColormapDialog(qt.QDialog): return data item = self._getItem() if item is not None: - return item.getColormappedData(copy=False) + colormapped = item.getColormappedData(copy=False) + if colormapped is not None: + self._colormapped = weakref.ref(colormapped) + else: + self._colormapped = None + return colormapped return None def _colormapAboutToFinalize(self, weakrefColormap): @@ -1418,7 +1486,6 @@ class ColormapDialog(qt.QDialog): self._histoWidget.setFiniteRange((xmin, xmax)) with utils.blockSignals(self._autoButtons): self._autoButtons.setAutoRange((autoMin, autoMax)) - self._autoscaleModeLabel.setEnabled(autoMin or autoMax) def accept(self): self.storeCurrentState() @@ -1487,7 +1554,6 @@ class ColormapDialog(qt.QDialog): self._minValue.setEnabled(False) self._maxValue.setEnabled(False) self._autoButtons.setEnabled(False) - self._autoscaleModeLabel.setEnabled(False) self._histoWidget.setVisible(False) self._histoWidget.setFiniteRange((None, None)) else: @@ -1508,7 +1574,7 @@ class ColormapDialog(qt.QDialog): self._gammaSpinBox.setValue( colormap.getGammaNormalizationParameter()) self._gammaSpinBox.setEnabled( - colormap.getNormalization() == 'gamma' and + colormap.getNormalization() == Colormap.GAMMA and colormap.isEditable()) with utils.blockSignals(self._autoScaleCombo): self._autoScaleCombo.setCurrentMode(colormap.getAutoscaleMode()) @@ -1530,7 +1596,6 @@ class ColormapDialog(qt.QDialog): with utils.blockSignals(self._maxValue): self._maxValue.setValue(vmax or dataRange[1], isAuto=vmax is None) self._maxValue.setEnabled(colormap.isEditable()) - self._autoscaleModeLabel.setEnabled(vmin is None or vmax is None) with utils.blockSignals(self._histoWidget): self._histoWidget.setVisible(True) @@ -1653,12 +1718,13 @@ class ColormapDialog(qt.QDialog): self._maxValue.setValue(xmax) self._setColormapRange(xmin, xmax) - def _histogramRangeMoving(self, vmin, vmax): + def _histogramRangeMoving(self, vmin, vmax, gammaPos): """Callback executed when for colormap range displayed in the histogram widget is moving. :param vmin: Update of the minimum range, else None :param vmax: Update of the maximum range, else None + :param gammaPos: Update of the gamma location, else None """ colormap = self.getColormap() if vmin is not None: @@ -1669,11 +1735,31 @@ class ColormapDialog(qt.QDialog): with self._colormapChange: colormap.setVMax(vmax) self._maxValue.setValue(vmax) + if gammaPos is not None: + vmin, vmax = self._histoWidget.getFiniteRange() + if vmax < vmin: + gamma = 1 + elif gammaPos >= vmax: + gamma = self._gammaSpinBox.maximum() + elif gammaPos <= vmin: + gamma = self._gammaSpinBox.minimum() + else: + gamma = numpy.clip( + numpy.log(0.5)/numpy.log((gammaPos - vmin) / (vmax - vmin)), + self._gammaSpinBox.minimum(), + self._gammaSpinBox.maximum(), + ) + with self._colormapChange: + colormap.setGammaNormalizationParameter(gamma) + with utils.blockSignals(self._gammaSpinBox): + self._gammaSpinBox.setValue(gamma) - def _histogramRangeMoved(self, vmin, vmax): + def _histogramRangeMoved(self, vmin, vmax, gammaPos): """Callback executed when for colormap range displayed in the histogram widget has finished to move """ + if vmin is None and vmax is None: + return xmin = self._minValue.getValue() xmax = self._maxValue.getValue() if vmin is None: @@ -1713,7 +1799,6 @@ class ColormapDialog(qt.QDialog): self._roiForColormapManager = None if not checked: # Reset button status - self._selectedAreaButton.setWaiting(False) self._selectedAreaButton.setText("Selection") return @@ -1727,7 +1812,6 @@ class ColormapDialog(qt.QDialog): self._selectedAreaButton.setChecked(False) return # no-op - self._selectedAreaButton.setWaiting(True) self._selectedAreaButton.setText("Draw Area...") self._roiForColormapManager = RegionOfInterestManager(parent=plotWidget) @@ -1743,11 +1827,12 @@ class ColormapDialog(qt.QDialog): self._selectedAreaButton.setChecked(False) def __roiFinalized(self, roi): - self._selectedAreaButton.setChecked(False) if roi is not None: ox, oy = roi.getOrigin() width, height = roi.getSize() self.setColormapRangeFromDataBounds((ox, ox+width, oy, oy+height)) + # clear ROI + self._roiForColormapManager.removeRoi(roi) def keyPressEvent(self, event): """Override key handling. diff --git a/src/silx/gui/dialog/DataFileDialog.py b/src/silx/gui/dialog/DataFileDialog.py index 0d0382d..75b1721 100644 --- a/src/silx/gui/dialog/DataFileDialog.py +++ b/src/silx/gui/dialog/DataFileDialog.py @@ -1,4 +1,3 @@ -# coding: utf-8 # /*########################################################################## # # Copyright (c) 2016-2021 European Synchrotron Radiation Facility diff --git a/src/silx/gui/dialog/DatasetDialog.py b/src/silx/gui/dialog/DatasetDialog.py index c5ee295..5d8af0d 100644 --- a/src/silx/gui/dialog/DatasetDialog.py +++ b/src/silx/gui/dialog/DatasetDialog.py @@ -1,4 +1,3 @@ -# coding: utf-8 # /*########################################################################## # # Copyright (c) 2018-2021 European Synchrotron Radiation Facility diff --git a/src/silx/gui/dialog/FileTypeComboBox.py b/src/silx/gui/dialog/FileTypeComboBox.py index 92529bc..0ffc3a5 100644 --- a/src/silx/gui/dialog/FileTypeComboBox.py +++ b/src/silx/gui/dialog/FileTypeComboBox.py @@ -1,4 +1,3 @@ -# coding: utf-8 # /*########################################################################## # # Copyright (c) 2016 European Synchrotron Radiation Facility diff --git a/src/silx/gui/dialog/GroupDialog.py b/src/silx/gui/dialog/GroupDialog.py index e129a51..fb85d83 100644 --- a/src/silx/gui/dialog/GroupDialog.py +++ b/src/silx/gui/dialog/GroupDialog.py @@ -1,4 +1,3 @@ -# coding: utf-8 # /*########################################################################## # # Copyright (c) 2018-2021 European Synchrotron Radiation Facility diff --git a/src/silx/gui/dialog/ImageFileDialog.py b/src/silx/gui/dialog/ImageFileDialog.py index 83c6d95..ed455f3 100644 --- a/src/silx/gui/dialog/ImageFileDialog.py +++ b/src/silx/gui/dialog/ImageFileDialog.py @@ -1,4 +1,3 @@ -# coding: utf-8 # /*########################################################################## # # Copyright (c) 2016-2021 European Synchrotron Radiation Facility diff --git a/src/silx/gui/dialog/SafeFileIconProvider.py b/src/silx/gui/dialog/SafeFileIconProvider.py index 1e06b64..141bedf 100644 --- a/src/silx/gui/dialog/SafeFileIconProvider.py +++ b/src/silx/gui/dialog/SafeFileIconProvider.py @@ -1,4 +1,3 @@ -# coding: utf-8 # /*########################################################################## # # Copyright (c) 2016 European Synchrotron Radiation Facility diff --git a/src/silx/gui/dialog/SafeFileSystemModel.py b/src/silx/gui/dialog/SafeFileSystemModel.py index 1ec7153..b9f3913 100644 --- a/src/silx/gui/dialog/SafeFileSystemModel.py +++ b/src/silx/gui/dialog/SafeFileSystemModel.py @@ -1,4 +1,3 @@ -# coding: utf-8 # /*########################################################################## # # Copyright (c) 2016-2021 European Synchrotron Radiation Facility diff --git a/src/silx/gui/dialog/__init__.py b/src/silx/gui/dialog/__init__.py index 77c5949..c1dc89a 100644 --- a/src/silx/gui/dialog/__init__.py +++ b/src/silx/gui/dialog/__init__.py @@ -1,4 +1,3 @@ -# coding: utf-8 # /*########################################################################## # # Copyright (c) 2016 European Synchrotron Radiation Facility diff --git a/src/silx/gui/dialog/setup.py b/src/silx/gui/dialog/setup.py deleted file mode 100644 index 48ab8d8..0000000 --- a/src/silx/gui/dialog/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__ = "23/10/2017" - -from numpy.distutils.misc_util import Configuration - - -def configuration(parent_package='', top_path=None): - config = Configuration('dialog', parent_package, top_path) - config.add_subpackage('test') - return config - - -if __name__ == "__main__": - from numpy.distutils.core import setup - setup(configuration=configuration) diff --git a/src/silx/gui/dialog/test/__init__.py b/src/silx/gui/dialog/test/__init__.py index 71128fb..b03339f 100644 --- a/src/silx/gui/dialog/test/__init__.py +++ b/src/silx/gui/dialog/test/__init__.py @@ -1,4 +1,3 @@ -# coding: utf-8 # /*########################################################################## # # Copyright (c) 2016 European Synchrotron Radiation Facility diff --git a/src/silx/gui/dialog/test/test_colormapdialog.py b/src/silx/gui/dialog/test/test_colormapdialog.py index 16a5ab2..1bfd584 100644 --- a/src/silx/gui/dialog/test/test_colormapdialog.py +++ b/src/silx/gui/dialog/test/test_colormapdialog.py @@ -1,7 +1,6 @@ -# coding: utf-8 # /*########################################################################## # -# Copyright (c) 2016-2021 European Synchrotron Radiation Facility +# Copyright (c) 2016-2022 European Synchrotron Radiation Facility # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal @@ -51,7 +50,7 @@ def colormap(): @pytest.fixture -def colormapDialog(qapp, qapp_utils): +def colormapDialog(qapp): dialog = ColormapDialog.ColormapDialog() dialog.setAttribute(qt.Qt.WA_DeleteOnClose) yield weakref.proxy(dialog) @@ -59,6 +58,7 @@ def colormapDialog(qapp, qapp_utils): from silx.gui.qt import inspect if inspect.isValid(dialog): dialog.close() + del dialog qapp.processEvents() @@ -85,6 +85,7 @@ class TestColormapDialog(TestCaseQt, ParametricTestCase): modification are correctly updated if an other colormapdialog is editing the same colormap""" colormapDiag2 = ColormapDialog.ColormapDialog() + colormapDiag2.setAttribute(qt.Qt.WA_DeleteOnClose) colormapDiag2.setColormap(self.colormap) colormapDiag2.show() self.colormapDiag.setColormap(self.colormap) @@ -106,6 +107,8 @@ class TestColormapDialog(TestCaseQt, ParametricTestCase): self.assertTrue(int(colormapDiag2._minValue.getValue()) == 10) self.assertTrue(int(colormapDiag2._maxValue.getValue()) == 20) colormapDiag2.close() + del colormapDiag2 + self.qapp.processEvents() def testGUIModalOk(self): """Make sure the colormap is modified if gone through accept""" @@ -214,8 +217,8 @@ class TestColormapDialog(TestCaseQt, ParametricTestCase): self.assertTrue(self.colormapDiag._minValue.isEnabled()) self.assertTrue(self.colormapDiag._maxValue.isEnabled()) else: - self.assertFalse(self.colormapDiag._minValue._numVal.isEnabled()) - self.assertFalse(self.colormapDiag._maxValue._numVal.isEnabled()) + self.assertTrue(self.colormapDiag._minValue._numVal.isReadOnly()) + self.assertTrue(self.colormapDiag._maxValue._numVal.isReadOnly()) def testColormapDel(self): """Check behavior if the colormap has been deleted outside. For now @@ -244,13 +247,14 @@ class TestColormapDialog(TestCaseQt, ParametricTestCase): self.colormap.setVRange(11, 201) self.assertTrue(self.colormapDiag._minValue.getValue() == 11) self.assertTrue(self.colormapDiag._maxValue.getValue() == 201) - self.assertTrue(self.colormapDiag._minValue._numVal.isEnabled()) - self.assertTrue(self.colormapDiag._maxValue._numVal.isEnabled()) + self.assertFalse(self.colormapDiag._minValue._numVal.isReadOnly()) + self.assertFalse(self.colormapDiag._maxValue._numVal.isReadOnly()) self.assertFalse(self.colormapDiag._minValue.isAutoChecked()) self.assertFalse(self.colormapDiag._maxValue.isAutoChecked()) self.colormap.setVRange(None, None) - self.assertFalse(self.colormapDiag._minValue._numVal.isEnabled()) - self.assertFalse(self.colormapDiag._maxValue._numVal.isEnabled()) + self.qapp.processEvents() + self.assertTrue(self.colormapDiag._minValue._numVal.isReadOnly()) + self.assertTrue(self.colormapDiag._maxValue._numVal.isReadOnly()) self.assertTrue(self.colormapDiag._minValue.isAutoChecked()) self.assertTrue(self.colormapDiag._maxValue.isAutoChecked()) diff --git a/src/silx/gui/dialog/test/test_datafiledialog.py b/src/silx/gui/dialog/test/test_datafiledialog.py index 8411c67..32d75c2 100644 --- a/src/silx/gui/dialog/test/test_datafiledialog.py +++ b/src/silx/gui/dialog/test/test_datafiledialog.py @@ -1,7 +1,6 @@ -# coding: utf-8 # /*########################################################################## # -# Copyright (c) 2016 European Synchrotron Radiation Facility +# Copyright (c) 2016-2022 European Synchrotron Radiation Facility # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal @@ -89,7 +88,13 @@ def setUpModule(): def tearDownModule(): global _tmpDirectory - shutil.rmtree(_tmpDirectory) + for _ in range(10): + try: + shutil.rmtree(_tmpDirectory) + except PermissionError: # Might fail on appveyor + testutils.TestCaseQt.qWait(500) + else: + break _tmpDirectory = None @@ -491,7 +496,7 @@ class TestDataFileDialogInteraction(testutils.TestCaseQt, _UtilsMixin): for i in range(model.rowCount(rootIndex)): index = model.index(i, 0, rootIndex) flags = model.flags(index) - isEnabled = (int(flags) & qt.Qt.ItemIsEnabled) != 0 + isEnabled = flags & qt.Qt.ItemIsEnabled == qt.Qt.ItemIsEnabled if isEnabled: selectable += 1 return selectable diff --git a/src/silx/gui/dialog/test/test_imagefiledialog.py b/src/silx/gui/dialog/test/test_imagefiledialog.py index 9e204b9..79c12ed 100644 --- a/src/silx/gui/dialog/test/test_imagefiledialog.py +++ b/src/silx/gui/dialog/test/test_imagefiledialog.py @@ -1,7 +1,6 @@ -# coding: utf-8 # /*########################################################################## # -# Copyright (c) 2016 European Synchrotron Radiation Facility +# Copyright (c) 2016-2022 European Synchrotron Radiation Facility # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal @@ -96,7 +95,13 @@ def setUpModule(): def tearDownModule(): global _tmpDirectory - shutil.rmtree(_tmpDirectory) + for _ in range(10): + try: + shutil.rmtree(_tmpDirectory) + except PermissionError: # Might fail on appveyor + testutils.TestCaseQt.qWait(500) + else: + break _tmpDirectory = None @@ -516,7 +521,7 @@ class TestImageFileDialogInteraction(testutils.TestCaseQt, _UtilsMixin): for i in range(model.rowCount(rootIndex)): index = model.index(i, 0, rootIndex) flags = model.flags(index) - isEnabled = (int(flags) & qt.Qt.ItemIsEnabled) != 0 + isEnabled = flags & qt.Qt.ItemIsEnabled == qt.Qt.ItemIsEnabled if isEnabled: selectable += 1 return selectable diff --git a/src/silx/gui/dialog/utils.py b/src/silx/gui/dialog/utils.py index 4c48930..e07cf9f 100644 --- a/src/silx/gui/dialog/utils.py +++ b/src/silx/gui/dialog/utils.py @@ -1,4 +1,3 @@ -# coding: utf-8 # /*########################################################################## # # Copyright (c) 2016-2021 European Synchrotron Radiation Facility diff --git a/src/silx/gui/fit/BackgroundWidget.py b/src/silx/gui/fit/BackgroundWidget.py index 7703ee1..9ab63e4 100644 --- a/src/silx/gui/fit/BackgroundWidget.py +++ b/src/silx/gui/fit/BackgroundWidget.py @@ -1,4 +1,3 @@ -# coding: utf-8 #/*########################################################################## # Copyright (C) 2004-2021 V.A. Sole, European Synchrotron Radiation Facility # diff --git a/src/silx/gui/fit/FitConfig.py b/src/silx/gui/fit/FitConfig.py index 48ebca2..09dbfaa 100644 --- a/src/silx/gui/fit/FitConfig.py +++ b/src/silx/gui/fit/FitConfig.py @@ -1,4 +1,3 @@ -# coding: utf-8 # /*########################################################################## # Copyright (C) 2004-2021 V.A. Sole, European Synchrotron Radiation Facility # diff --git a/src/silx/gui/fit/FitWidget.py b/src/silx/gui/fit/FitWidget.py index 52ecafe..88f95cf 100644 --- a/src/silx/gui/fit/FitWidget.py +++ b/src/silx/gui/fit/FitWidget.py @@ -1,4 +1,3 @@ -# coding: utf-8 # /*########################################################################## # # Copyright (c) 2004-2021 European Synchrotron Radiation Facility diff --git a/src/silx/gui/fit/FitWidgets.py b/src/silx/gui/fit/FitWidgets.py index 0fcc6b7..7bcf28c 100644 --- a/src/silx/gui/fit/FitWidgets.py +++ b/src/silx/gui/fit/FitWidgets.py @@ -1,4 +1,3 @@ -# coding: utf-8 # /*########################################################################## # Copyright (C) 2004-2021 European Synchrotron Radiation Facility # diff --git a/src/silx/gui/fit/Parameters.py b/src/silx/gui/fit/Parameters.py index daa72f3..e9601a8 100644 --- a/src/silx/gui/fit/Parameters.py +++ b/src/silx/gui/fit/Parameters.py @@ -1,4 +1,3 @@ -# coding: utf-8 # /*########################################################################## # Copyright (C) 2004-2021 European Synchrotron Radiation Facility # diff --git a/src/silx/gui/fit/__init__.py b/src/silx/gui/fit/__init__.py index e4fd3ab..478ea22 100644 --- a/src/silx/gui/fit/__init__.py +++ b/src/silx/gui/fit/__init__.py @@ -1,4 +1,3 @@ -# coding: utf-8 # /*########################################################################## # Copyright (C) 2016 European Synchrotron Radiation Facility # diff --git a/src/silx/gui/fit/setup.py b/src/silx/gui/fit/setup.py deleted file mode 100644 index 6672363..0000000 --- a/src/silx/gui/fit/setup.py +++ /dev/null @@ -1,43 +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__ = ["P. Knobel"] -__license__ = "MIT" -__date__ = "21/07/2016" - - -from numpy.distutils.misc_util import Configuration - - -def configuration(parent_package='', top_path=None): - config = Configuration('fit', parent_package, top_path) - config.add_subpackage('test') - - return config - - -if __name__ == "__main__": - from numpy.distutils.core import setup - - setup(configuration=configuration) diff --git a/src/silx/gui/fit/test/__init__.py b/src/silx/gui/fit/test/__init__.py index 71128fb..b03339f 100644 --- a/src/silx/gui/fit/test/__init__.py +++ b/src/silx/gui/fit/test/__init__.py @@ -1,4 +1,3 @@ -# coding: utf-8 # /*########################################################################## # # Copyright (c) 2016 European Synchrotron Radiation Facility diff --git a/src/silx/gui/fit/test/testBackgroundWidget.py b/src/silx/gui/fit/test/testBackgroundWidget.py index b8570f7..353d3d5 100644 --- a/src/silx/gui/fit/test/testBackgroundWidget.py +++ b/src/silx/gui/fit/test/testBackgroundWidget.py @@ -1,4 +1,3 @@ -# coding: utf-8 # /*########################################################################## # # Copyright (c) 2016 European Synchrotron Radiation Facility diff --git a/src/silx/gui/fit/test/testFitConfig.py b/src/silx/gui/fit/test/testFitConfig.py index 53da2dd..114ff62 100644 --- a/src/silx/gui/fit/test/testFitConfig.py +++ b/src/silx/gui/fit/test/testFitConfig.py @@ -1,4 +1,3 @@ -# coding: utf-8 # /*########################################################################## # # Copyright (c) 2016 European Synchrotron Radiation Facility diff --git a/src/silx/gui/fit/test/testFitWidget.py b/src/silx/gui/fit/test/testFitWidget.py index abe9d89..fe61268 100644 --- a/src/silx/gui/fit/test/testFitWidget.py +++ b/src/silx/gui/fit/test/testFitWidget.py @@ -1,4 +1,3 @@ -# coding: utf-8 # /*########################################################################## # # Copyright (c) 2016 European Synchrotron Radiation Facility diff --git a/src/silx/gui/hdf5/Hdf5Formatter.py b/src/silx/gui/hdf5/Hdf5Formatter.py index 6c3de41..4dbb0fc 100644 --- a/src/silx/gui/hdf5/Hdf5Formatter.py +++ b/src/silx/gui/hdf5/Hdf5Formatter.py @@ -1,4 +1,3 @@ -# coding: utf-8 # /*########################################################################## # # Copyright (c) 2017-2021 European Synchrotron Radiation Facility diff --git a/src/silx/gui/hdf5/Hdf5HeaderView.py b/src/silx/gui/hdf5/Hdf5HeaderView.py index 7255ce0..6d306e5 100644 --- a/src/silx/gui/hdf5/Hdf5HeaderView.py +++ b/src/silx/gui/hdf5/Hdf5HeaderView.py @@ -1,4 +1,3 @@ -# coding: utf-8 # /*########################################################################## # # Copyright (c) 2016-2021 European Synchrotron Radiation Facility diff --git a/src/silx/gui/hdf5/Hdf5Item.py b/src/silx/gui/hdf5/Hdf5Item.py index e07f835..8f20649 100755 --- a/src/silx/gui/hdf5/Hdf5Item.py +++ b/src/silx/gui/hdf5/Hdf5Item.py @@ -1,4 +1,3 @@ -# coding: utf-8 # /*########################################################################## # # Copyright (c) 2016-2019 European Synchrotron Radiation Facility @@ -31,6 +30,7 @@ __date__ = "17/01/2019" import logging import collections import enum +from typing import Optional from .. import qt from .. import icons @@ -62,10 +62,21 @@ class Hdf5Item(Hdf5Node): tree structure. """ - def __init__(self, text, obj, parent, key=None, h5Class=None, linkClass=None, populateAll=False): + def __init__( + self, + text: Optional[str], + obj, + parent, + key=None, + h5Class=None, + linkClass=None, + populateAll=False, + openedPath: Optional[str] = None, + ): """ - :param str text: text displayed + :param text: text displayed :param object obj: Pointer to a h5py-link object. See the `obj` attribute. + :param openedPath: The path with which the item was opened if any """ self.__obj = obj self.__key = key @@ -76,7 +87,7 @@ class Hdf5Item(Hdf5Node): self.__linkClass = linkClass self.__description = None self.__nx_class = None - Hdf5Node.__init__(self, parent, populateAll=populateAll) + Hdf5Node.__init__(self, parent, populateAll=populateAll, openedPath=openedPath) def _getCanonicalName(self): parent = self.parent @@ -209,7 +220,7 @@ class Hdf5Item(Hdf5Node): self.__isBroken = True else: self.__obj = obj - if not self.isGroupObj(): + if silx.io.utils.get_h5_class(obj) not in [silx.io.utils.H5Type.GROUP, silx.io.utils.H5Type.FILE]: try: # pre-fetch of the data if obj.shape is None: diff --git a/src/silx/gui/hdf5/Hdf5LoadingItem.py b/src/silx/gui/hdf5/Hdf5LoadingItem.py index f11d252..70d015c 100644 --- a/src/silx/gui/hdf5/Hdf5LoadingItem.py +++ b/src/silx/gui/hdf5/Hdf5LoadingItem.py @@ -1,4 +1,3 @@ -# coding: utf-8 # /*########################################################################## # # Copyright (c) 2016 European Synchrotron Radiation Facility @@ -27,6 +26,7 @@ __authors__ = ["V. Valls"] __license__ = "MIT" __date__ = "06/07/2018" +from typing import Optional from .. import qt from .Hdf5Node import Hdf5Node @@ -39,9 +39,15 @@ class Hdf5LoadingItem(Hdf5Node): At the end of the loading this item is replaced by the loaded one. """ - def __init__(self, text, parent, animatedIcon): + def __init__( + self, + text, + parent, + animatedIcon, + openedPath: Optional[str] = None, + ): """Constructor""" - Hdf5Node.__init__(self, parent) + Hdf5Node.__init__(self, parent, openedPath=openedPath) self.__text = text self.__animatedIcon = animatedIcon self.__animatedIcon.register(self) diff --git a/src/silx/gui/hdf5/Hdf5Node.py b/src/silx/gui/hdf5/Hdf5Node.py index be16535..0d58748 100644 --- a/src/silx/gui/hdf5/Hdf5Node.py +++ b/src/silx/gui/hdf5/Hdf5Node.py @@ -1,4 +1,3 @@ -# coding: utf-8 # /*########################################################################## # # Copyright (c) 2016 European Synchrotron Radiation Facility @@ -28,6 +27,7 @@ __license__ = "MIT" __date__ = "24/07/2018" import weakref +from typing import Optional class Hdf5Node(object): @@ -36,16 +36,24 @@ class Hdf5Node(object): It provides link to the childs and to the parents, and a link to an external object. """ - def __init__(self, parent=None, populateAll=False): + def __init__( + self, + parent=None, + populateAll=False, + openedPath: Optional[str]=None, + ): """ Constructor :param Hdf5Node parent: Parent of the node, if exists, else None :param bool populateAll: If true, populate all the tree node. Else everything is lazy loaded. + :param openedPath: + The url or filename the node was created from, None if not directly created """ self.__child = None self.__parent = None + self.__openedPath = openedPath if parent is not None: self.__parent = weakref.ref(parent) if populateAll: @@ -60,6 +68,11 @@ class Hdf5Node(object): return "%s/?" % (parent._getCanonicalName()) @property + def _openedPath(self) -> Optional[str]: + """url or filename the node was created from, None if not directly created""" + return self.__openedPath + + @property def parent(self): """Parent of the node, or None if the node is a root diff --git a/src/silx/gui/hdf5/Hdf5TreeModel.py b/src/silx/gui/hdf5/Hdf5TreeModel.py index a32f7cf..8ac800a 100644 --- a/src/silx/gui/hdf5/Hdf5TreeModel.py +++ b/src/silx/gui/hdf5/Hdf5TreeModel.py @@ -1,4 +1,3 @@ -# coding: utf-8 # /*########################################################################## # # Copyright (c) 2016-2021 European Synchrotron Radiation Facility @@ -30,6 +29,7 @@ __date__ = "12/03/2019" import os import logging +from typing import Optional import functools from .. import qt from .. import icons @@ -39,6 +39,9 @@ from .Hdf5LoadingItem import Hdf5LoadingItem from . import _utils from ... import io as silx_io +import h5py + + _logger = logging.getLogger(__name__) @@ -98,7 +101,13 @@ class LoadingItemRunnable(qt.QRunnable): :rtpye: Hdf5Node """ text = _createRootLabel(h5obj) - item = Hdf5Item(text=text, obj=h5obj, parent=oldItem.parent, populateAll=True) + item = Hdf5Item( + text=text, + obj=h5obj, + parent=oldItem.parent, + populateAll=True, + openedPath=oldItem._openedPath, + ) return item def run(self): @@ -554,10 +563,25 @@ class Hdf5TreeModel(qt.QAbstractItemModel): filename = node.obj.filename self.insertFileAsync(filename, index.row(), synchronizingNode=node) + @staticmethod + def __areH5pyObjectEqual(obj1, obj2): + """Compare commonh5/h5py object without comparing data""" + if isinstance(obj1, h5py.HLObject): # Priority to h5py __eq__ + return obj1 == obj2 + + # else compare commonh5 objects + if not isinstance(obj2, type(obj1)): + return False + def key(item): + if item.file is None: + return item.name + return item.file.filename, item.file.mode, item.name + return key(obj1) == key(obj2) + def h5pyObjectRow(self, h5pyObject): for row in range(self.__root.childCount()): item = self.__root.child(row) - if item.obj == h5pyObject: + if self.__areH5pyObjectEqual(item.obj, h5pyObject): return row return -1 @@ -572,7 +596,7 @@ class Hdf5TreeModel(qt.QAbstractItemModel): index = 0 while index < self.__root.childCount(): item = self.__root.child(index) - if item.obj == h5pyObject: + if self.__areH5pyObjectEqual(item.obj, h5pyObject): qindex = self.index(index, 0, qt.QModelIndex()) self.synchronizeIndex(qindex) index += 1 @@ -602,13 +626,19 @@ class Hdf5TreeModel(qt.QAbstractItemModel): index = 0 while index < self.__root.childCount(): item = self.__root.child(index) - if item.obj == h5pyObject: + if self.__areH5pyObjectEqual(item.obj, h5pyObject): qindex = self.index(index, 0, qt.QModelIndex()) self.removeIndex(qindex) else: index += 1 - def insertH5pyObject(self, h5pyObject, text=None, row=-1): + def insertH5pyObject( + self, + h5pyObject, + text: Optional[str] = None, + row: int = -1, + filename: Optional[str] = None, + ): """Append an HDF5 object from h5py to the tree. :param h5pyObject: File handle/descriptor for a :class:`h5py.File` @@ -618,7 +648,15 @@ class Hdf5TreeModel(qt.QAbstractItemModel): text = _createRootLabel(h5pyObject) if row == -1: row = self.__root.childCount() - self.insertNode(row, Hdf5Item(text=text, obj=h5pyObject, parent=self.__root)) + self.insertNode( + row, + Hdf5Item( + text=text, + obj=h5pyObject, + parent=self.__root, + openedPath=filename, + ) + ) def hasPendingOperations(self): return len(self.__runnerSet) > 0 @@ -630,7 +668,12 @@ class Hdf5TreeModel(qt.QAbstractItemModel): # create temporary item if synchronizingNode is None: text = os.path.basename(filename) - item = Hdf5LoadingItem(text=text, parent=self.__root, animatedIcon=self.__animatedIcon) + item = Hdf5LoadingItem( + text=text, + parent=self.__root, + animatedIcon=self.__animatedIcon, + openedPath=filename, + ) self.insertNode(row, item) else: item = synchronizingNode @@ -655,7 +698,7 @@ class Hdf5TreeModel(qt.QAbstractItemModel): if self.__ownFiles: self.__openedFiles.append(h5file) self.sigH5pyObjectLoaded.emit(h5file) - self.insertH5pyObject(h5file, row=row) + self.insertH5pyObject(h5file, row=row, filename=filename) except IOError: _logger.debug("File '%s' can't be read.", filename, exc_info=True) raise diff --git a/src/silx/gui/hdf5/Hdf5TreeView.py b/src/silx/gui/hdf5/Hdf5TreeView.py index b276618..da35d15 100644 --- a/src/silx/gui/hdf5/Hdf5TreeView.py +++ b/src/silx/gui/hdf5/Hdf5TreeView.py @@ -1,4 +1,3 @@ -# coding: utf-8 # /*########################################################################## # # Copyright (c) 2016-2021 European Synchrotron Radiation Facility diff --git a/src/silx/gui/hdf5/NexusSortFilterProxyModel.py b/src/silx/gui/hdf5/NexusSortFilterProxyModel.py index 9c3533f..1b80c3e 100644 --- a/src/silx/gui/hdf5/NexusSortFilterProxyModel.py +++ b/src/silx/gui/hdf5/NexusSortFilterProxyModel.py @@ -1,4 +1,3 @@ -# coding: utf-8 # /*########################################################################## # # Copyright (c) 2016-2018 European Synchrotron Radiation Facility diff --git a/src/silx/gui/hdf5/__init__.py b/src/silx/gui/hdf5/__init__.py index 1b5a602..2243484 100644 --- a/src/silx/gui/hdf5/__init__.py +++ b/src/silx/gui/hdf5/__init__.py @@ -1,4 +1,3 @@ -# coding: utf-8 # /*########################################################################## # # Copyright (c) 2016-2017 European Synchrotron Radiation Facility diff --git a/src/silx/gui/hdf5/_utils.py b/src/silx/gui/hdf5/_utils.py index 8f32252..1d1b4cb 100644 --- a/src/silx/gui/hdf5/_utils.py +++ b/src/silx/gui/hdf5/_utils.py @@ -1,4 +1,3 @@ -# coding: utf-8 # /*########################################################################## # # Copyright (c) 2016-2021 European Synchrotron Radiation Facility diff --git a/src/silx/gui/hdf5/setup.py b/src/silx/gui/hdf5/setup.py deleted file mode 100644 index 786a851..0000000 --- a/src/silx/gui/hdf5/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__ = "28/09/2016" - - -from numpy.distutils.misc_util import Configuration - - -def configuration(parent_package='', top_path=None): - config = Configuration('hdf5', parent_package, top_path) - config.add_subpackage('test') - return config - - -if __name__ == "__main__": - from numpy.distutils.core import setup - setup(configuration=configuration) diff --git a/src/silx/gui/hdf5/test/__init__.py b/src/silx/gui/hdf5/test/__init__.py index 71128fb..b03339f 100644 --- a/src/silx/gui/hdf5/test/__init__.py +++ b/src/silx/gui/hdf5/test/__init__.py @@ -1,4 +1,3 @@ -# coding: utf-8 # /*########################################################################## # # Copyright (c) 2016 European Synchrotron Radiation Facility diff --git a/src/silx/gui/hdf5/test/test_hdf5.py b/src/silx/gui/hdf5/test/test_hdf5.py index 9b1b88a..6e77e1d 100755 --- a/src/silx/gui/hdf5/test/test_hdf5.py +++ b/src/silx/gui/hdf5/test/test_hdf5.py @@ -1,4 +1,3 @@ -# coding: utf-8 # /*########################################################################## # # Copyright (c) 2016-2021 European Synchrotron Radiation Facility diff --git a/src/silx/gui/icons.py b/src/silx/gui/icons.py index 1493b92..b7a9000 100644 --- a/src/silx/gui/icons.py +++ b/src/silx/gui/icons.py @@ -1,4 +1,3 @@ -# coding: utf-8 # /*########################################################################## # # Copyright (c) 2016 European Synchrotron Radiation Facility diff --git a/src/silx/gui/plot/AlphaSlider.py b/src/silx/gui/plot/AlphaSlider.py index da55b1e..486ca6f 100644 --- a/src/silx/gui/plot/AlphaSlider.py +++ b/src/silx/gui/plot/AlphaSlider.py @@ -1,4 +1,3 @@ -# coding: utf-8 # /*########################################################################## # # Copyright (c) 2017-2021 European Synchrotron Radiation Facility diff --git a/src/silx/gui/plot/ColorBar.py b/src/silx/gui/plot/ColorBar.py index 8cafc06..247da07 100644 --- a/src/silx/gui/plot/ColorBar.py +++ b/src/silx/gui/plot/ColorBar.py @@ -1,4 +1,3 @@ -# coding: utf-8 # /*########################################################################## # # Copyright (c) 2016-2021 European Synchrotron Radiation Facility @@ -590,7 +589,7 @@ class _ColorScale(qt.QWidget): def mouseMoveEvent(self, event): tooltip = str(self.getValueFromRelativePosition( - self._getRelativePosition(event.y()))) + self._getRelativePosition(qt.getMouseEventPosition(event)[1]))) qt.QToolTip.showText(event.globalPos(), tooltip, self) super(_ColorScale, self).mouseMoveEvent(event) diff --git a/src/silx/gui/plot/Colormap.py b/src/silx/gui/plot/Colormap.py index 22fea7f..8eaee84 100644 --- a/src/silx/gui/plot/Colormap.py +++ b/src/silx/gui/plot/Colormap.py @@ -1,4 +1,3 @@ -# coding: utf-8 # /*########################################################################## # # Copyright (c) 2015-2018 European Synchrotron Radiation Facility diff --git a/src/silx/gui/plot/ColormapDialog.py b/src/silx/gui/plot/ColormapDialog.py index 7c66cb8..0c0df2c 100644 --- a/src/silx/gui/plot/ColormapDialog.py +++ b/src/silx/gui/plot/ColormapDialog.py @@ -1,4 +1,3 @@ -# coding: utf-8 # /*########################################################################## # # Copyright (c) 2004-2018 European Synchrotron Radiation Facility @@ -24,8 +23,6 @@ # ###########################################################################*/ """Deprecated module providing ColormapDialog.""" -from __future__ import absolute_import - __authors__ = ["T. Vincent", "H.Payno"] __license__ = "MIT" __date__ = "24/04/2018" diff --git a/src/silx/gui/plot/Colors.py b/src/silx/gui/plot/Colors.py index 277e104..34ee815 100644 --- a/src/silx/gui/plot/Colors.py +++ b/src/silx/gui/plot/Colors.py @@ -1,4 +1,3 @@ -# coding: utf-8 # /*########################################################################## # # Copyright (c) 2004-2017 European Synchrotron Radiation Facility @@ -24,8 +23,6 @@ # ###########################################################################*/ """Color conversion function, color dictionary and colormap tools.""" -from __future__ import absolute_import - __authors__ = ["V.A. Sole", "T. Vincent"] __license__ = "MIT" __date__ = "14/06/2018" diff --git a/src/silx/gui/plot/CompareImages.py b/src/silx/gui/plot/CompareImages.py index 857fc79..80e0db3 100644 --- a/src/silx/gui/plot/CompareImages.py +++ b/src/silx/gui/plot/CompareImages.py @@ -1,4 +1,3 @@ -# coding: utf-8 # /*########################################################################## # # Copyright (c) 2018-2021 European Synchrotron Radiation Facility diff --git a/src/silx/gui/plot/ComplexImageView.py b/src/silx/gui/plot/ComplexImageView.py index 4eee3b0..7febd19 100644 --- a/src/silx/gui/plot/ComplexImageView.py +++ b/src/silx/gui/plot/ComplexImageView.py @@ -1,4 +1,3 @@ -# coding: utf-8 # /*########################################################################## # # Copyright (c) 2017-2021 European Synchrotron Radiation Facility @@ -28,8 +27,6 @@ The :class:`ComplexImageView` widget is dedicated to visualize a single 2D datas of complex data. """ -from __future__ import absolute_import - __authors__ = ["Vincent Favre-Nicolin", "T. Vincent"] __license__ = "MIT" __date__ = "24/04/2018" diff --git a/src/silx/gui/plot/CurvesROIWidget.py b/src/silx/gui/plot/CurvesROIWidget.py index 132d398..f0cc7f3 100644 --- a/src/silx/gui/plot/CurvesROIWidget.py +++ b/src/silx/gui/plot/CurvesROIWidget.py @@ -1,7 +1,6 @@ -# coding: utf-8 # /*########################################################################## # -# Copyright (c) 2004-2021 European Synchrotron Radiation Facility +# Copyright (c) 2004-2022 European Synchrotron Radiation Facility # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal @@ -175,8 +174,8 @@ class CurvesROIWidget(qt.QWidget): self._isConnected = False # True if connected to plot signals self._isInit = False - # expose API - self.getROIListAndDict = self.roiTable.getROIListAndDict + def getROIListAndDict(self): + return self.roiTable.getROIListAndDict() def getPlotWidget(self): """Returns the associated PlotWidget or None @@ -1568,14 +1567,6 @@ class CurvesROIDockWidget(qt.QDockWidget): action.setIcon(icons.getQIcon('plot-roi')) return action - 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_() - qt.QDockWidget.showEvent(self, event) - @property def currentROI(self): return self.roiWidget.currentRoi diff --git a/src/silx/gui/plot/ImageStack.py b/src/silx/gui/plot/ImageStack.py index 1588a31..e2bed9d 100644 --- a/src/silx/gui/plot/ImageStack.py +++ b/src/silx/gui/plot/ImageStack.py @@ -1,4 +1,3 @@ -# coding: utf-8 # /*########################################################################## # # Copyright (c) 2020-2021 European Synchrotron Radiation Facility diff --git a/src/silx/gui/plot/ImageView.py b/src/silx/gui/plot/ImageView.py index f8b830a..a451b2d 100644 --- a/src/silx/gui/plot/ImageView.py +++ b/src/silx/gui/plot/ImageView.py @@ -1,4 +1,3 @@ -# coding: utf-8 # /*########################################################################## # # Copyright (c) 2015-2021 European Synchrotron Radiation Facility @@ -37,9 +36,6 @@ Basic usage of :class:`ImageView` is through the following methods: For an example of use, see `imageview.py` in :ref:`sample-code`. """ -from __future__ import division - - __authors__ = ["T. Vincent"] __license__ = "MIT" __date__ = "26/04/2018" diff --git a/src/silx/gui/plot/Interaction.py b/src/silx/gui/plot/Interaction.py index 6213889..053fbe5 100644 --- a/src/silx/gui/plot/Interaction.py +++ b/src/silx/gui/plot/Interaction.py @@ -1,4 +1,3 @@ -# coding: utf-8 # /*########################################################################## # # Copyright (c) 2014-2020 European Synchrotron Radiation Facility diff --git a/src/silx/gui/plot/ItemsSelectionDialog.py b/src/silx/gui/plot/ItemsSelectionDialog.py index c0504b0..c303c6b 100644 --- a/src/silx/gui/plot/ItemsSelectionDialog.py +++ b/src/silx/gui/plot/ItemsSelectionDialog.py @@ -1,4 +1,3 @@ -# coding: utf-8 # /*########################################################################## # # Copyright (c) 2017-2021 European Synchrotron Radiation Facility diff --git a/src/silx/gui/plot/LegendSelector.py b/src/silx/gui/plot/LegendSelector.py index d439387..4d8ebe9 100755 --- a/src/silx/gui/plot/LegendSelector.py +++ b/src/silx/gui/plot/LegendSelector.py @@ -1,7 +1,6 @@ -# coding: utf-8 # /*########################################################################## # -# Copyright (c) 2004-2021 European Synchrotron Radiation Facility +# Copyright (c) 2004-2022 European Synchrotron Radiation Facility # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal @@ -1030,10 +1029,3 @@ class LegendsDockWidget(qt.QDockWidget): self.plot.sigContentChanged.disconnect(self.updateLegends) self.plot.sigActiveCurveChanged.disconnect(self.updateLegends) self._isConnected = False - - 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_() diff --git a/src/silx/gui/plot/LimitsHistory.py b/src/silx/gui/plot/LimitsHistory.py index a323548..7215e37 100644 --- a/src/silx/gui/plot/LimitsHistory.py +++ b/src/silx/gui/plot/LimitsHistory.py @@ -1,4 +1,3 @@ -# coding: utf-8 # /*########################################################################## # # Copyright (c) 2017 European Synchrotron Radiation Facility diff --git a/src/silx/gui/plot/MaskToolsWidget.py b/src/silx/gui/plot/MaskToolsWidget.py index 522be48..46b532c 100644 --- a/src/silx/gui/plot/MaskToolsWidget.py +++ b/src/silx/gui/plot/MaskToolsWidget.py @@ -1,4 +1,3 @@ -# coding: utf-8 # /*########################################################################## # # Copyright (c) 2017-2021 European Synchrotron Radiation Facility @@ -30,7 +29,6 @@ This widget is meant to work with :class:`silx.gui.plot.PlotWidget`. - :class:`MaskToolsWidget`: GUI for :class:`Mask` - :class:`MaskToolsDockWidget`: DockWidget to integrate in :class:`PlotWindow` """ -from __future__ import division __authors__ = ["T. Vincent", "P. Knobel"] __license__ = "MIT" diff --git a/src/silx/gui/plot/PlotActions.py b/src/silx/gui/plot/PlotActions.py index dd16221..f32be3c 100644 --- a/src/silx/gui/plot/PlotActions.py +++ b/src/silx/gui/plot/PlotActions.py @@ -1,4 +1,3 @@ -# coding: utf-8 # /*########################################################################## # # Copyright (c) 2004-2017 European Synchrotron Radiation Facility diff --git a/src/silx/gui/plot/PlotEvents.py b/src/silx/gui/plot/PlotEvents.py index 83f253c..be875d7 100644 --- a/src/silx/gui/plot/PlotEvents.py +++ b/src/silx/gui/plot/PlotEvents.py @@ -1,4 +1,3 @@ -# coding: utf-8 # /*########################################################################## # # Copyright (c) 2004-2016 European Synchrotron Radiation Facility diff --git a/src/silx/gui/plot/PlotInteraction.py b/src/silx/gui/plot/PlotInteraction.py index 6ebe6b1..c4d64a5 100644 --- a/src/silx/gui/plot/PlotInteraction.py +++ b/src/silx/gui/plot/PlotInteraction.py @@ -1,4 +1,3 @@ -# coding: utf-8 # /*########################################################################## # # Copyright (c) 2014-2021 European Synchrotron Radiation Facility diff --git a/src/silx/gui/plot/PlotToolButtons.py b/src/silx/gui/plot/PlotToolButtons.py index 3970896..a810ce1 100644 --- a/src/silx/gui/plot/PlotToolButtons.py +++ b/src/silx/gui/plot/PlotToolButtons.py @@ -1,4 +1,3 @@ -# coding: utf-8 # /*########################################################################## # # Copyright (c) 2004-2020 European Synchrotron Radiation Facility diff --git a/src/silx/gui/plot/PlotTools.py b/src/silx/gui/plot/PlotTools.py index 5929473..35d0f48 100644 --- a/src/silx/gui/plot/PlotTools.py +++ b/src/silx/gui/plot/PlotTools.py @@ -1,4 +1,3 @@ -# coding: utf-8 # /*########################################################################## # # Copyright (c) 2016-2018 European Synchrotron Radiation Facility @@ -25,8 +24,6 @@ """Set of widgets to associate with a :class:'PlotWidget'. """ -from __future__ import absolute_import - __authors__ = ["T. Vincent"] __license__ = "MIT" __date__ = "01/03/2018" diff --git a/src/silx/gui/plot/PlotWidget.py b/src/silx/gui/plot/PlotWidget.py index 6cb5ef5..f07ef30 100755 --- a/src/silx/gui/plot/PlotWidget.py +++ b/src/silx/gui/plot/PlotWidget.py @@ -1,7 +1,6 @@ -# coding: utf-8 # /*########################################################################## # -# Copyright (c) 2004-2021 European Synchrotron Radiation Facility +# Copyright (c) 2004-2022 European Synchrotron Radiation Facility # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal @@ -26,9 +25,6 @@ The :class:`PlotWidget` implements the plot API initially provided in PyMca. """ -from __future__ import division - - __authors__ = ["V.A. Sole", "T. Vincent"] __license__ = "MIT" __date__ = "21/12/2018" @@ -42,6 +38,7 @@ from collections import OrderedDict, namedtuple from contextlib import contextmanager import datetime as dt import itertools +import numbers import typing import warnings @@ -902,10 +899,13 @@ class PlotWidget(qt.QMainWindow): 'image': (items.ImageBase,), 'scatter': (items.Scatter,), 'marker': (items.MarkerBase,), - 'item': (items.Shape, - items.BoundingRect, - items.XAxisExtent, - items.YAxisExtent), + 'item': ( + items.Line, + items.Shape, + items.BoundingRect, + items.XAxisExtent, + items.YAxisExtent, + ), 'histogram': (items.Histogram,), } """Mapping kind to item classes of this kind""" @@ -1130,8 +1130,8 @@ class PlotWidget(qt.QMainWindow): :type xerror: A float, or a numpy.ndarray of float32. If it is an array, it can either be a 1D array of same length as the data or a 2D array with 2 rows - of same length as the data: row 0 for positive errors, - row 1 for negative errors. + of same length as the data: row 0 for lower errors, + row 1 for upper errors. :param yerror: Values with the uncertainties on the y values :type yerror: A float, or a numpy.ndarray of float32. See xerror. :param int z: Layer on which to draw the curve (default: 1) @@ -1540,8 +1540,8 @@ class PlotWidget(qt.QMainWindow): :type xerror: A float, or a numpy.ndarray of float32. If it is an array, it can either be a 1D array of same length as the data or a 2D array with 2 rows - of same length as the data: row 0 for positive errors, - row 1 for negative errors. + of same length as the data: row 0 for lower errors, + row 1 for upper errors. :param yerror: Values with the uncertainties on the y values :type yerror: A float, or a numpy.ndarray of float32. See xerror. :param int z: Layer on which to draw the scatter (default: 1) @@ -3261,10 +3261,13 @@ class PlotWidget(qt.QMainWindow): def dataToPixel(self, x=None, y=None, axis="left", check=True): """Convert a position in data coordinates to a position in pixels. - :param float x: The X coordinate in data space. If None (default) - the middle position of the displayed data is used. - :param float y: The Y coordinate in data space. If None (default) - the middle position of the displayed data is used. + :param x: The X coordinate in data space. If None (default) + the middle position of the displayed data is used. + :type x: float or 1D numpy array of float + :param y: The Y coordinate in data space. If None (default) + the middle position of the displayed data is used. + :type y: float or 1D numpy array of float + :param str axis: The Y axis to use for the conversion ('left' or 'right'). :param bool check: True to return None if outside displayed area, @@ -3272,7 +3275,7 @@ class PlotWidget(qt.QMainWindow): :returns: The corresponding position in pixels or None if the data position is not in the displayed area and check is True. - :rtype: A tuple of 2 floats: (xPixel, yPixel) or None. + :rtype: A tuple of 2 floats or 2 arrays of float: (xPixel, yPixel) or None. """ assert axis in ("left", "right") @@ -3285,12 +3288,26 @@ class PlotWidget(qt.QMainWindow): if y is None: y = 0.5 * (ymax + ymin) + if isinstance(x, numbers.Real) != isinstance(y, numbers.Real): + raise ValueError("x and y must be of the same type") + if not isinstance(x, numbers.Real) and (x.shape != y.shape or x.ndim != 1): + raise ValueError("x and y must be 1D arrays of the same length") + if check: - if x > xmax or x < xmin: - return None + isOutside = numpy.logical_or( + numpy.logical_or(x > xmax, x < xmin), + numpy.logical_or(y > ymax, y < ymin) + ) - if y > ymax or y < ymin: - return None + if numpy.any(isOutside): + if isinstance(x, numbers.Real): + return None + else: # Filter-out points that are outside + x = numpy.array(x, copy=True, dtype=numpy.float64) + x[isOutside] = numpy.nan + + y = numpy.array(y, copy=True, dtype=numpy.float64) + y[isOutside] = numpy.nan return self._backend.dataToPixel(x, y, axis=axis) @@ -3318,7 +3335,10 @@ class PlotWidget(qt.QMainWindow): if check: left, top, width, height = self.getPlotBoundsInPixels() - if not (left <= x <= left + width and top <= y <= top + height): + isOutside = numpy.logical_or( + numpy.logical_or(x < left, x > left + width), + numpy.logical_or(y < top, y > top + height)) + if numpy.any(isOutside): return None return self._backend.pixelToData(x, y, axis) @@ -3603,7 +3623,7 @@ class PlotWidget(qt.QMainWindow): qapp = qt.QApplication.instance() event = qt.QMouseEvent( qt.QEvent.MouseMove, - self.getWidgetHandle().mapFromGlobal(qt.QCursor.pos()), + qt.QPointF(self.getWidgetHandle().mapFromGlobal(qt.QCursor.pos())), qt.Qt.NoButton, qapp.mouseButtons(), qapp.keyboardModifiers()) diff --git a/src/silx/gui/plot/PlotWindow.py b/src/silx/gui/plot/PlotWindow.py index 0349585..e8da174 100644 --- a/src/silx/gui/plot/PlotWindow.py +++ b/src/silx/gui/plot/PlotWindow.py @@ -1,7 +1,6 @@ -# coding: utf-8 # /*########################################################################## # -# Copyright (c) 2004-2021 European Synchrotron Radiation Facility +# Copyright (c) 2004-2022 European Synchrotron Radiation Facility # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal @@ -215,7 +214,6 @@ class PlotWindow(PlotWidget): # lazy loaded actions needed by the controlButton menu self._consoleAction = None - self._statsAction = None self._panWithArrowKeysAction = None self._crosshairAction = None @@ -401,14 +399,19 @@ class PlotWindow(PlotWidget): custom_banner=banner, parent=self) self.addTabbedDockWidget(self._consoleDockWidget) - # self._consoleDockWidget.setVisible(True) self._consoleDockWidget.toggleViewAction().toggled.connect( - self.getConsoleAction().setChecked) + self._consoleDockWidgetToggled) self._consoleDockWidget.setVisible(isChecked) - def _toggleStatsVisibility(self, isChecked=False): - self.getStatsWidget().parent().setVisible(isChecked) + def _consoleVisibilityTriggered(self, isChecked): + if isChecked and self.isVisible(): + self._consoleDockWidget.show() + self._consoleDockWidget.raise_() + + def _consoleDockWidgetToggled(self, isChecked): + if self.isVisible(): + self.getConsoleAction().setChecked(isChecked) def _createToolBar(self, title, parent): """Create a QToolBar from the QAction of the PlotWindow. @@ -522,6 +525,17 @@ class PlotWindow(PlotWidget): self._handleFirstDockWidgetShow) self.addTabbedDockWidget(dockWidget) + def _handleDockWidgetViewActionTriggered(self, checked): + if checked: + action = self.sender() + if action is None: + return + dockWidget = action.parent() + if dockWidget is None: + return + dockWidget.show() # Show needed here for raise to have an effect + dockWidget.raise_() + def getColorBarWidget(self): """Returns the embedded :class:`ColorBarWidget` widget. @@ -536,6 +550,8 @@ class PlotWindow(PlotWidget): if self._legendsDockWidget is None: self._legendsDockWidget = LegendsDockWidget(plot=self) self._legendsDockWidget.hide() + self._legendsDockWidget.toggleViewAction().triggered.connect( + self._handleDockWidgetViewActionTriggered) self._legendsDockWidget.visibilityChanged.connect( self._handleFirstDockWidgetShow) return self._legendsDockWidget @@ -547,6 +563,8 @@ class PlotWindow(PlotWidget): self._curvesROIDockWidget = CurvesROIDockWidget( plot=self, name='Regions Of Interest') self._curvesROIDockWidget.hide() + self._curvesROIDockWidget.toggleViewAction().triggered.connect( + self._handleDockWidgetViewActionTriggered) self._curvesROIDockWidget.visibilityChanged.connect( self._handleFirstDockWidgetShow) return self._curvesROIDockWidget @@ -568,6 +586,8 @@ class PlotWindow(PlotWidget): self._maskToolsDockWidget = MaskToolsDockWidget( plot=self, name='Mask') self._maskToolsDockWidget.hide() + self._maskToolsDockWidget.toggleViewAction().triggered.connect( + self._handleDockWidgetViewActionTriggered) self._maskToolsDockWidget.visibilityChanged.connect( self._handleFirstDockWidgetShow) return self._maskToolsDockWidget @@ -583,9 +603,9 @@ class PlotWindow(PlotWidget): self._statsDockWidget.layout().setContentsMargins(0, 0, 0, 0) statsWidget = BasicStatsWidget(parent=self, plot=self) self._statsDockWidget.setWidget(statsWidget) - statsWidget.sigVisibilityChanged.connect( - self.getStatsAction().setChecked) self._statsDockWidget.hide() + self._statsDockWidget.toggleViewAction().triggered.connect( + self._handleDockWidgetViewActionTriggered) self._statsDockWidget.visibilityChanged.connect( self._handleFirstDockWidgetShow) return self._statsDockWidget.widget() @@ -618,6 +638,8 @@ class PlotWindow(PlotWidget): self._consoleAction.setCheckable(True) if IPythonDockWidget is not None: self._consoleAction.toggled.connect(self._toggleConsoleVisibility) + self._consoleAction.triggered.connect(self._consoleVisibilityTriggered) + else: self._consoleAction.setEnabled(False) return self._consoleAction @@ -648,12 +670,7 @@ class PlotWindow(PlotWidget): return self._panWithArrowKeysAction def getStatsAction(self): - if self._statsAction is None: - self._statsAction = qt.QAction('Curves stats', self) - self._statsAction.setCheckable(True) - self._statsAction.setChecked(self.getStatsWidget().parent().isVisible()) - self._statsAction.toggled.connect(self._toggleStatsVisibility) - return self._statsAction + return self.getStatsWidget().parent().toggleViewAction() def getRoiAction(self): """QAction toggling curve ROI dock widget diff --git a/src/silx/gui/plot/PrintPreviewToolButton.py b/src/silx/gui/plot/PrintPreviewToolButton.py index 30967e4..9069ac3 100644 --- a/src/silx/gui/plot/PrintPreviewToolButton.py +++ b/src/silx/gui/plot/PrintPreviewToolButton.py @@ -1,4 +1,3 @@ -# coding: utf-8 # /*########################################################################## # # Copyright (c) 2017-2021 European Synchrotron Radiation Facility @@ -101,7 +100,6 @@ plots on the same page. The plots all instantiate a app.exec() """ -from __future__ import absolute_import import logging from io import StringIO diff --git a/src/silx/gui/plot/Profile.py b/src/silx/gui/plot/Profile.py index 7565155..bf793c8 100644 --- a/src/silx/gui/plot/Profile.py +++ b/src/silx/gui/plot/Profile.py @@ -1,4 +1,3 @@ -# coding: utf-8 # /*########################################################################## # # Copyright (c) 2004-2021 European Synchrotron Radiation Facility diff --git a/src/silx/gui/plot/ProfileMainWindow.py b/src/silx/gui/plot/ProfileMainWindow.py index ce56cfd..09a5b41 100644 --- a/src/silx/gui/plot/ProfileMainWindow.py +++ b/src/silx/gui/plot/ProfileMainWindow.py @@ -1,4 +1,3 @@ -# coding: utf-8 # /*########################################################################## # # Copyright (c) 2017-2020 European Synchrotron Radiation Facility diff --git a/src/silx/gui/plot/ROIStatsWidget.py b/src/silx/gui/plot/ROIStatsWidget.py index 32a1395..732c60f 100644 --- a/src/silx/gui/plot/ROIStatsWidget.py +++ b/src/silx/gui/plot/ROIStatsWidget.py @@ -1,4 +1,3 @@ -# coding: utf-8 # /*########################################################################## # # Copyright (c) 2016-2021 European Synchrotron Radiation Facility diff --git a/src/silx/gui/plot/ScatterMaskToolsWidget.py b/src/silx/gui/plot/ScatterMaskToolsWidget.py index c242dfc..3f8c28b 100644 --- a/src/silx/gui/plot/ScatterMaskToolsWidget.py +++ b/src/silx/gui/plot/ScatterMaskToolsWidget.py @@ -1,4 +1,3 @@ -# coding: utf-8 # /*########################################################################## # # Copyright (c) 2018-2021 European Synchrotron Radiation Facility @@ -31,8 +30,6 @@ This widget is meant to work with a modified :class:`silx.gui.plot.PlotWidget` - :class:`ScatterMaskToolsDockWidget`: DockWidget to integrate in :class:`PlotWindow` """ -from __future__ import division - __authors__ = ["P. Knobel"] __license__ = "MIT" __date__ = "15/02/2019" diff --git a/src/silx/gui/plot/ScatterView.py b/src/silx/gui/plot/ScatterView.py index d3fd2e0..abacbef 100644 --- a/src/silx/gui/plot/ScatterView.py +++ b/src/silx/gui/plot/ScatterView.py @@ -1,4 +1,3 @@ -# coding: utf-8 # /*########################################################################## # # Copyright (c) 2018-2020 European Synchrotron Radiation Facility diff --git a/src/silx/gui/plot/StackView.py b/src/silx/gui/plot/StackView.py index 56793d7..5101f87 100644 --- a/src/silx/gui/plot/StackView.py +++ b/src/silx/gui/plot/StackView.py @@ -1,7 +1,6 @@ -# coding: utf-8 # /*########################################################################## # -# Copyright (c) 2016-2021 European Synchrotron Radiation Facility +# Copyright (c) 2016-2022 European Synchrotron Radiation Facility # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal @@ -230,7 +229,8 @@ class StackView(qt.QMainWindow): if silx.config.DEFAULT_PLOT_IMAGE_Y_AXIS_ORIENTATION == 'downward': self._plot.getYAxis().setInverted(True) - self._addColorBarAction() + self._plot.getColorBarAction().setVisible(True) + self._plot.getColorBarWidget().setVisible(True) self._profileToolBar = Profile3DToolBar(parent=self._plot, stackview=self) @@ -283,15 +283,6 @@ class StackView(qt.QMainWindow): signal=self.getStack(copy=False, returnNumpyArray=True)[0], signal_name="image_stack") - def _addColorBarAction(self): - self._plot.getColorBarWidget().setVisible(True) - actions = self._plot.toolBar().actions() - for index, action in enumerate(actions): - if action is self._plot.getColormapAction(): - break - self._colorbarAction = actions_control.ColorBarAction(self._plot, self._plot) - self._plot.toolBar().insertAction(actions[index + 1], self._colorbarAction) - def _plotCallback(self, eventDict): """Callback for plot events. @@ -1056,7 +1047,7 @@ class StackView(qt.QMainWindow): :rtype: QAction """ - return self._colorbarAction + return self._plot.getColorBarAction() def remove(self, legend=None, kind=('curve', 'image', 'item', 'marker')): diff --git a/src/silx/gui/plot/StatsWidget.py b/src/silx/gui/plot/StatsWidget.py index 00f78d0..b23946f 100644 --- a/src/silx/gui/plot/StatsWidget.py +++ b/src/silx/gui/plot/StatsWidget.py @@ -1,4 +1,3 @@ -# coding: utf-8 # /*########################################################################## # # Copyright (c) 2017-2021 European Synchrotron Radiation Facility diff --git a/src/silx/gui/plot/_BaseMaskToolsWidget.py b/src/silx/gui/plot/_BaseMaskToolsWidget.py index 407ab11..dda75e1 100644 --- a/src/silx/gui/plot/_BaseMaskToolsWidget.py +++ b/src/silx/gui/plot/_BaseMaskToolsWidget.py @@ -1,7 +1,6 @@ -# coding: utf-8 # /*########################################################################## # -# Copyright (c) 2017-2020 European Synchrotron Radiation Facility +# Copyright (c) 2017-2022 European Synchrotron Radiation Facility # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal @@ -25,7 +24,6 @@ """This module is a collection of base classes used in modules :mod:`.MaskToolsWidget` (images) and :mod:`.ScatterMaskToolsWidget` """ -from __future__ import division __authors__ = ["T. Vincent", "P. Knobel"] __license__ = "MIT" @@ -417,7 +415,7 @@ class BaseMaskToolsWidget(qt.QWidget): self._lastPencilPos = None self._multipleMasks = 'exclusive' - self._maskFileDir = qt.QDir.home().absolutePath() + self._maskFileDir = qt.QDir.current().absolutePath() self.plot.sigInteractiveModeChanged.connect( self._interactiveModeChanged) @@ -494,7 +492,7 @@ class BaseMaskToolsWidget(qt.QWidget): def maskFileDir(self): """The directory from which to load/save mask from/to files.""" if not os.path.isdir(self._maskFileDir): - self._maskFileDir = qt.QDir.home().absolutePath() + self._maskFileDir = qt.QDir.current().absolutePath() return self._maskFileDir @maskFileDir.setter @@ -632,7 +630,7 @@ class BaseMaskToolsWidget(qt.QWidget): invertAction.setText('Invert') icon = icons.getQIcon("mask-invert") invertAction.setIcon(icon) - invertAction.setShortcut(qt.Qt.CTRL + qt.Qt.Key_I) + invertAction.setShortcut(qt.QKeySequence(qt.Qt.CTRL | qt.Qt.Key_I)) invertAction.setToolTip('Invert current mask <b>%s</b>' % invertAction.shortcut().toString()) invertAction.triggered.connect(self._handleInvertMask) @@ -1273,10 +1271,3 @@ class BaseMaskToolsDockWidget(qt.QDockWidget): self.widget().setDirection(qt.QBoxLayout.LeftToRight) self.resize(self.widget().minimumSize()) self.adjustSize() - - 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_() diff --git a/src/silx/gui/plot/__init__.py b/src/silx/gui/plot/__init__.py index 3a141b3..129c4de 100644 --- a/src/silx/gui/plot/__init__.py +++ b/src/silx/gui/plot/__init__.py @@ -1,4 +1,3 @@ -# coding: utf-8 # /*########################################################################## # # Copyright (c) 2016-2018 European Synchrotron Radiation Facility diff --git a/src/silx/gui/plot/_utils/__init__.py b/src/silx/gui/plot/_utils/__init__.py index ed87b18..39fa7e4 100644 --- a/src/silx/gui/plot/_utils/__init__.py +++ b/src/silx/gui/plot/_utils/__init__.py @@ -1,4 +1,3 @@ -# coding: utf-8 # /*########################################################################## # # Copyright (c) 2004-2021 European Synchrotron Radiation Facility diff --git a/src/silx/gui/plot/_utils/delaunay.py b/src/silx/gui/plot/_utils/delaunay.py index 49ad05f..48b0db7 100644 --- a/src/silx/gui/plot/_utils/delaunay.py +++ b/src/silx/gui/plot/_utils/delaunay.py @@ -1,4 +1,3 @@ -# coding: utf-8 # /*########################################################################## # # Copyright (c) 2019 European Synchrotron Radiation Facility @@ -55,8 +54,7 @@ def delaunay(x, y): try: delaunay = _Delaunay(points) except (RuntimeError, ValueError): - _logger.error("Delaunay tesselation failed: %s", - sys.exc_info()[1]) + _logger.debug("Delaunay tesselation failed: %s", sys.exc_info()[1]) delaunay = None return delaunay diff --git a/src/silx/gui/plot/_utils/dtime_ticklayout.py b/src/silx/gui/plot/_utils/dtime_ticklayout.py index ebf775b..3c355d7 100644 --- a/src/silx/gui/plot/_utils/dtime_ticklayout.py +++ b/src/silx/gui/plot/_utils/dtime_ticklayout.py @@ -1,7 +1,6 @@ -# coding: utf-8 # /*########################################################################## # -# Copyright (c) 2014-2018 European Synchrotron Radiation Facility +# Copyright (c) 2014-2022 European Synchrotron Radiation Facility # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal @@ -24,8 +23,6 @@ # ###########################################################################*/ """This module implements date-time labels layout on graph axes.""" -from __future__ import absolute_import, division, unicode_literals - __authors__ = ["P. Kenter"] __license__ = "MIT" __date__ = "04/04/2018" @@ -212,6 +209,7 @@ def addValueToDate(dateTime, value, unit): :param float value: value to be added :param DtUnit unit: of the value :return: + :raises ValueError: unit is unsupported or result is out of datetime bounds """ #logger.debug("addValueToDate({}, {}, {})".format(dateTime, value, unit)) @@ -362,6 +360,9 @@ def findStartDate(dMin, dMax, nTicks): else: niceVal = math.floor(dVal / niceSpacing) * niceSpacing + if unit == DtUnit.YEARS and niceVal <= dt.MINYEAR: + niceVal = max(1, niceSpacing) + _logger.debug("StartValue: dVal = {}, niceVal: {} ({})" .format(dVal, niceVal, unit.name)) @@ -394,7 +395,10 @@ def dateRange(dMin, dMax, step, unit, includeFirstBeyond = False): dateTime = dMin while dateTime < dMax: yield dateTime - dateTime = addValueToDate(dateTime, step, unit) + try: + dateTime = addValueToDate(dateTime, step, unit) + except ValueError: + return # current dateTime is out of datetime bounds if includeFirstBeyond: yield dateTime @@ -420,12 +424,6 @@ def calcTicks(dMin, dMax, nTicks): includeFirstBeyond=True): result.append(d) - assert result[0] <= dMin, \ - "First nice date ({}) should be <= dMin {}".format(result[0], dMin) - - assert result[-1] >= dMax, \ - "Last nice date ({}) should be >= dMax {}".format(result[-1], dMax) - return result, niceSpacing, unit diff --git a/src/silx/gui/plot/_utils/panzoom.py b/src/silx/gui/plot/_utils/panzoom.py index 77efd10..8592ad0 100644 --- a/src/silx/gui/plot/_utils/panzoom.py +++ b/src/silx/gui/plot/_utils/panzoom.py @@ -1,4 +1,3 @@ -# coding: utf-8 # /*########################################################################## # # Copyright (c) 2004-2021 European Synchrotron Radiation Facility diff --git a/src/silx/gui/plot/_utils/setup.py b/src/silx/gui/plot/_utils/setup.py deleted file mode 100644 index 0271745..0000000 --- a/src/silx/gui/plot/_utils/setup.py +++ /dev/null @@ -1,42 +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__ = ["T. Vincent"] -__license__ = "MIT" -__date__ = "21/03/2017" - - -from numpy.distutils.misc_util import Configuration - - -def configuration(parent_package='', top_path=None): - config = Configuration('_utils', parent_package, top_path) - config.add_subpackage('test') - return config - - -if __name__ == "__main__": - from numpy.distutils.core import setup - - setup(configuration=configuration) diff --git a/src/silx/gui/plot/_utils/test/__init__.py b/src/silx/gui/plot/_utils/test/__init__.py index 3ad225d..78821ec 100644 --- a/src/silx/gui/plot/_utils/test/__init__.py +++ b/src/silx/gui/plot/_utils/test/__init__.py @@ -1,4 +1,3 @@ -# coding: utf-8 # /*########################################################################## # # Copyright (c) 2016-2018 European Synchrotron Radiation Facility diff --git a/src/silx/gui/plot/_utils/test/test_dtime_ticklayout.py b/src/silx/gui/plot/_utils/test/test_dtime_ticklayout.py index 8d35acf..87c0742 100644 --- a/src/silx/gui/plot/_utils/test/test_dtime_ticklayout.py +++ b/src/silx/gui/plot/_utils/test/test_dtime_ticklayout.py @@ -1,7 +1,6 @@ -# coding: utf-8 # /*########################################################################## # -# Copyright (c) 2015-2018 European Synchrotron Radiation Facility +# Copyright (c) 2015-2022 European Synchrotron Radiation Facility # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal @@ -23,57 +22,67 @@ # # ###########################################################################*/ -from __future__ import absolute_import, division, unicode_literals - __authors__ = ["P. Kenter"] __license__ = "MIT" __date__ = "06/04/2018" import datetime as dt -import unittest +import pytest + + +from silx.gui.plot._utils.dtime_ticklayout import calcTicks, DtUnit, SECONDS_PER_YEAR -from silx.gui.plot._utils.dtime_ticklayout import ( - calcTicks, DtUnit, SECONDS_PER_YEAR) +def testSmallMonthlySpacing(): + """Tests a range that did result in a spacing of less than 1 month. + It is impossible to add fractional month so the unit must be in days + """ + from dateutil import parser + d1 = parser.parse("2017-01-03 13:15:06.000044") + d2 = parser.parse("2017-03-08 09:16:16.307584") + _ticks, _units, spacing = calcTicks(d1, d2, nTicks=4) -class TestTickLayout(unittest.TestCase): - """Test ticks layout algorithms""" + assert spacing == DtUnit.DAYS - def testSmallMonthlySpacing(self): - """ Tests a range that did result in a spacing of less than 1 month. - It is impossible to add fractional month so the unit must be in days - """ - from dateutil import parser - d1 = parser.parse("2017-01-03 13:15:06.000044") - d2 = parser.parse("2017-03-08 09:16:16.307584") - _ticks, _units, spacing = calcTicks(d1, d2, nTicks=4) - self.assertEqual(spacing, DtUnit.DAYS) +def testNoCrash(): + """Creates many combinations of and number-of-ticks and end-dates; + tests that it doesn't give an exception and returns a reasonable number + of ticks. + """ + d1 = dt.datetime(2017, 1, 3, 13, 15, 6, 44) + value = 100e-6 # Start at 100 micro sec range. - def testNoCrash(self): - """ Creates many combinations of and number-of-ticks and end-dates; - tests that it doesn't give an exception and returns a reasonable number - of ticks. - """ - d1 = dt.datetime(2017, 1, 3, 13, 15, 6, 44) + while value <= 200 * SECONDS_PER_YEAR: - value = 100e-6 # Start at 100 micro sec range. + d2 = d1 + dt.timedelta(microseconds=value * 1e6) # end date range - while value <= 200 * SECONDS_PER_YEAR: + for numTicks in range(2, 12): + ticks, _, _ = calcTicks(d1, d2, numTicks) - d2 = d1 + dt.timedelta(microseconds=value*1e6) # end date range + margin = 2.5 + assert ( + numTicks / margin <= len(ticks) <= numTicks * margin + ), "Condition {} <= {} <= {} failed for # ticks={} and d2={}:".format( + numTicks / margin, len(ticks), numTicks * margin, numTicks, d2 + ) - for numTicks in range(2, 12): - ticks, _, _ = calcTicks(d1, d2, numTicks) + value = value * 1.5 # let date period grow exponentially - margin = 2.5 - self.assertTrue( - numTicks/margin <= len(ticks) <= numTicks*margin, - "Condition {} <= {} <= {} failed for # ticks={} and d2={}:" - .format(numTicks/margin, len(ticks), numTicks * margin, - numTicks, d2)) - value = value * 1.5 # let date period grow exponentially +@pytest.mark.parametrize( + "dMin, dMax", + [ + (dt.datetime(1, 1, 1), dt.datetime(400, 1, 1)), + (dt.datetime(4000, 1, 1), dt.datetime(9999, 1, 1)), + (dt.datetime(1, 1, 1), dt.datetime(9999, 12, 23)), + ], +) +def testCalcTicksOutOfBoundTicks(dMin, dMax): + """Test tick generation with values leading to out-of-bound ticks""" + ticks, _, unit = calcTicks(dMin, dMax, nTicks=5) + assert len(ticks) != 0 + assert unit == DtUnit.YEARS diff --git a/src/silx/gui/plot/_utils/test/test_ticklayout.py b/src/silx/gui/plot/_utils/test/test_ticklayout.py index 884b71b..8388c7e 100644 --- a/src/silx/gui/plot/_utils/test/test_ticklayout.py +++ b/src/silx/gui/plot/_utils/test/test_ticklayout.py @@ -1,4 +1,3 @@ -# coding: utf-8 # /*########################################################################## # # Copyright (c) 2015-2017 European Synchrotron Radiation Facility @@ -23,8 +22,6 @@ # # ###########################################################################*/ -from __future__ import absolute_import, division, unicode_literals - __authors__ = ["T. Vincent"] __license__ = "MIT" __date__ = "17/01/2018" diff --git a/src/silx/gui/plot/_utils/ticklayout.py b/src/silx/gui/plot/_utils/ticklayout.py index c9fd3e6..4266be0 100644 --- a/src/silx/gui/plot/_utils/ticklayout.py +++ b/src/silx/gui/plot/_utils/ticklayout.py @@ -1,4 +1,3 @@ -# coding: utf-8 # /*########################################################################## # # Copyright (c) 2014-2018 European Synchrotron Radiation Facility @@ -24,8 +23,6 @@ # ###########################################################################*/ """This module implements labels layout on graph axes.""" -from __future__ import absolute_import, division, unicode_literals - __authors__ = ["T. Vincent"] __license__ = "MIT" __date__ = "18/10/2016" diff --git a/src/silx/gui/plot/actions/PlotAction.py b/src/silx/gui/plot/actions/PlotAction.py index 2983775..de041dc 100644 --- a/src/silx/gui/plot/actions/PlotAction.py +++ b/src/silx/gui/plot/actions/PlotAction.py @@ -1,4 +1,3 @@ -# coding: utf-8 # /*########################################################################## # # Copyright (c) 2004-2017 European Synchrotron Radiation Facility @@ -27,9 +26,6 @@ The class :class:`.PlotAction` help the creation of a qt.QAction associated with a :class:`.PlotWidget`. """ -from __future__ import division - - __authors__ = ["V.A. Sole", "T. Vincent", "P. Knobel"] __license__ = "MIT" __date__ = "03/01/2018" diff --git a/src/silx/gui/plot/actions/PlotToolAction.py b/src/silx/gui/plot/actions/PlotToolAction.py index fbb0b0f..8c3b3c2 100644 --- a/src/silx/gui/plot/actions/PlotToolAction.py +++ b/src/silx/gui/plot/actions/PlotToolAction.py @@ -1,4 +1,3 @@ -# coding: utf-8 # /*########################################################################## # # Copyright (c) 2004-2020 European Synchrotron Radiation Facility @@ -27,9 +26,6 @@ The class :class:`.PlotToolAction` help the creation of a qt.QAction associating a tool window with a :class:`.PlotWidget`. """ -from __future__ import division - - __authors__ = ["V.A. Sole", "T. Vincent", "P. Knobel"] __license__ = "MIT" __date__ = "10/10/2018" diff --git a/src/silx/gui/plot/actions/__init__.py b/src/silx/gui/plot/actions/__init__.py index 930c728..3e606c6 100644 --- a/src/silx/gui/plot/actions/__init__.py +++ b/src/silx/gui/plot/actions/__init__.py @@ -1,4 +1,3 @@ -# coding: utf-8 # /*########################################################################## # # Copyright (c) 2017-2018 European Synchrotron Radiation Facility diff --git a/src/silx/gui/plot/actions/control.py b/src/silx/gui/plot/actions/control.py index 439985e..e75048a 100755 --- a/src/silx/gui/plot/actions/control.py +++ b/src/silx/gui/plot/actions/control.py @@ -1,4 +1,3 @@ -# coding: utf-8 # /*########################################################################## # # Copyright (c) 2004-2019 European Synchrotron Radiation Facility @@ -46,8 +45,6 @@ The following QAction are available: - :class:`ZoomOutAction` """ -from __future__ import division - __authors__ = ["V.A. Sole", "T. Vincent", "P. Knobel"] __license__ = "MIT" __date__ = "27/11/2020" diff --git a/src/silx/gui/plot/actions/fit.py b/src/silx/gui/plot/actions/fit.py index e130b24..3489f70 100644 --- a/src/silx/gui/plot/actions/fit.py +++ b/src/silx/gui/plot/actions/fit.py @@ -1,4 +1,3 @@ -# coding: utf-8 # /*########################################################################## # # Copyright (c) 2004-2021 European Synchrotron Radiation Facility @@ -32,8 +31,6 @@ The following QAction are available: .. autoclass:`.FitAction` """ -from __future__ import division - __authors__ = ["V.A. Sole", "T. Vincent", "P. Knobel"] __license__ = "MIT" __date__ = "10/10/2018" diff --git a/src/silx/gui/plot/actions/histogram.py b/src/silx/gui/plot/actions/histogram.py index be9f5a7..448dd55 100644 --- a/src/silx/gui/plot/actions/histogram.py +++ b/src/silx/gui/plot/actions/histogram.py @@ -1,4 +1,3 @@ -# coding: utf-8 # /*########################################################################## # # Copyright (c) 2004-2021 European Synchrotron Radiation Facility @@ -31,8 +30,6 @@ The following QAction are available: - :class:`PixelIntensitiesHistoAction` """ -from __future__ import division - __authors__ = ["V.A. Sole", "T. Vincent", "P. Knobel"] __date__ = "01/12/2020" __license__ = "MIT" @@ -64,7 +61,7 @@ class _ElidedLabel(ElidedLabel): def sizeHint(self): hint = super().sizeHint() - nbchar = max(len(self.getText()), 12) + nbchar = max(len(self.text()), 12) width = self.fontMetrics().boundingRect('#' * nbchar).width() return qt.QSize(max(hint.width(), width), hint.height()) diff --git a/src/silx/gui/plot/actions/io.py b/src/silx/gui/plot/actions/io.py index 7f4edd3..6cdd4d0 100644 --- a/src/silx/gui/plot/actions/io.py +++ b/src/silx/gui/plot/actions/io.py @@ -1,4 +1,3 @@ -# coding: utf-8 # /*########################################################################## # # Copyright (c) 2004-2021 European Synchrotron Radiation Facility @@ -33,8 +32,6 @@ The following QAction are available: - :class:`SaveAction` """ -from __future__ import division - __authors__ = ["V.A. Sole", "T. Vincent", "P. Knobel"] __license__ = "MIT" __date__ = "25/09/2020" diff --git a/src/silx/gui/plot/actions/medfilt.py b/src/silx/gui/plot/actions/medfilt.py index f86a377..25fcdb2 100644 --- a/src/silx/gui/plot/actions/medfilt.py +++ b/src/silx/gui/plot/actions/medfilt.py @@ -1,4 +1,3 @@ -# coding: utf-8 # /*########################################################################## # # Copyright (c) 2004-2020 European Synchrotron Radiation Facility @@ -34,8 +33,6 @@ The following QAction are available: """ -from __future__ import division - __authors__ = ["V.A. Sole", "T. Vincent", "P. Knobel"] __license__ = "MIT" diff --git a/src/silx/gui/plot/actions/mode.py b/src/silx/gui/plot/actions/mode.py index ee05256..7edc8bb 100644 --- a/src/silx/gui/plot/actions/mode.py +++ b/src/silx/gui/plot/actions/mode.py @@ -1,4 +1,3 @@ -# coding: utf-8 # /*########################################################################## # # Copyright (c) 2004-2018 European Synchrotron Radiation Facility @@ -32,8 +31,6 @@ The following QAction are available: - :class:`PanModeAction` """ -from __future__ import division - __authors__ = ["V. Valls"] __license__ = "MIT" __date__ = "16/08/2017" diff --git a/src/silx/gui/plot/backends/BackendBase.py b/src/silx/gui/plot/backends/BackendBase.py index 1e86807..d7653f3 100755 --- a/src/silx/gui/plot/backends/BackendBase.py +++ b/src/silx/gui/plot/backends/BackendBase.py @@ -1,4 +1,3 @@ -# coding: utf-8 # /*########################################################################## # # Copyright (c) 2004-2021 European Synchrotron Radiation Facility @@ -507,8 +506,10 @@ class BackendBase(object): """Convert a position in data space to a position in pixels in the widget. - :param float x: The X coordinate in data space. - :param float y: The Y coordinate in data space. + :param x: The X coordinate in data space. + :type x: float or sequence of float + :param y: The Y coordinate in data space. + :type y: float or sequence of float :param str axis: The Y axis to use for the conversion ('left' or 'right'). :returns: The corresponding position in pixels or diff --git a/src/silx/gui/plot/backends/BackendMatplotlib.py b/src/silx/gui/plot/backends/BackendMatplotlib.py index 7fe4ec0..8610ed1 100755 --- a/src/silx/gui/plot/backends/BackendMatplotlib.py +++ b/src/silx/gui/plot/backends/BackendMatplotlib.py @@ -1,7 +1,6 @@ -# coding: utf-8 # /*########################################################################## # -# Copyright (c) 2004-2021 European Synchrotron Radiation Facility +# Copyright (c) 2004-2022 European Synchrotron Radiation Facility # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal @@ -24,8 +23,6 @@ # ###########################################################################*/ """Matplotlib Plot backend.""" -from __future__ import division - __authors__ = ["V.A. Sole", "T. Vincent, H. Payno"] __license__ = "MIT" __date__ = "21/12/2018" @@ -33,7 +30,7 @@ __date__ = "21/12/2018" import logging import datetime as dt -from typing import Tuple +from typing import Tuple, Union import numpy from pkg_resources import parse_version as _parse_version @@ -64,6 +61,7 @@ from . import BackendBase from .. import items from .._utils import FLOAT32_MINPOS from .._utils.dtime_ticklayout import calcTicks, bestFormatString, timestamp +from ...qt import inspect as qt_inspect _PATCH_LINESTYLE = { "-": 'solid', @@ -166,8 +164,13 @@ class NiceDateLocator(Locator): vmin, vmax = vmax, vmin # vmin and vmax should be timestamps (i.e. seconds since 1 Jan 1970) - dtMin = dt.datetime.fromtimestamp(vmin, tz=self.tz) - dtMax = dt.datetime.fromtimestamp(vmax, tz=self.tz) + try: + dtMin = dt.datetime.fromtimestamp(vmin, tz=self.tz) + dtMax = dt.datetime.fromtimestamp(vmax, tz=self.tz) + except ValueError: + _logger.warning("Data range cannot be displayed with time axis") + return [] + dtTicks, self._spacing, self._unit = \ calcTicks(dtMin, dtMax, self.numTicks) @@ -1220,7 +1223,11 @@ class BackendMatplotlib(BackendBase.BackendBase): """Compatibility wrapper for devicePixelRatioF""" return 1. - def _mplToQtPosition(self, x: float, y: float) -> Tuple[float, float]: + def _mplToQtPosition( + self, + x: Union[float,numpy.ndarray], + y: Union[float,numpy.ndarray] + ) -> Tuple[Union[float,numpy.ndarray], Union[float,numpy.ndarray]]: """Convert matplotlib "display" space coord to Qt widget logical pixel """ ratio = self._getDevicePixelRatio() @@ -1238,7 +1245,8 @@ class BackendMatplotlib(BackendBase.BackendBase): def dataToPixel(self, x, y, axis): ax = self.ax2 if axis == "right" else self.ax - displayPos = ax.transData.transform_point((x, y)).transpose() + points = numpy.transpose((x, y)) + displayPos = ax.transData.transform(points).transpose() return self._mplToQtPosition(*displayPos) def pixelToData(self, x, y, axis): @@ -1325,7 +1333,7 @@ class BackendMatplotlib(BackendBase.BackendBase): self._synchronizeForegroundColors() -class BackendMatplotlibQt(FigureCanvasQTAgg, BackendMatplotlib): +class BackendMatplotlibQt(BackendMatplotlib, FigureCanvasQTAgg): """QWidget matplotlib backend using a QtAgg canvas. It adds fast overlay drawing and mouse event management. @@ -1516,6 +1524,10 @@ class BackendMatplotlibQt(FigureCanvasQTAgg, BackendMatplotlib): self._drawOverlays() def replot(self): + if not qt_inspect.isValid(self): + _logger.info("replot requested but widget no longer exists") + return + with self._plot._paintContext(): BackendMatplotlib._replot(self) diff --git a/src/silx/gui/plot/backends/BackendOpenGL.py b/src/silx/gui/plot/backends/BackendOpenGL.py index f1a12af..d7e8346 100755 --- a/src/silx/gui/plot/backends/BackendOpenGL.py +++ b/src/silx/gui/plot/backends/BackendOpenGL.py @@ -1,7 +1,6 @@ -# coding: utf-8 # /*########################################################################## # -# Copyright (c) 2014-2021 European Synchrotron Radiation Facility +# Copyright (c) 2014-2022 European Synchrotron Radiation Facility # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal @@ -24,8 +23,6 @@ # ############################################################################*/ """OpenGL Plot backend.""" -from __future__ import division - __authors__ = ["T. Vincent"] __license__ = "MIT" __date__ = "21/12/2018" @@ -196,7 +193,7 @@ class BackendOpenGL(BackendBase.BackendBase, glu.OpenGLWidget): So, the caller should not modify these arrays afterwards. """ - def __init__(self, plot, parent=None, f=qt.Qt.WindowFlags()): + def __init__(self, plot, parent=None, f=qt.Qt.Widget): glu.OpenGLWidget.__init__(self, parent, alphaBufferSize=8, depthBufferSize=0, @@ -205,6 +202,8 @@ class BackendOpenGL(BackendBase.BackendBase, glu.OpenGLWidget): f=f) BackendBase.BackendBase.__init__(self, plot, parent) + self.__isOpenGLValid = False + self._backgroundColor = 1., 1., 1., 1. self._dataBackgroundColor = 1., 1., 1., 1. @@ -236,7 +235,11 @@ class BackendOpenGL(BackendBase.BackendBase, glu.OpenGLWidget): # QWidget - _MOUSE_BTNS = {1: 'left', 2: 'right', 4: 'middle'} + _MOUSE_BTNS = { + qt.Qt.LeftButton: 'left', + qt.Qt.RightButton: 'right', + qt.Qt.MiddleButton: 'middle', + } def sizeHint(self): return qt.QSize(8 * 80, 6 * 80) # Mimic MatplotlibBackend @@ -244,12 +247,12 @@ class BackendOpenGL(BackendBase.BackendBase, glu.OpenGLWidget): def mousePressEvent(self, event): if event.button() not in self._MOUSE_BTNS: return super(BackendOpenGL, self).mousePressEvent(event) - self._plot.onMousePress( - event.x(), event.y(), self._MOUSE_BTNS[event.button()]) + x, y = qt.getMouseEventPosition(event) + self._plot.onMousePress(x, y, self._MOUSE_BTNS[event.button()]) event.accept() def mouseMoveEvent(self, event): - qtPos = event.x(), event.y() + qtPos = qt.getMouseEventPosition(event) previousMousePosInPixels = self._mousePosInPixels if qtPos == self._mouseInPlotArea(*qtPos): @@ -270,17 +273,14 @@ class BackendOpenGL(BackendBase.BackendBase, glu.OpenGLWidget): def mouseReleaseEvent(self, event): if event.button() not in self._MOUSE_BTNS: return super(BackendOpenGL, self).mouseReleaseEvent(event) - self._plot.onMouseRelease( - event.x(), event.y(), self._MOUSE_BTNS[event.button()]) + x, y = qt.getMouseEventPosition(event) + self._plot.onMouseRelease(x, y, self._MOUSE_BTNS[event.button()]) event.accept() def wheelEvent(self, event): delta = event.angleDelta().y() angleInDegrees = delta / 8. - if qt.BINDING == "PySide6": - x, y = event.position().x(), event.position().y() - else: - x, y = event.x(), event.y() + x, y = qt.getMouseEventPosition(event) self._plot.onMouseWheel(x, y, angleInDegrees) event.accept() @@ -290,7 +290,9 @@ class BackendOpenGL(BackendBase.BackendBase, glu.OpenGLWidget): # OpenGLWidget API def initializeGL(self): - gl.testGL() + self.__isOpenGLValid = gl.testGL() + if not self.__isOpenGLValid: + return gl.glClearStencil(0) @@ -379,6 +381,9 @@ class BackendOpenGL(BackendBase.BackendBase, glu.OpenGLWidget): self._renderOverlayGL() def paintGL(self): + if not self.__isOpenGLValid: + return + plot = self._plotRef() if plot is None: return @@ -422,7 +427,11 @@ class BackendOpenGL(BackendBase.BackendBase, glu.OpenGLWidget): pixelOffset = 3 context = glutils.RenderContext( - isXLog=isXLog, isYLog=isYLog, dpi=self.getDotsPerInch()) + isXLog=isXLog, + isYLog=isYLog, + dpi=self.getDotsPerInch(), + plotFrame=self._plotFrame, + ) for plotItem in self.getItemsFromBackToFront( condition=lambda i: i.isVisible() and i.isOverlay() == overlay): @@ -526,7 +535,7 @@ class BackendOpenGL(BackendBase.BackendBase, glu.OpenGLWidget): color = item['color'] intensity = color[0] * 0.299 + color[1] * 0.587 + color[2] * 0.114 - bgColor = (1., 1., 1., 0.5) if intensity <= 0.5 else (0., 0., 0., 0.5) + bgColor = (1., 1., 1., 0.75) if intensity <= 0.5 else (0., 0., 0., 0.75) if xCoord is None or yCoord is None: if xCoord is None: # Horizontal line in data space pixelPos = self._plotFrame.dataToPixel( @@ -612,20 +621,15 @@ class BackendOpenGL(BackendBase.BackendBase, glu.OpenGLWidget): # For now simple implementation: using a curve for each marker # Should pack all markers to a single set of points - markerCurve = glutils.GLPlotCurve2D( - numpy.array((pixelPos[0],), dtype=numpy.float64), - numpy.array((pixelPos[1],), dtype=numpy.float64), + marker = glutils.Points2D( + (pixelPos[0],), + (pixelPos[1],), marker=item['symbol'], - markerColor=item['color'], - markerSize=11) - - context = glutils.RenderContext( - matrix=self.matScreenProj, - isXLog=False, - isYLog=False, - dpi=self.getDotsPerInch()) - markerCurve.render(context) - markerCurve.discard() + color=item['color'], + size=11, + ) + context.matrix = self.matScreenProj + marker.render(context) else: _logger.error('Unsupported item: %s', str(item)) diff --git a/src/silx/gui/plot/backends/__init__.py b/src/silx/gui/plot/backends/__init__.py index 966d9df..d75a943 100644 --- a/src/silx/gui/plot/backends/__init__.py +++ b/src/silx/gui/plot/backends/__init__.py @@ -1,4 +1,3 @@ -# coding: utf-8 # /*########################################################################## # # Copyright (c) 2017 European Synchrotron Radiation Facility diff --git a/src/silx/gui/plot/backends/glutils/GLPlotCurve.py b/src/silx/gui/plot/backends/glutils/GLPlotCurve.py index e4667b4..65bb6f0 100644 --- a/src/silx/gui/plot/backends/glutils/GLPlotCurve.py +++ b/src/silx/gui/plot/backends/glutils/GLPlotCurve.py @@ -1,7 +1,6 @@ -# coding: utf-8 # /*########################################################################## # -# Copyright (c) 2014-2021 European Synchrotron Radiation Facility +# Copyright (c) 2014-2022 European Synchrotron Radiation Facility # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal @@ -26,8 +25,6 @@ This module provides classes to render 2D lines and scatter plots """ -from __future__ import division - __authors__ = ["T. Vincent"] __license__ = "MIT" __date__ = "03/04/2017" @@ -303,7 +300,7 @@ class GLLines2D(object): #version 120 uniform mat4 matrix; - uniform vec2 halfViewportSize; + uniform float distanceScale; attribute float xPos; attribute float yPos; attribute vec4 color; @@ -314,11 +311,7 @@ class GLLines2D(object): void main(void) { gl_Position = matrix * vec4(xPos, yPos, 0., 1.); - //Estimate distance in pixels - vec2 probe = vec2(matrix * vec4(1., 1., 0., 0.)) * - halfViewportSize; - float pixelPerDataEstimate = length(probe)/sqrt(2.); - vDist = distance * pixelPerDataEstimate; + vDist = distance * distanceScale; vColor = color; } """, @@ -427,10 +420,6 @@ class GLLines2D(object): program = self._DASH_PROGRAM program.use() - x, y, viewWidth, viewHeight = gl.glGetFloatv(gl.GL_VIEWPORT) - gl.glUniform2f(program.uniforms['halfViewportSize'], - 0.5 * viewWidth, 0.5 * viewHeight) - dashPeriod = self.dashPeriod * width if self.style == DOTTED: dash = (0.2 * dashPeriod, @@ -457,6 +446,13 @@ class GLLines2D(object): dash2ndColor = self.dash2ndColor gl.glUniform4f(program.uniforms['dash2ndColor'], *dash2ndColor) + viewWidth = gl.glGetFloatv(gl.GL_VIEWPORT)[2] + xNDCPerData = ( + numpy.dot(context.matrix, [1., 0., 0., 1.])[0] - + numpy.dot(context.matrix, [0., 0., 0., 1.])[0]) + xPixelPerData = 0.5 * viewWidth * xNDCPerData + gl.glUniform1f(program.uniforms['distanceScale'], xPixelPerData) + distAttrib = program.attributes['distance'] gl.glEnableVertexAttribArray(distAttrib) if isinstance(self.distVboData, VertexBufferAttrib): @@ -515,11 +511,12 @@ class GLLines2D(object): gl.glDisable(gl.GL_LINE_SMOOTH) -def distancesFromArrays(xData, yData): +def distancesFromArrays(xData, yData, ratio: float=1.): """Returns distances between each points :param numpy.ndarray xData: X coordinate of points :param numpy.ndarray yData: Y coordinate of points + :param ratio: Y/X pixel per data resolution ratio :rtype: numpy.ndarray """ # Split array into sub-shapes at not finite points @@ -534,11 +531,11 @@ def distancesFromArrays(xData, yData): if begin == end: # Empty shape continue elif end - begin == 1: # Single element - distances.append([0]) + distances.append(numpy.array([0], dtype=numpy.float32)) else: deltas = numpy.dstack(( numpy.ediff1d(xData[begin:end], to_begin=numpy.float32(0.)), - numpy.ediff1d(yData[begin:end], to_begin=numpy.float32(0.))))[0] + numpy.ediff1d(yData[begin:end] * ratio, to_begin=numpy.float32(0.))))[0] distances.append( numpy.cumsum(numpy.sqrt(numpy.sum(deltas ** 2, axis=1)))) return numpy.concatenate(distances) @@ -561,7 +558,7 @@ CARET_UP = "caretup" CARET_DOWN = "caretdown" -class _Points2D(object): +class Points2D(object): """Object rendering curve markers :param xVboData: X coordinates VBO @@ -809,8 +806,18 @@ class _Points2D(object): self.size = size self.offset = offset + if (xVboData is not None and + not isinstance(xVboData, VertexBufferAttrib)): + xVboData = numpy.array(xVboData, copy=False, dtype=numpy.float32) self.xVboData = xVboData + + if (yVboData is not None and + not isinstance(yVboData, VertexBufferAttrib)): + yVboData = numpy.array(yVboData, copy=False, dtype=numpy.float32) self.yVboData = yVboData + + if colorVboData is not None: + assert isinstance(colorVboData, VertexBufferAttrib) self.colorVboData = colorVboData self.useColorVboData = colorVboData is not None @@ -894,18 +901,33 @@ class _Points2D(object): gl.glDisableVertexAttribArray(cAttrib) gl.glVertexAttrib4f(cAttrib, *self.color) - xAttrib = program.attributes['xPos'] - gl.glEnableVertexAttribArray(xAttrib) - self.xVboData.setVertexAttrib(xAttrib) + xPosAttrib = program.attributes['xPos'] + gl.glEnableVertexAttribArray(xPosAttrib) + if isinstance(self.xVboData, VertexBufferAttrib): + self.xVboData.setVertexAttrib(xPosAttrib) + else: + gl.glVertexAttribPointer(xPosAttrib, + 1, + gl.GL_FLOAT, + False, + 0, + self.xVboData) + + yPosAttrib = program.attributes['yPos'] + gl.glEnableVertexAttribArray(yPosAttrib) + if isinstance(self.yVboData, VertexBufferAttrib): + self.yVboData.setVertexAttrib(yPosAttrib) + else: + gl.glVertexAttribPointer(yPosAttrib, + 1, + gl.GL_FLOAT, + False, + 0, + self.yVboData) - yAttrib = program.attributes['yPos'] - gl.glEnableVertexAttribArray(yAttrib) - self.yVboData.setVertexAttrib(yAttrib) gl.glDrawArrays(gl.GL_POINTS, 0, self.xVboData.size) - gl.glUseProgram(0) - # error bars ################################################################## @@ -959,9 +981,9 @@ class _ErrorBars(object): self._lines = GLLines2D( None, None, color=color, drawMode=gl.GL_LINES, offset=offset) - self._xErrPoints = _Points2D( + self._xErrPoints = Points2D( None, None, color=color, marker=V_LINE, offset=offset) - self._yErrPoints = _Points2D( + self._yErrPoints = Points2D( None, None, color=color, marker=H_LINE, offset=offset) def _buildVertices(self): @@ -1117,6 +1139,7 @@ class GLPlotCurve2D(GLPlotItem): baseline=None, isYLog=False): super().__init__() + self._ratio = None self.colorData = colorData # Compute x bounds @@ -1192,7 +1215,7 @@ class GLPlotCurve2D(GLPlotItem): self.lines.dashPeriod = lineDashPeriod self.lines.offset = self.offset - self.points = _Points2D() + self.points = Points2D() self.points.marker = marker self.points.color = markerColor self.points.size = markerSize @@ -1228,14 +1251,14 @@ class GLPlotCurve2D(GLPlotItem): def init(cls): """OpenGL context initialization""" GLLines2D.init() - _Points2D.init() + Points2D.init() def prepare(self): """Rendering preparation: build indices and bounding box vertices""" if self.xVboData is None: xAttrib, yAttrib, cAttrib, dAttrib = None, None, None, None if self.lineStyle in (DASHED, DASHDOT, DOTTED): - dists = distancesFromArrays(self.xData, self.yData) + dists = distancesFromArrays(self.xData, self.yData, self._ratio) if self.colorData is None: xAttrib, yAttrib, dAttrib = vertexBuffer( (self.xData, self.yData, dists)) @@ -1262,6 +1285,17 @@ class GLPlotCurve2D(GLPlotItem): :param RenderContext context: Rendering information """ + if self.lineStyle in (DASHED, DASHDOT, DOTTED): + visibleRanges = context.plotFrame.transformedDataRanges + xLimits = visibleRanges.x + yLimits = visibleRanges.y if self.yaxis == 'left' else visibleRanges.y2 + width, height = context.plotFrame.plotSize + ratio = (height * (xLimits[1] - xLimits[0])) / (width * (yLimits[1] - yLimits[0])) + if self._ratio is None or abs(1. - ratio/self._ratio) > 0.05: # Tolerate 5% difference + # Rebuild curve buffers to update distances + self._ratio = ratio + self.discard() + self.prepare() if self.fill is not None: self.fill.render(context) diff --git a/src/silx/gui/plot/backends/glutils/GLPlotFrame.py b/src/silx/gui/plot/backends/glutils/GLPlotFrame.py index 1fccb02..e5fabf2 100644 --- a/src/silx/gui/plot/backends/glutils/GLPlotFrame.py +++ b/src/silx/gui/plot/backends/glutils/GLPlotFrame.py @@ -1,7 +1,6 @@ -# coding: utf-8 # /*########################################################################## # -# Copyright (c) 2014-2021 European Synchrotron Radiation Facility +# Copyright (c) 2014-2022 European Synchrotron Radiation Facility # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal @@ -39,6 +38,8 @@ import datetime as dt import math import weakref import logging +import numbers +from typing import Optional, Union from collections import namedtuple import numpy @@ -358,8 +359,12 @@ class PlotAxis(object): yield ((xPixel, yPixel), dataPos, text) else: # Time series - dtMin = dt.datetime.fromtimestamp(dataMin, tz=self.timeZone) - dtMax = dt.datetime.fromtimestamp(dataMax, tz=self.timeZone) + try: + dtMin = dt.datetime.fromtimestamp(dataMin, tz=self.timeZone) + dtMax = dt.datetime.fromtimestamp(dataMax, tz=self.timeZone) + except ValueError: + _logger.warning("Data range cannot be displayed with time axis") + return # Range is out of bound of the datetime tickDateTimes, spacing, unit = calcTicksAdaptive( dtMin, dtMax, nbPixels, tickDensity) @@ -984,6 +989,26 @@ class GLPlotFrame2D(GLPlotFrame): return self._transformedDataY2ProjMat + @staticmethod + def __applyLog( + data: Union[float, numpy.ndarray], + isLog: bool + ) -> Optional[Union[float, numpy.ndarray]]: + """Apply log to data filtering out """ + if not isLog: + return data + + if isinstance(data, numbers.Real): + return None if data < FLOAT32_MINPOS else math.log10(data) + + isBelowMin = data < FLOAT32_MINPOS + if numpy.any(isBelowMin): + data = numpy.array(data, copy=True, dtype=numpy.float64) + data[isBelowMin] = numpy.nan + + with numpy.errstate(divide='ignore'): + return numpy.log10(data) + def dataToPixel(self, x, y, axis='left'): """Convert data coordinate to widget pixel coordinate. """ @@ -991,19 +1016,13 @@ class GLPlotFrame2D(GLPlotFrame): trBounds = self.transformedDataRanges - if self.xAxis.isLog: - if x < FLOAT32_MINPOS: - return None - xDataTr = math.log10(x) - else: - xDataTr = x + xDataTr = self.__applyLog(x, self.xAxis.isLog) + if xDataTr is None: + return None - if self.yAxis.isLog: - if y < FLOAT32_MINPOS: - return None - yDataTr = math.log10(y) - else: - yDataTr = y + yDataTr = self.__applyLog(y, self.yAxis.isLog) + if yDataTr is None: + return None # Non-orthogonal axes if self.baseVectors != self.DEFAULT_BASE_VECTORS: @@ -1015,20 +1034,23 @@ class GLPlotFrame2D(GLPlotFrame): plotWidth, plotHeight = self.plotSize - xPixel = int(self.margins.left + - plotWidth * (xDataTr - trBounds.x[0]) / - (trBounds.x[1] - trBounds.x[0])) + xPixel = (self.margins.left + + plotWidth * (xDataTr - trBounds.x[0]) / + (trBounds.x[1] - trBounds.x[0])) usedAxis = trBounds.y if axis == "left" else trBounds.y2 yOffset = (plotHeight * (yDataTr - usedAxis[0]) / (usedAxis[1] - usedAxis[0])) if self.isYAxisInverted: - yPixel = int(self.margins.top + yOffset) + yPixel = self.margins.top + yOffset else: - yPixel = int(self.size[1] - self.margins.bottom - yOffset) + yPixel = self.size[1] - self.margins.bottom - yOffset - return xPixel, yPixel + return ( + int(xPixel) if isinstance(xPixel, numbers.Real) else xPixel.astype(numpy.int64), + int(yPixel) if isinstance(yPixel, numbers.Real) else yPixel.astype(numpy.int64), + ) def pixelToData(self, x, y, axis="left"): """Convert pixel position to data coordinates. diff --git a/src/silx/gui/plot/backends/glutils/GLPlotImage.py b/src/silx/gui/plot/backends/glutils/GLPlotImage.py index 3ad94b9..8353911 100644 --- a/src/silx/gui/plot/backends/glutils/GLPlotImage.py +++ b/src/silx/gui/plot/backends/glutils/GLPlotImage.py @@ -1,4 +1,3 @@ -# coding: utf-8 # /*########################################################################## # # Copyright (c) 2014-2021 European Synchrotron Radiation Facility @@ -159,6 +158,7 @@ class GLPlotColormap(_GLPlotData2D): } uniform sampler2D data; + uniform float data_scale; uniform sampler2D cmap_texture; uniform int cmap_normalization; uniform float cmap_parameter; @@ -174,35 +174,35 @@ class GLPlotColormap(_GLPlotData2D): const float oneOverLog10 = 0.43429448190325176; void main(void) { - float data = texture2D(data, textureCoords()).r; - float value = data; + float raw_data = texture2D(data, textureCoords()).r * data_scale; + float value = 0.; if (cmap_normalization == 1) { /*Logarithm mapping*/ - if (value > 0.) { + if (raw_data > 0.) { value = clamp(cmap_oneOverRange * - (oneOverLog10 * log(value) - cmap_min), + (oneOverLog10 * log(raw_data) - cmap_min), 0., 1.); } else { value = 0.; } } else if (cmap_normalization == 2) { /*Square root mapping*/ - if (value >= 0.) { - value = clamp(cmap_oneOverRange * (sqrt(value) - cmap_min), + if (raw_data >= 0.) { + value = clamp(cmap_oneOverRange * (sqrt(raw_data) - cmap_min), 0., 1.); } else { value = 0.; } } else if (cmap_normalization == 3) { /*Gamma correction mapping*/ value = pow( - clamp(cmap_oneOverRange * (value - cmap_min), 0., 1.), + clamp(cmap_oneOverRange * (raw_data - cmap_min), 0., 1.), cmap_parameter); } else if (cmap_normalization == 4) { /* arcsinh mapping */ /* asinh = log(x + sqrt(x*x + 1) for compatibility with GLSL 1.20 */ - value = clamp(cmap_oneOverRange * (log(value + sqrt(value*value + 1.0)) - cmap_min), 0., 1.); + value = clamp(cmap_oneOverRange * (log(raw_data + sqrt(raw_data*raw_data + 1.0)) - cmap_min), 0., 1.); } else { /*Linear mapping and fallback*/ - value = clamp(cmap_oneOverRange * (value - cmap_min), 0., 1.); + value = clamp(cmap_oneOverRange * (raw_data - cmap_min), 0., 1.); } - if (isnan(data)) { + if (isnan(raw_data)) { gl_FragColor = nancolor; } else { gl_FragColor = texture2D(cmap_texture, vec2(value, 0.5)); @@ -355,9 +355,10 @@ class GLPlotColormap(_GLPlotData2D): if self.data.dtype in (numpy.uint16, numpy.uint8): # Using unsigned int as normalized integer in OpenGL - # So normalize range - maxInt = float(numpy.iinfo(self.data.dtype).max) - dataMin, dataMax = dataMin / maxInt, dataMax / maxInt + # So revert normalization in the shader + dataScale = float(numpy.iinfo(self.data.dtype).max) + else: + dataScale = 1. if self.normalization == 'log': dataMin = math.log10(dataMin) @@ -378,6 +379,7 @@ class GLPlotColormap(_GLPlotData2D): else: # Linear and fallback normID = 0 + gl.glUniform1f(prog.uniforms['data_scale'], dataScale) gl.glUniform1i(prog.uniforms['cmap_texture'], self._cmap_texture.texUnit) gl.glUniform1i(prog.uniforms['cmap_normalization'], normID) diff --git a/src/silx/gui/plot/backends/glutils/GLPlotItem.py b/src/silx/gui/plot/backends/glutils/GLPlotItem.py index ae13091..58f5f41 100644 --- a/src/silx/gui/plot/backends/glutils/GLPlotItem.py +++ b/src/silx/gui/plot/backends/glutils/GLPlotItem.py @@ -1,7 +1,6 @@ -# coding: utf-8 # /*########################################################################## # -# Copyright (c) 2020-2021 European Synchrotron Radiation Facility +# Copyright (c) 2020-2022 European Synchrotron Radiation Facility # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal @@ -40,13 +39,14 @@ class RenderContext: :param float dpi: Number of device pixels per inch """ - def __init__(self, matrix=None, isXLog=False, isYLog=False, dpi=96.): + def __init__(self, matrix=None, isXLog=False, isYLog=False, dpi=96., plotFrame=None): self.matrix = matrix """Current transformation matrix""" self.__isXLog = isXLog self.__isYLog = isYLog self.__dpi = dpi + self.__plotFrame = plotFrame @property def isXLog(self): @@ -63,6 +63,11 @@ class RenderContext: """Number of device pixels per inch""" return self.__dpi + @property + def plotFrame(self): + """Current PlotFrame""" + return self.__plotFrame + class GLPlotItem: """Base class for primitives used in the PlotWidget OpenGL backend""" diff --git a/src/silx/gui/plot/backends/glutils/GLPlotTriangles.py b/src/silx/gui/plot/backends/glutils/GLPlotTriangles.py index fbe9e02..a67afd9 100644 --- a/src/silx/gui/plot/backends/glutils/GLPlotTriangles.py +++ b/src/silx/gui/plot/backends/glutils/GLPlotTriangles.py @@ -1,4 +1,3 @@ -# coding: utf-8 # /*########################################################################## # # Copyright (c) 2019-2021 European Synchrotron Radiation Facility diff --git a/src/silx/gui/plot/backends/glutils/GLSupport.py b/src/silx/gui/plot/backends/glutils/GLSupport.py index da6dffa..f5357e2 100644 --- a/src/silx/gui/plot/backends/glutils/GLSupport.py +++ b/src/silx/gui/plot/backends/glutils/GLSupport.py @@ -1,4 +1,3 @@ -# coding: utf-8 # /*########################################################################## # # Copyright (c) 2014-2018 European Synchrotron Radiation Facility diff --git a/src/silx/gui/plot/backends/glutils/GLText.py b/src/silx/gui/plot/backends/glutils/GLText.py index d6ae6fa..4862bff 100644 --- a/src/silx/gui/plot/backends/glutils/GLText.py +++ b/src/silx/gui/plot/backends/glutils/GLText.py @@ -1,4 +1,3 @@ -# coding: utf-8 # /*########################################################################## # # Copyright (c) 2014-2020 European Synchrotron Radiation Facility @@ -241,7 +240,7 @@ class Text2D(object): return vertices def render(self, matrix): - if not self.text: + if not self.text.strip(): return prog = self._program diff --git a/src/silx/gui/plot/backends/glutils/GLTexture.py b/src/silx/gui/plot/backends/glutils/GLTexture.py index 37fbdd0..caca111 100644 --- a/src/silx/gui/plot/backends/glutils/GLTexture.py +++ b/src/silx/gui/plot/backends/glutils/GLTexture.py @@ -1,4 +1,3 @@ -# coding: utf-8 # /*########################################################################## # # Copyright (c) 2014-2020 European Synchrotron Radiation Facility diff --git a/src/silx/gui/plot/backends/glutils/PlotImageFile.py b/src/silx/gui/plot/backends/glutils/PlotImageFile.py index 5fb6853..75ee50b 100644 --- a/src/silx/gui/plot/backends/glutils/PlotImageFile.py +++ b/src/silx/gui/plot/backends/glutils/PlotImageFile.py @@ -1,4 +1,3 @@ -# coding: utf-8 # /*########################################################################## # # Copyright (c) 2014-2020 European Synchrotron Radiation Facility diff --git a/src/silx/gui/plot/backends/glutils/__init__.py b/src/silx/gui/plot/backends/glutils/__init__.py index f87d7c1..bc15b78 100644 --- a/src/silx/gui/plot/backends/glutils/__init__.py +++ b/src/silx/gui/plot/backends/glutils/__init__.py @@ -1,4 +1,3 @@ -# coding: utf-8 # /*########################################################################## # # Copyright (c) 2014-2020 European Synchrotron Radiation Facility diff --git a/src/silx/gui/plot/items/__init__.py b/src/silx/gui/plot/items/__init__.py index 0fe29c2..6e26c64 100644 --- a/src/silx/gui/plot/items/__init__.py +++ b/src/silx/gui/plot/items/__init__.py @@ -1,7 +1,6 @@ -# coding: utf-8 # /*########################################################################## # -# Copyright (c) 2017-2021 European Synchrotron Radiation Facility +# Copyright (c) 2017-2022 European Synchrotron Radiation Facility # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal @@ -42,7 +41,7 @@ from .curve import Curve, CurveStyle # noqa from .histogram import Histogram # noqa from .image import ImageBase, ImageData, ImageDataBase, ImageRgba, ImageStack, MaskImageData # noqa from .image_aggregated import ImageDataAggregated # noqa -from .shape import Shape, BoundingRect, XAxisExtent, YAxisExtent # noqa +from .shape import Line, Shape, BoundingRect, XAxisExtent, YAxisExtent # noqa from .scatter import Scatter # noqa from .marker import MarkerBase, Marker, XMarker, YMarker # noqa from .axis import Axis, XAxis, YAxis, YRightAxis diff --git a/src/silx/gui/plot/items/_arc_roi.py b/src/silx/gui/plot/items/_arc_roi.py index 23416ec..40711b7 100644 --- a/src/silx/gui/plot/items/_arc_roi.py +++ b/src/silx/gui/plot/items/_arc_roi.py @@ -1,7 +1,6 @@ -# coding: utf-8 # /*########################################################################## # -# Copyright (c) 2018-2021 European Synchrotron Radiation Facility +# Copyright (c) 2018-2022 European Synchrotron Radiation Facility # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal @@ -635,7 +634,9 @@ class ArcROI(HandleBasedROI, items.LineMixIn, InteractionModeMixIn): innerRadius = geometry.radius - geometry.weight * 0.5 outerRadius = geometry.radius + geometry.weight * 0.5 - delta = 0.1 if geometry.endAngle >= geometry.startAngle else -0.1 + sign = numpy.sign(geometry.endAngle - geometry.startAngle) + delta = min(0.1, abs(geometry.startAngle - geometry.endAngle) / 100) * sign + if geometry.startAngle == geometry.endAngle: # Degenerated, it's a line (single radius) angle = geometry.startAngle @@ -654,7 +655,6 @@ class ArcROI(HandleBasedROI, items.LineMixIn, InteractionModeMixIn): points = [] points.append(geometry.center) points.append(geometry.startPoint) - delta = 0.1 if geometry.endAngle >= geometry.startAngle else -0.1 for angle in angles: direction = numpy.array([numpy.cos(angle), numpy.sin(angle)]) points.append(geometry.center + direction * outerRadius) diff --git a/src/silx/gui/plot/items/_band_roi.py b/src/silx/gui/plot/items/_band_roi.py new file mode 100644 index 0000000..a60a177 --- /dev/null +++ b/src/silx/gui/plot/items/_band_roi.py @@ -0,0 +1,370 @@ +# /*########################################################################## +# +# Copyright (c) 2022 European Synchrotron Radiation Facility +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# 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. +# +# ###########################################################################*/ +"""Rectangular ROI that can be rotated""" + +import functools +import logging +from typing import Iterable, List, NamedTuple, Optional, Sequence, Tuple +import numpy + +from ... import qt, utils +from .. import items +from ...colors import rgba +from silx.image.shapes import Polygon +from ....utils.proxy import docstring +from ._roi_base import _RegionOfInterestBase +from ._roi_base import HandleBasedROI +from ._roi_base import InteractionModeMixIn +from ._roi_base import RoiInteractionMode + + +logger = logging.getLogger(__name__) + + +class Point(NamedTuple): + x: float + y: float + + +class BandGeometry(NamedTuple): + begin: Point + end: Point + width: float + + @staticmethod + def create( + begin: Sequence[float] = (0.0, 0.0), + end: Sequence[float] = (0.0, 0.0), + width: Optional[float] = None, + ): + begin = Point(float(begin[0]), float(begin[1])) + end = Point(float(end[0]), float(end[1])) + if width is None: + width = 0.1 * numpy.linalg.norm(numpy.array(end) - begin) + return BandGeometry(begin, end, max(0.0, float(width))) + + @property + @functools.lru_cache() + def normal(self) -> Point: + vector = numpy.array(self.end) - self.begin + length = numpy.linalg.norm(vector) + if length == 0: + return Point(0.0, 0.0) + return Point(-vector[1] / length, vector[0] / length) + + @property + @functools.lru_cache() + def center(self) -> Point: + return Point(*(0.5 * (numpy.array(self.begin) + self.end))) + + @property + @functools.lru_cache() + def corners(self) -> Tuple[Point, Point, Point, Point]: + """Returns a 4-uple of (x,y) position in float""" + offset = 0.5 * self.width * numpy.array(self.normal) + return tuple( + map( + lambda p: Point(*p), + ( + self.begin - offset, + self.begin + offset, + self.end + offset, + self.end - offset, + ), + ) + ) + + @property + @functools.lru_cache() + def slope(self) -> float: + """Slope of the line (begin, end), infinity for a vertical line""" + if self.begin.x == self.end.x: + return float('inf') + return (self.end.y - self.begin.y) / (self.end.x - self.begin.x) + + @property + @functools.lru_cache() + def intercept(self) -> float: + """Intercept of the line (begin, end) or value of x for vertical line""" + if self.begin.x == self.end.x: + return self.begin.x + return self.begin.y - self.slope * self.begin.x + + @property + @functools.lru_cache() + def edgesIntercept(self) -> Tuple[float, float]: + """Intercepts of lines describing band edges""" + offset = 0.5 * self.width * numpy.array(self.normal) + if self.begin.x == self.end.x: + return self.begin.x - offset[0], self.begin.x + offset[0] + return ( + self.begin.y - offset[1] - self.slope * (self.begin.x - offset[0]), + self.begin.y + offset[1] - self.slope * (self.begin.x + offset[0]), + ) + + def contains(self, position: Sequence[float]) -> bool: + return Polygon(self.corners).is_inside(*position) + + +class BandROI(HandleBasedROI, items.LineMixIn, InteractionModeMixIn): + """A ROI identifying a line in a 2D plot. + + This ROI provides 1 anchor for each boundary of the line, plus an center + in the center to translate the full ROI. + """ + + ICON = "add-shape-rotated-rectangle" + NAME = "band ROI" + SHORT_NAME = "band" + """Metadata for this kind of ROI""" + + _plotShape = "line" + """Plot shape which is used for the first interaction""" + + BoundedMode = RoiInteractionMode("Bounded", "Band is bounded on both sides") + """Interaction mode for a rectangular band ROI""" + + UnboundedMode = RoiInteractionMode("Unbounded", "Band is unbounded on both sides") + """Interaction mode for unlimited band ROI """ + + def __init__(self, parent=None): + HandleBasedROI.__init__(self, parent=parent) + items.LineMixIn.__init__(self) + self.__availableInteractionModes = set((self.BoundedMode, self.UnboundedMode)) + InteractionModeMixIn.__init__(self) + + self.__handleBegin = self.addHandle() + self.__handleEnd = self.addHandle() + self.__handleCenter = self.addTranslateHandle() + self.__handleLabel = self.addLabelHandle() + self.__handleWidthUp = self.addHandle() + self.__handleWidthUp._setConstraint(self.__handleWidthUpConstraint) + self.__handleWidthUp.setSymbol("d") + self.__handleWidthDown = self.addHandle() + self.__handleWidthDown._setConstraint(self.__handleWidthDownConstraint) + self.__handleWidthDown.setSymbol("d") + + self.__geometry = BandGeometry.create() + + self.__lineUp = items.Line() + self.__lineUp.setVisible(False) + self.__lineMiddle = items.Line() + self.__lineMiddle.setLineWidth(1) + self.__lineMiddle.setVisible(False) + self.__lineDown = items.Line() + self.__lineDown.setVisible(False) + + self.__shape = items.Shape("polygon") + self.__shape.setPoints(self.__geometry.corners) + self.__shape.setFill(False) + + for item in (self.__lineUp, self.__lineMiddle, self.__lineDown, self.__shape): + item.setColor(rgba(self.getColor())) + item.setOverlay(True) + item.setLineStyle(self.getLineStyle()) + if item != self.__lineMiddle: + item.setLineWidth(self.getLineWidth()) + self.addItem(item) + + self._initInteractionMode(self.BoundedMode) + self._interactiveModeUpdated(self.BoundedMode) + + def availableInteractionModes(self) -> List[RoiInteractionMode]: + """Returns the list of available interaction modes""" + return list(self.__availableInteractionModes) + + def setAvailableInteractionModes(self, modes: Iterable[RoiInteractionMode]) -> None: + """Allows to restrict interaction modes of the ROI. + + :param modes: Subset of BandROI interaction modes: + :attr:`BoundedMode` and :attr:`UnboundedMode`. + """ + modes = set(modes) + if not modes <= set((self.BoundedMode, self.UnboundedMode)): + raise ValueError("Unsupported interaction modes") + self.__availableInteractionModes = set(modes) + if self.getInteractionMode() not in self.__availableInteractionModes: + self.setInteractionMode(self.availableInteractionModes()[0]) + + def _interactiveModeUpdated(self, modeId: RoiInteractionMode): + """Set the interaction mode.""" + if modeId is self.BoundedMode: + self.__lineDown.setVisible(False) + self.__lineMiddle.setVisible(False) + self.__lineUp.setVisible(False) + self.__shape.setVisible(True) + elif modeId is self.UnboundedMode: + self.__lineDown.setVisible(True) + self.__lineMiddle.setVisible(True) + self.__lineUp.setVisible(True) + self.__shape.setVisible(False) + else: + raise RuntimeError("Unsupported interactive mode") + + def _updated(self, event=None, checkVisibility=True): + if event == items.ItemChangedType.VISIBLE: + if self.isVisible(): + self._interactiveModeUpdated(self.getInteractionMode()) + else: + self.__lineDown.setVisible(False) + self.__lineMiddle.setVisible(False) + self.__lineUp.setVisible(False) + self.__shape.setVisible(False) + super()._updated(event, checkVisibility) + + def _updatedStyle(self, event, style): + super()._updatedStyle(event, style) + for item in (self.__lineUp, self.__lineMiddle, self.__lineDown, self.__shape): + item.setColor(style.getColor()) + item.setLineStyle(style.getLineStyle()) + if item != self.__lineMiddle: + item.setLineWidth(style.getLineWidth()) + + def setFirstShapePoints(self, points): + assert len(points) == 2 + self.setGeometry(*points) + + def _updateText(self, text): + self.__handleLabel.setText(text) + + def getGeometry(self) -> BandGeometry: + """Returns the geometric description of the ROI""" + return self.__geometry + + def setGeometry( + self, + begin: Sequence[float], + end: Sequence[float], + width: Optional[float] = None, + ): + """Set the geometry of the ROI + + :param begin: Starting point as (x, y) + :paran end: Closing point as (x, y) + :param width: Width of the ROI + """ + geometry = BandGeometry.create(begin, end, width) + if self.__geometry == geometry: + return + + self.__geometry = geometry + + with utils.blockSignals(self.__handleBegin): + self.__handleBegin.setPosition(*geometry.begin) + with utils.blockSignals(self.__handleEnd): + self.__handleEnd.setPosition(*geometry.end) + with utils.blockSignals(self.__handleCenter): + self.__handleCenter.setPosition(*geometry.center) + with utils.blockSignals(self.__handleLabel): + lowerCorner = geometry.corners[numpy.array(geometry.corners)[:, 1].argmin()] + self.__handleLabel.setPosition(*lowerCorner) + + delta = 0.5 * geometry.width * numpy.array(geometry.normal) + with utils.blockSignals(self.__handleWidthUp): + self.__handleWidthUp.setPosition(*(geometry.center + delta)) + with utils.blockSignals(self.__handleWidthDown): + self.__handleWidthDown.setPosition(*(geometry.center - delta)) + + self.__lineDown.setSlope(geometry.slope) + self.__lineDown.setIntercept(geometry.edgesIntercept[0]) + self.__lineMiddle.setSlope(geometry.slope) + self.__lineMiddle.setIntercept(geometry.intercept) + self.__lineUp.setSlope(geometry.slope) + self.__lineUp.setIntercept(geometry.edgesIntercept[1]) + self.__shape.setPoints(geometry.corners) + self.sigRegionChanged.emit() + + def __updateGeometry( + self, + begin: Optional[Sequence[float]] = None, + end: Optional[Sequence[float]] = None, + width: Optional[float] = None, + ): + geometry = self.getGeometry() + self.setGeometry( + geometry.begin if begin is None else begin, + geometry.end if end is None else end, + geometry.width if width is None else width, + ) + + @staticmethod + def __snap(point: Tuple[float, float], fixed: Tuple[float, float]) -> Tuple[float, float]: + """Snap point so that vector [point, fixed] snap to direction 0, 45 or 90 degrees + + :return: the snapped point position. + """ + vector = point[0] - fixed[0], point[1] - fixed[1] + angle = numpy.arctan2(vector[1], vector[0]) + snapAngle = numpy.pi/4 * numpy.round(angle / (numpy.pi/4)) + length = numpy.linalg.norm(vector) + return ( + fixed[0] + length * numpy.cos(snapAngle), + fixed[1] + length * numpy.sin(snapAngle) + ) + + def handleDragUpdated(self, handle, origin, previous, current): + geometry = self.getGeometry() + if handle is self.__handleBegin: + if qt.QApplication.keyboardModifiers() & qt.Qt.ShiftModifier: + self.__updateGeometry(begin=self.__snap(current, geometry.end)) + return + self.__updateGeometry(begin=current) + return + if handle is self.__handleEnd: + if qt.QApplication.keyboardModifiers() & qt.Qt.ShiftModifier: + self.__updateGeometry(end=self.__snap(current, geometry.begin)) + return + self.__updateGeometry(end=current) + return + if handle is self.__handleCenter: + delta = current - previous + self.__updateGeometry(geometry.begin + delta, geometry.end + delta) + return + if handle in (self.__handleWidthUp, self.__handleWidthDown): + offset = numpy.dot(geometry.normal, current - previous) + if handle is self.__handleWidthDown: + offset *= -1 + self.__updateGeometry( + geometry.begin, + geometry.end, + geometry.width + 2 * offset, + ) + + def __handleWidthUpConstraint(self, x: float, y: float) -> Tuple[float, float]: + geometry = self.getGeometry() + offset = max(0, numpy.dot(geometry.normal, numpy.array((x, y)) - geometry.center)) + return tuple(geometry.center + offset * numpy.array(geometry.normal)) + + def __handleWidthDownConstraint(self, x: float, y: float) -> Tuple[float, float]: + geometry = self.getGeometry() + offset = max(0, -numpy.dot(geometry.normal, numpy.array((x, y)) - geometry.center)) + return tuple(geometry.center - offset * numpy.array(geometry.normal)) + + @docstring(_RegionOfInterestBase) + def contains(self, position): + return self.getGeometry().contains(position) + + def __str__(self): + begin, end, width = self.getGeometry() + return f"{self.__class__.__name__}(begin=({begin[0]:g}, {begin[1]:g}), end=({end[0]:g}, {end[1]:g}), width={width:g})" diff --git a/src/silx/gui/plot/items/_pick.py b/src/silx/gui/plot/items/_pick.py index 8c8e781..631a30a 100644 --- a/src/silx/gui/plot/items/_pick.py +++ b/src/silx/gui/plot/items/_pick.py @@ -1,4 +1,3 @@ -# coding: utf-8 # /*########################################################################## # # Copyright (c) 2019-2020 European Synchrotron Radiation Facility diff --git a/src/silx/gui/plot/items/_roi_base.py b/src/silx/gui/plot/items/_roi_base.py index 3eb6cf4..765a538 100644 --- a/src/silx/gui/plot/items/_roi_base.py +++ b/src/silx/gui/plot/items/_roi_base.py @@ -1,4 +1,3 @@ -# coding: utf-8 # /*########################################################################## # # Copyright (c) 2018-2020 European Synchrotron Radiation Facility diff --git a/src/silx/gui/plot/items/axis.py b/src/silx/gui/plot/items/axis.py index c73323e..fa3f6d7 100644 --- a/src/silx/gui/plot/items/axis.py +++ b/src/silx/gui/plot/items/axis.py @@ -1,7 +1,6 @@ -# coding: utf-8 # /*########################################################################## # -# Copyright (c) 2017-2021 European Synchrotron Radiation Facility +# Copyright (c) 2017-2022 European Synchrotron Radiation Facility # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal @@ -480,26 +479,10 @@ class YRightAxis(Axis): """ Axis.__init__(self, plot) self.__mainAxis = mainAxis - - @property - def sigInvertedChanged(self): - """Signal emitted when axis orientation has changed""" - return self.__mainAxis.sigInvertedChanged - - @property - def sigScaleChanged(self): - """Signal emitted when axis scale has changed""" - return self.__mainAxis.sigScaleChanged - - @property - def _sigLogarithmicChanged(self): - """Signal emitted when axis scale has changed to or from logarithmic""" - return self.__mainAxis._sigLogarithmicChanged - - @property - def sigAutoScaleChanged(self): - """Signal emitted when axis autoscale has changed""" - return self.__mainAxis.sigAutoScaleChanged + self.__mainAxis.sigInvertedChanged.connect(self.sigInvertedChanged.emit) + self.__mainAxis.sigScaleChanged.connect(self.sigScaleChanged.emit) + self.__mainAxis._sigLogarithmicChanged.connect(self._sigLogarithmicChanged.emit) + self.__mainAxis.sigAutoScaleChanged.connect(self.sigAutoScaleChanged.emit) def _internalSetCurrentLabel(self, label): self._getBackend().setGraphYLabel(label, axis='right') diff --git a/src/silx/gui/plot/items/complex.py b/src/silx/gui/plot/items/complex.py index abb64ad..82d821f 100644 --- a/src/silx/gui/plot/items/complex.py +++ b/src/silx/gui/plot/items/complex.py @@ -1,4 +1,3 @@ -# coding: utf-8 # /*########################################################################## # # Copyright (c) 2017-2021 European Synchrotron Radiation Facility @@ -25,8 +24,6 @@ """This module provides the :class:`ImageComplexData` of the :class:`Plot`. """ -from __future__ import absolute_import - __authors__ = ["Vincent Favre-Nicolin", "T. Vincent"] __license__ = "MIT" __date__ = "14/06/2018" diff --git a/src/silx/gui/plot/items/core.py b/src/silx/gui/plot/items/core.py index fa3b8cf..074c168 100644 --- a/src/silx/gui/plot/items/core.py +++ b/src/silx/gui/plot/items/core.py @@ -1,7 +1,6 @@ -# coding: utf-8 # /*########################################################################## # -# Copyright (c) 2017-2021 European Synchrotron Radiation Facility +# Copyright (c) 2017-2022 European Synchrotron Radiation Facility # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal @@ -37,8 +36,7 @@ except ImportError: # Python2 support from copy import deepcopy import logging import enum -from typing import Optional, Tuple -import warnings +from typing import Optional, Tuple, Union import weakref import numpy @@ -244,6 +242,8 @@ class Item(qt.QObject): # When visibility has changed, always mark as dirty self._updated(ItemChangedType.VISIBLE, checkVisibility=False) + if visible: + self._visibleBoundsChanged() def isOverlay(self): """Return true if item is drawn as an overlay. @@ -492,7 +492,8 @@ class DataItem(Item): :param bool checkVisibility: """ if not checkVisibility or self.isVisible(): - self._visibleBoundsChanged() + if self.isVisible(): + self._visibleBoundsChanged() # TODO hackish data range implementation plot = self.getPlot() @@ -1479,6 +1480,31 @@ class PointsBase(DataItem, SymbolMixIn, AlphaMixIn): return x, y, xerror, yerror + @staticmethod + def __minMaxDataWithError( + data: numpy.ndarray, + error: Optional[Union[float, numpy.ndarray]], + positiveOnly: bool + ) -> Tuple[float]: + if error is None: + min_, max_ = min_max(data, finite=True) + return min_, max_ + + # float, 1D or 2D array + dataMinusError = data - numpy.atleast_2d(error)[0] + dataMinusError = dataMinusError[numpy.isfinite(dataMinusError)] + if positiveOnly: + dataMinusError = dataMinusError[dataMinusError > 0] + min_ = numpy.nan if dataMinusError.size == 0 else numpy.min(dataMinusError) + + dataPlusError = data + numpy.atleast_2d(error)[-1] + dataPlusError = dataPlusError[numpy.isfinite(dataPlusError)] + if positiveOnly: + dataPlusError = dataPlusError[dataPlusError > 0] + max_ = numpy.nan if dataPlusError.size == 0 else numpy.max(dataPlusError) + + return min_, max_ + def _getBounds(self): if self.getXData(copy=False).size == 0: # Empty data return None @@ -1491,7 +1517,6 @@ class PointsBase(DataItem, SymbolMixIn, AlphaMixIn): xPositive = False yPositive = False - # TODO bounds do not take error bars into account if (xPositive, yPositive) not in self._boundsCache: # use the getData class method because instance method can be # overloaded to return additional arrays @@ -1500,12 +1525,13 @@ class PointsBase(DataItem, SymbolMixIn, AlphaMixIn): # hack to avoid duplicating caching mechanism in Scatter # (happens when cached data is used, caching done using # Scatter._logFilterData) - x, y, _xerror, _yerror = data[0], data[1], data[3], data[4] + x, y, xerror, yerror = data[0], data[1], data[3], data[4] else: - x, y, _xerror, _yerror = data + x, y, xerror, yerror = data + + xmin, xmax = self.__minMaxDataWithError(x, xerror, xPositive) + ymin, ymax = self.__minMaxDataWithError(y, yerror, yPositive) - xmin, xmax = min_max(x, finite=True) - ymin, ymax = min_max(y, finite=True) self._boundsCache[(xPositive, yPositive)] = tuple([ (bound if bound is not None else numpy.nan) for bound in (xmin, xmax, ymin, ymax)]) @@ -1600,8 +1626,8 @@ class PointsBase(DataItem, SymbolMixIn, AlphaMixIn): :type xerror: A float, or a numpy.ndarray of float32. If it is an array, it can either be a 1D array of same length as the data or a 2D array with 2 rows - of same length as the data: row 0 for positive errors, - row 1 for negative errors. + of same length as the data: row 0 for lower errors, + row 1 for upper errors. :param yerror: Values with the uncertainties on the y values. :type yerror: A float, or a numpy.ndarray of float32. See xerror. :param bool copy: True make a copy of the data (default), diff --git a/src/silx/gui/plot/items/curve.py b/src/silx/gui/plot/items/curve.py index 7cbe26e..93e4719 100644 --- a/src/silx/gui/plot/items/curve.py +++ b/src/silx/gui/plot/items/curve.py @@ -1,4 +1,3 @@ -# coding: utf-8 # /*########################################################################## # # Copyright (c) 2017-2021 European Synchrotron Radiation Facility diff --git a/src/silx/gui/plot/items/histogram.py b/src/silx/gui/plot/items/histogram.py index 16bbefa..007f0c7 100644 --- a/src/silx/gui/plot/items/histogram.py +++ b/src/silx/gui/plot/items/histogram.py @@ -1,4 +1,3 @@ -# coding: utf-8 # /*########################################################################## # # Copyright (c) 2017-2021 European Synchrotron Radiation Facility diff --git a/src/silx/gui/plot/items/image.py b/src/silx/gui/plot/items/image.py index 5cc719b..eaee05a 100644 --- a/src/silx/gui/plot/items/image.py +++ b/src/silx/gui/plot/items/image.py @@ -1,4 +1,3 @@ -# coding: utf-8 # /*########################################################################## # # Copyright (c) 2017-2021 European Synchrotron Radiation Facility diff --git a/src/silx/gui/plot/items/image_aggregated.py b/src/silx/gui/plot/items/image_aggregated.py index 75fdd59..ffd41b2 100644 --- a/src/silx/gui/plot/items/image_aggregated.py +++ b/src/silx/gui/plot/items/image_aggregated.py @@ -1,4 +1,3 @@ -# coding: utf-8 # /*########################################################################## # # Copyright (c) 2021 European Synchrotron Radiation Facility diff --git a/src/silx/gui/plot/items/marker.py b/src/silx/gui/plot/items/marker.py index 50d070c..7596eb0 100755 --- a/src/silx/gui/plot/items/marker.py +++ b/src/silx/gui/plot/items/marker.py @@ -1,4 +1,3 @@ -# coding: utf-8 # /*########################################################################## # # Copyright (c) 2017-2020 European Synchrotron Radiation Facility diff --git a/src/silx/gui/plot/items/roi.py b/src/silx/gui/plot/items/roi.py index 38a1424..559e7e0 100644 --- a/src/silx/gui/plot/items/roi.py +++ b/src/silx/gui/plot/items/roi.py @@ -1,7 +1,6 @@ -# coding: utf-8 # /*########################################################################## # -# Copyright (c) 2018-2020 European Synchrotron Radiation Facility +# Copyright (c) 2018-2022 European Synchrotron Radiation Facility # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal @@ -50,6 +49,7 @@ from ._roi_base import _RegionOfInterestBase from ._roi_base import RegionOfInterest from ._roi_base import HandleBasedROI from ._arc_roi import ArcROI # noqa +from ._band_roi import BandROI # noqa from ._roi_base import InteractionModeMixIn # noqa from ._roi_base import RoiInteractionMode # noqa diff --git a/src/silx/gui/plot/items/scatter.py b/src/silx/gui/plot/items/scatter.py index fdc66f7..96fb311 100644 --- a/src/silx/gui/plot/items/scatter.py +++ b/src/silx/gui/plot/items/scatter.py @@ -1,7 +1,6 @@ -# coding: utf-8 # /*########################################################################## # -# Copyright (c) 2017-2021 European Synchrotron Radiation Facility +# Copyright (c) 2017-2022 European Synchrotron Radiation Facility # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal @@ -25,9 +24,6 @@ """This module provides the :class:`Scatter` item of the :class:`Plot`. """ -from __future__ import division - - __authors__ = ["T. Vincent", "P. Knobel"] __license__ = "MIT" __date__ = "29/03/2017" @@ -950,8 +946,8 @@ class Scatter(PointsBase, ColormapMixIn, ScatterVisualizationMixIn): :type xerror: A float, or a numpy.ndarray of float32. If it is an array, it can either be a 1D array of same length as the data or a 2D array with 2 rows - of same length as the data: row 0 for positive errors, - row 1 for negative errors. + of same length as the data: row 0 for lower errors, + row 1 for upper errors. :param yerror: Values with the uncertainties on the y values :type yerror: A float, or a numpy.ndarray of float32. See xerror. :param alpha: Values with the transparency (between 0 and 1) diff --git a/src/silx/gui/plot/items/shape.py b/src/silx/gui/plot/items/shape.py index 00ac5f5..dc35864 100644 --- a/src/silx/gui/plot/items/shape.py +++ b/src/silx/gui/plot/items/shape.py @@ -1,7 +1,6 @@ -# coding: utf-8 # /*########################################################################## # -# Copyright (c) 2017-2021 European Synchrotron Radiation Facility +# Copyright (c) 2017-2022 European Synchrotron Radiation Facility # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal @@ -35,17 +34,78 @@ import logging import numpy from ... import colors +from ..utils.intersections import lines_intersection from .core import ( Item, DataItem, - ColorMixIn, FillMixIn, ItemChangedType, LineMixIn, YAxisMixIn) + AlphaMixIn, ColorMixIn, FillMixIn, ItemChangedType, ItemMixInBase, LineMixIn, YAxisMixIn) _logger = logging.getLogger(__name__) +class _OverlayItem(Item): + """Item with settable overlay""" + + def __init__(self): + self.__overlay = False + Item.__init__(self) + + def isOverlay(self) -> bool: + """Return true if shape is drawn as an overlay""" + return self.__overlay + + def setOverlay(self, overlay: bool): + """Set the overlay state of the shape + + :param overlay: True to make it an overlay + """ + overlay = bool(overlay) + if overlay != self.__overlay: + self.__overlay = overlay + self._updated(ItemChangedType.OVERLAY) + + +class _TwoColorsLineMixIn(LineMixIn): + """Mix-in class for items with a background color for dashes""" + + def __init__(self): + LineMixIn.__init__(self) + self.__backgroundColor = None + + def getLineBgColor(self): + """Returns the RGBA background color of dash line + + :rtype: 4-tuple of float in [0, 1] or array of colors + """ + return self.__backgroundColor + + def setLineBgColor(self, color, copy: bool=True): + """Set dash line background color + + :param color: color(s) to be used + :type color: str ("#RRGGBB") or (npoints, 4) unsigned byte array or + one of the predefined color names defined in colors.py + :param copy: True (Default) to get a copy, + False to use internal representation (do not modify!) + """ + if color is not None: + if isinstance(color, str): + color = colors.rgba(color) + else: + color = numpy.array(color, copy=copy) + # TODO more checks + improve color array support + if color.ndim == 1: # Single RGBA color + color = colors.rgba(color) + else: # Array of colors + assert color.ndim == 2 + + self.__backgroundColor = color + self._updated(ItemChangedType.LINE_BG_COLOR) + + # TODO probably make one class for each kind of shape # TODO check fill:polygon/polyline + fill = duplicated -class Shape(Item, ColorMixIn, FillMixIn, LineMixIn): +class Shape(_OverlayItem, ColorMixIn, FillMixIn, _TwoColorsLineMixIn): """Description of a shape item :param str type_: The type of shape in: @@ -53,16 +113,13 @@ class Shape(Item, ColorMixIn, FillMixIn, LineMixIn): """ def __init__(self, type_): - Item.__init__(self) + _OverlayItem.__init__(self) ColorMixIn.__init__(self) FillMixIn.__init__(self) - LineMixIn.__init__(self) - self._overlay = False + _TwoColorsLineMixIn.__init__(self) assert type_ in ('hline', 'polygon', 'rectangle', 'vline', 'polylines') self._type = type_ self._points = () - self._lineBgColor = None - self._handle = None def _addBackendRenderer(self, backend): @@ -79,23 +136,6 @@ class Shape(Item, ColorMixIn, FillMixIn, LineMixIn): linewidth=self.getLineWidth(), linebgcolor=self.getLineBgColor()) - def isOverlay(self): - """Return true if shape is drawn as an overlay - - :rtype: bool - """ - return self._overlay - - def setOverlay(self, overlay): - """Set the overlay state of the shape - - :param bool overlay: True to make it an overlay - """ - overlay = bool(overlay) - if overlay != self._overlay: - self._overlay = overlay - self._updated(ItemChangedType.OVERLAY) - def getType(self): """Returns the type of shape to draw. @@ -126,34 +166,6 @@ class Shape(Item, ColorMixIn, FillMixIn, LineMixIn): self._points = numpy.array(points, copy=copy) self._updated(ItemChangedType.DATA) - def getLineBgColor(self): - """Returns the RGBA color of the item - :rtype: 4-tuple of float in [0, 1] or array of colors - """ - return self._lineBgColor - - def setLineBgColor(self, color, copy=True): - """Set item color - :param color: color(s) to be used - :type color: str ("#RRGGBB") or (npoints, 4) unsigned byte array or - one of the predefined color names defined in colors.py - :param bool copy: True (Default) to get a copy, - False to use internal representation (do not modify!) - """ - if color is not None: - if isinstance(color, str): - color = colors.rgba(color) - else: - color = numpy.array(color, copy=copy) - # TODO more checks + improve color array support - if color.ndim == 1: # Single RGBA color - color = colors.rgba(color) - else: # Array of colors - assert color.ndim == 2 - - self._lineBgColor = color - self._updated(ItemChangedType.LINE_BG_COLOR) - class BoundingRect(DataItem, YAxisMixIn): """An invisible shape which enforce the plot view to display the defined @@ -285,3 +297,108 @@ class YAxisExtent(_BaseExtent, YAxisMixIn): def __init__(self): _BaseExtent.__init__(self, axis='y') YAxisMixIn.__init__(self) + + +class Line(_OverlayItem, AlphaMixIn, ColorMixIn, _TwoColorsLineMixIn): + """Description of a infinite line item as y = slope * x + interecpt + + Warning: If slope is not finite, then the line is x = intercept. + """ + + def __init__(self, slope: float=0, intercept: float=0): + assert numpy.isfinite(intercept) + + _OverlayItem.__init__(self) + AlphaMixIn.__init__(self) + ColorMixIn.__init__(self) + _TwoColorsLineMixIn.__init__(self) + self.__slope = float(slope) + self.__intercept = float(intercept) + self.__coordinates = None + self._setVisibleBoundsTracking(True) + + def __updatePoints(self): + if not self.isVisible(): + return + + plot = self.getPlot() + if plot is None or not plot.isVisible(): + return + + xmin, xmax = plot.getXAxis().getLimits() + ymin, ymax = plot.getYAxis().getLimits() + + slope = self.getSlope() + intercept = self.getIntercept() + + if not numpy.isfinite(slope): + if not xmin <= intercept <= xmax: + coordinates = None + else: + coordinates = (intercept, intercept), (ymin, ymax) + else: + ycoords = slope * xmin + intercept, slope * xmax + intercept + + if min(ycoords) < ymax and max(ycoords) > ymin: + coordinates = (xmin, xmax), ycoords + else: + coordinates = None + + if coordinates != self.__coordinates: + self.__coordinates = coordinates + self._updated() + + def _visibleBoundsChanged(self, *args) -> None: + """Override method to benefit from bounds tracking""" + self.__updatePoints() + return super()._visibleBoundsChanged(*args) + + def setSlope(self, slope: float): + slope = float(slope) + if slope != self.__slope: + self.__slope = slope + self.__updatePoints() + self._updated(ItemChangedType.DATA) + + def getSlope(self) -> float: + return self.__slope + + def setIntercept(self, intercept: float): + intercept = float(intercept) + assert numpy.isfinite(intercept) + if intercept != self.__intercept: + self.__intercept = intercept + self.__updatePoints() + self._updated(ItemChangedType.DATA) + + def getIntercept(self) -> float: + return self.__intercept + + def setSlopeInterceptFromPoints(self, point0, point1): + """Set slope and intercept from 2 (x, y) points""" + x0, y0 = point0 + x1, y1 = point1 + if x0 == x1: # Special case: vertical line + self.setSlope(float("inf")) + self.setIntercept(x0) + return + + slope = (y1 - y0) / (x1 - x0) + self.setSlope(slope) + self.setIntercept(y0 - x0 * slope) + + def _addBackendRenderer(self, backend): + """Update backend renderer""" + if self.__coordinates is None: + return None + + return backend.addShape( + *self.__coordinates, + shape='polylines', + color=self.getColor(), + fill=False, + overlay=self.isOverlay(), + linestyle=self.getLineStyle(), + linewidth=self.getLineWidth(), + linebgcolor=self.getLineBgColor(), + ) diff --git a/src/silx/gui/plot/matplotlib/Colormap.py b/src/silx/gui/plot/matplotlib/Colormap.py index dc432b2..1131df8 100644 --- a/src/silx/gui/plot/matplotlib/Colormap.py +++ b/src/silx/gui/plot/matplotlib/Colormap.py @@ -1,4 +1,3 @@ -# coding: utf-8 # /*########################################################################## # Copyright (C) 2017-2020 European Synchrotron Radiation Facility # diff --git a/src/silx/gui/plot/matplotlib/__init__.py b/src/silx/gui/plot/matplotlib/__init__.py index e787240..155ffd4 100644 --- a/src/silx/gui/plot/matplotlib/__init__.py +++ b/src/silx/gui/plot/matplotlib/__init__.py @@ -1,4 +1,3 @@ -# coding: utf-8 # /*########################################################################## # # Copyright (c) 2020 European Synchrotron Radiation Facility diff --git a/src/silx/gui/plot/setup.py b/src/silx/gui/plot/setup.py deleted file mode 100644 index e0b2c91..0000000 --- a/src/silx/gui/plot/setup.py +++ /dev/null @@ -1,54 +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. -# -# ###########################################################################*/ -__authors__ = ["T. Vincent"] -__license__ = "MIT" -__date__ = "29/06/2017" - - -from numpy.distutils.misc_util import Configuration - - -def configuration(parent_package='', top_path=None): - config = Configuration('plot', parent_package, top_path) - config.add_subpackage('_utils') - config.add_subpackage('utils') - config.add_subpackage('matplotlib') - config.add_subpackage('stats') - config.add_subpackage('backends') - config.add_subpackage('backends.glutils') - config.add_subpackage('items') - config.add_subpackage('test') - config.add_subpackage('tools') - config.add_subpackage('tools.profile') - config.add_subpackage('tools.test') - config.add_subpackage('actions') - - return config - - -if __name__ == "__main__": - from numpy.distutils.core import setup - - setup(configuration=configuration) diff --git a/src/silx/gui/plot/stats/__init__.py b/src/silx/gui/plot/stats/__init__.py index 04a5327..dfaa865 100644 --- a/src/silx/gui/plot/stats/__init__.py +++ b/src/silx/gui/plot/stats/__init__.py @@ -1,4 +1,3 @@ -# coding: utf-8 # /*########################################################################## # # Copyright (c) 2017 European Synchrotron Radiation Facility diff --git a/src/silx/gui/plot/stats/stats.py b/src/silx/gui/plot/stats/stats.py index a81f7bb..d266d5c 100644 --- a/src/silx/gui/plot/stats/stats.py +++ b/src/silx/gui/plot/stats/stats.py @@ -1,7 +1,6 @@ -# coding: utf-8 # /*########################################################################## # -# Copyright (c) 2017-2021 European Synchrotron Radiation Facility +# Copyright (c) 2017-2022 European Synchrotron Radiation Facility # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal @@ -871,7 +870,7 @@ class StatCOM(StatBase): values = numpy.ma.array(context.values, mask=context.mask, dtype=numpy.float64) sum_ = numpy.sum(values) - if sum_ == 0.: + if sum_ == 0. or numpy.ma.is_masked(sum_): return (numpy.nan,) * len(context.axes) if context.isStructuredData(): diff --git a/src/silx/gui/plot/stats/statshandler.py b/src/silx/gui/plot/stats/statshandler.py index 17578d8..1531ba2 100644 --- a/src/silx/gui/plot/stats/statshandler.py +++ b/src/silx/gui/plot/stats/statshandler.py @@ -1,7 +1,6 @@ -# coding: utf-8 # /*########################################################################## # -# Copyright (c) 2017-2019 European Synchrotron Radiation Facility +# Copyright (c) 2017-2022 European Synchrotron Radiation Facility # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal @@ -32,6 +31,9 @@ __date__ = "05/06/2018" import logging +import numbers + +import numpy from silx.gui import qt from silx.gui.plot import stats as statsmdl @@ -72,11 +74,14 @@ class StatFormatter(object): self.tabWidgetItemClass = qItemClass def format(self, val): - if self.formatter is None or val is None: - return str(val) - else: + if val is None or numpy.ma.is_masked(val): + return "--" + + if self.formatter is not None and isinstance(val, numbers.Number): return self.formatter.format(val) + return str(val) + class StatsHandler(object): """ diff --git a/src/silx/gui/plot/test/__init__.py b/src/silx/gui/plot/test/__init__.py index 3ad225d..78821ec 100644 --- a/src/silx/gui/plot/test/__init__.py +++ b/src/silx/gui/plot/test/__init__.py @@ -1,4 +1,3 @@ -# coding: utf-8 # /*########################################################################## # # Copyright (c) 2016-2018 European Synchrotron Radiation Facility diff --git a/src/silx/gui/plot/test/testAlphaSlider.py b/src/silx/gui/plot/test/testAlphaSlider.py index ca57bf5..8641da7 100644 --- a/src/silx/gui/plot/test/testAlphaSlider.py +++ b/src/silx/gui/plot/test/testAlphaSlider.py @@ -1,4 +1,3 @@ -# coding: utf-8 # /*########################################################################## # # Copyright (c) 2017-2019 European Synchrotron Radiation Facility diff --git a/src/silx/gui/plot/test/testColorBar.py b/src/silx/gui/plot/test/testColorBar.py index 3dc8ff1..199726b 100644 --- a/src/silx/gui/plot/test/testColorBar.py +++ b/src/silx/gui/plot/test/testColorBar.py @@ -1,4 +1,3 @@ -# coding: utf-8 # /*########################################################################## # # Copyright (c) 2016-2021 European Synchrotron Radiation Facility diff --git a/src/silx/gui/plot/test/testCompareImages.py b/src/silx/gui/plot/test/testCompareImages.py index cf54b99..9b5065d 100644 --- a/src/silx/gui/plot/test/testCompareImages.py +++ b/src/silx/gui/plot/test/testCompareImages.py @@ -1,4 +1,3 @@ -# coding: utf-8 # /*########################################################################## # # Copyright (c) 2016-2017 European Synchrotron Radiation Facility diff --git a/src/silx/gui/plot/test/testComplexImageView.py b/src/silx/gui/plot/test/testComplexImageView.py index 46025b9..c26df25 100644 --- a/src/silx/gui/plot/test/testComplexImageView.py +++ b/src/silx/gui/plot/test/testComplexImageView.py @@ -1,4 +1,3 @@ -# coding: utf-8 # /*########################################################################## # # Copyright (c) 2017-2020 European Synchrotron Radiation Facility diff --git a/src/silx/gui/plot/test/testCurvesROIWidget.py b/src/silx/gui/plot/test/testCurvesROIWidget.py index d7dfafd..32ac057 100644 --- a/src/silx/gui/plot/test/testCurvesROIWidget.py +++ b/src/silx/gui/plot/test/testCurvesROIWidget.py @@ -1,4 +1,3 @@ -# coding: utf-8 # /*########################################################################## # # Copyright (c) 2016-2021 European Synchrotron Radiation Facility @@ -348,6 +347,8 @@ class TestRoiWidgetSignals(TestCaseQt): """Test Signals emitted by the RoiWidgetSignals""" def setUp(self): + super().setUp() + self.plot = Plot1D() x = range(20) y = range(20) @@ -368,8 +369,15 @@ class TestRoiWidgetSignals(TestCaseQt): self.qWaitForWindowExposed(self.curves_roi_widget) def tearDown(self): - self.plot = None - self.curves_roi_widget = None + self.plot.setAttribute(qt.Qt.WA_DeleteOnClose) + self.plot.close() + del self.plot + + self.curves_roi_widget.setAttribute(qt.Qt.WA_DeleteOnClose) + self.curves_roi_widget.close() + del self.curves_roi_widget + + super().tearDown() def testSigROISignalAddRmRois(self): """Test SigROISignal when adding and removing ROIS""" diff --git a/src/silx/gui/plot/test/testImageStack.py b/src/silx/gui/plot/test/testImageStack.py index 5c44691..702f0fe 100644 --- a/src/silx/gui/plot/test/testImageStack.py +++ b/src/silx/gui/plot/test/testImageStack.py @@ -1,4 +1,3 @@ -# coding: utf-8 # /*########################################################################## # # Copyright (c) 2020 European Synchrotron Radiation Facility diff --git a/src/silx/gui/plot/test/testImageView.py b/src/silx/gui/plot/test/testImageView.py index 7c1355f..9fb6a5d 100644 --- a/src/silx/gui/plot/test/testImageView.py +++ b/src/silx/gui/plot/test/testImageView.py @@ -1,4 +1,3 @@ -# coding: utf-8 # /*########################################################################## # # Copyright (c) 2017-2021 European Synchrotron Radiation Facility diff --git a/src/silx/gui/plot/test/testInteraction.py b/src/silx/gui/plot/test/testInteraction.py index d136b21..459b132 100644 --- a/src/silx/gui/plot/test/testInteraction.py +++ b/src/silx/gui/plot/test/testInteraction.py @@ -1,4 +1,3 @@ -# coding: utf-8 # /*########################################################################## # # Copyright (c) 2016-2020 European Synchrotron Radiation Facility diff --git a/src/silx/gui/plot/test/testItem.py b/src/silx/gui/plot/test/testItem.py index 0b15dc3..7b4f636 100644 --- a/src/silx/gui/plot/test/testItem.py +++ b/src/silx/gui/plot/test/testItem.py @@ -1,4 +1,3 @@ -# coding: utf-8 # /*########################################################################## # # Copyright (c) 2017-2021 European Synchrotron Radiation Facility diff --git a/src/silx/gui/plot/test/testLegendSelector.py b/src/silx/gui/plot/test/testLegendSelector.py index c40875d..3a596ac 100644 --- a/src/silx/gui/plot/test/testLegendSelector.py +++ b/src/silx/gui/plot/test/testLegendSelector.py @@ -1,4 +1,3 @@ -# coding: utf-8 # /*########################################################################## # # Copyright (c) 2004-2016 European Synchrotron Radiation Facility diff --git a/src/silx/gui/plot/test/testLimitConstraints.py b/src/silx/gui/plot/test/testLimitConstraints.py index 0bd8e50..04a53e1 100644 --- a/src/silx/gui/plot/test/testLimitConstraints.py +++ b/src/silx/gui/plot/test/testLimitConstraints.py @@ -1,4 +1,3 @@ -# coding: utf-8 # /*########################################################################## # # Copyright (c) 2016-2018 European Synchrotron Radiation Facility diff --git a/src/silx/gui/plot/test/testMaskToolsWidget.py b/src/silx/gui/plot/test/testMaskToolsWidget.py index 522ca51..5f36ec2 100644 --- a/src/silx/gui/plot/test/testMaskToolsWidget.py +++ b/src/silx/gui/plot/test/testMaskToolsWidget.py @@ -1,4 +1,3 @@ -# coding: utf-8 # /*########################################################################## # # Copyright (c) 2016-2017 European Synchrotron Radiation Facility diff --git a/src/silx/gui/plot/test/testPixelIntensityHistoAction.py b/src/silx/gui/plot/test/testPixelIntensityHistoAction.py index 14a467d..43d7588 100644 --- a/src/silx/gui/plot/test/testPixelIntensityHistoAction.py +++ b/src/silx/gui/plot/test/testPixelIntensityHistoAction.py @@ -1,4 +1,3 @@ -# coding: utf-8 # /*########################################################################## # # Copyright (c) 2016-2018 European Synchrotron Radiation Facility diff --git a/src/silx/gui/plot/test/testPlotActions.py b/src/silx/gui/plot/test/testPlotActions.py index f38e05b..4006ab9 100644 --- a/src/silx/gui/plot/test/testPlotActions.py +++ b/src/silx/gui/plot/test/testPlotActions.py @@ -1,4 +1,3 @@ -# coding: utf-8 # /*########################################################################## # # Copyright (c) 2016-2020 European Synchrotron Radiation Facility diff --git a/src/silx/gui/plot/test/testPlotInteraction.py b/src/silx/gui/plot/test/testPlotInteraction.py index fba364e..17aad97 100644 --- a/src/silx/gui/plot/test/testPlotInteraction.py +++ b/src/silx/gui/plot/test/testPlotInteraction.py @@ -1,4 +1,3 @@ -# coding: utf-8 # /*########################################################################## # # Copyright (c) 2016=2017 European Synchrotron Radiation Facility diff --git a/src/silx/gui/plot/test/testPlotWidget.py b/src/silx/gui/plot/test/testPlotWidget.py index f6e108d..19a34a9 100755 --- a/src/silx/gui/plot/test/testPlotWidget.py +++ b/src/silx/gui/plot/test/testPlotWidget.py @@ -1,7 +1,6 @@ -# coding: utf-8 # /*########################################################################## # -# Copyright (c) 2016-2021 European Synchrotron Radiation Facility +# Copyright (c) 2016-2022 European Synchrotron Radiation Facility # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal @@ -1651,6 +1650,26 @@ class TestPlotCurveLog(PlotWidgetTestCase, ParametricTestCase): self.qapp.processEvents() + if xError is None: + dataMin, dataMax = numpy.min(self.xData), numpy.max(self.xData) + else: + xMinusError = self.xData - numpy.atleast_2d(xError)[0] + dataMin = numpy.min(xMinusError[xMinusError > 0]) + xPlusError = self.xData + numpy.atleast_2d(xError)[-1] + dataMax = numpy.max(xPlusError[xPlusError > 0]) + plotMin, plotMax = self.plot.getXAxis().getLimits() + assert numpy.allclose((dataMin, dataMax), (plotMin, plotMax)) + + if yError is None: + dataMin, dataMax = numpy.min(self.yData), numpy.max(self.yData) + else: + yMinusError = self.yData - numpy.atleast_2d(yError)[0] + dataMin = numpy.min(yMinusError[yMinusError > 0]) + yPlusError = self.yData + numpy.atleast_2d(yError)[-1] + dataMax = numpy.max(yPlusError[yPlusError > 0]) + plotMin, plotMax = self.plot.getYAxis().getLimits() + assert numpy.allclose((dataMin, dataMax), (plotMin, plotMax)) + self.plot.clear() self.plot.resetZoom() self.qapp.processEvents() diff --git a/src/silx/gui/plot/test/testPlotWidgetNoBackend.py b/src/silx/gui/plot/test/testPlotWidgetNoBackend.py index 4914929..787d5a8 100644 --- a/src/silx/gui/plot/test/testPlotWidgetNoBackend.py +++ b/src/silx/gui/plot/test/testPlotWidgetNoBackend.py @@ -1,4 +1,3 @@ -# coding: utf-8 # /*########################################################################## # # Copyright (c) 2016-2020 European Synchrotron Radiation Facility diff --git a/src/silx/gui/plot/test/testPlotWindow.py b/src/silx/gui/plot/test/testPlotWindow.py index 9e1497f..8e3f1df 100644 --- a/src/silx/gui/plot/test/testPlotWindow.py +++ b/src/silx/gui/plot/test/testPlotWindow.py @@ -1,4 +1,3 @@ -# coding: utf-8 # /*########################################################################## # # Copyright (c) 2016-2020 European Synchrotron Radiation Facility diff --git a/src/silx/gui/plot/test/testRoiStatsWidget.py b/src/silx/gui/plot/test/testRoiStatsWidget.py index eb29267..2c1c6b3 100644 --- a/src/silx/gui/plot/test/testRoiStatsWidget.py +++ b/src/silx/gui/plot/test/testRoiStatsWidget.py @@ -1,4 +1,3 @@ -# coding: utf-8 # /*########################################################################## # # Copyright (c) 2016-2019 European Synchrotron Radiation Facility diff --git a/src/silx/gui/plot/test/testSaveAction.py b/src/silx/gui/plot/test/testSaveAction.py index 9280fb6..d5a06c6 100644 --- a/src/silx/gui/plot/test/testSaveAction.py +++ b/src/silx/gui/plot/test/testSaveAction.py @@ -1,4 +1,3 @@ -# coding: utf-8 # /*########################################################################## # # Copyright (c) 2017-2019 European Synchrotron Radiation Facility diff --git a/src/silx/gui/plot/test/testScatterMaskToolsWidget.py b/src/silx/gui/plot/test/testScatterMaskToolsWidget.py index 447ee58..68375b0 100644 --- a/src/silx/gui/plot/test/testScatterMaskToolsWidget.py +++ b/src/silx/gui/plot/test/testScatterMaskToolsWidget.py @@ -1,4 +1,3 @@ -# coding: utf-8 # /*########################################################################## # # Copyright (c) 2016-2017 European Synchrotron Radiation Facility diff --git a/src/silx/gui/plot/test/testScatterView.py b/src/silx/gui/plot/test/testScatterView.py index d11d4d8..692612d 100644 --- a/src/silx/gui/plot/test/testScatterView.py +++ b/src/silx/gui/plot/test/testScatterView.py @@ -1,4 +1,3 @@ -# coding: utf-8 # /*########################################################################## # # Copyright (c) 2018 European Synchrotron Radiation Facility diff --git a/src/silx/gui/plot/test/testStackView.py b/src/silx/gui/plot/test/testStackView.py index 0d18113..aba8678 100644 --- a/src/silx/gui/plot/test/testStackView.py +++ b/src/silx/gui/plot/test/testStackView.py @@ -1,4 +1,3 @@ -# coding: utf-8 # /*########################################################################## # # Copyright (c) 2016-2020 European Synchrotron Radiation Facility diff --git a/src/silx/gui/plot/test/testStats.py b/src/silx/gui/plot/test/testStats.py index 0a792a4..c5d5181 100644 --- a/src/silx/gui/plot/test/testStats.py +++ b/src/silx/gui/plot/test/testStats.py @@ -1,4 +1,3 @@ -# coding: utf-8 # /*########################################################################## # # Copyright (c) 2016-2021 European Synchrotron Radiation Facility diff --git a/src/silx/gui/plot/test/testUtilsAxis.py b/src/silx/gui/plot/test/testUtilsAxis.py index dd4a689..879ec73 100644 --- a/src/silx/gui/plot/test/testUtilsAxis.py +++ b/src/silx/gui/plot/test/testUtilsAxis.py @@ -1,4 +1,3 @@ -# coding: utf-8 # /*########################################################################## # # Copyright (c) 2016 European Synchrotron Radiation Facility diff --git a/src/silx/gui/plot/test/utils.py b/src/silx/gui/plot/test/utils.py index 64fca56..faa40bb 100644 --- a/src/silx/gui/plot/test/utils.py +++ b/src/silx/gui/plot/test/utils.py @@ -1,4 +1,3 @@ -# coding: utf-8 # /*########################################################################## # # Copyright (c) 2016-2021 European Synchrotron Radiation Facility diff --git a/src/silx/gui/plot/tools/CurveLegendsWidget.py b/src/silx/gui/plot/tools/CurveLegendsWidget.py index 4a517dd..c9b0101 100644 --- a/src/silx/gui/plot/tools/CurveLegendsWidget.py +++ b/src/silx/gui/plot/tools/CurveLegendsWidget.py @@ -1,4 +1,3 @@ -# coding: utf-8 # /*########################################################################## # # Copyright (c) 2018-2020 European Synchrotron Radiation Facility @@ -25,8 +24,6 @@ """This module provides a widget to display :class:`PlotWidget` curve legends. """ -from __future__ import division - __authors__ = ["T. Vincent"] __license__ = "MIT" __date__ = "20/07/2018" diff --git a/src/silx/gui/plot/tools/LimitsToolBar.py b/src/silx/gui/plot/tools/LimitsToolBar.py index fc192a6..d7f4bf5 100644 --- a/src/silx/gui/plot/tools/LimitsToolBar.py +++ b/src/silx/gui/plot/tools/LimitsToolBar.py @@ -1,4 +1,3 @@ -# coding: utf-8 # /*########################################################################## # # Copyright (c) 2016-2018 European Synchrotron Radiation Facility @@ -25,9 +24,6 @@ """A toolbar to display and edit limits of a PlotWidget """ - -from __future__ import division - __authors__ = ["V.A. Sole", "T. Vincent"] __license__ = "MIT" __date__ = "16/10/2017" diff --git a/src/silx/gui/plot/tools/PositionInfo.py b/src/silx/gui/plot/tools/PositionInfo.py index 8b95fbc..cb16b80 100644 --- a/src/silx/gui/plot/tools/PositionInfo.py +++ b/src/silx/gui/plot/tools/PositionInfo.py @@ -1,4 +1,3 @@ -# coding: utf-8 # /*########################################################################## # # Copyright (c) 2016-2021 European Synchrotron Radiation Facility @@ -27,8 +26,6 @@ It can be configured to provide more information. """ -from __future__ import division - __authors__ = ["V.A. Sole", "T. Vincent"] __license__ = "MIT" __date__ = "16/10/2017" @@ -239,7 +236,7 @@ class PositionInfo(qt.QWidget): ratio = qt.QGuiApplication.primaryScreen().devicePixelRatio() # Baseline squared distance threshold - distInPixels = (self.SNAP_THRESHOLD_DIST * ratio)**2 + sqDistInPixels = (self.SNAP_THRESHOLD_DIST * ratio)**2 for item in selectedItems: if (snappingMode & self.SNAPPING_SYMBOLS_ONLY and ( @@ -263,33 +260,36 @@ class PositionInfo(qt.QWidget): break else: # Curve, Scatter - xArray = item.getXData(copy=False) - yArray = item.getYData(copy=False) - closestIndex = numpy.argmin( - pow(xArray - x, 2) + pow(yArray - y, 2)) - - xClosest = xArray[closestIndex] - yClosest = yArray[closestIndex] + result = item.pick(xPixel, yPixel) + if result is None: + continue + indices = result.getIndices(copy=False) + if indices is None: + continue if isinstance(item, items.YAxisMixIn): axis = item.getYAxis() else: axis = 'left' - closestInPixels = plot.dataToPixel( - xClosest, yClosest, axis=axis) - if closestInPixels is not None: - curveDistInPixels = ( - (closestInPixels[0] - xPixel)**2 + - (closestInPixels[1] - yPixel)**2) - - if curveDistInPixels <= distInPixels: - # Update label style sheet - styleSheet = "color: rgb(0, 0, 0);" + xArray = item.getXData(copy=False)[indices] + yArray = item.getYData(copy=False)[indices] + pixelPositions = plot.dataToPixel(xArray, yArray, axis=axis) + if pixelPositions is None: + continue + sqDistances = (pixelPositions[0] - xPixel)**2 + (pixelPositions[1] - yPixel)**2 + if not numpy.any(numpy.isfinite(sqDistances)): + continue + closestIndex = numpy.nanargmin(sqDistances) + closestSqDistInPixels = sqDistances[closestIndex] + + if closestSqDistInPixels <= sqDistInPixels: + # Update label style sheet + styleSheet = "color: rgb(0, 0, 0);" - # if close enough, snap to data point coord - xData, yData = xClosest, yClosest - distInPixels = curveDistInPixels + # if close enough, snap to data point coord + xData, yData = xArray[closestIndex], yArray[closestIndex] + sqDistInPixels = closestSqDistInPixels for label, name, func in self._fields: label.setStyleSheet(styleSheet) diff --git a/src/silx/gui/plot/tools/RadarView.py b/src/silx/gui/plot/tools/RadarView.py index 7076835..886f37e 100644 --- a/src/silx/gui/plot/tools/RadarView.py +++ b/src/silx/gui/plot/tools/RadarView.py @@ -1,4 +1,3 @@ -# coding: utf-8 # /*########################################################################## # # Copyright (c) 2015-2018 European Synchrotron Radiation Facility diff --git a/src/silx/gui/plot/tools/__init__.py b/src/silx/gui/plot/tools/__init__.py index 09f468c..5b6b74c 100644 --- a/src/silx/gui/plot/tools/__init__.py +++ b/src/silx/gui/plot/tools/__init__.py @@ -1,4 +1,3 @@ -# coding: utf-8 # /*########################################################################## # # Copyright (c) 2018 European Synchrotron Radiation Facility diff --git a/src/silx/gui/plot/tools/profile/ScatterProfileToolBar.py b/src/silx/gui/plot/tools/profile/ScatterProfileToolBar.py index 44187ef..09f90b7 100644 --- a/src/silx/gui/plot/tools/profile/ScatterProfileToolBar.py +++ b/src/silx/gui/plot/tools/profile/ScatterProfileToolBar.py @@ -1,4 +1,3 @@ -# coding: utf-8 # /*########################################################################## # # Copyright (c) 2018-2019 European Synchrotron Radiation Facility diff --git a/src/silx/gui/plot/tools/profile/__init__.py b/src/silx/gui/plot/tools/profile/__init__.py index d91191e..a72b5d2 100644 --- a/src/silx/gui/plot/tools/profile/__init__.py +++ b/src/silx/gui/plot/tools/profile/__init__.py @@ -1,4 +1,3 @@ -# coding: utf-8 # /*########################################################################## # # Copyright (c) 2018 European Synchrotron Radiation Facility diff --git a/src/silx/gui/plot/tools/profile/core.py b/src/silx/gui/plot/tools/profile/core.py index 200f5cf..5d4a674 100644 --- a/src/silx/gui/plot/tools/profile/core.py +++ b/src/silx/gui/plot/tools/profile/core.py @@ -1,4 +1,3 @@ -# coding: utf-8 # /*########################################################################## # # Copyright (c) 2018-2020 European Synchrotron Radiation Facility diff --git a/src/silx/gui/plot/tools/profile/editors.py b/src/silx/gui/plot/tools/profile/editors.py index 80e0452..1d6f198 100644 --- a/src/silx/gui/plot/tools/profile/editors.py +++ b/src/silx/gui/plot/tools/profile/editors.py @@ -1,4 +1,3 @@ -# coding: utf-8 # /*########################################################################## # # Copyright (c) 2018-2020 European Synchrotron Radiation Facility @@ -252,7 +251,10 @@ class ProfileRoiEditorAction(qt.QWidgetAction): return layout = widget.layout() if previousEditor is not None: - previousEditor.sigDataCommited.disconnect(self._editorDataCommited) + try: + previousEditor.sigDataCommited.disconnect(self._editorDataCommited) + except (RuntimeError, TypeError): + pass layout.removeWidget(previousEditor) previousEditor.deleteLater() if editor is not None: diff --git a/src/silx/gui/plot/tools/profile/manager.py b/src/silx/gui/plot/tools/profile/manager.py index 4a22bc0..58c1c86 100644 --- a/src/silx/gui/plot/tools/profile/manager.py +++ b/src/silx/gui/plot/tools/profile/manager.py @@ -1,4 +1,3 @@ -# coding: utf-8 # /*########################################################################## # # Copyright (c) 2018-2021 European Synchrotron Radiation Facility @@ -180,6 +179,8 @@ class ProfileWindow(qt.QMainWindow): plot.setDataMargins(yMinMargin=0.1, yMaxMargin=0.1) plot.setGraphYLabel('Profile') plot.setGraphXLabel('') + positionInfo = plot.getPositionInfoWidget() + positionInfo.setSnappingMode(positionInfo.SNAPPING_CURVE) return plot def createPlot2D(self, parent, backend): diff --git a/src/silx/gui/plot/tools/profile/rois.py b/src/silx/gui/plot/tools/profile/rois.py index 9eef622..042aff1 100644 --- a/src/silx/gui/plot/tools/profile/rois.py +++ b/src/silx/gui/plot/tools/profile/rois.py @@ -1,4 +1,3 @@ -# coding: utf-8 # /*########################################################################## # # Copyright (c) 2018-2021 European Synchrotron Radiation Facility @@ -945,13 +944,19 @@ class _DefaultScatterProfileSliceRoiMixIn(core.ProfileRoiMixIn): if major_axis: # slice in the middle of the scatter - start = max_grid_second // 2 * max_grid_first - vslice = axis[start:start + max_grid_second] + actual_size_grid_second = len(axis) // max_grid_first + start = actual_size_grid_second // 2 * max_grid_first + vslice = axis[start:start + max_grid_first] + if len(vslice) == 0: + return None index = argnearest(vslice, position) slicing = slice(index, None, max_grid_first) else: # slice in the middle of the scatter - vslice = axis[max_grid_second // 2::max_grid_second] + actual_size_grid_second = len(axis) // max_grid_first + vslice = axis[actual_size_grid_second // 2::max_grid_second] + if len(vslice) == 0: + return None index = argnearest(vslice, position) start = index * max_grid_second slicing = slice(start, start + max_grid_second) @@ -1086,11 +1091,14 @@ class _DefaultImageStackProfileRoiMixIn(_DefaultImageProfileRoiMixIn): coords, profile, profileName, xLabel = createProfile2(currentData) + profileManager = self.getProfileManager() + plot = profileManager.getPlotWidget() + data = core.ImageProfileData( coords=coords, profile=profile, - title=profileName, - xLabel=xLabel, + title=_relabelAxes(plot, profileName), + xLabel=_relabelAxes(plot, xLabel), yLabel="Profile", colormap=colormap, ) diff --git a/src/silx/gui/plot/tools/profile/toolbar.py b/src/silx/gui/plot/tools/profile/toolbar.py index 4a9a195..12a734a 100644 --- a/src/silx/gui/plot/tools/profile/toolbar.py +++ b/src/silx/gui/plot/tools/profile/toolbar.py @@ -1,4 +1,3 @@ -# coding: utf-8 # /*########################################################################## # # Copyright (c) 2018-2019 European Synchrotron Radiation Facility diff --git a/src/silx/gui/plot/tools/roi.py b/src/silx/gui/plot/tools/roi.py index e4be6a7..1da692c 100644 --- a/src/silx/gui/plot/tools/roi.py +++ b/src/silx/gui/plot/tools/roi.py @@ -1,7 +1,6 @@ -# coding: utf-8 # /*########################################################################## # -# Copyright (c) 2018-2021 European Synchrotron Radiation Facility +# Copyright (c) 2018-2022 European Synchrotron Radiation Facility # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal @@ -381,6 +380,7 @@ class RegionOfInterestManager(qt.QObject): roi_items.VerticalLineROI, roi_items.ArcROI, roi_items.HorizontalRangeROI, + roi_items.BandROI, ) def __init__(self, parent): diff --git a/src/silx/gui/plot/tools/test/__init__.py b/src/silx/gui/plot/tools/test/__init__.py index aa4a601..2e682d7 100644 --- a/src/silx/gui/plot/tools/test/__init__.py +++ b/src/silx/gui/plot/tools/test/__init__.py @@ -1,4 +1,3 @@ -# coding: utf-8 # /*########################################################################## # # Copyright (c) 2018 European Synchrotron Radiation Facility diff --git a/src/silx/gui/plot/tools/test/testCurveLegendsWidget.py b/src/silx/gui/plot/tools/test/testCurveLegendsWidget.py index 37af10e..657d328 100644 --- a/src/silx/gui/plot/tools/test/testCurveLegendsWidget.py +++ b/src/silx/gui/plot/tools/test/testCurveLegendsWidget.py @@ -1,4 +1,3 @@ -# coding: utf-8 # /*########################################################################## # # Copyright (c) 2018 European Synchrotron Radiation Facility diff --git a/src/silx/gui/plot/tools/test/testProfile.py b/src/silx/gui/plot/tools/test/testProfile.py index 829f49e..ad40e67 100644 --- a/src/silx/gui/plot/tools/test/testProfile.py +++ b/src/silx/gui/plot/tools/test/testProfile.py @@ -1,4 +1,3 @@ -# coding: utf-8 # /*########################################################################## # # Copyright (c) 2018-2021 European Synchrotron Radiation Facility diff --git a/src/silx/gui/plot/tools/test/testROI.py b/src/silx/gui/plot/tools/test/testROI.py index 21697d1..6ce1553 100644 --- a/src/silx/gui/plot/tools/test/testROI.py +++ b/src/silx/gui/plot/tools/test/testROI.py @@ -1,4 +1,3 @@ -# coding: utf-8 # /*########################################################################## # # Copyright (c) 2018-2020 European Synchrotron Radiation Facility @@ -268,6 +267,12 @@ class TestRoiItems(TestCaseQt): self.assertAlmostEqual(item.getMax(), vmax) self.assertAlmostEqual(item.getCenter(), 2) + def testBand_getToSetGeometry(self): + """Test that we can use getGeometry as input to setGeometry""" + item = roi_items.BandROI() + item.setFirstShapePoints(numpy.array([[5, 10], [50, 100]])) + item.setGeometry(*item.getGeometry()) + class TestRegionOfInterestManager(TestCaseQt, ParametricTestCase): """Tests for RegionOfInterestManager class""" @@ -577,7 +582,7 @@ class TestRegionOfInterestManager(TestCaseQt, ParametricTestCase): manager.addRoi(item) self.qapp.processEvents() - # Drag the center + # Drag the center widget = self.plot.getWidgetHandle() mx, my = self.plot.dataToPixel(*center) self.mouseMove(widget, pos=(mx, my)) @@ -680,3 +685,56 @@ class TestRegionOfInterestManager(TestCaseQt, ParametricTestCase): manager.clear() self.qapp.processEvents() + + def testBandRoiSwitchMode(self): + """Make sure we can switch mode by clicking on the ROI""" + xlimit = self.plot.getXAxis().getLimits() + ylimit = self.plot.getYAxis().getLimits() + xcenter = 0.5 * (xlimit[0] + xlimit[1]) + ycenter = 0.5 * (ylimit[0] + ylimit[1]) + + # Create the line + manager = roi.RegionOfInterestManager(self.plot) + item = roi_items.BandROI() + item.setGeometry( + (xlimit[0], ycenter), + (xlimit[1], ycenter), + 20, + ) + item.setEditable(True) + item.setSelectable(True) + manager.addRoi(item) + self.qapp.processEvents() + + # Initial state + assert item.getInteractionMode() is roi_items.BandROI.BoundedMode + self.qWait(500) + + # Click on the center + widget = self.plot.getWidgetHandle() + mx, my = self.plot.dataToPixel(xcenter, ycenter) + + # Select the ROI + self.mouseMove(widget, pos=(mx, my)) + self.mouseClick(widget, qt.Qt.LeftButton, pos=(mx, my)) + self.qWait(500) + assert item.getInteractionMode() is roi_items.BandROI.BoundedMode + + # Change the mode + self.mouseMove(widget, pos=(mx, my)) + self.mouseClick(widget, qt.Qt.LeftButton, pos=(mx, my)) + self.qWait(500) + assert item.getInteractionMode() is roi_items.BandROI.UnboundedMode + + # Set available modes that exclude the current one + item.setAvailableInteractionModes([roi_items.BandROI.BoundedMode]) + assert item.getInteractionMode() is roi_items.BandROI.BoundedMode + + # Clicking does not change the mode since there is only one + self.mouseMove(widget, pos=(mx, my)) + self.mouseClick(widget, qt.Qt.LeftButton, pos=(mx, my)) + self.qWait(500) + assert item.getInteractionMode() is roi_items.BandROI.BoundedMode + + manager.clear() + self.qapp.processEvents() diff --git a/src/silx/gui/plot/tools/test/testScatterProfileToolBar.py b/src/silx/gui/plot/tools/test/testScatterProfileToolBar.py index 582a276..9b9caa1 100644 --- a/src/silx/gui/plot/tools/test/testScatterProfileToolBar.py +++ b/src/silx/gui/plot/tools/test/testScatterProfileToolBar.py @@ -1,4 +1,3 @@ -# coding: utf-8 # /*########################################################################## # # Copyright (c) 2018 European Synchrotron Radiation Facility diff --git a/src/silx/gui/plot/tools/test/testTools.py b/src/silx/gui/plot/tools/test/testTools.py index 846f641..507b922 100644 --- a/src/silx/gui/plot/tools/test/testTools.py +++ b/src/silx/gui/plot/tools/test/testTools.py @@ -1,4 +1,3 @@ -# coding: utf-8 # /*########################################################################## # # Copyright (c) 2016-2021 European Synchrotron Radiation Facility diff --git a/src/silx/gui/plot/tools/toolbars.py b/src/silx/gui/plot/tools/toolbars.py index 3df7d06..bb89942 100644 --- a/src/silx/gui/plot/tools/toolbars.py +++ b/src/silx/gui/plot/tools/toolbars.py @@ -1,4 +1,3 @@ -# coding: utf-8 # /*########################################################################## # # Copyright (c) 2018-2020 European Synchrotron Radiation Facility diff --git a/src/silx/gui/plot/utils/__init__.py b/src/silx/gui/plot/utils/__init__.py index 3187f6b..61e45b4 100644 --- a/src/silx/gui/plot/utils/__init__.py +++ b/src/silx/gui/plot/utils/__init__.py @@ -1,4 +1,3 @@ -# coding: utf-8 # /*########################################################################## # # Copyright (c) 2016-2017 European Synchrotron Radiation Facility diff --git a/src/silx/gui/plot/utils/axis.py b/src/silx/gui/plot/utils/axis.py index 5cf8ad9..419a71c 100644 --- a/src/silx/gui/plot/utils/axis.py +++ b/src/silx/gui/plot/utils/axis.py @@ -1,4 +1,3 @@ -# coding: utf-8 # /*########################################################################## # # Copyright (c) 2017-2021 European Synchrotron Radiation Facility diff --git a/src/silx/gui/plot/utils/intersections.py b/src/silx/gui/plot/utils/intersections.py index 53f2546..4f6ed23 100644 --- a/src/silx/gui/plot/utils/intersections.py +++ b/src/silx/gui/plot/utils/intersections.py @@ -1,4 +1,3 @@ -# coding: utf-8 # /*########################################################################## # # Copyright (c) 2017-2018 European Synchrotron Radiation Facility diff --git a/src/silx/gui/plot3d/ParamTreeView.py b/src/silx/gui/plot3d/ParamTreeView.py index 2593860..b648251 100644 --- a/src/silx/gui/plot3d/ParamTreeView.py +++ b/src/silx/gui/plot3d/ParamTreeView.py @@ -1,4 +1,3 @@ -# coding: utf-8 # /*########################################################################## # # Copyright (c) 2017-2021 European Synchrotron Radiation Facility @@ -33,8 +32,6 @@ This module contains: :class:`Vector4DEditor`, :class:`IntSliderEditor`, :class:`BooleanEditor` """ -from __future__ import absolute_import - __authors__ = ["T. Vincent"] __license__ = "MIT" __date__ = "05/12/2017" diff --git a/src/silx/gui/plot3d/Plot3DWidget.py b/src/silx/gui/plot3d/Plot3DWidget.py index a90d34c..09e06a2 100644 --- a/src/silx/gui/plot3d/Plot3DWidget.py +++ b/src/silx/gui/plot3d/Plot3DWidget.py @@ -1,7 +1,6 @@ -# coding: utf-8 # /*########################################################################## # -# Copyright (c) 2015-2021 European Synchrotron Radiation Facility +# Copyright (c) 2015-2022 European Synchrotron Radiation Facility # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal @@ -24,8 +23,6 @@ # ###########################################################################*/ """This module provides a Qt widget embedding an OpenGL scene.""" -from __future__ import absolute_import - __authors__ = ["T. Vincent"] __license__ = "MIT" __date__ = "24/04/2018" @@ -125,7 +122,7 @@ class Plot3DWidget(glu.OpenGLWidget): LINEAR = 'linear' """Linear fog through the whole scene""" - def __init__(self, parent=None, f=qt.Qt.WindowFlags()): + def __init__(self, parent=None, f=qt.Qt.Widget): self._firstRender = True super(Plot3DWidget, self).__init__( @@ -380,10 +377,7 @@ class Plot3DWidget(glu.OpenGLWidget): return convertArrayToQImage(image) def wheelEvent(self, event): - if qt.BINDING == "PySide6": - x, y = event.position().x(), event.position().y() - else: - x, y = event.x(), event.y() + x, y = qt.getMouseEventPosition(event) xpixel = x * self.getDevicePixelRatio() ypixel = y * self.getDevicePixelRatio() angle = event.angleDelta().y() / 8. @@ -431,11 +425,16 @@ class Plot3DWidget(glu.OpenGLWidget): super(Plot3DWidget, self).keyReleaseEvent(event) # Mouse events # - _MOUSE_BTNS = {1: 'left', 2: 'right', 4: 'middle'} + _MOUSE_BTNS = { + qt.Qt.LeftButton: 'left', + qt.Qt.RightButton: 'right', + qt.Qt.MiddleButton: 'middle', + } def mousePressEvent(self, event): - xpixel = event.x() * self.getDevicePixelRatio() - ypixel = event.y() * self.getDevicePixelRatio() + x, y = qt.getMouseEventPosition(event) + xpixel = x * self.getDevicePixelRatio() + ypixel = y * self.getDevicePixelRatio() btn = self._MOUSE_BTNS[event.button()] event.accept() @@ -444,8 +443,9 @@ class Plot3DWidget(glu.OpenGLWidget): self.eventHandler.handleEvent('press', xpixel, ypixel, btn) def mouseMoveEvent(self, event): - xpixel = event.x() * self.getDevicePixelRatio() - ypixel = event.y() * self.getDevicePixelRatio() + x, y = qt.getMouseEventPosition(event) + xpixel = x * self.getDevicePixelRatio() + ypixel = y * self.getDevicePixelRatio() event.accept() if self.eventHandler is not None and self.isValid(): @@ -453,8 +453,9 @@ class Plot3DWidget(glu.OpenGLWidget): self.eventHandler.handleEvent('move', xpixel, ypixel) def mouseReleaseEvent(self, event): - xpixel = event.x() * self.getDevicePixelRatio() - ypixel = event.y() * self.getDevicePixelRatio() + x, y = qt.getMouseEventPosition(event) + xpixel = x * self.getDevicePixelRatio() + ypixel = y * self.getDevicePixelRatio() btn = self._MOUSE_BTNS[event.button()] event.accept() diff --git a/src/silx/gui/plot3d/Plot3DWindow.py b/src/silx/gui/plot3d/Plot3DWindow.py index 470b966..882f4cd 100644 --- a/src/silx/gui/plot3d/Plot3DWindow.py +++ b/src/silx/gui/plot3d/Plot3DWindow.py @@ -1,4 +1,3 @@ -# coding: utf-8 # /*########################################################################## # # Copyright (c) 2015-2019 European Synchrotron Radiation Facility @@ -25,8 +24,6 @@ """This module provides a QMainWindow with a 3D scene and associated toolbar. """ -from __future__ import absolute_import - __authors__ = ["T. Vincent"] __license__ = "MIT" __date__ = "26/01/2017" diff --git a/src/silx/gui/plot3d/SFViewParamTree.py b/src/silx/gui/plot3d/SFViewParamTree.py index b269a6a..cc78cec 100644 --- a/src/silx/gui/plot3d/SFViewParamTree.py +++ b/src/silx/gui/plot3d/SFViewParamTree.py @@ -1,4 +1,3 @@ -# coding: utf-8 # /*########################################################################## # # Copyright (c) 2015-2021 European Synchrotron Radiation Facility @@ -26,8 +25,6 @@ This module provides a tree widget to set/view parameters of a ScalarFieldView. """ -from __future__ import absolute_import - __authors__ = ["D. N."] __license__ = "MIT" __date__ = "24/04/2018" diff --git a/src/silx/gui/plot3d/ScalarFieldView.py b/src/silx/gui/plot3d/ScalarFieldView.py index b2bb254..0633221 100644 --- a/src/silx/gui/plot3d/ScalarFieldView.py +++ b/src/silx/gui/plot3d/ScalarFieldView.py @@ -1,4 +1,3 @@ -# coding: utf-8 # /*########################################################################## # # Copyright (c) 2015-2020 European Synchrotron Radiation Facility @@ -28,8 +27,6 @@ It supports iso-surfaces, a cutting plane and the definition of a region of interest. """ -from __future__ import absolute_import - __authors__ = ["T. Vincent"] __license__ = "MIT" __date__ = "14/06/2018" diff --git a/src/silx/gui/plot3d/SceneWidget.py b/src/silx/gui/plot3d/SceneWidget.py index 883f5e7..910820c 100644 --- a/src/silx/gui/plot3d/SceneWidget.py +++ b/src/silx/gui/plot3d/SceneWidget.py @@ -1,4 +1,3 @@ -# coding: utf-8 # /*########################################################################## # # Copyright (c) 2017-2019 European Synchrotron Radiation Facility @@ -24,8 +23,6 @@ # ###########################################################################*/ """This module provides a widget to view data sets in 3D.""" -from __future__ import absolute_import - __authors__ = ["T. Vincent"] __license__ = "MIT" __date__ = "24/04/2018" diff --git a/src/silx/gui/plot3d/SceneWindow.py b/src/silx/gui/plot3d/SceneWindow.py index 052a4dc..d88cfa9 100644 --- a/src/silx/gui/plot3d/SceneWindow.py +++ b/src/silx/gui/plot3d/SceneWindow.py @@ -1,4 +1,3 @@ -# coding: utf-8 # /*########################################################################## # # Copyright (c) 2017-2019 European Synchrotron Radiation Facility @@ -25,8 +24,6 @@ """This module provides a QMainWindow with a 3D SceneWidget and toolbars. """ -from __future__ import absolute_import - __authors__ = ["T. Vincent"] __license__ = "MIT" __date__ = "29/11/2017" diff --git a/src/silx/gui/plot3d/__init__.py b/src/silx/gui/plot3d/__init__.py index af74613..e0cb688 100644 --- a/src/silx/gui/plot3d/__init__.py +++ b/src/silx/gui/plot3d/__init__.py @@ -1,4 +1,3 @@ -# coding: utf-8 # /*########################################################################## # # Copyright (c) 2015-2017 European Synchrotron Radiation Facility @@ -27,7 +26,6 @@ This package provides widgets displaying 3D content based on OpenGL. It depends on PyOpenGL and PyQtx.QtOpenGL or PyQt>=5.4. """ -from __future__ import absolute_import __authors__ = ["T. Vincent"] __license__ = "MIT" diff --git a/src/silx/gui/plot3d/_model/__init__.py b/src/silx/gui/plot3d/_model/__init__.py index 4b16e32..fd8eafb 100644 --- a/src/silx/gui/plot3d/_model/__init__.py +++ b/src/silx/gui/plot3d/_model/__init__.py @@ -1,4 +1,3 @@ -# coding: utf-8 # /*########################################################################## # # Copyright (c) 2017-2018 European Synchrotron Radiation Facility @@ -26,8 +25,6 @@ This package provides :class:`SceneWidget` content and parameters model. """ -from __future__ import absolute_import - __authors__ = ["T. Vincent"] __license__ = "MIT" __date__ = "11/01/2018" diff --git a/src/silx/gui/plot3d/_model/core.py b/src/silx/gui/plot3d/_model/core.py index e8e0820..30d45ec 100644 --- a/src/silx/gui/plot3d/_model/core.py +++ b/src/silx/gui/plot3d/_model/core.py @@ -1,4 +1,3 @@ -# coding: utf-8 # /*########################################################################## # # Copyright (c) 2017-2018 European Synchrotron Radiation Facility @@ -26,8 +25,6 @@ This module provides base classes to implement models for 3D scene content. """ -from __future__ import absolute_import, division - __authors__ = ["T. Vincent"] __license__ = "MIT" __date__ = "11/01/2018" diff --git a/src/silx/gui/plot3d/_model/items.py b/src/silx/gui/plot3d/_model/items.py index 492f44b..c6bf69a 100644 --- a/src/silx/gui/plot3d/_model/items.py +++ b/src/silx/gui/plot3d/_model/items.py @@ -1,4 +1,3 @@ -# coding: utf-8 # /*########################################################################## # # Copyright (c) 2017-2021 European Synchrotron Radiation Facility @@ -26,8 +25,6 @@ This module provides base classes to implement models for 3D scene content """ -from __future__ import absolute_import, division - __authors__ = ["T. Vincent"] __license__ = "MIT" __date__ = "24/04/2018" diff --git a/src/silx/gui/plot3d/_model/model.py b/src/silx/gui/plot3d/_model/model.py index 186838f..5276878 100644 --- a/src/silx/gui/plot3d/_model/model.py +++ b/src/silx/gui/plot3d/_model/model.py @@ -1,4 +1,3 @@ -# coding: utf-8 # /*########################################################################## # # Copyright (c) 2017-2018 European Synchrotron Radiation Facility @@ -26,8 +25,6 @@ This module provides the :class:`SceneWidget` content and parameters model. """ -from __future__ import absolute_import, division - __authors__ = ["T. Vincent"] __license__ = "MIT" __date__ = "11/01/2018" diff --git a/src/silx/gui/plot3d/actions/Plot3DAction.py b/src/silx/gui/plot3d/actions/Plot3DAction.py index 94b9572..a2ee93c 100644 --- a/src/silx/gui/plot3d/actions/Plot3DAction.py +++ b/src/silx/gui/plot3d/actions/Plot3DAction.py @@ -1,4 +1,3 @@ -# coding: utf-8 # /*########################################################################## # # Copyright (c) 2016-2018 European Synchrotron Radiation Facility @@ -24,8 +23,6 @@ # ###########################################################################*/ """Base class for QAction attached to a Plot3DWidget.""" -from __future__ import absolute_import, division - __authors__ = ["T. Vincent"] __license__ = "MIT" __date__ = "06/09/2017" diff --git a/src/silx/gui/plot3d/actions/__init__.py b/src/silx/gui/plot3d/actions/__init__.py index 26243cf..e6c7312 100644 --- a/src/silx/gui/plot3d/actions/__init__.py +++ b/src/silx/gui/plot3d/actions/__init__.py @@ -1,4 +1,3 @@ -# coding: utf-8 # /*########################################################################## # # Copyright (c) 2017 European Synchrotron Radiation Facility diff --git a/src/silx/gui/plot3d/actions/io.py b/src/silx/gui/plot3d/actions/io.py index 25f4ade..f8a1d86 100644 --- a/src/silx/gui/plot3d/actions/io.py +++ b/src/silx/gui/plot3d/actions/io.py @@ -1,4 +1,3 @@ -# coding: utf-8 # /*########################################################################## # # Copyright (c) 2016-2021 European Synchrotron Radiation Facility @@ -27,8 +26,6 @@ It provides QAction to copy, save (snapshot and video), print a Plot3DWidget. """ -from __future__ import absolute_import, division - __authors__ = ["T. Vincent"] __license__ = "MIT" __date__ = "06/09/2017" diff --git a/src/silx/gui/plot3d/actions/mode.py b/src/silx/gui/plot3d/actions/mode.py index b9cd7c8..179fe05 100644 --- a/src/silx/gui/plot3d/actions/mode.py +++ b/src/silx/gui/plot3d/actions/mode.py @@ -1,4 +1,3 @@ -# coding: utf-8 # /*########################################################################## # # Copyright (c) 2017-2021 European Synchrotron Radiation Facility @@ -28,8 +27,6 @@ It provides QAction to rotate or pan a Plot3DWidget as well as toggle a picking mode. """ -from __future__ import absolute_import, division - __authors__ = ["T. Vincent"] __license__ = "MIT" __date__ = "06/09/2017" diff --git a/src/silx/gui/plot3d/actions/viewpoint.py b/src/silx/gui/plot3d/actions/viewpoint.py index d764c40..c3d640e 100644 --- a/src/silx/gui/plot3d/actions/viewpoint.py +++ b/src/silx/gui/plot3d/actions/viewpoint.py @@ -1,4 +1,3 @@ -# coding: utf-8 # /*########################################################################## # # Copyright (c) 2017-2018 European Synchrotron Radiation Facility @@ -27,8 +26,6 @@ It provides QAction to rotate or pan a Plot3DWidget. """ -from __future__ import absolute_import, division - __authors__ = ["T. Vincent"] __license__ = "MIT" __date__ = "03/10/2017" diff --git a/src/silx/gui/plot3d/items/__init__.py b/src/silx/gui/plot3d/items/__init__.py index e7c4af1..3d22103 100644 --- a/src/silx/gui/plot3d/items/__init__.py +++ b/src/silx/gui/plot3d/items/__init__.py @@ -1,4 +1,3 @@ -# coding: utf-8 # /*########################################################################## # # Copyright (c) 2017-2021 European Synchrotron Radiation Facility @@ -25,8 +24,6 @@ """This package provides classes that describes :class:`.SceneWidget` content. """ -from __future__ import absolute_import - __authors__ = ["T. Vincent"] __license__ = "MIT" __date__ = "15/11/2017" diff --git a/src/silx/gui/plot3d/items/_pick.py b/src/silx/gui/plot3d/items/_pick.py index 0d6a495..49e1a5b 100644 --- a/src/silx/gui/plot3d/items/_pick.py +++ b/src/silx/gui/plot3d/items/_pick.py @@ -1,4 +1,3 @@ -# coding: utf-8 # /*########################################################################## # # Copyright (c) 2018-2020 European Synchrotron Radiation Facility @@ -25,8 +24,6 @@ """This module provides classes supporting item picking. """ -from __future__ import absolute_import - __authors__ = ["T. Vincent"] __license__ = "MIT" __date__ = "24/09/2018" diff --git a/src/silx/gui/plot3d/items/clipplane.py b/src/silx/gui/plot3d/items/clipplane.py index 3e819d0..83a3c0e 100644 --- a/src/silx/gui/plot3d/items/clipplane.py +++ b/src/silx/gui/plot3d/items/clipplane.py @@ -1,4 +1,3 @@ -# coding: utf-8 # /*########################################################################## # # Copyright (c) 2017-2018 European Synchrotron Radiation Facility @@ -25,8 +24,6 @@ """This module provides a scene clip plane class. """ -from __future__ import absolute_import - __authors__ = ["T. Vincent"] __license__ = "MIT" __date__ = "15/11/2017" diff --git a/src/silx/gui/plot3d/items/core.py b/src/silx/gui/plot3d/items/core.py index 0388ce7..5fbe62c 100644 --- a/src/silx/gui/plot3d/items/core.py +++ b/src/silx/gui/plot3d/items/core.py @@ -1,4 +1,3 @@ -# coding: utf-8 # /*########################################################################## # # Copyright (c) 2017-2021 European Synchrotron Radiation Facility @@ -25,8 +24,6 @@ """This module provides the base class for items of the :class:`.SceneWidget`. """ -from __future__ import absolute_import - __authors__ = ["T. Vincent"] __license__ = "MIT" __date__ = "15/11/2017" diff --git a/src/silx/gui/plot3d/items/image.py b/src/silx/gui/plot3d/items/image.py index 5a50459..669e97d 100644 --- a/src/silx/gui/plot3d/items/image.py +++ b/src/silx/gui/plot3d/items/image.py @@ -1,4 +1,3 @@ -# coding: utf-8 # /*########################################################################## # # Copyright (c) 2017-2021 European Synchrotron Radiation Facility @@ -25,8 +24,6 @@ """This module provides 2D data and RGB(A) image item class. """ -from __future__ import absolute_import - __authors__ = ["T. Vincent"] __license__ = "MIT" __date__ = "15/11/2017" diff --git a/src/silx/gui/plot3d/items/mesh.py b/src/silx/gui/plot3d/items/mesh.py index 4e19939..dc1df3e 100644 --- a/src/silx/gui/plot3d/items/mesh.py +++ b/src/silx/gui/plot3d/items/mesh.py @@ -1,4 +1,3 @@ -# coding: utf-8 # /*########################################################################## # # Copyright (c) 2017-2020 European Synchrotron Radiation Facility @@ -25,8 +24,6 @@ """This module provides regular mesh item class. """ -from __future__ import absolute_import - __authors__ = ["T. Vincent"] __license__ = "MIT" __date__ = "17/07/2018" diff --git a/src/silx/gui/plot3d/items/mixins.py b/src/silx/gui/plot3d/items/mixins.py index f512365..45b569d 100644 --- a/src/silx/gui/plot3d/items/mixins.py +++ b/src/silx/gui/plot3d/items/mixins.py @@ -1,4 +1,3 @@ -# coding: utf-8 # /*########################################################################## # # Copyright (c) 2017-2020 European Synchrotron Radiation Facility diff --git a/src/silx/gui/plot3d/items/scatter.py b/src/silx/gui/plot3d/items/scatter.py index 24abaa5..c93db88 100644 --- a/src/silx/gui/plot3d/items/scatter.py +++ b/src/silx/gui/plot3d/items/scatter.py @@ -1,4 +1,3 @@ -# coding: utf-8 # /*########################################################################## # # Copyright (c) 2017-2020 European Synchrotron Radiation Facility @@ -25,8 +24,6 @@ """This module provides 2D and 3D scatter data item class. """ -from __future__ import absolute_import - __authors__ = ["T. Vincent"] __license__ = "MIT" __date__ = "15/11/2017" diff --git a/src/silx/gui/plot3d/items/volume.py b/src/silx/gui/plot3d/items/volume.py index f80fea2..b3007fa 100644 --- a/src/silx/gui/plot3d/items/volume.py +++ b/src/silx/gui/plot3d/items/volume.py @@ -1,4 +1,3 @@ -# coding: utf-8 # /*########################################################################## # # Copyright (c) 2017-2020 European Synchrotron Radiation Facility @@ -25,8 +24,6 @@ """This module provides 3D array item class and its sub-items. """ -from __future__ import absolute_import - __authors__ = ["T. Vincent"] __license__ = "MIT" __date__ = "24/04/2018" diff --git a/src/silx/gui/plot3d/scene/__init__.py b/src/silx/gui/plot3d/scene/__init__.py index 9671725..9f7c470 100644 --- a/src/silx/gui/plot3d/scene/__init__.py +++ b/src/silx/gui/plot3d/scene/__init__.py @@ -1,4 +1,3 @@ -# coding: utf-8 # /*########################################################################## # # Copyright (c) 2015-2017 European Synchrotron Radiation Facility diff --git a/src/silx/gui/plot3d/scene/axes.py b/src/silx/gui/plot3d/scene/axes.py index e35e5e1..9f6ac6c 100644 --- a/src/silx/gui/plot3d/scene/axes.py +++ b/src/silx/gui/plot3d/scene/axes.py @@ -1,4 +1,3 @@ -# coding: utf-8 # /*########################################################################## # # Copyright (c) 2016-2018 European Synchrotron Radiation Facility @@ -24,8 +23,6 @@ # ###########################################################################*/ """Primitive displaying a text field in the scene.""" -from __future__ import absolute_import, division, unicode_literals - __authors__ = ["T. Vincent"] __license__ = "MIT" __date__ = "17/10/2016" diff --git a/src/silx/gui/plot3d/scene/camera.py b/src/silx/gui/plot3d/scene/camera.py index 90de7ed..a6bc642 100644 --- a/src/silx/gui/plot3d/scene/camera.py +++ b/src/silx/gui/plot3d/scene/camera.py @@ -1,4 +1,3 @@ -# coding: utf-8 # /*########################################################################## # # Copyright (c) 2015-2018 European Synchrotron Radiation Facility @@ -24,8 +23,6 @@ # ###########################################################################*/ """This module provides classes to handle a perspective projection in 3D.""" -from __future__ import absolute_import, division, unicode_literals - __authors__ = ["T. Vincent"] __license__ = "MIT" __date__ = "25/07/2016" diff --git a/src/silx/gui/plot3d/scene/core.py b/src/silx/gui/plot3d/scene/core.py index 43838fe..c32a2c1 100644 --- a/src/silx/gui/plot3d/scene/core.py +++ b/src/silx/gui/plot3d/scene/core.py @@ -1,4 +1,3 @@ -# coding: utf-8 # /*########################################################################## # # Copyright (c) 2015-2019 European Synchrotron Radiation Facility @@ -32,8 +31,6 @@ Nodes with children are provided with :class:`PrivateGroup` and Leaf rendering nodes should inherit from :class:`Elem`. """ -from __future__ import absolute_import, division, unicode_literals - __authors__ = ["T. Vincent"] __license__ = "MIT" __date__ = "25/07/2016" diff --git a/src/silx/gui/plot3d/scene/cutplane.py b/src/silx/gui/plot3d/scene/cutplane.py index 88147df..bfd578f 100644 --- a/src/silx/gui/plot3d/scene/cutplane.py +++ b/src/silx/gui/plot3d/scene/cutplane.py @@ -1,4 +1,3 @@ -# coding: utf-8 # /*########################################################################## # # Copyright (c) 2016-2020 European Synchrotron Radiation Facility @@ -25,8 +24,6 @@ """A cut plane in a 3D texture: hackish implementation... """ -from __future__ import absolute_import, division, unicode_literals - __authors__ = ["T. Vincent"] __license__ = "MIT" __date__ = "11/01/2018" diff --git a/src/silx/gui/plot3d/scene/event.py b/src/silx/gui/plot3d/scene/event.py index 98f8f8b..637eddf 100644 --- a/src/silx/gui/plot3d/scene/event.py +++ b/src/silx/gui/plot3d/scene/event.py @@ -1,4 +1,3 @@ -# coding: utf-8 # /*########################################################################## # # Copyright (c) 2015-2017 European Synchrotron Radiation Facility @@ -24,8 +23,6 @@ # ###########################################################################*/ """This module provides a simple generic notification system.""" -from __future__ import absolute_import, division, unicode_literals - __authors__ = ["T. Vincent"] __license__ = "MIT" __date__ = "17/07/2018" diff --git a/src/silx/gui/plot3d/scene/function.py b/src/silx/gui/plot3d/scene/function.py index 2deb785..3d0a62f 100644 --- a/src/silx/gui/plot3d/scene/function.py +++ b/src/silx/gui/plot3d/scene/function.py @@ -1,4 +1,3 @@ -# coding: utf-8 # /*########################################################################## # # Copyright (c) 2015-2020 European Synchrotron Radiation Facility @@ -24,8 +23,6 @@ # ###########################################################################*/ """This module provides functions to add to shaders.""" -from __future__ import absolute_import, division, unicode_literals - __authors__ = ["T. Vincent"] __license__ = "MIT" __date__ = "17/07/2018" diff --git a/src/silx/gui/plot3d/scene/interaction.py b/src/silx/gui/plot3d/scene/interaction.py index 14a54dc..91fab23 100644 --- a/src/silx/gui/plot3d/scene/interaction.py +++ b/src/silx/gui/plot3d/scene/interaction.py @@ -1,4 +1,3 @@ -# coding: utf-8 # /*########################################################################## # # Copyright (c) 2015-2019 European Synchrotron Radiation Facility @@ -24,8 +23,6 @@ # ###########################################################################*/ """This module provides interaction to plug on the scene graph.""" -from __future__ import absolute_import - __authors__ = ["T. Vincent"] __license__ = "MIT" __date__ = "25/07/2016" diff --git a/src/silx/gui/plot3d/scene/primitives.py b/src/silx/gui/plot3d/scene/primitives.py index 7f35c3c..6d3c4ff 100644 --- a/src/silx/gui/plot3d/scene/primitives.py +++ b/src/silx/gui/plot3d/scene/primitives.py @@ -1,4 +1,3 @@ -# coding: utf-8 # /*########################################################################## # # Copyright (c) 2015-2021 European Synchrotron Radiation Facility @@ -23,8 +22,6 @@ # # ###########################################################################*/ -from __future__ import absolute_import, division, unicode_literals - __authors__ = ["T. Vincent"] __license__ = "MIT" __date__ = "24/04/2018" diff --git a/src/silx/gui/plot3d/scene/test/__init__.py b/src/silx/gui/plot3d/scene/test/__init__.py index 3bb978e..4bdcc18 100644 --- a/src/silx/gui/plot3d/scene/test/__init__.py +++ b/src/silx/gui/plot3d/scene/test/__init__.py @@ -1,4 +1,3 @@ -# coding: utf-8 # /*########################################################################## # # Copyright (c) 2015-2017 European Synchrotron Radiation Facility diff --git a/src/silx/gui/plot3d/scene/test/test_transform.py b/src/silx/gui/plot3d/scene/test/test_transform.py index 69e991b..2998c65 100644 --- a/src/silx/gui/plot3d/scene/test/test_transform.py +++ b/src/silx/gui/plot3d/scene/test/test_transform.py @@ -1,4 +1,3 @@ -# coding: utf-8 # /*########################################################################## # # Copyright (c) 2015-2017 European Synchrotron Radiation Facility @@ -23,8 +22,6 @@ # # ###########################################################################*/ -from __future__ import absolute_import, division, unicode_literals - __authors__ = ["T. Vincent"] __license__ = "MIT" __date__ = "05/01/2017" diff --git a/src/silx/gui/plot3d/scene/test/test_utils.py b/src/silx/gui/plot3d/scene/test/test_utils.py index 65d0ce0..a9ba6bc 100644 --- a/src/silx/gui/plot3d/scene/test/test_utils.py +++ b/src/silx/gui/plot3d/scene/test/test_utils.py @@ -1,4 +1,3 @@ -# coding: utf-8 # /*########################################################################## # # Copyright (c) 2015-2017 European Synchrotron Radiation Facility @@ -23,8 +22,6 @@ # # ###########################################################################*/ -from __future__ import absolute_import, division, unicode_literals - __authors__ = ["T. Vincent"] __license__ = "MIT" __date__ = "17/01/2018" diff --git a/src/silx/gui/plot3d/scene/text.py b/src/silx/gui/plot3d/scene/text.py index bacc2e6..3c4e692 100644 --- a/src/silx/gui/plot3d/scene/text.py +++ b/src/silx/gui/plot3d/scene/text.py @@ -1,4 +1,3 @@ -# coding: utf-8 # /*########################################################################## # # Copyright (c) 2016-2020 European Synchrotron Radiation Facility @@ -24,8 +23,6 @@ # ###########################################################################*/ """Primitive displaying a text field in the scene.""" -from __future__ import absolute_import, division, unicode_literals - __authors__ = ["T. Vincent"] __license__ = "MIT" __date__ = "24/04/2018" diff --git a/src/silx/gui/plot3d/scene/transform.py b/src/silx/gui/plot3d/scene/transform.py index 43b739b..5c2cbb3 100644 --- a/src/silx/gui/plot3d/scene/transform.py +++ b/src/silx/gui/plot3d/scene/transform.py @@ -1,4 +1,3 @@ -# coding: utf-8 # /*########################################################################## # # Copyright (c) 2015-2020 European Synchrotron Radiation Facility @@ -24,8 +23,6 @@ # ###########################################################################*/ """This module provides 4x4 matrix operation and classes to handle them.""" -from __future__ import absolute_import, division, unicode_literals - __authors__ = ["T. Vincent"] __license__ = "MIT" __date__ = "25/07/2016" diff --git a/src/silx/gui/plot3d/scene/utils.py b/src/silx/gui/plot3d/scene/utils.py index c6cd129..48fc2f5 100644 --- a/src/silx/gui/plot3d/scene/utils.py +++ b/src/silx/gui/plot3d/scene/utils.py @@ -1,4 +1,3 @@ -# coding: utf-8 # /*########################################################################## # # Copyright (c) 2015-2020 European Synchrotron Radiation Facility @@ -27,8 +26,6 @@ This module provides functions to generate indices, to check intersection and to handle planes. """ -from __future__ import absolute_import, division, unicode_literals - __authors__ = ["T. Vincent"] __license__ = "MIT" __date__ = "25/07/2016" diff --git a/src/silx/gui/plot3d/scene/viewport.py b/src/silx/gui/plot3d/scene/viewport.py index 6de640e..bff77e2 100644 --- a/src/silx/gui/plot3d/scene/viewport.py +++ b/src/silx/gui/plot3d/scene/viewport.py @@ -1,4 +1,3 @@ -# coding: utf-8 # /*########################################################################## # # Copyright (c) 2015-2019 European Synchrotron Radiation Facility @@ -29,8 +28,6 @@ The attribute :attr:`scene` is the root group of the scene tree. :class:`RenderContext` handles the current state during rendering. """ -from __future__ import absolute_import, division, unicode_literals - __authors__ = ["T. Vincent"] __license__ = "MIT" __date__ = "24/04/2018" diff --git a/src/silx/gui/plot3d/scene/window.py b/src/silx/gui/plot3d/scene/window.py index b92c404..c8f4cee 100644 --- a/src/silx/gui/plot3d/scene/window.py +++ b/src/silx/gui/plot3d/scene/window.py @@ -1,4 +1,3 @@ -# coding: utf-8 # /*########################################################################## # # Copyright (c) 2015-2018 European Synchrotron Radiation Facility @@ -32,8 +31,6 @@ The :class:`Context` and :class:`ContextGL2` represent the operating system OpenGL context and handle OpenGL resources. """ -from __future__ import absolute_import, division, unicode_literals - __authors__ = ["T. Vincent"] __license__ = "MIT" __date__ = "10/01/2017" diff --git a/src/silx/gui/plot3d/setup.py b/src/silx/gui/plot3d/setup.py deleted file mode 100644 index 59c0230..0000000 --- a/src/silx/gui/plot3d/setup.py +++ /dev/null @@ -1,50 +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. -# -# ###########################################################################*/ -__authors__ = ["T. Vincent"] -__license__ = "MIT" -__date__ = "25/07/2016" - - -from numpy.distutils.misc_util import Configuration - - -def configuration(parent_package='', top_path=None): - config = Configuration('plot3d', parent_package, top_path) - config.add_subpackage('_model') - config.add_subpackage('actions') - config.add_subpackage('items') - config.add_subpackage('scene') - config.add_subpackage('scene.test') - config.add_subpackage('tools') - config.add_subpackage('tools.test') - config.add_subpackage('test') - config.add_subpackage('utils') - return config - - -if __name__ == "__main__": - from numpy.distutils.core import setup - - setup(configuration=configuration) diff --git a/src/silx/gui/plot3d/test/__init__.py b/src/silx/gui/plot3d/test/__init__.py index 83491ad..f8afa83 100644 --- a/src/silx/gui/plot3d/test/__init__.py +++ b/src/silx/gui/plot3d/test/__init__.py @@ -1,4 +1,3 @@ -# coding: utf-8 # /*########################################################################## # # Copyright (c) 2015-2019 European Synchrotron Radiation Facility diff --git a/src/silx/gui/plot3d/test/testGL.py b/src/silx/gui/plot3d/test/testGL.py index a7309a9..d1d53ef 100644 --- a/src/silx/gui/plot3d/test/testGL.py +++ b/src/silx/gui/plot3d/test/testGL.py @@ -1,4 +1,3 @@ -# coding: utf-8 # /*########################################################################## # # Copyright (c) 2017 European Synchrotron Radiation Facility @@ -29,7 +28,7 @@ __date__ = "10/08/2017" import logging -import unittest +import pytest from silx.gui._glutils import gl, OpenGLWidget from silx.gui.utils.testutils import TestCaseQt @@ -39,6 +38,7 @@ from silx.gui import qt _logger = logging.getLogger(__name__) +@pytest.mark.usefixtures("use_opengl") class TestOpenGL(TestCaseQt): """Tests of OpenGL widget.""" diff --git a/src/silx/gui/plot3d/test/testScalarFieldView.py b/src/silx/gui/plot3d/test/testScalarFieldView.py index e6535fc..1e06e3f 100644 --- a/src/silx/gui/plot3d/test/testScalarFieldView.py +++ b/src/silx/gui/plot3d/test/testScalarFieldView.py @@ -1,4 +1,3 @@ -# coding: utf-8 # /*########################################################################## # # Copyright (c) 2017-2018 European Synchrotron Radiation Facility @@ -29,7 +28,7 @@ __date__ = "17/01/2018" import logging -import unittest +import pytest import numpy @@ -44,6 +43,7 @@ from silx.gui.plot3d.SFViewParamTree import TreeView _logger = logging.getLogger(__name__) +@pytest.mark.usefixtures("use_opengl") class TestScalarFieldView(TestCaseQt, ParametricTestCase): """Tests of ScalarFieldView widget.""" diff --git a/src/silx/gui/plot3d/test/testSceneWidget.py b/src/silx/gui/plot3d/test/testSceneWidget.py index fc96781..e7f3b3f 100644 --- a/src/silx/gui/plot3d/test/testSceneWidget.py +++ b/src/silx/gui/plot3d/test/testSceneWidget.py @@ -1,4 +1,3 @@ -# coding: utf-8 # /*########################################################################## # # Copyright (c) 2019 European Synchrotron Radiation Facility @@ -28,7 +27,7 @@ __license__ = "MIT" __date__ = "06/03/2019" -import unittest +import pytest import numpy @@ -39,6 +38,7 @@ from silx.gui import qt from silx.gui.plot3d.SceneWidget import SceneWidget +@pytest.mark.usefixtures("use_opengl") class TestSceneWidget(TestCaseQt, ParametricTestCase): """Tests SceneWidget picking feature""" diff --git a/src/silx/gui/plot3d/test/testSceneWidgetPicking.py b/src/silx/gui/plot3d/test/testSceneWidgetPicking.py index d4d8db7..c0ad3b0 100644 --- a/src/silx/gui/plot3d/test/testSceneWidgetPicking.py +++ b/src/silx/gui/plot3d/test/testSceneWidgetPicking.py @@ -1,4 +1,3 @@ -# coding: utf-8 # /*########################################################################## # # Copyright (c) 2018-2019 European Synchrotron Radiation Facility @@ -28,7 +27,7 @@ __license__ = "MIT" __date__ = "03/10/2018" -import unittest +import pytest import numpy @@ -39,6 +38,7 @@ from silx.gui import qt from silx.gui.plot3d.SceneWidget import SceneWidget, items +@pytest.mark.usefixtures("use_opengl") class TestSceneWidgetPicking(TestCaseQt, ParametricTestCase): """Tests SceneWidget picking feature""" diff --git a/src/silx/gui/plot3d/test/testSceneWindow.py b/src/silx/gui/plot3d/test/testSceneWindow.py index 6b61335..09e097c 100644 --- a/src/silx/gui/plot3d/test/testSceneWindow.py +++ b/src/silx/gui/plot3d/test/testSceneWindow.py @@ -1,4 +1,3 @@ -# coding: utf-8 # /*########################################################################## # # Copyright (c) 2019-2021 European Synchrotron Radiation Facility @@ -28,7 +27,7 @@ __license__ = "MIT" __date__ = "22/03/2019" -import unittest +import pytest import numpy @@ -39,6 +38,8 @@ from silx.gui import qt from silx.gui.plot3d.SceneWindow import SceneWindow from silx.gui.plot3d.items import HeightMapData, HeightMapRGBA + +@pytest.mark.usefixtures("use_opengl") class TestSceneWindow(TestCaseQt, ParametricTestCase): """Tests SceneWidget picking feature""" diff --git a/src/silx/gui/plot3d/test/testStatsWidget.py b/src/silx/gui/plot3d/test/testStatsWidget.py index d452eb5..e1411bf 100644 --- a/src/silx/gui/plot3d/test/testStatsWidget.py +++ b/src/silx/gui/plot3d/test/testStatsWidget.py @@ -1,4 +1,3 @@ -# coding: utf-8 # /*########################################################################## # # Copyright (c) 2019 European Synchrotron Radiation Facility @@ -28,7 +27,7 @@ __license__ = "MIT" __date__ = "25/01/2019" -import unittest +import pytest import numpy @@ -43,6 +42,7 @@ from silx.gui.plot3d.ScalarFieldView import ScalarFieldView from silx.gui.plot3d.SceneWidget import SceneWidget, items +@pytest.mark.usefixtures("use_opengl") class TestSceneWidget(TestCaseQt, ParametricTestCase): """Tests StatsWidget combined with SceneWidget""" diff --git a/src/silx/gui/plot3d/tools/GroupPropertiesWidget.py b/src/silx/gui/plot3d/tools/GroupPropertiesWidget.py index 146c2cd..922df3a 100644 --- a/src/silx/gui/plot3d/tools/GroupPropertiesWidget.py +++ b/src/silx/gui/plot3d/tools/GroupPropertiesWidget.py @@ -1,4 +1,3 @@ -# coding: utf-8 # /*########################################################################## # # Copyright (c) 2018-2021 European Synchrotron Radiation Facility @@ -24,8 +23,6 @@ # ###########################################################################*/ """:class:`GroupPropertiesWidget` allows to reset properties in a GroupItem.""" -from __future__ import absolute_import - __authors__ = ["T. Vincent"] __license__ = "MIT" __date__ = "24/04/2018" diff --git a/src/silx/gui/plot3d/tools/PositionInfoWidget.py b/src/silx/gui/plot3d/tools/PositionInfoWidget.py index 99d6356..1998533 100644 --- a/src/silx/gui/plot3d/tools/PositionInfoWidget.py +++ b/src/silx/gui/plot3d/tools/PositionInfoWidget.py @@ -1,4 +1,3 @@ -# coding: utf-8 # /*########################################################################## # # Copyright (c) 2018-2021 European Synchrotron Radiation Facility @@ -25,8 +24,6 @@ """This module provides a widget that displays data values of a SceneWidget. """ -from __future__ import absolute_import - __authors__ = ["T. Vincent"] __license__ = "MIT" __date__ = "01/10/2018" diff --git a/src/silx/gui/plot3d/tools/ViewpointTools.py b/src/silx/gui/plot3d/tools/ViewpointTools.py index 0607382..ab26c96 100644 --- a/src/silx/gui/plot3d/tools/ViewpointTools.py +++ b/src/silx/gui/plot3d/tools/ViewpointTools.py @@ -1,4 +1,3 @@ -# coding: utf-8 # /*########################################################################## # # Copyright (c) 2015-2018 European Synchrotron Radiation Facility @@ -24,8 +23,6 @@ # ###########################################################################*/ """This module provides a toolbar to control Plot3DWidget viewpoint.""" -from __future__ import absolute_import - __authors__ = ["T. Vincent"] __license__ = "MIT" __date__ = "08/09/2017" diff --git a/src/silx/gui/plot3d/tools/__init__.py b/src/silx/gui/plot3d/tools/__init__.py index c8b8d21..5e2c76c 100644 --- a/src/silx/gui/plot3d/tools/__init__.py +++ b/src/silx/gui/plot3d/tools/__init__.py @@ -1,4 +1,3 @@ -# coding: utf-8 # /*########################################################################## # # Copyright (c) 2017 European Synchrotron Radiation Facility diff --git a/src/silx/gui/plot3d/tools/test/__init__.py b/src/silx/gui/plot3d/tools/test/__init__.py index 86741ed..a6032b9 100644 --- a/src/silx/gui/plot3d/tools/test/__init__.py +++ b/src/silx/gui/plot3d/tools/test/__init__.py @@ -1,4 +1,3 @@ -# coding: utf-8 # /*########################################################################## # # Copyright (c) 2018 European Synchrotron Radiation Facility diff --git a/src/silx/gui/plot3d/tools/test/testPositionInfoWidget.py b/src/silx/gui/plot3d/tools/test/testPositionInfoWidget.py index 17fb3db..e988817 100644 --- a/src/silx/gui/plot3d/tools/test/testPositionInfoWidget.py +++ b/src/silx/gui/plot3d/tools/test/testPositionInfoWidget.py @@ -1,4 +1,3 @@ -# coding: utf-8 # /*########################################################################## # # Copyright (c) 2018 European Synchrotron Radiation Facility diff --git a/src/silx/gui/plot3d/tools/toolbars.py b/src/silx/gui/plot3d/tools/toolbars.py index d4f32db..c89f6c6 100644 --- a/src/silx/gui/plot3d/tools/toolbars.py +++ b/src/silx/gui/plot3d/tools/toolbars.py @@ -1,4 +1,3 @@ -# coding: utf-8 # /*########################################################################## # # Copyright (c) 2016-2018 European Synchrotron Radiation Facility @@ -37,8 +36,6 @@ It provides the following toolbars: - Print """ -from __future__ import absolute_import - __authors__ = ["T. Vincent"] __license__ = "MIT" __date__ = "06/09/2017" diff --git a/src/silx/gui/plot3d/utils/__init__.py b/src/silx/gui/plot3d/utils/__init__.py index 99d3e08..3cf3825 100644 --- a/src/silx/gui/plot3d/utils/__init__.py +++ b/src/silx/gui/plot3d/utils/__init__.py @@ -1,4 +1,3 @@ -# coding: utf-8 # /*########################################################################## # # Copyright (c) 2016-2017 European Synchrotron Radiation Facility diff --git a/src/silx/gui/plot3d/utils/mng.py b/src/silx/gui/plot3d/utils/mng.py index 8049a2f..52f619f 100644 --- a/src/silx/gui/plot3d/utils/mng.py +++ b/src/silx/gui/plot3d/utils/mng.py @@ -1,4 +1,3 @@ -# coding: utf-8 # /*########################################################################## # # Copyright (c) 2016-2020 European Synchrotron Radiation Facility @@ -28,8 +27,6 @@ It only supports RGB888 images of the same shape stored as MNG-VLC (very low complexity) format. """ -from __future__ import absolute_import - __authors__ = ["T. Vincent"] __license__ = "MIT" __date__ = "15/12/2016" diff --git a/src/silx/gui/printer.py b/src/silx/gui/printer.py index 761fa0f..c0af97f 100644 --- a/src/silx/gui/printer.py +++ b/src/silx/gui/printer.py @@ -1,4 +1,3 @@ -# coding: utf-8 # /*########################################################################## # # Copyright (c) 2018 European Synchrotron Radiation Facility @@ -25,8 +24,6 @@ """This module provides a singleton QPrinter used by default by silx widgets. """ -from __future__ import absolute_import - __authors__ = ["T. Vincent"] __license__ = "MIT" __date__ = "01/03/2018" diff --git a/src/silx/gui/qt/__init__.py b/src/silx/gui/qt/__init__.py index 915c89b..bc75041 100644 --- a/src/silx/gui/qt/__init__.py +++ b/src/silx/gui/qt/__init__.py @@ -1,4 +1,3 @@ -# coding: utf-8 # /*########################################################################## # # Copyright (c) 2004-2021 European Synchrotron Radiation Facility @@ -25,11 +24,12 @@ """Common wrapper over Python Qt bindings: - `PyQt5 <http://pyqt.sourceforge.net/Docs/PyQt5/>`_ -- `PySide2 <https://pypi.org/project/PySide2/>`_ - `PySide6 <https://pypi.org/project/PySide6/>`_ +- `PySide2 <https://pypi.org/project/PySide2/>`_ +- `PyQt6 <https://pypi.org/project/PyQt6/>`_ If a Qt binding is already loaded, it will use it, otherwise the different -Qt bindings are tried in this order: PyQt5, PySide2, PySide6. +Qt bindings are tried in this order: PyQt5, PySide6, PySide2, PyQt6. The name of the loaded Qt binding is stored in the BINDING variable. diff --git a/src/silx/gui/qt/_pyqt6.py b/src/silx/gui/qt/_pyqt6.py new file mode 100644 index 0000000..15b49bb --- /dev/null +++ b/src/silx/gui/qt/_pyqt6.py @@ -0,0 +1,64 @@ +# coding: utf-8 +# /*########################################################################## +# +# Copyright (c) 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. +# +# ###########################################################################*/ +"""PyQt6 backward compatibility patching""" + +__authors__ = ["Thomas VINCENT"] +__license__ = "MIT" +__date__ = "02/09/2021" + +import enum +import logging + +import PyQt6.sip +from PyQt6.QtCore import Qt + + +_logger = logging.getLogger(__name__) + + +def patch_enums(*modules): + """Patch PyQt6 modules to provide backward compatibility of enum values + + :param modules: Modules to patch (e.g., PyQt6.QtCore). + """ + for module in modules: + for clsName in dir(module): + cls = getattr(module, clsName, None) + if isinstance(cls, PyQt6.sip.wrappertype) and clsName.startswith('Q'): + for qenumName in dir(cls): + if qenumName[0].isupper(): + qenum = getattr(cls, qenumName, None) + if isinstance(qenum, enum.EnumMeta): + if qenum is getattr(cls.__mro__[1], qenumName, None): + continue # Only handle it once + for item in qenum: + # Special cases to avoid overrides and mimic PySide6 + if clsName == 'QColorSpace' and qenumName in ( + 'Primaries', 'TransferFunction'): + break + if qenumName in ('DeviceType', 'PointerType'): + break + + setattr(cls, item.name, item) diff --git a/src/silx/gui/qt/_pyside_dynamic.py b/src/silx/gui/qt/_pyside_dynamic.py index a841eae..80520ac 100644 --- a/src/silx/gui/qt/_pyside_dynamic.py +++ b/src/silx/gui/qt/_pyside_dynamic.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- # Taken from: https://gist.github.com/cpbotha/1b42a20c8f3eb9bb7cb8 # Plus: https://github.com/spyder-ide/qtpy/commit/001a862c401d757feb63025f88dbb4601d353c84 diff --git a/src/silx/gui/qt/_qt.py b/src/silx/gui/qt/_qt.py index f62f4c8..b92fce2 100644 --- a/src/silx/gui/qt/_qt.py +++ b/src/silx/gui/qt/_qt.py @@ -1,7 +1,6 @@ -# coding: utf-8 # /*########################################################################## # -# Copyright (c) 2004-2021 European Synchrotron Radiation Facility +# Copyright (c) 2004-2022 European Synchrotron Radiation Facility # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal @@ -26,22 +25,23 @@ __authors__ = ["V.A. Sole"] __license__ = "MIT" -__date__ = "23/05/2018" +__date__ = "12/01/2022" import logging import sys import traceback +from silx.utils import deprecation _logger = logging.getLogger(__name__) BINDING = None -"""The name of the Qt binding in use: PyQt5, PySide2, PySide6.""" +"""The name of the Qt binding in use: PyQt5, PySide2, PySide6, PyQt6.""" QtBinding = None # noqa -"""The Qt binding module in use: PyQt5, PySide2, PySide6.""" +"""The Qt binding module in use: PyQt5, PySide2, PySide6, PyQt6.""" HAS_SVG = False """True if Qt provides support for Scalable Vector Graphics (QtSVG).""" @@ -50,7 +50,7 @@ HAS_OPENGL = False """True if Qt provides support for OpenGL (QtOpenGL).""" # First check for an already loaded wrapper -for _binding in ('PySide2', 'PyQt5', 'PySide6'): +for _binding in ('PySide2', 'PyQt5', 'PySide6', 'PyQt6'): if _binding + '.QtCore' in sys.modules: BINDING = _binding break @@ -61,27 +61,40 @@ else: # Then try Qt bindings if 'PyQt5' in sys.modules: del sys.modules["PyQt5"] try: - import PySide2.QtCore # noqa + import PySide6.QtCore # noqa except ImportError: - if 'PySide2' in sys.modules: - del sys.modules["PySide2"] + if 'PySide6' in sys.modules: + del sys.modules["PySide6"] try: - import PySide6.QtCore # noqa + import PySide2.QtCore # noqa except ImportError: - if 'PySide6' in sys.modules: - del sys.modules["PySide6"] - raise ImportError( - 'No Qt wrapper found. Install PyQt5, PySide2, PySide6.') + if 'PySide2' in sys.modules: + del sys.modules["PySide2"] + try: + import PyQt6.QtCore # noqa + except ImportError: + if 'PyQt6' in sys.modules: + del sys.modules["PyQt6"] + + raise ImportError( + 'No Qt wrapper found. Install PyQt5, PySide2, PySide6, PyQt6.') + else: + BINDING = 'PyQt6' else: - BINDING = 'PySide6' + BINDING = 'PySide2' else: - BINDING = 'PySide2' + BINDING = 'PySide6' else: BINDING = 'PyQt5' if BINDING == 'PyQt5': _logger.debug('Using PyQt5 bindings') + from PyQt5 import QtCore + if sys.version_info >= (3, 10) and QtCore.PYQT_VERSION < 0x50e02: + raise RuntimeError( + "PyQt5 v%s is not supported, please upgrade it." % QtCore.PYQT_VERSION_STR + ) import PyQt5 as QtBinding # noqa @@ -121,7 +134,12 @@ if BINDING == 'PyQt5': elif BINDING == 'PySide2': - _logger.debug('Using PySide2 bindings') + deprecation.deprecated_warning( + type_="Qt Binding", + name="PySide2", + replacement="PySide6", + since_version="1.1", + ) import PySide2 as QtBinding # noqa @@ -156,7 +174,7 @@ elif BINDING == 'PySide2': return super().exec_(*args, **kwargs) # QtWidgets - class QApplication(_ExecMixIn, QApplication): pass + QApplication.exec = QApplication.exec_ class QColorDialog(_ExecMixIn, QColorDialog): pass class QDialog(_ExecMixIn, QDialog): pass class QErrorMessage(_ExecMixIn, QErrorMessage): pass @@ -189,7 +207,7 @@ elif BINDING == 'PySide6': from PySide6.QtOpenGL import * # noqa from PySide6.QtOpenGLWidgets import QOpenGLWidget # noqa except ImportError: - _logger.info("PySide6.QtOpenGL not available") + _logger.info("PySide6's QtOpenGL or QtOpenGLWidgets not available") HAS_OPENGL = False else: HAS_OPENGL = True @@ -204,8 +222,63 @@ elif BINDING == 'PySide6': pyqtSignal = Signal + +elif BINDING == 'PyQt6': + _logger.debug('Using PyQt6 bindings') + + # Monkey-patch module to expose enum values for compatibility + # All Qt modules loaded here should be patched. + from . import _pyqt6 + from PyQt6 import QtCore + if QtCore.PYQT_VERSION < int("0x60300", 16): + raise RuntimeError( + "PyQt6 v%s is not supported, please upgrade it." % QtCore.PYQT_VERSION_STR + ) + + from PyQt6 import QtGui, QtWidgets, QtPrintSupport, QtOpenGL, QtSvg + from PyQt6 import QtTest as _QtTest + _pyqt6.patch_enums( + QtCore, QtGui, QtWidgets, QtPrintSupport, QtOpenGL, QtSvg, _QtTest) + + import PyQt6 as QtBinding # noqa + + from PyQt6.QtCore import * # noqa + from PyQt6.QtGui import * # noqa + from PyQt6.QtWidgets import * # noqa + from PyQt6.QtPrintSupport import * # noqa + + try: + from PyQt6.QtOpenGL import * # noqa + from PyQt6.QtOpenGLWidgets import QOpenGLWidget # noqa + except ImportError: + _logger.info("PyQt6's QtOpenGL or QtOpenGLWidgets not available") + HAS_OPENGL = False + else: + HAS_OPENGL = True + + try: + from PyQt6.QtSvg import * # noqa + except ImportError: + _logger.info("PyQt6.QtSvg not available") + HAS_SVG = False + else: + HAS_SVG = True + + from PyQt6.uic import loadUi # noqa + + Signal = pyqtSignal + + Property = pyqtProperty + + Slot = pyqtSlot + + # Disable PyQt6 cooperative multi-inheritance since other bindings do not provide it. + # See https://www.riverbankcomputing.com/static/Docs/PyQt6/multiinheritance.html?highlight=inheritance + class _Foo(object): pass + class QObject(QObject, _Foo): pass + else: - raise ImportError('No Qt wrapper found. Install PyQt5, PySide2 or PySide6') + raise ImportError('No Qt wrapper found. Install PyQt5, PySide2, PySide6 or PyQt6') # provide a exception handler but not implement it by default diff --git a/src/silx/gui/qt/_utils.py b/src/silx/gui/qt/_utils.py index 5dced95..fb2b8ce 100644 --- a/src/silx/gui/qt/_utils.py +++ b/src/silx/gui/qt/_utils.py @@ -1,4 +1,3 @@ -# coding: utf-8 # /*########################################################################## # # Copyright (c) 2004-2021 European Synchrotron Radiation Facility @@ -33,6 +32,19 @@ __date__ = "30/11/2016" from . import _qt +def getMouseEventPosition(event): + """Qt5/Qt6 compatibility wrapper to access QMouseEvent position + + :param QMouseEvent event: + :returns: (x, y) as a tuple of float + """ + if _qt.BINDING in ("PyQt5", "PySide2"): + return float(event.x()), float(event.y()) + # Qt6 + position = event.position() + return position.x(), position.y() + + def supportedImageFormats(): """Return a set of string of file format extensions supported by the Qt runtime.""" diff --git a/src/silx/gui/qt/inspect.py b/src/silx/gui/qt/inspect.py index b9a0d1d..c7fe32a 100644 --- a/src/silx/gui/qt/inspect.py +++ b/src/silx/gui/qt/inspect.py @@ -1,4 +1,3 @@ -# coding: utf-8 # /*########################################################################## # # Copyright (c) 2018-2021 European Synchrotron Radiation Facility @@ -69,6 +68,21 @@ elif qt.BINDING == 'PySide2': elif qt.BINDING == 'PySide6': from shiboken6 import isValid, createdByPython, ownedByPython # noqa +elif qt.BINDING == 'PyQt6': + from PyQt6.sip import isdeleted as _isdeleted # noqa + from PyQt6.sip import ispycreated as createdByPython # noqa + from PyQt6.sip import ispyowned as ownedByPython # noqa + + def isValid(obj): + """Returns True if underlying C++ object is valid. + + :param QObject obj: + :rtype: bool + """ + return not _isdeleted(obj) + + + else: raise ImportError("Unsupported Qt binding %s" % qt.BINDING) diff --git a/src/silx/gui/setup.py b/src/silx/gui/setup.py deleted file mode 100644 index 04a2bac..0000000 --- a/src/silx/gui/setup.py +++ /dev/null @@ -1,55 +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. -# -# ###########################################################################*/ -__authors__ = ["T. Vincent"] -__license__ = "MIT" -__date__ = "28/11/2017" - - -from numpy.distutils.misc_util import Configuration - - -def configuration(parent_package='', top_path=None): - config = Configuration('gui', parent_package, top_path) - config.add_subpackage('_glutils') - config.add_subpackage('qt') - config.add_subpackage('plot') - config.add_subpackage('fit') - config.add_subpackage('hdf5') - config.add_subpackage('widgets') - config.add_subpackage('test') - config.add_subpackage('plot3d') - config.add_subpackage('data') - config.add_subpackage('dialog') - config.add_subpackage('utils') - config.add_subpackage('utils.glutils') - config.add_subpackage('utils.test') - - return config - - -if __name__ == "__main__": - from numpy.distutils.core import setup - - setup(configuration=configuration) diff --git a/src/silx/gui/test/__init__.py b/src/silx/gui/test/__init__.py index 00d6216..d9e06fc 100644 --- a/src/silx/gui/test/__init__.py +++ b/src/silx/gui/test/__init__.py @@ -1,4 +1,3 @@ -# coding: utf-8 # /*########################################################################## # # Copyright (c) 2016-2020 European Synchrotron Radiation Facility diff --git a/src/silx/gui/test/test_colors.py b/src/silx/gui/test/test_colors.py index fa87d7d..b0e6139 100755 --- a/src/silx/gui/test/test_colors.py +++ b/src/silx/gui/test/test_colors.py @@ -1,4 +1,3 @@ -# coding: utf-8 # /*########################################################################## # # Copyright (c) 2015-2020 European Synchrotron Radiation Facility @@ -25,8 +24,6 @@ """This module provides the Colormap object """ -from __future__ import absolute_import - __authors__ = ["H.Payno"] __license__ = "MIT" __date__ = "09/11/2018" diff --git a/src/silx/gui/test/test_console.py b/src/silx/gui/test/test_console.py index 21f3564..f636287 100644 --- a/src/silx/gui/test/test_console.py +++ b/src/silx/gui/test/test_console.py @@ -1,4 +1,3 @@ -# coding: utf-8 # /*########################################################################## # # Copyright (c) 2016 European Synchrotron Radiation Facility @@ -24,8 +23,6 @@ # ###########################################################################*/ """Basic tests for IPython console widget""" -from __future__ import print_function - __authors__ = ["P. Knobel"] __license__ = "MIT" __date__ = "05/12/2016" diff --git a/src/silx/gui/test/test_icons.py b/src/silx/gui/test/test_icons.py index 154adf6..59c7e00 100644 --- a/src/silx/gui/test/test_icons.py +++ b/src/silx/gui/test/test_icons.py @@ -1,4 +1,3 @@ -# coding: utf-8 # /*########################################################################## # # Copyright (c) 2016-2021 European Synchrotron Radiation Facility diff --git a/src/silx/gui/test/test_qt.py b/src/silx/gui/test/test_qt.py index 8554744..692d7f7 100644 --- a/src/silx/gui/test/test_qt.py +++ b/src/silx/gui/test/test_qt.py @@ -1,4 +1,3 @@ -# coding: utf-8 # /*########################################################################## # # Copyright (c) 2016-2021 European Synchrotron Radiation Facility diff --git a/src/silx/gui/test/utils.py b/src/silx/gui/test/utils.py index db4c0ee..1cfee67 100644 --- a/src/silx/gui/test/utils.py +++ b/src/silx/gui/test/utils.py @@ -1,4 +1,3 @@ -# coding: utf-8 # /*########################################################################## # # Copyright (c) 2016-2018 European Synchrotron Radiation Facility @@ -24,8 +23,6 @@ # ###########################################################################*/ """Color conversion function, color dictionary and colormap tools.""" -from __future__ import absolute_import - __authors__ = ["V. Valls"] __license__ = "MIT" __date__ = "05/10/2018" diff --git a/src/silx/gui/utils/__init__.py b/src/silx/gui/utils/__init__.py index 726ad74..4fae646 100755 --- a/src/silx/gui/utils/__init__.py +++ b/src/silx/gui/utils/__init__.py @@ -1,4 +1,3 @@ -# coding: utf-8 # /*########################################################################## # # Copyright (c) 2018-2019 European Synchrotron Radiation Facility diff --git a/src/silx/gui/utils/concurrent.py b/src/silx/gui/utils/concurrent.py index c27374f..242e804 100644 --- a/src/silx/gui/utils/concurrent.py +++ b/src/silx/gui/utils/concurrent.py @@ -1,4 +1,3 @@ -# coding: utf-8 # /*########################################################################## # # Copyright (c) 2018 European Synchrotron Radiation Facility @@ -25,8 +24,6 @@ """This module allows to run a function in Qt main thread from another thread """ -from __future__ import absolute_import - __authors__ = ["T. Vincent"] __license__ = "MIT" __date__ = "09/03/2018" diff --git a/src/silx/gui/utils/glutils/__init__.py b/src/silx/gui/utils/glutils/__init__.py index 20e611e..2651402 100644 --- a/src/silx/gui/utils/glutils/__init__.py +++ b/src/silx/gui/utils/glutils/__init__.py @@ -1,4 +1,3 @@ -# coding: utf-8 # /*########################################################################## # # Copyright (c) 2020-2021 European Synchrotron Radiation Facility @@ -52,14 +51,15 @@ class _isOpenGLAvailableResult: return '<_isOpenGLAvailableResult: %s, "%s">' % (self.status, self.error) -def _runtimeOpenGLCheck(version): +def _runtimeOpenGLCheck(version, shareOpenGLContexts): """Run OpenGL check in a subprocess. This is done by starting a subprocess that displays a Qt OpenGL widget. :param List[int] version: The minimal required OpenGL version as a 2-tuple (major, minor). - Default: (2, 1) + :param bool shareOpenGLContexts: + True to test the `QApplication` with `AA_ShareOpenGLContexts`. :return: An error string that is empty if no error occured :rtype: str """ @@ -69,10 +69,10 @@ def _runtimeOpenGLCheck(version): [os.path.abspath(p) for p in sys.path]) try: - error = subprocess.check_output( - [sys.executable, '-s', '-S', __file__, major, minor], - env=env, - timeout=2) + cmd = [sys.executable, '-s', '-S', __file__, major, minor] + if shareOpenGLContexts: + cmd.append("--shareOpenGLContexts") + error = subprocess.check_output(cmd, env=env, timeout=2) except subprocess.TimeoutExpired: status = False error = "Qt OpenGL widget hang" @@ -90,7 +90,7 @@ def _runtimeOpenGLCheck(version): _runtimeCheckCache = {} # Cache runtime check results: {version: result} -def isOpenGLAvailable(version=(2, 1), runtimeCheck=True): +def isOpenGLAvailable(version=(2, 1), runtimeCheck=True, shareOpenGLContexts=False): """Check if OpenGL is available through Qt and actually working. After some basic tests, this is done by starting a subprocess that @@ -99,8 +99,12 @@ def isOpenGLAvailable(version=(2, 1), runtimeCheck=True): :param List[int] version: The minimal required OpenGL version as a 2-tuple (major, minor). Default: (2, 1) + :param bool shareOpenGLContexts: + True to test the `QApplication` with `AA_ShareOpenGLContexts`. + This only can be checked with `runtimeCheck` enabled. + Default is false. :param bool runtimeCheck: - True (default) to run the test creating a Qt OpenGL widgt in a subprocess, + True (default) to run the test creating a Qt OpenGL widget in a subprocess, False to avoid this check. :return: A result object that evaluates to True if successful and which has a `status` boolean attribute (True if successful) and @@ -131,12 +135,13 @@ def isOpenGLAvailable(version=(2, 1), runtimeCheck=True): result = _isOpenGLAvailableResult(error == '', error) + keyCache = version, shareOpenGLContexts if result: # No error so far, runtime check - if version in _runtimeCheckCache: # Use cache - result = _runtimeCheckCache[version] + if keyCache in _runtimeCheckCache: # Use cache + result = _runtimeCheckCache[keyCache] elif runtimeCheck: # Run test in subprocess - result = _runtimeOpenGLCheck(version) - _runtimeCheckCache[version] = result + result = _runtimeOpenGLCheck(version, shareOpenGLContexts) + _runtimeCheckCache[keyCache] = result return result @@ -178,9 +183,12 @@ if __name__ == "__main__": parser = argparse.ArgumentParser() parser.add_argument('major') parser.add_argument('minor') + parser.add_argument('--shareOpenGLContexts', action="store_true") args = parser.parse_args(args=sys.argv[1:]) + if args.shareOpenGLContexts: + qt.QCoreApplication.setAttribute(qt.Qt.AA_ShareOpenGLContexts) app = qt.QApplication([]) window = qt.QMainWindow(flags= qt.Qt.Popup | diff --git a/src/silx/gui/utils/image.py b/src/silx/gui/utils/image.py index 96f50ab..1757e3e 100644 --- a/src/silx/gui/utils/image.py +++ b/src/silx/gui/utils/image.py @@ -1,7 +1,6 @@ -# coding: utf-8 # /*########################################################################## # -# Copyright (c) 2017-2021 European Synchrotron Radiation Facility +# Copyright (c) 2017-2022 European Synchrotron Radiation Facility # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal @@ -28,9 +27,6 @@ - :func:`convertQImageToArray` """ -from __future__ import division - - __authors__ = ["T. Vincent"] __license__ = "MIT" __date__ = "04/09/2018" @@ -118,6 +114,8 @@ def convertQImageToArray(image): ptr = image.bits() if qt.BINDING == 'PyQt5': ptr.setsize(image.byteCount()) + elif qt.BINDING == 'PyQt6': + ptr.setsize(image.sizeInBytes()) elif qt.BINDING in ('PySide2', 'PySide6'): ptr = ptr.tobytes() else: diff --git a/src/silx/gui/utils/matplotlib.py b/src/silx/gui/utils/matplotlib.py index 90257f8..277a303 100644 --- a/src/silx/gui/utils/matplotlib.py +++ b/src/silx/gui/utils/matplotlib.py @@ -1,7 +1,6 @@ -# coding: utf-8 # /*########################################################################## # -# Copyright (c) 2016-2021 European Synchrotron Radiation Facility +# Copyright (c) 2016-2022 European Synchrotron Radiation Facility # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal @@ -23,8 +22,6 @@ # # ###########################################################################*/ -from __future__ import absolute_import - """This module initializes matplotlib and sets-up the backend to use. It MUST be imported prior to any other import of matplotlib. @@ -38,27 +35,116 @@ __license__ = "MIT" __date__ = "02/05/2018" +import io from pkg_resources import parse_version import matplotlib +import numpy from .. import qt +def rasterMathText(text, font, size=-1, weight=-1, italic=False, devicePixelRatio=1.0): + """Raster text using matplotlib supporting latex-like math syntax. + + 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) + """ + # Implementation adapted from: + # https://github.com/matplotlib/matplotlib/blob/d624571a19aec7c7d4a24123643288fc27db17e7/lib/matplotlib/mathtext.py#L264 + + # Lazy import to avoid imports before setting matplotlib's rcParams + from matplotlib.font_manager import FontProperties + from matplotlib.mathtext import MathTextParser + from matplotlib import figure + + dpi = 96 # default + qapp = qt.QApplication.instance() + if qapp: + screen = qapp.primaryScreen() + if screen: + dpi = screen.logicalDotsPerInchY() + + # Make sure dpi is even, it causes issues with array reshape otherwise + dpi = ((dpi * devicePixelRatio) // 2) * 2 + + stripped_text = text.strip("\n") + + parser = MathTextParser("path") + width, height, depth, _, _ = parser.parse(stripped_text, dpi=dpi) + width *= 2 + height *= 2 * (stripped_text.count("\n") + 1) + + if not isinstance(font, qt.QFont): + font = qt.QFont(font, size, weight, italic) + prop = FontProperties( + family=font.family(), + style="italic" if font.italic() else "normal", + weight=10 * font.weight(), + size=font.pointSize(), + ) + + fig = figure.Figure(figsize=(width / dpi, height / dpi)) + fig.text(0, depth / height, stripped_text, fontproperties=prop) + with io.BytesIO() as buffer: + fig.savefig(buffer, dpi=dpi, format="raw") + buffer.seek(0) + image = numpy.frombuffer(buffer.read(), dtype=numpy.uint8).reshape( + int(height), int(width), 4 + ) + + # RGB to inverted R channel + array = 255 - image[:, :, 0] + + # Remove leading and trailing empty columns/rows but one on each side + filled_rows = numpy.nonzero(numpy.sum(array, axis=1))[0] + filled_columns = numpy.nonzero(numpy.sum(array, axis=0))[0] + if len(filled_rows) == 0 or len(filled_columns) == 0: + return array, image.shape[0] - 1 + + clipped_array = numpy.ascontiguousarray( + array[ + max(0, filled_rows[0] - 1) : filled_rows[-1] + 2, + max(0, filled_columns[0] - 1) : filled_columns[-1] + 2, + ] + ) + + return clipped_array, image.shape[0] - 1 # baseline not available + + def _matplotlib_use(backend, force): """Wrapper of `matplotlib.use` to set-up backend. - It adds extra initialization for PySide2 with matplotlib < 2.2. + It adds extra initialization for PySide2 with matplotlib < 2.2. """ # This is kept for compatibility with matplotlib < 2.2 - if (parse_version(matplotlib.__version__) < parse_version('2.2') and - qt.BINDING == 'PySide2'): - matplotlib.rcParams['backend.qt5'] = 'PySide2' + if ( + parse_version(matplotlib.__version__) < parse_version("2.2") + and qt.BINDING == "PySide2" + ): + matplotlib.rcParams["backend.qt5"] = "PySide2" matplotlib.use(backend, force=force) -if qt.BINDING in ('PySide6', 'PyQt5', 'PySide2'): - _matplotlib_use('Qt5Agg', force=False) +if qt.BINDING in ("PySide6", "PyQt6", "PyQt5", "PySide2"): + _matplotlib_use("Qt5Agg", force=False) from matplotlib.backends.backend_qt5agg import FigureCanvasQTAgg # noqa else: diff --git a/src/silx/gui/utils/projecturl.py b/src/silx/gui/utils/projecturl.py index 0832c2e..116017e 100644 --- a/src/silx/gui/utils/projecturl.py +++ b/src/silx/gui/utils/projecturl.py @@ -1,4 +1,3 @@ -# coding: utf-8 # # Project: Azimuthal integration # https://github.com/silx-kit/silx @@ -23,8 +22,6 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN # THE SOFTWARE. -from __future__ import absolute_import, print_function, division - """Provide convenient URL for silx-kit projects.""" __author__ = "Valentin Valls" diff --git a/src/silx/gui/utils/qtutils.py b/src/silx/gui/utils/qtutils.py index 9682913..d686a48 100755 --- a/src/silx/gui/utils/qtutils.py +++ b/src/silx/gui/utils/qtutils.py @@ -1,4 +1,3 @@ -# coding: utf-8
# /*##########################################################################
#
# Copyright (c) 2020 European Synchrotron Radiation Facility
diff --git a/src/silx/gui/utils/signal.py b/src/silx/gui/utils/signal.py index 359f5cc..cd376a9 100644 --- a/src/silx/gui/utils/signal.py +++ b/src/silx/gui/utils/signal.py @@ -1,5 +1,4 @@ #!/usr/bin/env python -# coding: utf-8 # /*########################################################################## # # Copyright (c) 2012 University of North Carolina at Chapel Hill, Luke Campagnola diff --git a/src/silx/gui/utils/test/__init__.py b/src/silx/gui/utils/test/__init__.py index 15cd186..7a8edb9 100755 --- a/src/silx/gui/utils/test/__init__.py +++ b/src/silx/gui/utils/test/__init__.py @@ -1,4 +1,3 @@ -# coding: utf-8 # /*########################################################################## # # Copyright (c) 2018-2020 European Synchrotron Radiation Facility diff --git a/src/silx/gui/utils/test/test.py b/src/silx/gui/utils/test/test.py index 0208d64..42bf5a2 100644 --- a/src/silx/gui/utils/test/test.py +++ b/src/silx/gui/utils/test/test.py @@ -1,4 +1,3 @@ -# coding: utf-8 # /*########################################################################## # # Copyright (c) 2019-2021 European Synchrotron Radiation Facility @@ -24,8 +23,6 @@ # ###########################################################################*/ """Test of functions available in silx.gui.utils module.""" -from __future__ import absolute_import - __authors__ = ["T. Vincent"] __license__ = "MIT" __date__ = "01/08/2019" diff --git a/src/silx/gui/utils/test/test_async.py b/src/silx/gui/utils/test/test_async.py index 7304ca9..1fd8509 100644 --- a/src/silx/gui/utils/test/test_async.py +++ b/src/silx/gui/utils/test/test_async.py @@ -1,4 +1,3 @@ -# coding: utf-8 # /*########################################################################## # # Copyright (c) 2018 European Synchrotron Radiation Facility @@ -24,8 +23,6 @@ # ###########################################################################*/ """Test of async module.""" -from __future__ import absolute_import - __authors__ = ["T. Vincent"] __license__ = "MIT" __date__ = "09/03/2018" diff --git a/src/silx/gui/utils/test/test_glutils.py b/src/silx/gui/utils/test/test_glutils.py index 7c9831b..4921f16 100644 --- a/src/silx/gui/utils/test/test_glutils.py +++ b/src/silx/gui/utils/test/test_glutils.py @@ -1,4 +1,3 @@ -# coding: utf-8 # /*########################################################################## # # Copyright (c) 2020 European Synchrotron Radiation Facility @@ -30,26 +29,24 @@ __date__ = "15/01/2020" import logging -import unittest +import pytest + from silx.gui.utils.glutils import isOpenGLAvailable _logger = logging.getLogger(__name__) -class TestIsOpenGLAvailable(unittest.TestCase): - """Test isOpenGLAvailable""" - - def test(self): - for version in ((2, 1), (2, 1), (1000, 1)): - with self.subTest(version=version): - result = isOpenGLAvailable(version=version) - _logger.info("isOpenGLAvailable returned: %s", str(result)) - if version[0] == 1000: - self.assertFalse(result) - if not result: - self.assertFalse(result.status) - self.assertTrue(len(result.error) > 0) - else: - self.assertTrue(result.status) - self.assertTrue(len(result.error) == 0) +@pytest.mark.parametrize("params", (((2, 1), False), ((2, 1), False), ((1000, 1), False), ((2, 1), True))) +def testOpenGLAvailable(params): + version, shareOpenGLContexts = params + result = isOpenGLAvailable(version=version, shareOpenGLContexts=shareOpenGLContexts) + _logger.info("isOpenGLAvailable returned: %s", str(result)) + if version[0] == 1000: + assert not result + if not result: + assert not result.status + assert len(result.error) > 0 + else: + assert result.status + assert len(result.error) == 0 diff --git a/src/silx/gui/utils/test/test_image.py b/src/silx/gui/utils/test/test_image.py index 62316b0..07bc396 100644 --- a/src/silx/gui/utils/test/test_image.py +++ b/src/silx/gui/utils/test/test_image.py @@ -1,4 +1,3 @@ -# coding: utf-8 # /*########################################################################## # # Copyright (c) 2017-2018 European Synchrotron Radiation Facility diff --git a/src/silx/gui/utils/test/test_qtutils.py b/src/silx/gui/utils/test/test_qtutils.py index c00280b..c5ff2d2 100755 --- a/src/silx/gui/utils/test/test_qtutils.py +++ b/src/silx/gui/utils/test/test_qtutils.py @@ -1,4 +1,3 @@ -# coding: utf-8 # /*########################################################################## # # Copyright (c) 2019 European Synchrotron Radiation Facility @@ -24,8 +23,6 @@ # ###########################################################################*/ """Test of functions available in silx.gui.utils module.""" -from __future__ import absolute_import - __authors__ = ["T. Vincent"] __license__ = "MIT" __date__ = "01/08/2019" diff --git a/src/silx/gui/utils/test/test_testutils.py b/src/silx/gui/utils/test/test_testutils.py index 07294a7..e8a0123 100644 --- a/src/silx/gui/utils/test/test_testutils.py +++ b/src/silx/gui/utils/test/test_testutils.py @@ -1,4 +1,3 @@ -# coding: utf-8 # /*########################################################################## # # Copyright (c) 2017-2019 European Synchrotron Radiation Facility diff --git a/src/silx/gui/utils/testutils.py b/src/silx/gui/utils/testutils.py index 40c8237..1ec9b0b 100644 --- a/src/silx/gui/utils/testutils.py +++ b/src/silx/gui/utils/testutils.py @@ -1,7 +1,6 @@ -# coding: utf-8 # /*########################################################################## # -# Copyright (c) 2016-2021 European Synchrotron Radiation Facility +# Copyright (c) 2016-2022 European Synchrotron Radiation Facility # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal @@ -26,7 +25,7 @@ __authors__ = ["T. Vincent"] __license__ = "MIT" -__date__ = "05/10/2018" +__date__ = "22/07/2022" import gc @@ -49,6 +48,8 @@ elif qt.BINDING == 'PyQt5': from PyQt5.QtTest import QTest elif qt.BINDING == 'PySide6': from PySide6.QtTest import QTest +elif qt.BINDING == 'PyQt6': + from PyQt6.QtTest import QTest else: raise ImportError('Unsupported Qt bindings') @@ -140,10 +141,14 @@ class TestCaseQt(unittest.TestCase): def _currentTestSucceeded(self): if hasattr(self, '_outcome'): - # For Python >= 3.4 - result = self.defaultTestResult() # these 2 methods have no side effects - if hasattr(self._outcome, 'errors'): - self._feedErrorsToResult(result, self._outcome.errors) + if hasattr(self, '_feedErrorsToResult'): + # For Python 3.4 -3.10 + result = self.defaultTestResult() # these 2 methods have no side effects + if hasattr(self._outcome, 'errors'): + self._feedErrorsToResult(result, self._outcome.errors) + else: + # Python 3.11+ + result = self._outcome.result else: # For Python < 3.4 result = getattr(self, '_outcomeForDoCleanups', self._resultForDoCleanups) @@ -155,11 +160,11 @@ class TestCaseQt(unittest.TestCase): def _checkForUnreleasedWidgets(self): """Test fixture checking that no more widgets exists.""" - gc.collect() - if self.__previousWidgets is None: return # Do not test for leaking widgets with PySide2 + gc.collect() + widgets = [widget for widget in self.qapp.allWidgets() if (widget not in self.__previousWidgets and _inspect.createdByPython(widget))] @@ -253,7 +258,7 @@ class TestCaseQt(unittest.TestCase): See QTest.mouseClick for details. """ if modifier is None: - modifier = qt.Qt.KeyboardModifiers() + modifier = self.qapp.keyboardModifiers() pos = qt.QPoint(int(pos[0]), int(pos[1])) if pos is not None else qt.QPoint() QTest.mouseClick(widget, button, modifier, pos, delay) self.qWait(20) @@ -264,7 +269,7 @@ class TestCaseQt(unittest.TestCase): See QTest.mouseDClick for details. """ if modifier is None: - modifier = qt.Qt.KeyboardModifiers() + modifier = self.qapp.keyboardModifiers() pos = qt.QPoint(int(pos[0]), int(pos[1])) if pos is not None else qt.QPoint() QTest.mouseDClick(widget, button, modifier, pos, delay) self.qWait(20) @@ -284,7 +289,7 @@ class TestCaseQt(unittest.TestCase): See QTest.mousePress for details. """ if modifier is None: - modifier = qt.Qt.KeyboardModifiers() + modifier = self.qapp.keyboardModifiers() pos = qt.QPoint(int(pos[0]), int(pos[1])) if pos is not None else qt.QPoint() QTest.mousePress(widget, button, modifier, pos, delay) self.qWait(20) @@ -295,7 +300,7 @@ class TestCaseQt(unittest.TestCase): See QTest.mouseRelease for details. """ if modifier is None: - modifier = qt.Qt.KeyboardModifiers() + modifier = self.qapp.keyboardModifiers() pos = qt.QPoint(int(pos[0]), int(pos[1])) if pos is not None else qt.QPoint() QTest.mouseRelease(widget, button, modifier, pos, delay) self.qWait(20) @@ -486,7 +491,7 @@ def getQToolButtonFromAction(action): :param QAction action: The QAction from which to get QToolButton. :return: A QToolButton associated to action or None. """ - if qt.BINDING == "PySide6": + if qt.BINDING in ("PySide6", "PyQt6"): widgets = action.associatedObjects() else: widgets = action.associatedWidgets() diff --git a/src/silx/gui/widgets/BoxLayoutDockWidget.py b/src/silx/gui/widgets/BoxLayoutDockWidget.py index 3d2b853..aa45153 100644 --- a/src/silx/gui/widgets/BoxLayoutDockWidget.py +++ b/src/silx/gui/widgets/BoxLayoutDockWidget.py @@ -1,4 +1,3 @@ -# coding: utf-8 # /*########################################################################## # # Copyright (c) 2018 European Synchrotron Radiation Facility diff --git a/src/silx/gui/widgets/ColormapNameComboBox.py b/src/silx/gui/widgets/ColormapNameComboBox.py index fa8faf1..388b032 100644 --- a/src/silx/gui/widgets/ColormapNameComboBox.py +++ b/src/silx/gui/widgets/ColormapNameComboBox.py @@ -1,4 +1,3 @@ -# coding: utf-8 # /*########################################################################## # # Copyright (c) 2004-2018 European Synchrotron Radiation Facility @@ -25,8 +24,6 @@ """A QComboBox to display prefered colormaps """ -from __future__ import division - __authors__ = ["V.A. Sole", "T. Vincent", "H. Payno"] __license__ = "MIT" __date__ = "27/11/2018" diff --git a/src/silx/gui/widgets/ElidedLabel.py b/src/silx/gui/widgets/ElidedLabel.py index 7c6dfb5..3760ec0 100644 --- a/src/silx/gui/widgets/ElidedLabel.py +++ b/src/silx/gui/widgets/ElidedLabel.py @@ -1,4 +1,3 @@ -# coding: utf-8 # /*########################################################################## # # Copyright (c) 2004-2021 European Synchrotron Radiation Facility @@ -28,17 +27,18 @@ __license__ = "MIT" __date__ = "07/12/2018" +from ...utils.deprecation import deprecated from silx.gui import qt class ElidedLabel(qt.QLabel): - """QLabel with an edile property. + """QLabel with an elide property. - By default if the text is too big, it is elided on the right. + By default if the text is too long, it is elided on the right. This mode can be changed with :func:`setElideMode`. - In case the text is elided, the full content is displayed as part of the + In this case the text is elided, the full content is displayed as part of the tool tip. This behavior can be disabled with :func:`setTextAsToolTip`. """ @@ -83,25 +83,41 @@ class ElidedLabel(qt.QLabel): else: qt.QLabel.setToolTip(self, self.__toolTip) - # Properties + # Inherited properties + + def text(self): + """Returns the text defined by the user. + + It can be different from the one really displayed, depending on the + `elideMode` defined for this widget. + """ + return self.__text + + @deprecated(replacement='text', since_version='1.1.0') + def getText(self): + return self.text() def setText(self, text): self.__text = text self.__updateText() - def getText(self): - return self.__text + def toolTip(self): + """Returns the tooltip defined by the user. - text = qt.Property(str, getText, setText) + It can be different from the one really displayed, if `textAsToolTip` was + set to true. + """ + return self.__toolTip + + @deprecated(replacement='toolTip', since_version='1.1.0') + def getToolTip(self): + return self.toolTip() def setToolTip(self, toolTip): self.__toolTip = toolTip self.__updateToolTip() - def getToolTip(self): - return self.__toolTip - - toolTip = qt.Property(str, getToolTip, setToolTip) + # New properties def setElideMode(self, elideMode): """Set the elide mode. @@ -118,7 +134,7 @@ class ElidedLabel(qt.QLabel): """ return self.__elideMode - elideMode = qt.Property(qt.Qt.TextElideMode, getToolTip, setToolTip) + elideMode = qt.Property(qt.Qt.TextElideMode, getElideMode, setElideMode) def setTextAsToolTip(self, enabled): """Enable displaying text as part of the tooltip if it is elided. diff --git a/src/silx/gui/widgets/FloatEdit.py b/src/silx/gui/widgets/FloatEdit.py index 08ed67d..61f518f 100644 --- a/src/silx/gui/widgets/FloatEdit.py +++ b/src/silx/gui/widgets/FloatEdit.py @@ -1,4 +1,3 @@ -# coding: utf-8 # /*########################################################################## # # Copyright (c) 2004-2021 European Synchrotron Radiation Facility @@ -25,8 +24,6 @@ """Module contains a float editor """ -from __future__ import division - __authors__ = ["V.A. Sole", "T. Vincent"] __license__ = "MIT" __date__ = "02/10/2017" diff --git a/src/silx/gui/widgets/FlowLayout.py b/src/silx/gui/widgets/FlowLayout.py index 3c4c9dd..917aa09 100644 --- a/src/silx/gui/widgets/FlowLayout.py +++ b/src/silx/gui/widgets/FlowLayout.py @@ -1,4 +1,3 @@ -# coding: utf-8 # /*########################################################################## # # Copyright (c) 2018 European Synchrotron Radiation Facility @@ -25,8 +24,6 @@ """This module provides a flow layout for QWidget: :class:`FlowLayout`. """ -from __future__ import division - __authors__ = ["T. Vincent"] __license__ = "MIT" __date__ = "20/07/2018" diff --git a/src/silx/gui/widgets/FormGridLayout.py b/src/silx/gui/widgets/FormGridLayout.py new file mode 100644 index 0000000..6068d30 --- /dev/null +++ b/src/silx/gui/widgets/FormGridLayout.py @@ -0,0 +1,74 @@ +# /*########################################################################## +# +# Copyright (c) 2022 European Synchrotron Radiation Facility +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# 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 form layout for QWidget: :class:`FormGridLayout`. +""" + +__authors__ = ["V. Valls"] +__license__ = "MIT" +__date__ = "29/09/2022" + + +import typing +from .. import qt + + +class FormGridLayout(qt.QGridLayout): + """A layout with the API of :class:`qt.QFormLayout` based on a :class:`qt.QGridLayout`. + + This allow a bit more flexibility, like allow vertical expanding + of the rows. + """ + def __init__(self, parent): + super(FormGridLayout, self).__init__(parent) + self.__cursor = 0 + + def _addCell(self, something, row, column, rowSpan=1, columnSpan=1): + if isinstance(something, qt.QLayout): + self.addLayout(something, row, column, rowSpan, columnSpan) + else: + if isinstance(something, str): + something = qt.QLabel(something) + self.addWidget(something, row, column, rowSpan, columnSpan) + + def addRow(self, label: typing.Union[str, qt.QWidget, qt.QLayout], field: typing.Union[None, qt.QWidget, qt.QLayout] = None): + """ + Adds a new row to the bottom of this form layout. + + If field is defined, the given label and field are added. + + Else, the label is a widget and spans both columns. + """ + if field is None: + self._addCell(label, self.__cursor, 0, 1, 2) + else: + self._addCell(label, self.__cursor, 0) + self._addCell(field, self.__cursor, 1) + self.__cursor += 1 + + def addItem(self, item: qt.QLayoutItem): + """ + Adds a new layout item to the bottom of this form layout. + """ + super(FormGridLayout, self).addItem(item) + self.__cursor += 1 diff --git a/src/silx/gui/widgets/FrameBrowser.py b/src/silx/gui/widgets/FrameBrowser.py index 671991f..17a9148 100644 --- a/src/silx/gui/widgets/FrameBrowser.py +++ b/src/silx/gui/widgets/FrameBrowser.py @@ -1,4 +1,3 @@ -# coding: utf-8 # /*########################################################################## # # Copyright (c) 2016-2018 European Synchrotron Radiation Facility diff --git a/src/silx/gui/widgets/HierarchicalTableView.py b/src/silx/gui/widgets/HierarchicalTableView.py index 3ccf4c7..6e6329b 100644 --- a/src/silx/gui/widgets/HierarchicalTableView.py +++ b/src/silx/gui/widgets/HierarchicalTableView.py @@ -1,4 +1,3 @@ -# coding: utf-8 # /*########################################################################## # # Copyright (c) 2016-2017 European Synchrotron Radiation Facility diff --git a/src/silx/gui/widgets/LegendIconWidget.py b/src/silx/gui/widgets/LegendIconWidget.py index 1c95e41..d0d2f5c 100755 --- a/src/silx/gui/widgets/LegendIconWidget.py +++ b/src/silx/gui/widgets/LegendIconWidget.py @@ -1,4 +1,3 @@ -# coding: utf-8 # /*########################################################################## # # Copyright (c) 2004-2018 European Synchrotron Radiation Facility diff --git a/src/silx/gui/widgets/MedianFilterDialog.py b/src/silx/gui/widgets/MedianFilterDialog.py index dd4a00d..982736c 100644 --- a/src/silx/gui/widgets/MedianFilterDialog.py +++ b/src/silx/gui/widgets/MedianFilterDialog.py @@ -1,4 +1,3 @@ -# coding: utf-8 # /*########################################################################## # # Copyright (c) 2017-2018 European Synchrotron Radiation Facility diff --git a/src/silx/gui/widgets/MultiModeAction.py b/src/silx/gui/widgets/MultiModeAction.py index 502275d..b40d285 100644 --- a/src/silx/gui/widgets/MultiModeAction.py +++ b/src/silx/gui/widgets/MultiModeAction.py @@ -1,4 +1,3 @@ -# coding: utf-8 # /*########################################################################## # # Copyright (c) 2004-2018 European Synchrotron Radiation Facility diff --git a/src/silx/gui/widgets/PeriodicTable.py b/src/silx/gui/widgets/PeriodicTable.py index 6fed109..1fc3bab 100644 --- a/src/silx/gui/widgets/PeriodicTable.py +++ b/src/silx/gui/widgets/PeriodicTable.py @@ -1,4 +1,3 @@ -# coding: utf-8 # /*########################################################################## # # Copyright (c) 2004-2021 European Synchrotron Radiation Facility diff --git a/src/silx/gui/widgets/PrintGeometryDialog.py b/src/silx/gui/widgets/PrintGeometryDialog.py index 98ff8d1..db905fb 100644 --- a/src/silx/gui/widgets/PrintGeometryDialog.py +++ b/src/silx/gui/widgets/PrintGeometryDialog.py @@ -1,4 +1,3 @@ -# coding: utf-8 # /*########################################################################## # # Copyright (c) 2017-2021 European Synchrotron Radiation Facility diff --git a/src/silx/gui/widgets/PrintPreview.py b/src/silx/gui/widgets/PrintPreview.py index 53e0a1f..dd6af1f 100644 --- a/src/silx/gui/widgets/PrintPreview.py +++ b/src/silx/gui/widgets/PrintPreview.py @@ -1,4 +1,3 @@ -# coding: utf-8 # /*########################################################################## # # Copyright (c) 2004-2021 European Synchrotron Radiation Facility diff --git a/src/silx/gui/widgets/RangeSlider.py b/src/silx/gui/widgets/RangeSlider.py index 61b73fc..4db0470 100644 --- a/src/silx/gui/widgets/RangeSlider.py +++ b/src/silx/gui/widgets/RangeSlider.py @@ -1,4 +1,3 @@ -# coding: utf-8 # /*########################################################################## # # Copyright (c) 2015-2021 European Synchrotron Radiation Facility @@ -27,7 +26,6 @@ .. image:: img/RangeSlider.png :align: center """ -from __future__ import absolute_import, division __authors__ = ["D. Naudet", "T. Vincent"] __license__ = "MIT" diff --git a/src/silx/gui/widgets/TableWidget.py b/src/silx/gui/widgets/TableWidget.py index 50eb9e2..9bada5e 100644 --- a/src/silx/gui/widgets/TableWidget.py +++ b/src/silx/gui/widgets/TableWidget.py @@ -1,4 +1,3 @@ -# coding: utf-8 # /*########################################################################## # # Copyright (c) 2004-2021 European Synchrotron Radiation Facility diff --git a/src/silx/gui/widgets/ThreadPoolPushButton.py b/src/silx/gui/widgets/ThreadPoolPushButton.py index 949b6ef..8a1d428 100644 --- a/src/silx/gui/widgets/ThreadPoolPushButton.py +++ b/src/silx/gui/widgets/ThreadPoolPushButton.py @@ -1,4 +1,3 @@ -# coding: utf-8 # /*########################################################################## # # Copyright (c) 2016-2018 European Synchrotron Radiation Facility diff --git a/src/silx/gui/widgets/WaitingPushButton.py b/src/silx/gui/widgets/WaitingPushButton.py index 443dc9a..8bd9ea0 100644 --- a/src/silx/gui/widgets/WaitingPushButton.py +++ b/src/silx/gui/widgets/WaitingPushButton.py @@ -1,4 +1,3 @@ -# coding: utf-8 # /*########################################################################## # # Copyright (c) 2004-2021 European Synchrotron Radiation Facility diff --git a/src/silx/gui/widgets/__init__.py b/src/silx/gui/widgets/__init__.py index 9d0299d..cab7ef6 100644 --- a/src/silx/gui/widgets/__init__.py +++ b/src/silx/gui/widgets/__init__.py @@ -1,4 +1,3 @@ -# coding: utf-8 # /*########################################################################## # # Copyright (c) 2016-2018 European Synchrotron Radiation Facility diff --git a/src/silx/gui/widgets/setup.py b/src/silx/gui/widgets/setup.py deleted file mode 100644 index e96ac8d..0000000 --- a/src/silx/gui/widgets/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__ = "11/10/2016" - - -from numpy.distutils.misc_util import Configuration - - -def configuration(parent_package='', top_path=None): - config = Configuration('widgets', parent_package, top_path) - config.add_subpackage('test') - return config - - -if __name__ == "__main__": - from numpy.distutils.core import setup - setup(configuration=configuration) diff --git a/src/silx/gui/widgets/test/__init__.py b/src/silx/gui/widgets/test/__init__.py index 243dbc7..03af6f2 100644 --- a/src/silx/gui/widgets/test/__init__.py +++ b/src/silx/gui/widgets/test/__init__.py @@ -1,4 +1,3 @@ -# coding: utf-8 # /*########################################################################## # # Copyright (c) 2020 European Synchrotron Radiation Facility diff --git a/src/silx/gui/widgets/test/test_boxlayoutdockwidget.py b/src/silx/gui/widgets/test/test_boxlayoutdockwidget.py index 5df8df9..dd0ddf4 100644 --- a/src/silx/gui/widgets/test/test_boxlayoutdockwidget.py +++ b/src/silx/gui/widgets/test/test_boxlayoutdockwidget.py @@ -1,4 +1,3 @@ -# coding: utf-8 # /*########################################################################## # # Copyright (c) 2018 European Synchrotron Radiation Facility diff --git a/src/silx/gui/widgets/test/test_elidedlabel.py b/src/silx/gui/widgets/test/test_elidedlabel.py index 693e43c..d7e2cdc 100644 --- a/src/silx/gui/widgets/test/test_elidedlabel.py +++ b/src/silx/gui/widgets/test/test_elidedlabel.py @@ -1,7 +1,6 @@ -# coding: utf-8 # /*########################################################################## # -# Copyright (c) 2020 European Synchrotron Radiation Facility +# Copyright (c) 2020-2022 European Synchrotron Radiation Facility # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal @@ -27,8 +26,6 @@ __license__ = "MIT" __date__ = "08/06/2020" -import unittest - from silx.gui import qt from silx.gui.widgets.ElidedLabel import ElidedLabel from silx.gui.utils import testutils @@ -47,11 +44,18 @@ class TestElidedLabel(testutils.TestCaseQt): del self.label self.qapp.processEvents() + def testQLabelApi(self): + """Test overrided API from QLabel""" + self.label.setText("a") + assert self.label.text() == "a" + self.label.setToolTip("b") + assert self.label.toolTip() == "b" + def testElidedValue(self): """Test elided text""" raw = "mmmmmmmmmmmmmmmmmmmmmmmmmmmmmmm" self.label.setText(raw) - self.label.setFixedWidth(30) + self.label.setFixedWidth(40) displayedText = qt.QLabel.text(self.label) self.assertNotEqual(raw, displayedText) self.assertIn("…", displayedText) @@ -98,3 +102,21 @@ class TestElidedLabel(testutils.TestCaseQt): displayedTooltip = qt.QLabel.toolTip(self.label) self.assertNotIn(raw1, displayedTooltip) self.assertIn(raw2, displayedTooltip) + + def testTooltip(self): + """Test tooltip when elided""" + self.label.setToolTip("Fooo") + assert self.label.toolTip() == "Fooo" + displayedTooltip = qt.QLabel.toolTip(self.label) + assert displayedTooltip == "Fooo" + + def testElidedTextAndTooltip(self): + """Test tooltip when elided""" + raw1 = "nnnnnnnnnnnnnnnnnnnnnnnnnnnnnnn" + self.label.setText(raw1) + self.label.setFixedWidth(30) + self.label.setToolTip("Fooo") + displayedTooltip = qt.QLabel.toolTip(self.label) + assert self.label.toolTip() == "Fooo" + assert "Fooo" in displayedTooltip + assert raw1 in displayedTooltip diff --git a/src/silx/gui/widgets/test/test_flowlayout.py b/src/silx/gui/widgets/test/test_flowlayout.py index 85d7cfe..07f6697 100644 --- a/src/silx/gui/widgets/test/test_flowlayout.py +++ b/src/silx/gui/widgets/test/test_flowlayout.py @@ -1,4 +1,3 @@ -# coding: utf-8 # /*########################################################################## # # Copyright (c) 2018 European Synchrotron Radiation Facility diff --git a/src/silx/gui/widgets/test/test_framebrowser.py b/src/silx/gui/widgets/test/test_framebrowser.py index 8233622..7fa621b 100644 --- a/src/silx/gui/widgets/test/test_framebrowser.py +++ b/src/silx/gui/widgets/test/test_framebrowser.py @@ -1,4 +1,3 @@ -# coding: utf-8 # /*########################################################################## # # Copyright (c) 2018 European Synchrotron Radiation Facility diff --git a/src/silx/gui/widgets/test/test_hierarchicaltableview.py b/src/silx/gui/widgets/test/test_hierarchicaltableview.py index 302086a..8f6a2a0 100644 --- a/src/silx/gui/widgets/test/test_hierarchicaltableview.py +++ b/src/silx/gui/widgets/test/test_hierarchicaltableview.py @@ -1,4 +1,3 @@ -# coding: utf-8 # /*########################################################################## # # Copyright (c) 2016-2021 European Synchrotron Radiation Facility diff --git a/src/silx/gui/widgets/test/test_legendiconwidget.py b/src/silx/gui/widgets/test/test_legendiconwidget.py index fe320f6..cfebc62 100644 --- a/src/silx/gui/widgets/test/test_legendiconwidget.py +++ b/src/silx/gui/widgets/test/test_legendiconwidget.py @@ -1,4 +1,3 @@ -# coding: utf-8 # /*########################################################################## # # Copyright (c) 2020 European Synchrotron Radiation Facility diff --git a/src/silx/gui/widgets/test/test_periodictable.py b/src/silx/gui/widgets/test/test_periodictable.py index de9e1af..f687e36 100644 --- a/src/silx/gui/widgets/test/test_periodictable.py +++ b/src/silx/gui/widgets/test/test_periodictable.py @@ -1,4 +1,3 @@ -# coding: utf-8 # /*########################################################################## # # Copyright (c) 2016-2017 European Synchrotron Radiation Facility diff --git a/src/silx/gui/widgets/test/test_printpreview.py b/src/silx/gui/widgets/test/test_printpreview.py index 8602666..b703d63 100644 --- a/src/silx/gui/widgets/test/test_printpreview.py +++ b/src/silx/gui/widgets/test/test_printpreview.py @@ -1,4 +1,3 @@ -# coding: utf-8 # /*########################################################################## # # Copyright (c) 2017 European Synchrotron Radiation Facility diff --git a/src/silx/gui/widgets/test/test_rangeslider.py b/src/silx/gui/widgets/test/test_rangeslider.py index f829857..6ed50af 100644 --- a/src/silx/gui/widgets/test/test_rangeslider.py +++ b/src/silx/gui/widgets/test/test_rangeslider.py @@ -1,4 +1,3 @@ -# coding: utf-8 # /*########################################################################## # # Copyright (c) 2018 European Synchrotron Radiation Facility diff --git a/src/silx/gui/widgets/test/test_tablewidget.py b/src/silx/gui/widgets/test/test_tablewidget.py index 09122ca..9b1e53f 100644 --- a/src/silx/gui/widgets/test/test_tablewidget.py +++ b/src/silx/gui/widgets/test/test_tablewidget.py @@ -1,4 +1,3 @@ -# coding: utf-8 # /*########################################################################## # # Copyright (c) 2016 European Synchrotron Radiation Facility diff --git a/src/silx/gui/widgets/test/test_threadpoolpushbutton.py b/src/silx/gui/widgets/test/test_threadpoolpushbutton.py index 3808be0..a3eca33 100644 --- a/src/silx/gui/widgets/test/test_threadpoolpushbutton.py +++ b/src/silx/gui/widgets/test/test_threadpoolpushbutton.py @@ -1,4 +1,3 @@ -# coding: utf-8 # /*########################################################################## # # Copyright (c) 2016-2021 European Synchrotron Radiation Facility |