diff options
Diffstat (limited to 'silx/gui')
331 files changed, 0 insertions, 127699 deletions
diff --git a/silx/gui/__init__.py b/silx/gui/__init__.py deleted file mode 100644 index b796e20..0000000 --- a/silx/gui/__init__.py +++ /dev/null @@ -1,49 +0,0 @@ -# coding: utf-8 -# /*########################################################################## -# -# Copyright (c) 2016-2018 European Synchrotron Radiation Facility -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. -# -# ###########################################################################*/ -"""This package provides a set of Qt widgets. - -It contains the following sub-packages and modules: - -- silx.gui.colors: Functions to handle colors and colormap -- silx.gui.console: IPython console widget -- silx.gui.data: - Widgets for displaying data arrays using table views and plot widgets -- silx.gui.dialog: Specific dialog widgets -- silx.gui.fit: Widgets for controlling curve fitting -- silx.gui.hdf5: Widgets for displaying content relative to HDF5 format -- silx.gui.icons: Functions to access embedded icons -- silx.gui.plot: Widgets for 1D and 2D plotting and related tools -- silx.gui.plot3d: Widgets for visualizing data in 3D based on OpenGL -- silx.gui.printer: Shared printer used by the library -- silx.gui.qt: Common wrapper over different Python Qt binding -- silx.gui.utils: Miscellaneous helpers for Qt -- silx.gui.widgets: Miscellaneous standalone widgets - -See silx documentation: http://www.silx.org/doc/silx/latest/ -""" - -__authors__ = ["T. Vincent"] -__license__ = "MIT" -__date__ = "23/05/2016" diff --git a/silx/gui/_glutils/Context.py b/silx/gui/_glutils/Context.py deleted file mode 100644 index c62dbb9..0000000 --- a/silx/gui/_glutils/Context.py +++ /dev/null @@ -1,75 +0,0 @@ -# coding: utf-8 -# /*########################################################################## -# -# Copyright (c) 2014-2019 European Synchrotron Radiation Facility -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. -# -# ###########################################################################*/ -"""Abstraction of OpenGL context. - -It defines a way to get current OpenGL context to support multiple -OpenGL contexts. -""" - -__authors__ = ["T. Vincent"] -__license__ = "MIT" -__date__ = "25/07/2016" - -import contextlib - - -class _DEFAULT_CONTEXT(object): - """The default value for OpenGL context""" - pass - -_context = _DEFAULT_CONTEXT -"""The current OpenGL context""" - - -def getCurrent(): - """Returns platform dependent object of current OpenGL context. - - This is useful to associate OpenGL resources with the context they are - created in. - - :return: Platform specific OpenGL context - """ - return _context - - -def setCurrent(context=_DEFAULT_CONTEXT): - """Set a platform dependent OpenGL context - - :param context: Platform dependent GL context - """ - global _context - _context = context - - -@contextlib.contextmanager -def current(context): - """Context manager setting the platform-dependent GL context - - :param context: Platform dependent GL context - """ - previous_context = getCurrent() - setCurrent(context) - yield - setCurrent(previous_context) diff --git a/silx/gui/_glutils/FramebufferTexture.py b/silx/gui/_glutils/FramebufferTexture.py deleted file mode 100644 index e065030..0000000 --- a/silx/gui/_glutils/FramebufferTexture.py +++ /dev/null @@ -1,165 +0,0 @@ -# coding: utf-8 -# /*########################################################################## -# -# Copyright (c) 2014-2020 European Synchrotron Radiation Facility -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. -# -# ###########################################################################*/ -"""Association of a texture and a framebuffer object for off-screen rendering. -""" - -__authors__ = ["T. Vincent"] -__license__ = "MIT" -__date__ = "25/07/2016" - - -import logging - -from . import gl -from .Texture import Texture - - -_logger = logging.getLogger(__name__) - - -class FramebufferTexture(object): - """Framebuffer with a texture. - - Aimed at off-screen rendering to texture. - - :param internalFormat: OpenGL texture internal format - :param shape: Shape (height, width) of the framebuffer and texture - :type shape: 2-tuple of int - :param stencilFormat: Stencil renderbuffer format - :param depthFormat: Depth renderbuffer format - :param kwargs: Extra arguments for :class:`Texture` constructor - """ - - _PACKED_FORMAT = gl.GL_DEPTH24_STENCIL8, gl.GL_DEPTH_STENCIL - - def __init__(self, - internalFormat, - shape, - stencilFormat=gl.GL_DEPTH24_STENCIL8, - depthFormat=gl.GL_DEPTH24_STENCIL8, - **kwargs): - - self._texture = Texture(internalFormat, shape=shape, **kwargs) - self._texture.prepare() - - self._previousFramebuffer = 0 # Used by with statement - - self._name = gl.glGenFramebuffers(1) - - with self: # Bind FBO - # Attachments - gl.glFramebufferTexture2D(gl.GL_FRAMEBUFFER, - gl.GL_COLOR_ATTACHMENT0, - gl.GL_TEXTURE_2D, - self._texture.name, - 0) - - height, width = self._texture.shape - - if stencilFormat is not None: - self._stencilId = gl.glGenRenderbuffers(1) - gl.glBindRenderbuffer(gl.GL_RENDERBUFFER, self._stencilId) - gl.glRenderbufferStorage(gl.GL_RENDERBUFFER, - stencilFormat, - width, height) - gl.glFramebufferRenderbuffer(gl.GL_FRAMEBUFFER, - gl.GL_STENCIL_ATTACHMENT, - gl.GL_RENDERBUFFER, - self._stencilId) - else: - self._stencilId = None - - if depthFormat is not None: - if self._stencilId and depthFormat in self._PACKED_FORMAT: - self._depthId = self._stencilId - else: - self._depthId = gl.glGenRenderbuffers(1) - gl.glBindRenderbuffer(gl.GL_RENDERBUFFER, self._depthId) - gl.glRenderbufferStorage(gl.GL_RENDERBUFFER, - depthFormat, - width, height) - gl.glFramebufferRenderbuffer(gl.GL_FRAMEBUFFER, - gl.GL_DEPTH_ATTACHMENT, - gl.GL_RENDERBUFFER, - self._depthId) - else: - self._depthId = None - - assert (gl.glCheckFramebufferStatus(gl.GL_FRAMEBUFFER) == - gl.GL_FRAMEBUFFER_COMPLETE) - - @property - def shape(self): - """Shape of the framebuffer (height, width)""" - return self._texture.shape - - @property - def texture(self): - """The texture this framebuffer is rendering to. - - The life-cycle of the texture is managed by this object""" - return self._texture - - @property - def name(self): - """OpenGL name of the framebuffer""" - if self._name is not None: - return self._name - else: - raise RuntimeError("No OpenGL framebuffer resource, \ - discard has already been called") - - def bind(self): - """Bind this framebuffer for rendering""" - gl.glBindFramebuffer(gl.GL_FRAMEBUFFER, self.name) - - # with statement - - def __enter__(self): - self._previousFramebuffer = gl.glGetInteger(gl.GL_FRAMEBUFFER_BINDING) - self.bind() - - def __exit__(self, exctype, excvalue, traceback): - gl.glBindFramebuffer(gl.GL_FRAMEBUFFER, self._previousFramebuffer) - self._previousFramebuffer = None - - def discard(self): - """Delete associated OpenGL resources including texture""" - if self._name is not None: - gl.glDeleteFramebuffers(self._name) - self._name = None - - if self._stencilId is not None: - gl.glDeleteRenderbuffers(self._stencilId) - if self._stencilId == self._depthId: - self._depthId = None - self._stencilId = None - if self._depthId is not None: - gl.glDeleteRenderbuffers(self._depthId) - self._depthId = None - - self._texture.discard() # Also discard the texture - else: - _logger.warning("Discard has already been called") diff --git a/silx/gui/_glutils/OpenGLWidget.py b/silx/gui/_glutils/OpenGLWidget.py deleted file mode 100644 index 5e3fcb8..0000000 --- a/silx/gui/_glutils/OpenGLWidget.py +++ /dev/null @@ -1,423 +0,0 @@ -# coding: utf-8 -# /*########################################################################## -# -# Copyright (c) 2017-2020 European Synchrotron Radiation Facility -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. -# -# ###########################################################################*/ -"""This package provides a compatibility layer for OpenGL widget. - -It provides a compatibility layer for Qt OpenGL widget used in silx -across Qt<=5.3 QtOpenGL.QGLWidget and QOpenGLWidget. -""" - -__authors__ = ["T. Vincent"] -__license__ = "MIT" -__date__ = "22/11/2019" - - -import logging -import sys - -from .. import qt -from ..utils.glutils import isOpenGLAvailable -from .._glutils import gl - - -_logger = logging.getLogger(__name__) - - -if not hasattr(qt, 'QOpenGLWidget') and not hasattr(qt, 'QGLWidget'): - OpenGLWidget = None - -else: - if hasattr(qt, 'QOpenGLWidget'): # PyQt>=5.4 - _logger.info('Using QOpenGLWidget') - _BaseOpenGLWidget = qt.QOpenGLWidget - - else: - _logger.info('Using QGLWidget') - _BaseOpenGLWidget = qt.QGLWidget - - class _OpenGLWidget(_BaseOpenGLWidget): - """Wrapper over QOpenGLWidget and QGLWidget""" - - sigOpenGLContextError = qt.Signal(str) - """Signal emitted when an OpenGL context error is detected at runtime. - - It provides the error reason as a str. - """ - - def __init__(self, parent, - alphaBufferSize=0, - depthBufferSize=24, - stencilBufferSize=8, - version=(2, 0), - f=qt.Qt.WindowFlags()): - # True if using QGLWidget, False if using QOpenGLWidget - self.__legacy = not hasattr(qt, 'QOpenGLWidget') - - self.__devicePixelRatio = 1.0 - self.__requestedOpenGLVersion = int(version[0]), int(version[1]) - self.__isValid = False - - if self.__legacy: # QGLWidget - format_ = qt.QGLFormat() - format_.setAlphaBufferSize(alphaBufferSize) - format_.setAlpha(alphaBufferSize != 0) - format_.setDepthBufferSize(depthBufferSize) - format_.setDepth(depthBufferSize != 0) - format_.setStencilBufferSize(stencilBufferSize) - format_.setStencil(stencilBufferSize != 0) - format_.setVersion(*self.__requestedOpenGLVersion) - format_.setDoubleBuffer(True) - - super(_OpenGLWidget, self).__init__(format_, parent, None, f) - - else: # QOpenGLWidget - super(_OpenGLWidget, self).__init__(parent, f) - - format_ = qt.QSurfaceFormat() - format_.setAlphaBufferSize(alphaBufferSize) - format_.setDepthBufferSize(depthBufferSize) - format_.setStencilBufferSize(stencilBufferSize) - format_.setVersion(*self.__requestedOpenGLVersion) - format_.setSwapBehavior(qt.QSurfaceFormat.DoubleBuffer) - self.setFormat(format_) - - # Enable receiving mouse move events when no buttons are pressed - self.setMouseTracking(True) - - def getDevicePixelRatio(self): - """Returns the ratio device-independent / device pixel size - - It should be either 1.0 or 2.0. - - :return: Scale factor between screen and Qt units - :rtype: float - """ - return self.__devicePixelRatio - - def getRequestedOpenGLVersion(self): - """Returns the requested OpenGL version. - - :return: (major, minor) - :rtype: 2-tuple of int""" - return self.__requestedOpenGLVersion - - def getOpenGLVersion(self): - """Returns the available OpenGL version. - - :return: (major, minor) - :rtype: 2-tuple of int""" - if self.__legacy: # QGLWidget - supportedVersion = 0, 0 - - # Go through all OpenGL version flags checking support - flags = self.format().openGLVersionFlags() - for version in ((1, 1), (1, 2), (1, 3), (1, 4), (1, 5), - (2, 0), (2, 1), - (3, 0), (3, 1), (3, 2), (3, 3), - (4, 0)): - versionFlag = getattr(qt.QGLFormat, - 'OpenGL_Version_%d_%d' % version) - if not versionFlag & flags: - break - supportedVersion = version - return supportedVersion - - else: # QOpenGLWidget - return self.format().version() - - # QOpenGLWidget methods - - def isValid(self): - """Returns True if OpenGL is available. - - This adds extra checks to Qt isValid method. - - :rtype: bool - """ - return self.__isValid and super(_OpenGLWidget, self).isValid() - - def defaultFramebufferObject(self): - """Returns the framebuffer object handle. - - See :meth:`QOpenGLWidget.defaultFramebufferObject` - """ - if self.__legacy: # QGLWidget - return 0 - else: # QOpenGLWidget - return super(_OpenGLWidget, self).defaultFramebufferObject() - - # *GL overridden methods - - def initializeGL(self): - parent = self.parent() - if parent is None: - _logger.error('_OpenGLWidget has no parent') - return - - # Check OpenGL version - if self.getOpenGLVersion() >= self.getRequestedOpenGLVersion(): - try: - gl.glGetError() # clear any previous error (if any) - version = gl.glGetString(gl.GL_VERSION) - except: - version = None - - if version: - self.__isValid = True - else: - errMsg = 'OpenGL not available' - if sys.platform.startswith('linux'): - errMsg += ': If connected remotely, ' \ - 'GLX forwarding might be disabled.' - _logger.error(errMsg) - self.sigOpenGLContextError.emit(errMsg) - self.__isValid = False - - else: - errMsg = 'OpenGL %d.%d not available' % \ - self.getRequestedOpenGLVersion() - _logger.error('OpenGL widget disabled: %s', errMsg) - self.sigOpenGLContextError.emit(errMsg) - self.__isValid = False - - if self.isValid(): - parent.initializeGL() - - def paintGL(self): - parent = self.parent() - if parent is None: - _logger.error('_OpenGLWidget has no parent') - return - - if qt.BINDING in ('PyQt5', 'PySide2'): - devicePixelRatio = self.window().windowHandle().devicePixelRatio() - - if devicePixelRatio != self.getDevicePixelRatio(): - # Update devicePixelRatio and call resizeOpenGL - # as resizeGL is not always called. - self.__devicePixelRatio = devicePixelRatio - self.makeCurrent() - parent.resizeGL(self.width(), self.height()) - - if self.isValid(): - parent.paintGL() - - def resizeGL(self, width, height): - parent = self.parent() - if parent is None: - _logger.error('_OpenGLWidget has no parent') - return - - if self.isValid(): - # Call parent resizeGL with device-independent pixel unit - # This works over both QGLWidget and QOpenGLWidget - parent.resizeGL(self.width(), self.height()) - - -class OpenGLWidget(qt.QWidget): - """OpenGL widget wrapper over QGLWidget and QOpenGLWidget - - This wrapper API implements a subset of QOpenGLWidget API. - The constructor takes a different set of arguments. - Methods returning object like :meth:`context` returns either - QGL* or QOpenGL* objects. - - :param parent: Parent widget see :class:`QWidget` - :param int alphaBufferSize: - Size in bits of the alpha channel (default: 0). - Set to 0 to disable alpha channel. - :param int depthBufferSize: - Size in bits of the depth buffer (default: 24). - Set to 0 to disable depth buffer. - :param int stencilBufferSize: - Size in bits of the stencil buffer (default: 8). - Set to 0 to disable stencil buffer - :param version: Requested OpenGL version (default: (2, 0)). - :type version: 2-tuple of int - :param f: see :class:`QWidget` - """ - - def __init__(self, parent=None, - alphaBufferSize=0, - depthBufferSize=24, - stencilBufferSize=8, - version=(2, 0), - f=qt.Qt.WindowFlags()): - super(OpenGLWidget, self).__init__(parent, f) - - layout = qt.QHBoxLayout(self) - layout.setContentsMargins(0, 0, 0, 0) - self.setLayout(layout) - - self.__context = None - - _check = isOpenGLAvailable(version=version, runtimeCheck=False) - if _OpenGLWidget is None or not _check: - _logger.error('OpenGL-based widget disabled: %s', _check.error) - self.__openGLWidget = None - label = self._createErrorQLabel(_check.error) - self.layout().addWidget(label) - - else: - self.__openGLWidget = _OpenGLWidget( - parent=self, - alphaBufferSize=alphaBufferSize, - depthBufferSize=depthBufferSize, - stencilBufferSize=stencilBufferSize, - version=version, - f=f) - # Async connection need, otherwise issue when hiding OpenGL - # widget while doing the rendering.. - self.__openGLWidget.sigOpenGLContextError.connect( - self._handleOpenGLInitError, qt.Qt.QueuedConnection) - self.layout().addWidget(self.__openGLWidget) - - @staticmethod - def _createErrorQLabel(error): - """Create QLabel displaying error message in place of OpenGL widget - - :param str error: The error message to display""" - label = qt.QLabel() - label.setText('OpenGL-based widget disabled:\n%s' % error) - label.setAlignment(qt.Qt.AlignCenter) - label.setWordWrap(True) - return label - - def _handleOpenGLInitError(self, error): - """Handle runtime errors in OpenGL widget""" - if self.__openGLWidget is not None: - self.__openGLWidget.setVisible(False) - self.__openGLWidget.setParent(None) - self.__openGLWidget = None - - label = self._createErrorQLabel(error) - self.layout().addWidget(label) - - # Additional API - - def getDevicePixelRatio(self): - """Returns the ratio device-independent / device pixel size - - It should be either 1.0 or 2.0. - - :return: Scale factor between screen and Qt units - :rtype: float - """ - if self.__openGLWidget is None: - return 1. - else: - return self.__openGLWidget.getDevicePixelRatio() - - def getDotsPerInch(self): - """Returns current screen resolution as device pixels per inch. - - :rtype: float - """ - screen = self.window().windowHandle().screen() - if screen is not None: - # TODO check if this is correct on different OS/screen - # OK on macOS10.12/qt5.13.2 - dpi = screen.physicalDotsPerInch() * self.getDevicePixelRatio() - else: # Fallback - dpi = 96. * self.getDevicePixelRatio() - return dpi - - def getOpenGLVersion(self): - """Returns the available OpenGL version. - - :return: (major, minor) - :rtype: 2-tuple of int""" - if self.__openGLWidget is None: - return 0, 0 - else: - return self.__openGLWidget.getOpenGLVersion() - - # QOpenGLWidget API - - def isValid(self): - """Returns True if OpenGL with the requested version is available. - - :rtype: bool - """ - if self.__openGLWidget is None: - return False - else: - return self.__openGLWidget.isValid() - - def context(self): - """Return Qt OpenGL context object or None. - - See :meth:`QOpenGLWidget.context` and :meth:`QGLWidget.context` - """ - if self.__openGLWidget is None: - return None - else: - # Keep a reference on QOpenGLContext to make - # else PyQt5 keeps creating a new one. - self.__context = self.__openGLWidget.context() - return self.__context - - def defaultFramebufferObject(self): - """Returns the framebuffer object handle. - - See :meth:`QOpenGLWidget.defaultFramebufferObject` - """ - if self.__openGLWidget is None: - return 0 - else: - return self.__openGLWidget.defaultFramebufferObject() - - def makeCurrent(self): - """Make the underlying OpenGL widget's context current. - - See :meth:`QOpenGLWidget.makeCurrent` - """ - if self.__openGLWidget is not None: - self.__openGLWidget.makeCurrent() - - def update(self): - """Async update of the OpenGL widget. - - See :meth:`QOpenGLWidget.update` - """ - if self.__openGLWidget is not None: - self.__openGLWidget.update() - - # QOpenGLWidget API to override - - def initializeGL(self): - """Override to implement OpenGL initialization.""" - pass - - def paintGL(self): - """Override to implement OpenGL rendering.""" - pass - - def resizeGL(self, width, height): - """Override to implement resize of OpenGL framebuffer. - - :param int width: Width in device-independent pixels - :param int height: Height in device-independent pixels - """ - pass diff --git a/silx/gui/_glutils/Program.py b/silx/gui/_glutils/Program.py deleted file mode 100644 index 87eec5f..0000000 --- a/silx/gui/_glutils/Program.py +++ /dev/null @@ -1,202 +0,0 @@ -# coding: utf-8 -# /*########################################################################## -# -# Copyright (c) 2014-2019 European Synchrotron Radiation Facility -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. -# -# ###########################################################################*/ -"""This module provides a class to handle shader program compilation.""" - -__authors__ = ["T. Vincent"] -__license__ = "MIT" -__date__ = "25/07/2016" - - -import logging -import weakref - -import numpy - -from . import Context, gl - -_logger = logging.getLogger(__name__) - - -class Program(object): - """Wrap OpenGL shader program. - - The program is compiled lazily (i.e., at first program :meth:`use`). - When the program is compiled, it stores attributes and uniforms locations. - So, attributes and uniforms must be used after :meth:`use`. - - This object supports multiple OpenGL contexts. - - :param str vertexShader: The source of the vertex shader. - :param str fragmentShader: The source of the fragment shader. - :param str attrib0: - Attribute's name to bind to position 0 (default: 'position'). - On certain platform, this attribute MUST be active and with an - array attached to it in order for the rendering to occur.... - """ - - def __init__(self, vertexShader, fragmentShader, - attrib0='position'): - self._vertexShader = vertexShader - self._fragmentShader = fragmentShader - self._attrib0 = attrib0 - self._programs = weakref.WeakKeyDictionary() - - @staticmethod - def _compileGL(vertexShader, fragmentShader, attrib0): - program = gl.glCreateProgram() - - gl.glBindAttribLocation(program, 0, attrib0.encode('ascii')) - - vertex = gl.glCreateShader(gl.GL_VERTEX_SHADER) - gl.glShaderSource(vertex, vertexShader) - gl.glCompileShader(vertex) - if gl.glGetShaderiv(vertex, gl.GL_COMPILE_STATUS) != gl.GL_TRUE: - raise RuntimeError(gl.glGetShaderInfoLog(vertex)) - gl.glAttachShader(program, vertex) - gl.glDeleteShader(vertex) - - fragment = gl.glCreateShader(gl.GL_FRAGMENT_SHADER) - gl.glShaderSource(fragment, fragmentShader) - gl.glCompileShader(fragment) - if gl.glGetShaderiv(fragment, - gl.GL_COMPILE_STATUS) != gl.GL_TRUE: - raise RuntimeError(gl.glGetShaderInfoLog(fragment)) - gl.glAttachShader(program, fragment) - gl.glDeleteShader(fragment) - - gl.glLinkProgram(program) - if gl.glGetProgramiv(program, gl.GL_LINK_STATUS) != gl.GL_TRUE: - raise RuntimeError(gl.glGetProgramInfoLog(program)) - - attributes = {} - for index in range(gl.glGetProgramiv(program, - gl.GL_ACTIVE_ATTRIBUTES)): - name = gl.glGetActiveAttrib(program, index)[0] - namestr = name.decode('ascii') - attributes[namestr] = gl.glGetAttribLocation(program, name) - - uniforms = {} - for index in range(gl.glGetProgramiv(program, gl.GL_ACTIVE_UNIFORMS)): - name = gl.glGetActiveUniform(program, index)[0] - namestr = name.decode('ascii') - uniforms[namestr] = gl.glGetUniformLocation(program, name) - - return program, attributes, uniforms - - def _getProgramInfo(self): - glcontext = Context.getCurrent() - if glcontext not in self._programs: - raise RuntimeError( - "Program was not compiled for current OpenGL context.") - return self._programs[glcontext] - - @property - def attributes(self): - """Vertex attributes names and locations as a dict of {str: int}. - - WARNING: - Read-only usage. - To use only with a valid OpenGL context and after :meth:`use` - has been called for this context. - """ - return self._getProgramInfo()[1] - - @property - def uniforms(self): - """Program uniforms names and locations as a dict of {str: int}. - - WARNING: - Read-only usage. - To use only with a valid OpenGL context and after :meth:`use` - has been called for this context. - """ - return self._getProgramInfo()[2] - - @property - def program(self): - """OpenGL id of the program. - - WARNING: - To use only with a valid OpenGL context and after :meth:`use` - has been called for this context. - """ - return self._getProgramInfo()[0] - - # def discard(self): - # pass # Not implemented yet - - def use(self): - """Make use of the program, compiling it if necessary""" - glcontext = Context.getCurrent() - - if glcontext not in self._programs: - self._programs[glcontext] = self._compileGL( - self._vertexShader, - self._fragmentShader, - self._attrib0) - - if _logger.getEffectiveLevel() <= logging.DEBUG: - gl.glValidateProgram(self.program) - if gl.glGetProgramiv( - self.program, gl.GL_VALIDATE_STATUS) != gl.GL_TRUE: - _logger.debug('Cannot validate program: %s', - gl.glGetProgramInfoLog(self.program)) - - gl.glUseProgram(self.program) - - def setUniformMatrix(self, name, value, transpose=True, safe=False): - """Wrap glUniformMatrix[2|3|4]fv - - :param str name: The name of the uniform. - :param value: The 2D matrix (or the array of matrices, 3D). - Matrices are 2x2, 3x3 or 4x4. - :type value: numpy.ndarray with 2 or 3 dimensions of float32 - :param bool transpose: Whether to transpose (True, default) or not. - :param bool safe: False: raise an error if no uniform with this name; - True: silently ignores it. - - :raises KeyError: if no uniform corresponds to name. - """ - assert value.dtype == numpy.float32 - - shape = value.shape - assert len(shape) in (2, 3) - assert shape[-1] in (2, 3, 4) - assert shape[-1] == shape[-2] # As in OpenGL|ES 2.0 - - location = self.uniforms.get(name) - if location is not None: - count = 1 if len(shape) == 2 else shape[0] - transpose = gl.GL_TRUE if transpose else gl.GL_FALSE - - if shape[-1] == 2: - gl.glUniformMatrix2fv(location, count, transpose, value) - elif shape[-1] == 3: - gl.glUniformMatrix3fv(location, count, transpose, value) - elif shape[-1] == 4: - gl.glUniformMatrix4fv(location, count, transpose, value) - - elif not safe: - raise KeyError('No uniform: %s' % name) diff --git a/silx/gui/_glutils/Texture.py b/silx/gui/_glutils/Texture.py deleted file mode 100644 index c72135a..0000000 --- a/silx/gui/_glutils/Texture.py +++ /dev/null @@ -1,352 +0,0 @@ -# coding: utf-8 -# /*########################################################################## -# -# Copyright (c) 2014-2020 European Synchrotron Radiation Facility -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. -# -# ###########################################################################*/ -"""This module provides a class wrapping OpenGL 2D and 3D texture.""" - -__authors__ = ["T. Vincent"] -__license__ = "MIT" -__date__ = "04/10/2016" - - -try: - from collections import abc -except ImportError: # Python2 support - import collections as abc - -from ctypes import c_void_p -import logging - -import numpy - -from . import gl, utils - - -_logger = logging.getLogger(__name__) - - -class Texture(object): - """Base class to wrap OpenGL 2D and 3D texture - - :param internalFormat: OpenGL texture internal format - :param data: The data to copy to the texture or None for an empty texture - :type data: numpy.ndarray or None - :param format_: Input data format if different from internalFormat - :param shape: If data is None, shape of the texture - (height, width) or (depth, height, width) - :type shape: List[int] - :param int texUnit: The texture unit to use - :param minFilter: OpenGL texture minimization filter (default: GL_NEAREST) - :param magFilter: OpenGL texture magnification filter (default: GL_LINEAR) - :param wrap: Texture wrap mode for dimensions: (t, s) or (r, t, s) - If a single value is provided, it used for all dimensions. - :type wrap: OpenGL wrap mode or 2 or 3-tuple of wrap mode - """ - - def __init__(self, internalFormat, data=None, format_=None, - shape=None, texUnit=0, - minFilter=None, magFilter=None, wrap=None): - - self._internalFormat = internalFormat - if format_ is None: - format_ = self.internalFormat - - if data is None: - assert shape is not None - else: - assert shape is None - data = numpy.array(data, copy=False, order='C') - if format_ != gl.GL_RED: - shape = data.shape[:-1] # Last dimension is channels - else: - shape = data.shape - - self._deferredUpdates = [(format_, data, None)] - - assert len(shape) in (2, 3) - self._shape = tuple(shape) - self._ndim = len(shape) - - self.texUnit = texUnit - - self._texParameterUpdates = {} # Store texture params to update - - self._minFilter = minFilter if minFilter is not None else gl.GL_NEAREST - self._texParameterUpdates[gl.GL_TEXTURE_MIN_FILTER] = self._minFilter - - self._magFilter = magFilter if magFilter is not None else gl.GL_LINEAR - self._texParameterUpdates[gl.GL_TEXTURE_MAG_FILTER] = self._magFilter - - self._name = None # Store texture ID - - if wrap is not None: - if not isinstance(wrap, abc.Iterable): - wrap = [wrap] * self.ndim - - assert len(wrap) == self.ndim - - self._texParameterUpdates[gl.GL_TEXTURE_WRAP_S] = wrap[-1] - self._texParameterUpdates[gl.GL_TEXTURE_WRAP_T] = wrap[-2] - if self.ndim == 3: - self._texParameterUpdates[gl.GL_TEXTURE_WRAP_R] = wrap[0] - - @property - def target(self): - """OpenGL target type of this texture""" - return gl.GL_TEXTURE_2D if self.ndim == 2 else gl.GL_TEXTURE_3D - - @property - def ndim(self): - """The number of dimensions: 2 or 3""" - return self._ndim - - @property - def internalFormat(self): - """Texture internal format""" - return self._internalFormat - - @property - def shape(self): - """Shape of the texture: (height, width) or (depth, height, width)""" - return self._shape - - @property - def name(self): - """OpenGL texture name. - - It is None if not initialized or already discarded. - """ - return self._name - - @property - def minFilter(self): - """Minifying function parameter (GL_TEXTURE_MIN_FILTER)""" - return self._minFilter - - @minFilter.setter - def minFilter(self, minFilter): - if minFilter != self.minFilter: - self._minFilter = minFilter - self._texParameterUpdates[gl.GL_TEXTURE_MIN_FILTER] = minFilter - - @property - def magFilter(self): - """Magnification function parameter (GL_TEXTURE_MAG_FILTER)""" - return self._magFilter - - @magFilter.setter - def magFilter(self, magFilter): - if magFilter != self.magFilter: - self._magFilter = magFilter - self._texParameterUpdates[gl.GL_TEXTURE_MAG_FILTER] = magFilter - - def _isPrepareRequired(self) -> bool: - """Returns True if OpenGL texture needs to be updated. - - :rtype: bool - """ - return (self._name is None or - self._texParameterUpdates or - self._deferredUpdates) - - def _prepareAndBind(self, texUnit=None): - """Synchronizes the OpenGL texture""" - if self._name is None: - self._name = gl.glGenTextures(1) - - self._bind(texUnit) - - # Synchronizes texture parameters - for pname, param in self._texParameterUpdates.items(): - gl.glTexParameter(self.target, pname, param) - self._texParameterUpdates = {} - - # Copy data to texture - for format_, data, offset in self._deferredUpdates: - gl.glPixelStorei(gl.GL_UNPACK_ALIGNMENT, 1) - - # This are the defaults, useless to set if not modified - # gl.glPixelStorei(gl.GL_UNPACK_ROW_LENGTH, 0) - # gl.glPixelStorei(gl.GL_UNPACK_SKIP_PIXELS, 0) - # gl.glPixelStorei(gl.GL_UNPACK_SKIP_ROWS, 0) - # gl.glPixelStorei(gl.GL_UNPACK_IMAGE_HEIGHT, 0) - # gl.glPixelStorei(gl.GL_UNPACK_SKIP_IMAGES, 0) - - if data is None: - data = c_void_p(0) - type_ = gl.GL_UNSIGNED_BYTE - else: - type_ = utils.numpyToGLType(data.dtype) - - if offset is None: # Initialize texture - if self.ndim == 2: - _logger.debug( - 'Creating 2D texture shape: (%d, %d),' - ' internal format: %s, format: %s, type: %s', - self.shape[0], self.shape[1], - str(self.internalFormat), str(format_), str(type_)) - - gl.glTexImage2D( - gl.GL_TEXTURE_2D, - 0, - self.internalFormat, - self.shape[1], - self.shape[0], - 0, - format_, - type_, - data) - - else: - _logger.debug( - 'Creating 3D texture shape: (%d, %d, %d),' - ' internal format: %s, format: %s, type: %s', - self.shape[0], self.shape[1], self.shape[2], - str(self.internalFormat), str(format_), str(type_)) - - gl.glTexImage3D( - gl.GL_TEXTURE_3D, - 0, - self.internalFormat, - self.shape[2], - self.shape[1], - self.shape[0], - 0, - format_, - type_, - data) - - else: # Update already existing texture - if self.ndim == 2: - gl.glTexSubImage2D(gl.GL_TEXTURE_2D, - 0, - offset[1], - offset[0], - data.shape[1], - data.shape[0], - format_, - type_, - data) - - else: - gl.glTexSubImage3D(gl.GL_TEXTURE_3D, - 0, - offset[2], - offset[1], - offset[0], - data.shape[2], - data.shape[1], - data.shape[0], - format_, - type_, - data) - - self._deferredUpdates = [] - - def _bind(self, texUnit=None): - """Bind the texture to a texture unit. - - :param int texUnit: The texture unit to use - """ - if texUnit is None: - texUnit = self.texUnit - gl.glActiveTexture(gl.GL_TEXTURE0 + texUnit) - gl.glBindTexture(self.target, self.name) - - def _unbind(self, texUnit=None): - """Reset texture binding to a texture unit. - - :param int texUnit: The texture unit to use - """ - if texUnit is None: - texUnit = self.texUnit - gl.glActiveTexture(gl.GL_TEXTURE0 + texUnit) - gl.glBindTexture(self.target, 0) - - def prepare(self): - """Synchronizes the OpenGL texture. - - This method must be called with a current OpenGL context. - """ - if self._isPrepareRequired(): - self._prepareAndBind() - self._unbind() - - def bind(self, texUnit=None): - """Bind the texture to a texture unit. - - The OpenGL texture is updated if needed. - - This method must be called with a current OpenGL context. - - :param int texUnit: The texture unit to use - """ - if self._isPrepareRequired(): - self._prepareAndBind(texUnit) - else: - self._bind(texUnit) - - def discard(self): - """Delete associated OpenGL texture. - - This method must be called with a current OpenGL context. - """ - if self._name is not None: - gl.glDeleteTextures(self._name) - self._name = None - else: - _logger.warning("Texture not initialized or already discarded") - - # with statement - - def __enter__(self): - self.bind() - - def __exit__(self, exc_type, exc_val, exc_tb): - self._unbind() - - def update(self, format_, data, offset=(0, 0, 0), copy=True): - """Update the content of the texture. - - Texture is not resized, so data must fit into texture with the - given offset. - - This update is performed lazily during next call to - :meth:`prepare` or :meth:`bind`. - Data MUST not be changed until then. - - :param format_: The OpenGL format of the data - :param data: The data to use to update the texture - :param List[int] offset: Offset in the texture where to copy the data - :param bool copy: - True (default) to copy data, False to use as is (do not modify) - """ - data = numpy.array(data, copy=copy, order='C') - offset = tuple(offset) - - assert data.ndim == self.ndim - assert len(offset) >= self.ndim - for i in range(self.ndim): - assert offset[i] + data.shape[i] <= self.shape[i] - - self._deferredUpdates.append((format_, data, offset)) diff --git a/silx/gui/_glutils/VertexBuffer.py b/silx/gui/_glutils/VertexBuffer.py deleted file mode 100644 index b74b748..0000000 --- a/silx/gui/_glutils/VertexBuffer.py +++ /dev/null @@ -1,266 +0,0 @@ -# coding: utf-8 -# /*########################################################################## -# -# Copyright (c) 2014-2017 European Synchrotron Radiation Facility -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. -# -# ###########################################################################*/ -"""This module provides a class managing an OpenGL vertex buffer.""" - -__authors__ = ["T. Vincent"] -__license__ = "MIT" -__date__ = "10/01/2017" - - -import logging -from ctypes import c_void_p -import numpy - -from . import gl -from .utils import numpyToGLType, sizeofGLType - - -_logger = logging.getLogger(__name__) - - -class VertexBuffer(object): - """Object handling an OpenGL vertex buffer object - - :param data: Data used to fill the vertex buffer - :type data: numpy.ndarray or None - :param int size: Size in bytes of the buffer or None for data size - :param usage: OpenGL vertex buffer expected usage pattern: - GL_STREAM_DRAW, GL_STATIC_DRAW (default) or GL_DYNAMIC_DRAW - :param target: Target buffer: - GL_ARRAY_BUFFER (default) or GL_ELEMENT_ARRAY_BUFFER - """ - # OpenGL|ES 2.0 subset: - _USAGES = gl.GL_STREAM_DRAW, gl.GL_STATIC_DRAW, gl.GL_DYNAMIC_DRAW - _TARGETS = gl.GL_ARRAY_BUFFER, gl.GL_ELEMENT_ARRAY_BUFFER - - def __init__(self, - data=None, - size=None, - usage=None, - target=None): - if usage is None: - usage = gl.GL_STATIC_DRAW - assert usage in self._USAGES - - if target is None: - target = gl.GL_ARRAY_BUFFER - assert target in self._TARGETS - - self._target = target - self._usage = usage - - self._name = gl.glGenBuffers(1) - self.bind() - - if data is None: - assert size is not None - self._size = size - gl.glBufferData(self._target, - self._size, - c_void_p(0), - self._usage) - else: - data = numpy.array(data, copy=False, order='C') - if size is not None: - assert size <= data.nbytes - - self._size = size or data.nbytes - gl.glBufferData(self._target, - self._size, - data, - self._usage) - - gl.glBindBuffer(self._target, 0) - - @property - def target(self): - """The target buffer of the vertex buffer""" - return self._target - - @property - def usage(self): - """The expected usage of the vertex buffer""" - return self._usage - - @property - def name(self): - """OpenGL Vertex Buffer object name (int)""" - if self._name is not None: - return self._name - else: - raise RuntimeError("No OpenGL buffer resource, \ - discard has already been called") - - @property - def size(self): - """Size in bytes of the Vertex Buffer Object (int)""" - if self._size is not None: - return self._size - else: - raise RuntimeError("No OpenGL buffer resource, \ - discard has already been called") - - def bind(self): - """Bind the vertex buffer""" - gl.glBindBuffer(self._target, self.name) - - def update(self, data, offset=0, size=None): - """Update vertex buffer content. - - :param numpy.ndarray data: The data to put in the vertex buffer - :param int offset: Offset in bytes in the buffer where to put the data - :param int size: If provided, size of data to copy - """ - data = numpy.array(data, copy=False, order='C') - if size is None: - size = data.nbytes - assert offset + size <= self.size - with self: - gl.glBufferSubData(self._target, offset, size, data) - - def discard(self): - """Delete the vertex buffer""" - if self._name is not None: - gl.glDeleteBuffers(self._name) - self._name = None - self._size = None - else: - _logger.warning("Discard has already been called") - - # with statement - - def __enter__(self): - self.bind() - - def __exit__(self, exctype, excvalue, traceback): - gl.glBindBuffer(self._target, 0) - - -class VertexBufferAttrib(object): - """Describes data stored in a vertex buffer - - Convenient class to store info for glVertexAttribPointer calls - - :param VertexBuffer vbo: The vertex buffer storing the data - :param int type_: The OpenGL type of the data - :param int size: The number of data elements stored in the VBO - :param int dimension: The number of `type_` element(s) in [1, 4] - :param int offset: Start offset of data in the vertex buffer - :param int stride: Data stride in the vertex buffer - """ - - _GL_TYPES = gl.GL_UNSIGNED_BYTE, gl.GL_FLOAT, gl.GL_INT - - def __init__(self, - vbo, - type_, - size, - dimension=1, - offset=0, - stride=0, - normalization=False): - self.vbo = vbo - assert type_ in self._GL_TYPES - self.type_ = type_ - self.size = size - assert 1 <= dimension <= 4 - self.dimension = dimension - self.offset = offset - self.stride = stride - self.normalization = bool(normalization) - - @property - def itemsize(self): - """Size in bytes of a vertex buffer element (int)""" - return self.dimension * sizeofGLType(self.type_) - - itemSize = itemsize # Backward compatibility - - def setVertexAttrib(self, attribute): - """Call glVertexAttribPointer with objects information""" - normalization = gl.GL_TRUE if self.normalization else gl.GL_FALSE - with self.vbo: - gl.glVertexAttribPointer(attribute, - self.dimension, - self.type_, - normalization, - self.stride, - c_void_p(self.offset)) - - def copy(self): - return VertexBufferAttrib(self.vbo, - self.type_, - self.size, - self.dimension, - self.offset, - self.stride, - self.normalization) - - -def vertexBuffer(arrays, prefix=None, suffix=None, usage=None): - """Create a single vertex buffer from multiple 1D or 2D numpy arrays. - - It is possible to reserve memory before and after each array in the VBO - - :param arrays: Arrays of data to store - :type arrays: Iterable of numpy.ndarray - :param prefix: If given, number of elements to reserve before each array - :type prefix: Iterable of int or None - :param suffix: If given, number of elements to reserve after each array - :type suffix: Iterable of int or None - :param int usage: vertex buffer expected usage or None for default - :returns: List of VertexBufferAttrib objects sharing the same vertex buffer - """ - info = [] - vbosize = 0 - - if prefix is None: - prefix = (0,) * len(arrays) - if suffix is None: - suffix = (0,) * len(arrays) - - for data, pre, post in zip(arrays, prefix, suffix): - data = numpy.array(data, copy=False, order='C') - shape = data.shape - assert len(shape) <= 2 - type_ = numpyToGLType(data.dtype) - size = shape[0] + pre + post - dimension = 1 if len(shape) == 1 else shape[1] - sizeinbytes = size * dimension * sizeofGLType(type_) - sizeinbytes = 4 * ((sizeinbytes + 3) >> 2) # 4 bytes alignment - copyoffset = vbosize + pre * dimension * sizeofGLType(type_) - info.append((data, type_, size, dimension, - vbosize, sizeinbytes, copyoffset)) - vbosize += sizeinbytes - - vbo = VertexBuffer(size=vbosize, usage=usage) - - result = [] - for data, type_, size, dimension, offset, sizeinbytes, copyoffset in info: - copysize = data.shape[0] * dimension * sizeofGLType(type_) - vbo.update(data, offset=copyoffset, size=copysize) - result.append( - VertexBufferAttrib(vbo, type_, size, dimension, offset, 0)) - return result diff --git a/silx/gui/_glutils/__init__.py b/silx/gui/_glutils/__init__.py deleted file mode 100644 index e88affd..0000000 --- a/silx/gui/_glutils/__init__.py +++ /dev/null @@ -1,43 +0,0 @@ -# coding: utf-8 -# /*########################################################################## -# -# Copyright (c) 2014-2019 European Synchrotron Radiation Facility -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. -# -# ###########################################################################*/ -"""This package provides utility functions to handle OpenGL resources. - -The :mod:`gl` module provides a wrapper to OpenGL based on PyOpenGL. -""" - -__authors__ = ["T. Vincent"] -__license__ = "MIT" -__date__ = "25/07/2016" - - -# OpenGL convenient functions -from .OpenGLWidget import OpenGLWidget # noqa -from . import Context # noqa -from .FramebufferTexture import FramebufferTexture # noqa -from .Program import Program # noqa -from .Texture import Texture # noqa -from .VertexBuffer import VertexBuffer, VertexBufferAttrib, vertexBuffer # noqa -from .utils import sizeofGLType, isSupportedGLType, numpyToGLType # noqa -from .utils import segmentTrianglesIntersection # noqa diff --git a/silx/gui/_glutils/font.py b/silx/gui/_glutils/font.py deleted file mode 100644 index 6a4c489..0000000 --- a/silx/gui/_glutils/font.py +++ /dev/null @@ -1,163 +0,0 @@ -# coding: utf-8 -# /*########################################################################## -# -# Copyright (c) 2016-2020 European Synchrotron Radiation Facility -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. -# -# ###########################################################################*/ -"""Text rasterisation feature leveraging Qt font and text layout support.""" - -__authors__ = ["T. Vincent"] -__license__ = "MIT" -__date__ = "13/10/2016" - - -import logging -import numpy - -from ..utils.image import convertQImageToArray -from .. import qt - -_logger = logging.getLogger(__name__) - - -def getDefaultFontFamily(): - """Returns the default font family of the application""" - return qt.QApplication.instance().font().family() - - -# Font weights -ULTRA_LIGHT = 0 -"""Lightest characters: Minimum font weight""" - -LIGHT = 25 -"""Light characters""" - -NORMAL = 50 -"""Normal characters""" - -SEMI_BOLD = 63 -"""Between normal and bold characters""" - -BOLD = 74 -"""Thicker characters""" - -BLACK = 87 -"""Really thick characters""" - -ULTRA_BLACK = 99 -"""Thickest characters: Maximum font weight""" - - -def rasterText(text, font, - size=-1, - weight=-1, - italic=False, - devicePixelRatio=1.0): - """Raster text using Qt. - - It supports multiple lines. - - :param str text: The text to raster - :param font: Font name or QFont to use - :type font: str or :class:`QFont` - :param int size: - Font size in points - Used only if font is given as name. - :param int weight: - Font weight in [0, 99], see QFont.Weight. - Used only if font is given as name. - :param bool italic: - True for italic font (default: False). - Used only if font is given as name. - :param float devicePixelRatio: - The current ratio between device and device-independent pixel - (default: 1.0) - :return: Corresponding image in gray scale and baseline offset from top - :rtype: (HxW numpy.ndarray of uint8, int) - """ - if not text: - _logger.info("Trying to raster empty text, replaced by white space") - text = ' ' # Replace empty text by white space to produce an image - - if (devicePixelRatio != 1.0 and - not hasattr(qt.QImage, 'setDevicePixelRatio')): # Qt 4 - _logger.error('devicePixelRatio not supported') - devicePixelRatio = 1.0 - - if not isinstance(font, qt.QFont): - font = qt.QFont(font, size, weight, italic) - - # get text size - image = qt.QImage(1, 1, qt.QImage.Format_RGB888) - painter = qt.QPainter() - painter.begin(image) - painter.setPen(qt.Qt.white) - painter.setFont(font) - bounds = painter.boundingRect( - qt.QRect(0, 0, 4096, 4096), qt.Qt.TextExpandTabs, text) - painter.end() - - metrics = qt.QFontMetrics(font) - - # This does not provide the correct text bbox on macOS - # size = metrics.size(qt.Qt.TextExpandTabs, text) - # bounds = metrics.boundingRect( - # qt.QRect(0, 0, size.width(), size.height()), - # qt.Qt.TextExpandTabs, - # text) - - # Add extra border and handle devicePixelRatio - width = bounds.width() * devicePixelRatio + 2 - # align line size to 32 bits to ease conversion to numpy array - width = 4 * ((width + 3) // 4) - image = qt.QImage(int(width), - int(bounds.height() * devicePixelRatio + 2), - qt.QImage.Format_RGB888) - if (devicePixelRatio != 1.0 and - hasattr(image, 'setDevicePixelRatio')): # Qt 5 - image.setDevicePixelRatio(devicePixelRatio) - - # TODO if Qt5 use Format_Grayscale8 instead - image.fill(0) - - # Raster text - painter = qt.QPainter() - painter.begin(image) - painter.setPen(qt.Qt.white) - painter.setFont(font) - painter.drawText(bounds, qt.Qt.TextExpandTabs, text) - painter.end() - - array = convertQImageToArray(image) - - # RGB to R - array = numpy.ascontiguousarray(array[:, :, 0]) - - # Remove leading and trailing empty columns but one on each side - column_cumsum = numpy.cumsum(numpy.sum(array, axis=0)) - array = array[:, column_cumsum.argmin():column_cumsum.argmax() + 2] - - # Remove leading and trailing empty rows but one on each side - row_cumsum = numpy.cumsum(numpy.sum(array, axis=1)) - min_row = row_cumsum.argmin() - array = array[min_row:row_cumsum.argmax() + 2, :] - - return array, metrics.ascent() - min_row diff --git a/silx/gui/_glutils/gl.py b/silx/gui/_glutils/gl.py deleted file mode 100644 index 608d9ce..0000000 --- a/silx/gui/_glutils/gl.py +++ /dev/null @@ -1,168 +0,0 @@ -# coding: utf-8 -# /*########################################################################## -# -# Copyright (c) 2014-2017 European Synchrotron Radiation Facility -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. -# -# ###########################################################################*/ -"""This module loads PyOpenGL and provides a namespace for OpenGL.""" - -__authors__ = ["T. Vincent"] -__license__ = "MIT" -__date__ = "25/07/2016" - - -from contextlib import contextmanager as _contextmanager -from ctypes import c_uint -import logging - -_logger = logging.getLogger(__name__) - -import OpenGL -# Set the following to true for debugging -if _logger.getEffectiveLevel() <= logging.DEBUG: - _logger.debug('Enabling PyOpenGL debug flags') - OpenGL.ERROR_LOGGING = True - OpenGL.ERROR_CHECKING = True - OpenGL.ERROR_ON_COPY = True -else: - OpenGL.ERROR_LOGGING = False - OpenGL.ERROR_CHECKING = False - OpenGL.ERROR_ON_COPY = False - -import OpenGL.GL as _GL -from OpenGL.GL import * # noqa - -# Extentions core in OpenGL 3 -from OpenGL.GL.ARB import framebuffer_object as _FBO -from OpenGL.GL.ARB.framebuffer_object import * # noqa -from OpenGL.GL.ARB.texture_rg import GL_R32F, GL_R16F # noqa -from OpenGL.GL.ARB.texture_rg import GL_R16, GL_R8 # noqa - -# PyOpenGL 3.0.1 does not define it -try: - GLchar -except NameError: - from ctypes import c_char - GLchar = c_char - - -def testGL(): - """Test if required OpenGL version and extensions are available. - - This MUST be run with an active OpenGL context. - """ - version = glGetString(GL_VERSION).split()[0] # get version number - major, minor = int(version[0]), int(version[2]) - if major < 2 or (major == 2 and minor < 1): - raise RuntimeError( - "Requires at least OpenGL version 2.1, running with %s" % version) - - from OpenGL.GL.ARB.framebuffer_object import glInitFramebufferObjectARB - from OpenGL.GL.ARB.texture_rg import glInitTextureRgARB - - if not glInitFramebufferObjectARB(): - raise RuntimeError( - "OpenGL GL_ARB_framebuffer_object extension required !") - - if not glInitTextureRgARB(): - raise RuntimeError("OpenGL GL_ARB_texture_rg extension required !") - - -# Additional setup -if hasattr(glget, 'addGLGetConstant'): - glget.addGLGetConstant(GL_FRAMEBUFFER_BINDING, (1,)) - - -@_contextmanager -def enabled(capacity, enable=True): - """Context manager enabling an OpenGL capacity. - - This is not checking the current state of the capacity. - - :param capacity: The OpenGL capacity enum to enable/disable - :param bool enable: - True (default) to enable during context, False to disable - """ - if bool(enable) == glGetBoolean(capacity): - # Already in the right state: noop - yield - elif enable: - glEnable(capacity) - yield - glDisable(capacity) - else: - glDisable(capacity) - yield - glEnable(capacity) - - -def disabled(capacity, disable=True): - """Context manager disabling an OpenGL capacity. - - This is not checking the current state of the capacity. - - :param capacity: The OpenGL capacity enum to disable/enable - :param bool disable: - True (default) to disable during context, False to enable - """ - return enabled(capacity, not disable) - - -# Additional OpenGL wrapping - -def glGetActiveAttrib(program, index): - """Wrap PyOpenGL glGetActiveAttrib""" - bufsize = glGetProgramiv(program, GL_ACTIVE_ATTRIBUTE_MAX_LENGTH) - length = GLsizei() - size = GLint() - type_ = GLenum() - name = (GLchar * bufsize)() - - _GL.glGetActiveAttrib(program, index, bufsize, length, size, type_, name) - return name.value, size.value, type_.value - - -def glDeleteRenderbuffers(buffers): - if not hasattr(buffers, '__len__'): # Support single int argument - buffers = [buffers] - length = len(buffers) - _FBO.glDeleteRenderbuffers(length, (c_uint * length)(*buffers)) - - -def glDeleteFramebuffers(buffers): - if not hasattr(buffers, '__len__'): # Support single int argument - buffers = [buffers] - length = len(buffers) - _FBO.glDeleteFramebuffers(length, (c_uint * length)(*buffers)) - - -def glDeleteBuffers(buffers): - if not hasattr(buffers, '__len__'): # Support single int argument - buffers = [buffers] - length = len(buffers) - _GL.glDeleteBuffers(length, (c_uint * length)(*buffers)) - - -def glDeleteTextures(textures): - if not hasattr(textures, '__len__'): # Support single int argument - textures = [textures] - length = len(textures) - _GL.glDeleteTextures((c_uint * length)(*textures)) diff --git a/silx/gui/_glutils/utils.py b/silx/gui/_glutils/utils.py deleted file mode 100644 index d5627ef..0000000 --- a/silx/gui/_glutils/utils.py +++ /dev/null @@ -1,121 +0,0 @@ -# coding: utf-8 -# /*########################################################################## -# -# Copyright (c) 2014-2019 European Synchrotron Radiation Facility -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. -# -# ###########################################################################*/ -"""This module provides conversion functions between OpenGL and numpy types. -""" - -__authors__ = ["T. Vincent"] -__license__ = "MIT" -__date__ = "10/01/2017" - -import numpy - -from OpenGL.constants import BYTE_SIZES as _BYTE_SIZES -from OpenGL.constants import ARRAY_TO_GL_TYPE_MAPPING as _ARRAY_TO_GL_TYPE_MAPPING - - -def sizeofGLType(type_): - """Returns the size in bytes of an element of type `type_`""" - return _BYTE_SIZES[type_] - - -def isSupportedGLType(type_): - """Test if a numpy type or dtype can be converted to a GL type.""" - return numpy.dtype(type_).char in _ARRAY_TO_GL_TYPE_MAPPING - - -def numpyToGLType(type_): - """Returns the GL type corresponding the provided numpy type or dtype.""" - return _ARRAY_TO_GL_TYPE_MAPPING[numpy.dtype(type_).char] - - -def segmentTrianglesIntersection(segment, triangles): - """Check for segment/triangles intersection. - - This is based on signed tetrahedron volume comparison. - - See A. Kensler, A., Shirley, P. - Optimizing Ray-Triangle Intersection via Automated Search. - Symposium on Interactive Ray Tracing, vol. 0, p33-38 (2006) - - :param numpy.ndarray segment: - Segment end points as a 2x3 array of coordinates - :param numpy.ndarray triangles: - Nx3x3 array of triangles - :return: (triangle indices, segment parameter, barycentric coord) - Indices of intersected triangles, "depth" along the segment - of the intersection point and barycentric coordinates of intersection - point in the triangle. - :rtype: List[numpy.ndarray] - """ - # TODO triangles from vertices + indices - # TODO early rejection? e.g., check segment bbox vs triangle bbox - segment = numpy.asarray(segment) - assert segment.ndim == 2 - assert segment.shape == (2, 3) - - triangles = numpy.asarray(triangles) - assert triangles.ndim == 3 - assert triangles.shape[1] == 3 - - # Test line/triangles intersection - d = segment[1] - segment[0] - t0s0 = segment[0] - triangles[:, 0, :] - edge01 = triangles[:, 1, :] - triangles[:, 0, :] - edge02 = triangles[:, 2, :] - triangles[:, 0, :] - - dCrossEdge02 = numpy.cross(d, edge02) - t0s0CrossEdge01 = numpy.cross(t0s0, edge01) - volume = numpy.sum(dCrossEdge02 * edge01, axis=1) - del edge01 - subVolumes = numpy.empty((len(triangles), 3), dtype=triangles.dtype) - subVolumes[:, 1] = numpy.sum(dCrossEdge02 * t0s0, axis=1) - del dCrossEdge02 - subVolumes[:, 2] = numpy.sum(t0s0CrossEdge01 * d, axis=1) - subVolumes[:, 0] = volume - subVolumes[:, 1] - subVolumes[:, 2] - intersect = numpy.logical_or( - numpy.all(subVolumes >= 0., axis=1), # All positive - numpy.all(subVolumes <= 0., axis=1)) # All negative - intersect = numpy.where(intersect)[0] # Indices of intersected triangles - - # Get barycentric coordinates - barycentric = subVolumes[intersect] / volume[intersect].reshape(-1, 1) - del subVolumes - - # Test segment/triangles intersection - volAlpha = numpy.sum(t0s0CrossEdge01[intersect] * edge02[intersect], axis=1) - t = volAlpha / volume[intersect] # segment parameter of intersected triangles - del t0s0CrossEdge01 - del edge02 - del volAlpha - del volume - - inSegmentMask = numpy.logical_and(t >= 0., t <= 1.) - intersect = intersect[inSegmentMask] - t = t[inSegmentMask] - barycentric = barycentric[inSegmentMask] - - # Sort intersecting triangles by t - indices = numpy.argsort(t) - return intersect[indices], t[indices], barycentric[indices] diff --git a/silx/gui/colors.py b/silx/gui/colors.py deleted file mode 100755 index db837b5..0000000 --- a/silx/gui/colors.py +++ /dev/null @@ -1,1326 +0,0 @@ -# coding: utf-8 -# /*########################################################################## -# -# Copyright (c) 2015-2021 European Synchrotron Radiation Facility -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. -# -# ###########################################################################*/ -"""This module provides API to manage colors. -""" - -from __future__ import absolute_import - -__authors__ = ["T. Vincent", "H.Payno"] -__license__ = "MIT" -__date__ = "29/01/2019" - -import numpy -import logging -import collections -import warnings - -from silx.gui import qt -from silx.gui.utils import blockSignals -from silx.math.combo import min_max -from silx.math import colormap as _colormap -from silx.utils.exceptions import NotEditableError -from silx.utils import deprecation -from silx.resources import resource_filename as _resource_filename - - -_logger = logging.getLogger(__name__) - -try: - import silx.gui.utils.matplotlib # noqa Initalize matplotlib - from matplotlib import cm as _matplotlib_cm - from matplotlib.pyplot import colormaps as _matplotlib_colormaps -except ImportError: - _logger.info("matplotlib not available, only embedded colormaps available") - _matplotlib_cm = None - _matplotlib_colormaps = None - - -_COLORDICT = {} -"""Dictionary of common colors.""" - -_COLORDICT['b'] = _COLORDICT['blue'] = '#0000ff' -_COLORDICT['r'] = _COLORDICT['red'] = '#ff0000' -_COLORDICT['g'] = _COLORDICT['green'] = '#00ff00' -_COLORDICT['k'] = _COLORDICT['black'] = '#000000' -_COLORDICT['w'] = _COLORDICT['white'] = '#ffffff' -_COLORDICT['pink'] = '#ff66ff' -_COLORDICT['brown'] = '#a52a2a' -_COLORDICT['orange'] = '#ff9900' -_COLORDICT['violet'] = '#6600ff' -_COLORDICT['gray'] = _COLORDICT['grey'] = '#a0a0a4' -# _COLORDICT['darkGray'] = _COLORDICT['darkGrey'] = '#808080' -# _COLORDICT['lightGray'] = _COLORDICT['lightGrey'] = '#c0c0c0' -_COLORDICT['y'] = _COLORDICT['yellow'] = '#ffff00' -_COLORDICT['m'] = _COLORDICT['magenta'] = '#ff00ff' -_COLORDICT['c'] = _COLORDICT['cyan'] = '#00ffff' -_COLORDICT['darkBlue'] = '#000080' -_COLORDICT['darkRed'] = '#800000' -_COLORDICT['darkGreen'] = '#008000' -_COLORDICT['darkBrown'] = '#660000' -_COLORDICT['darkCyan'] = '#008080' -_COLORDICT['darkYellow'] = '#808000' -_COLORDICT['darkMagenta'] = '#800080' -_COLORDICT['transparent'] = '#00000000' - - -# FIXME: It could be nice to expose a functional API instead of that attribute -COLORDICT = _COLORDICT - - -_LUT_DESCRIPTION = collections.namedtuple("_LUT_DESCRIPTION", ["source", "cursor_color", "preferred"]) -"""Description of a LUT for internal purpose.""" - - -_AVAILABLE_LUTS = collections.OrderedDict([ - ('gray', _LUT_DESCRIPTION('builtin', 'pink', True)), - ('reversed gray', _LUT_DESCRIPTION('builtin', 'pink', True)), - ('red', _LUT_DESCRIPTION('builtin', 'green', True)), - ('green', _LUT_DESCRIPTION('builtin', 'pink', True)), - ('blue', _LUT_DESCRIPTION('builtin', 'yellow', True)), - ('viridis', _LUT_DESCRIPTION('resource', 'pink', True)), - ('cividis', _LUT_DESCRIPTION('resource', 'pink', True)), - ('magma', _LUT_DESCRIPTION('resource', 'green', True)), - ('inferno', _LUT_DESCRIPTION('resource', 'green', True)), - ('plasma', _LUT_DESCRIPTION('resource', 'green', True)), - ('temperature', _LUT_DESCRIPTION('builtin', 'pink', True)), - ('jet', _LUT_DESCRIPTION('matplotlib', 'pink', True)), - ('hsv', _LUT_DESCRIPTION('matplotlib', 'black', True)), -]) -"""Description for internal porpose of all the default LUT provided by the library.""" - - -DEFAULT_MIN_LIN = 0 -"""Default min value if in linear normalization""" -DEFAULT_MAX_LIN = 1 -"""Default max value if in linear normalization""" - - -def rgba(color, colorDict=None): - """Convert color code '#RRGGBB' and '#RRGGBBAA' to a tuple (R, G, B, A) - of floats. - - It also supports RGB(A) from uint8 in [0, 255], float in [0, 1], and - QColor as color argument. - - :param str color: The color to convert - :param dict colorDict: A dictionary of color name conversion to color code - :returns: RGBA colors as floats in [0., 1.] - :rtype: tuple - """ - if colorDict is None: - colorDict = _COLORDICT - - if hasattr(color, 'getRgbF'): # QColor support - color = color.getRgbF() - - values = numpy.asarray(color).ravel() - - if values.dtype.kind in 'iuf': # integer or float - # Color is an array - assert len(values) in (3, 4) - - # Convert from integers in [0, 255] to float in [0, 1] - if values.dtype.kind in 'iu': - values = values / 255. - - # Clip to [0, 1] - values[values < 0.] = 0. - values[values > 1.] = 1. - - if len(values) == 3: - return values[0], values[1], values[2], 1. - else: - return tuple(values) - - # We assume color is a string - if not color.startswith('#'): - color = colorDict[color] - - assert len(color) in (7, 9) and color[0] == '#' - r = int(color[1:3], 16) / 255. - g = int(color[3:5], 16) / 255. - b = int(color[5:7], 16) / 255. - a = int(color[7:9], 16) / 255. if len(color) == 9 else 1. - return r, g, b, a - - -def greyed(color, colorDict=None): - """Convert color code '#RRGGBB' and '#RRGGBBAA' to a grey color - (R, G, B, A). - - It also supports RGB(A) from uint8 in [0, 255], float in [0, 1], and - QColor as color argument. - - :param str color: The color to convert - :param dict colorDict: A dictionary of color name conversion to color code - :returns: RGBA colors as floats in [0., 1.] - :rtype: tuple - """ - r, g, b, a = rgba(color=color, colorDict=colorDict) - g = 0.21 * r + 0.72 * g + 0.07 * b - return g, g, g, a - - -def asQColor(color): - """Convert color code '#RRGGBB' and '#RRGGBBAA' to a `qt.QColor`. - - It also supports RGB(A) from uint8 in [0, 255], float in [0, 1], and - QColor as color argument. - - :param str color: The color to convert - :rtype: qt.QColor - """ - color = rgba(color) - return qt.QColor.fromRgbF(*color) - - -def cursorColorForColormap(colormapName): - """Get a color suitable for overlay over a colormap. - - :param str colormapName: The name of the colormap. - :return: Name of the color. - :rtype: str - """ - description = _AVAILABLE_LUTS.get(colormapName, None) - if description is not None: - color = description.cursor_color - if color is not None: - return color - return 'black' - - -# Colormap loader - -_COLORMAP_CACHE = {} -"""Cache already used colormaps as name: color LUT""" - - -def _arrayToRgba8888(colors): - """Convert colors from a numpy array using float (0..1) int or uint - (0..255) to uint8 RGBA. - - :param numpy.ndarray colors: Array of float int or uint colors to convert - :return: colors as uint8 - :rtype: numpy.ndarray - """ - assert len(colors.shape) == 2 - assert colors.shape[1] in (3, 4) - - if colors.dtype == numpy.uint8: - pass - elif colors.dtype.kind == 'f': - # Each bin is [N, N+1[ except the last one: [255, 256] - colors = numpy.clip(colors.astype(numpy.float64) * 256, 0., 255.) - colors = colors.astype(numpy.uint8) - elif colors.dtype.kind in 'iu': - colors = numpy.clip(colors, 0, 255) - colors = colors.astype(numpy.uint8) - - if colors.shape[1] == 3: - tmp = numpy.empty((len(colors), 4), dtype=numpy.uint8) - tmp[:, 0:3] = colors - tmp[:, 3] = 255 - colors = tmp - - return colors - - -def _createColormapLut(name): - """Returns the color LUT corresponding to a colormap name - - :param str name: Name of the colormap to load - :returns: Corresponding table of colors - :rtype: numpy.ndarray - :raise ValueError: If no colormap corresponds to name - """ - description = _AVAILABLE_LUTS.get(name) - use_mpl = False - if description is not None: - if description.source == "builtin": - # Build colormap LUT - lut = numpy.zeros((256, 4), dtype=numpy.uint8) - lut[:, 3] = 255 - - if name == 'gray': - lut[:, :3] = numpy.arange(256, dtype=numpy.uint8).reshape(-1, 1) - elif name == 'reversed gray': - lut[:, :3] = numpy.arange(255, -1, -1, dtype=numpy.uint8).reshape(-1, 1) - elif name == 'red': - lut[:, 0] = numpy.arange(256, dtype=numpy.uint8) - elif name == 'green': - lut[:, 1] = numpy.arange(256, dtype=numpy.uint8) - elif name == 'blue': - lut[:, 2] = numpy.arange(256, dtype=numpy.uint8) - elif name == 'temperature': - # Red - lut[128:192, 0] = numpy.arange(2, 255, 4, dtype=numpy.uint8) - lut[192:, 0] = 255 - # Green - lut[:64, 1] = numpy.arange(0, 255, 4, dtype=numpy.uint8) - lut[64:192, 1] = 255 - lut[192:, 1] = numpy.arange(252, -1, -4, dtype=numpy.uint8) - # Blue - lut[:64, 2] = 255 - lut[64:128, 2] = numpy.arange(254, 0, -4, dtype=numpy.uint8) - else: - raise RuntimeError("Built-in colormap not implemented") - return lut - - elif description.source == "resource": - # Load colormap LUT - colors = numpy.load(_resource_filename("gui/colormaps/%s.npy" % name)) - # Convert to uint8 and add alpha channel - lut = _arrayToRgba8888(colors) - return lut - - elif description.source == "matplotlib": - use_mpl = True - - else: - raise RuntimeError("Internal LUT source '%s' unsupported" % description.source) - - # Here it expect a matplotlib LUTs - - if use_mpl: - # matplotlib is mandatory - if _matplotlib_cm is None: - raise ValueError("The colormap '%s' expect matplotlib, but matplotlib is not installed" % name) - - if _matplotlib_cm is not None: # Try to load with matplotlib - colormap = _matplotlib_cm.get_cmap(name) - lut = colormap(numpy.linspace(0, 1, colormap.N, endpoint=True)) - lut = _arrayToRgba8888(lut) - return lut - - raise ValueError("Unknown colormap '%s'" % name) - - -def _getColormap(name): - """Returns the color LUT corresponding to a colormap name - - :param str name: Name of the colormap to load - :returns: Corresponding table of colors - :rtype: numpy.ndarray - :raise ValueError: If no colormap corresponds to name - """ - name = str(name) - if name not in _COLORMAP_CACHE: - lut = _createColormapLut(name) - _COLORMAP_CACHE[name] = lut - return _COLORMAP_CACHE[name] - - -# Normalizations - -class _NormalizationMixIn: - """Colormap normalization mix-in class""" - - DEFAULT_RANGE = 0, 1 - """Fallback for (vmin, vmax)""" - - def isValid(self, value): - """Check if a value is in the valid range for this normalization. - - Override in subclass. - - :param Union[float,numpy.ndarray] value: - :rtype: Union[bool,numpy.ndarray] - """ - if isinstance(value, collections.abc.Iterable): - return numpy.ones_like(value, dtype=numpy.bool_) - else: - return True - - def autoscale(self, data, mode): - """Returns range for given data and autoscale mode. - - :param Union[None,numpy.ndarray] data: - :param str mode: Autoscale mode, see :class:`Colormap` - :returns: Range as (min, max) - :rtype: Tuple[float,float] - """ - data = None if data is None else numpy.array(data, copy=False) - if data is None or data.size == 0: - return self.DEFAULT_RANGE - - if mode == Colormap.MINMAX: - vmin, vmax = self.autoscaleMinMax(data) - elif mode == Colormap.STDDEV3: - dmin, dmax = self.autoscaleMinMax(data) - stdmin, stdmax = self.autoscaleMean3Std(data) - if dmin is None: - vmin = stdmin - elif stdmin is None: - vmin = dmin - else: - vmin = max(dmin, stdmin) - - if dmax is None: - vmax = stdmax - elif stdmax is None: - vmax = dmax - else: - vmax = min(dmax, stdmax) - - else: - raise ValueError('Unsupported mode: %s' % mode) - - # Check returned range and handle fallbacks - if vmin is None or not numpy.isfinite(vmin): - vmin = self.DEFAULT_RANGE[0] - if vmax is None or not numpy.isfinite(vmax): - vmax = self.DEFAULT_RANGE[1] - if vmax < vmin: - vmax = vmin - return float(vmin), float(vmax) - - def autoscaleMinMax(self, data): - """Autoscale using min/max - - :param numpy.ndarray data: - :returns: (vmin, vmax) - :rtype: Tuple[float,float] - """ - data = data[self.isValid(data)] - if data.size == 0: - return None, None - result = min_max(data, min_positive=False, finite=True) - return result.minimum, result.maximum - - def autoscaleMean3Std(self, data): - """Autoscale using mean+/-3std - - This implementation only works for normalization that do NOT - use the data range. - Override this method for normalization using the range. - - :param numpy.ndarray data: - :returns: (vmin, vmax) - :rtype: Tuple[float,float] - """ - # Use [0, 1] as data range for normalization not using range - normdata = self.apply(data, 0., 1.) - if normdata.dtype.kind == 'f': # Replaces inf by NaN - normdata[numpy.isfinite(normdata) == False] = numpy.nan - if normdata.size == 0: # Fallback - return None, None - - with warnings.catch_warnings(): - warnings.simplefilter('ignore', category=RuntimeWarning) - # Ignore nanmean "Mean of empty slice" warning and - # nanstd "Degrees of freedom <= 0 for slice" warning - mean, std = numpy.nanmean(normdata), numpy.nanstd(normdata) - - return self.revert(mean - 3 * std, 0., 1.), self.revert(mean + 3 * std, 0., 1.) - - -class _LinearNormalizationMixIn(_NormalizationMixIn): - """Colormap normalization mix-in class specific to autoscale taken from initial range""" - - def autoscaleMean3Std(self, data): - """Autoscale using mean+/-3std - - Do the autoscale on the data itself, not the normalized data. - - :param numpy.ndarray data: - :returns: (vmin, vmax) - :rtype: Tuple[float,float] - """ - if data.dtype.kind == 'f': # Replaces inf by NaN - data = numpy.array(data, copy=True) # Work on a copy - data[numpy.isfinite(data) == False] = numpy.nan - if data.size == 0: # Fallback - return None, None - with warnings.catch_warnings(): - warnings.simplefilter('ignore', category=RuntimeWarning) - # Ignore nanmean "Mean of empty slice" warning and - # nanstd "Degrees of freedom <= 0 for slice" warning - mean, std = numpy.nanmean(data), numpy.nanstd(data) - return mean - 3 * std, mean + 3 * std - - -class _LinearNormalization(_colormap.LinearNormalization, _LinearNormalizationMixIn): - """Linear normalization""" - def __init__(self): - _colormap.LinearNormalization.__init__(self) - _LinearNormalizationMixIn.__init__(self) - - -class _LogarithmicNormalization(_colormap.LogarithmicNormalization, _NormalizationMixIn): - """Logarithm normalization""" - - DEFAULT_RANGE = 1, 10 - - def __init__(self): - _colormap.LogarithmicNormalization.__init__(self) - _NormalizationMixIn.__init__(self) - - def isValid(self, value): - return value > 0. - - def autoscaleMinMax(self, data): - result = min_max(data, min_positive=True, finite=True) - return result.min_positive, result.maximum - - -class _SqrtNormalization(_colormap.SqrtNormalization, _NormalizationMixIn): - """Square root normalization""" - - DEFAULT_RANGE = 0, 1 - - def __init__(self): - _colormap.SqrtNormalization.__init__(self) - _NormalizationMixIn.__init__(self) - - def isValid(self, value): - return value >= 0. - - -class _GammaNormalization(_colormap.PowerNormalization, _LinearNormalizationMixIn): - """Gamma correction normalization: - - Linear normalization to [0, 1] followed by power normalization. - - :param gamma: Gamma correction factor - """ - def __init__(self, gamma): - _colormap.PowerNormalization.__init__(self, gamma) - _LinearNormalizationMixIn.__init__(self) - - -class _ArcsinhNormalization(_colormap.ArcsinhNormalization, _NormalizationMixIn): - """Inverse hyperbolic sine normalization""" - - def __init__(self): - _colormap.ArcsinhNormalization.__init__(self) - _NormalizationMixIn.__init__(self) - - -class Colormap(qt.QObject): - """Description of a colormap - - If no `name` nor `colors` are provided, a default gray LUT is used. - - :param str name: Name of the colormap - :param tuple colors: optional, custom colormap. - Nx3 or Nx4 numpy array of RGB(A) colors, - either uint8 or float in [0, 1]. - If 'name' is None, then this array is used as the colormap. - :param str normalization: Normalization: 'linear' (default) or 'log' - :param vmin: Lower bound of the colormap or None for autoscale (default) - :type vmin: Union[None, float] - :param vmax: Upper bounds of the colormap or None for autoscale (default) - :type vmax: Union[None, float] - """ - - LINEAR = 'linear' - """constant for linear normalization""" - - LOGARITHM = 'log' - """constant for logarithmic normalization""" - - SQRT = 'sqrt' - """constant for square root normalization""" - - GAMMA = 'gamma' - """Constant for gamma correction normalization""" - - ARCSINH = 'arcsinh' - """constant for inverse hyperbolic sine normalization""" - - _BASIC_NORMALIZATIONS = { - LINEAR: _LinearNormalization(), - LOGARITHM: _LogarithmicNormalization(), - SQRT: _SqrtNormalization(), - ARCSINH: _ArcsinhNormalization(), - } - """Normalizations without parameters""" - - NORMALIZATIONS = LINEAR, LOGARITHM, SQRT, GAMMA, ARCSINH - """Tuple of managed normalizations""" - - MINMAX = 'minmax' - """constant for autoscale using min/max data range""" - - STDDEV3 = 'stddev3' - """constant for autoscale using mean +/- 3*std(data) - with a clamp on min/max of the data""" - - AUTOSCALE_MODES = (MINMAX, STDDEV3) - """Tuple of managed auto scale algorithms""" - - sigChanged = qt.Signal() - """Signal emitted when the colormap has changed.""" - - _DEFAULT_NAN_COLOR = 255, 255, 255, 0 - - def __init__(self, name=None, colors=None, normalization=LINEAR, vmin=None, vmax=None, autoscaleMode=MINMAX): - qt.QObject.__init__(self) - self._editable = True - self.__gamma = 2.0 - # Default NaN color: fully transparent white - self.__nanColor = numpy.array(self._DEFAULT_NAN_COLOR, dtype=numpy.uint8) - - assert normalization in Colormap.NORMALIZATIONS - assert autoscaleMode in Colormap.AUTOSCALE_MODES - - if normalization is Colormap.LOGARITHM: - if (vmin is not None and vmin < 0) or (vmax is not None and vmax < 0): - m = "Unsuported vmin (%s) and/or vmax (%s) given for a log scale." - m += ' Autoscale will be performed.' - m = m % (vmin, vmax) - _logger.warning(m) - vmin = None - vmax = None - - self._name = None - self._colors = None - - if colors is not None and name is not None: - deprecation.deprecated_warning("Argument", - name="silx.gui.plot.Colors", - reason="name and colors can't be used at the same time", - since_version="0.10.0", - skip_backtrace_count=1) - - colors = None - - if name is not None: - self.setName(name) # And resets colormap LUT - elif colors is not None: - self.setColormapLUT(colors) - else: - # Default colormap is grey - self.setName("gray") - - self._normalization = str(normalization) - self._autoscaleMode = str(autoscaleMode) - self._vmin = float(vmin) if vmin is not None else None - self._vmax = float(vmax) if vmax is not None else None - - def setFromColormap(self, other): - """Set this colormap using information from the `other` colormap. - - :param ~silx.gui.colors.Colormap other: Colormap to use as reference. - """ - if not self.isEditable(): - raise NotEditableError('Colormap is not editable') - if self == other: - return - with blockSignals(self): - name = other.getName() - if name is not None: - self.setName(name) - else: - self.setColormapLUT(other.getColormapLUT()) - self.setNaNColor(other.getNaNColor()) - self.setNormalization(other.getNormalization()) - self.setGammaNormalizationParameter( - other.getGammaNormalizationParameter()) - self.setAutoscaleMode(other.getAutoscaleMode()) - self.setVRange(*other.getVRange()) - self.setEditable(other.isEditable()) - self.sigChanged.emit() - - def getNColors(self, nbColors=None): - """Returns N colors computed by sampling the colormap regularly. - - :param nbColors: - The number of colors in the returned array or None for the default value. - The default value is the size of the colormap LUT. - :type nbColors: int or None - :return: 2D array of uint8 of shape (nbColors, 4) - :rtype: numpy.ndarray - """ - # Handle default value for nbColors - if nbColors is None: - return numpy.array(self._colors, copy=True) - else: - nbColors = int(nbColors) - colormap = self.copy() - colormap.setNormalization(Colormap.LINEAR) - colormap.setVRange(vmin=0, vmax=nbColors - 1) - colors = colormap.applyToData( - numpy.arange(nbColors, dtype=numpy.int32)) - return colors - - def getName(self): - """Return the name of the colormap - :rtype: str - """ - return self._name - - def setName(self, name): - """Set the name of the colormap to use. - - :param str name: The name of the colormap. - At least the following names are supported: 'gray', - 'reversed gray', 'temperature', 'red', 'green', 'blue', 'jet', - 'viridis', 'magma', 'inferno', 'plasma'. - """ - name = str(name) - if self._name == name: - return - if self.isEditable() is False: - raise NotEditableError('Colormap is not editable') - if name not in self.getSupportedColormaps(): - raise ValueError("Colormap name '%s' is not supported" % name) - self._name = name - self._colors = _getColormap(self._name) - self.sigChanged.emit() - - def getColormapLUT(self, copy=True): - """Return the list of colors for the colormap or None if not set. - - This returns None if the colormap was set with :meth:`setName`. - Use :meth:`getNColors` to get the colormap LUT for any colormap. - - :param bool copy: If true a copy of the numpy array is provided - :return: the list of colors for the colormap or None if not set - :rtype: numpy.ndarray or None - """ - if self._name is None: - return numpy.array(self._colors, copy=copy) - else: - return None - - def setColormapLUT(self, colors): - """Set the colors of the colormap. - - :param numpy.ndarray colors: the colors of the LUT. - If float, it is converted from [0, 1] to uint8 range. - Otherwise it is casted to uint8. - - .. warning: this will set the value of name to None - """ - if self.isEditable() is False: - raise NotEditableError('Colormap is not editable') - assert colors is not None - - colors = numpy.array(colors, copy=False) - if colors.shape == (): - raise TypeError("An array is expected for 'colors' argument. '%s' was found." % type(colors)) - assert len(colors) != 0 - assert colors.ndim >= 2 - colors.shape = -1, colors.shape[-1] - self._colors = _arrayToRgba8888(colors) - self._name = None - self.sigChanged.emit() - - def getNaNColor(self): - """Returns the color to use for Not-A-Number floating point value. - - :rtype: QColor - """ - return qt.QColor(*self.__nanColor) - - def setNaNColor(self, color): - """Set the color to use for Not-A-Number floating point value. - - :param color: RGB(A) color to use for NaN values - :type color: QColor, str, tuple of uint8 or float in [0., 1.] - """ - color = (numpy.array(rgba(color)) * 255).astype(numpy.uint8) - if not numpy.array_equal(self.__nanColor, color): - self.__nanColor = color - self.sigChanged.emit() - - def getNormalization(self): - """Return the normalization of the colormap. - - See :meth:`setNormalization` for returned values. - - :return: the normalization of the colormap - :rtype: str - """ - return self._normalization - - def setNormalization(self, norm): - """Set the colormap normalization. - - Accepted normalizations: 'log', 'linear', 'sqrt' - - :param str norm: the norm to set - """ - assert norm in self.NORMALIZATIONS - if self.isEditable() is False: - raise NotEditableError('Colormap is not editable') - self._normalization = str(norm) - self.sigChanged.emit() - - def setGammaNormalizationParameter(self, gamma: float) -> None: - """Set the gamma correction parameter. - - Only used for gamma correction normalization. - - :param float gamma: - :raise ValueError: If gamma is not valid - """ - if gamma < 0. or not numpy.isfinite(gamma): - raise ValueError("Gamma value not supported") - if gamma != self.__gamma: - self.__gamma = gamma - self.sigChanged.emit() - - def getGammaNormalizationParameter(self) -> float: - """Returns the gamma correction parameter value. - - :rtype: float - """ - return self.__gamma - - def getAutoscaleMode(self): - """Return the autoscale mode of the colormap ('minmax' or 'stddev3') - - :rtype: str - """ - return self._autoscaleMode - - def setAutoscaleMode(self, mode): - """Set the autoscale mode: either 'minmax' or 'stddev3' - - :param str mode: the mode to set - """ - if self.isEditable() is False: - raise NotEditableError('Colormap is not editable') - assert mode in self.AUTOSCALE_MODES - if mode != self._autoscaleMode: - self._autoscaleMode = mode - self.sigChanged.emit() - - def isAutoscale(self): - """Return True if both min and max are in autoscale mode""" - return self._vmin is None and self._vmax is None - - def getVMin(self): - """Return the lower bound of the colormap - - :return: the lower bound of the colormap - :rtype: float or None - """ - return self._vmin - - def setVMin(self, vmin): - """Set the minimal value of the colormap - - :param float vmin: Lower bound of the colormap or None for autoscale - (default) - value) - """ - if self.isEditable() is False: - raise NotEditableError('Colormap is not editable') - if vmin is not None: - if self._vmax is not None and vmin > self._vmax: - err = "Can't set vmin because vmin >= vmax. " \ - "vmin = %s, vmax = %s" % (vmin, self._vmax) - raise ValueError(err) - - self._vmin = vmin - self.sigChanged.emit() - - def getVMax(self): - """Return the upper bounds of the colormap or None - - :return: the upper bounds of the colormap or None - :rtype: float or None - """ - return self._vmax - - def setVMax(self, vmax): - """Set the maximal value of the colormap - - :param float vmax: Upper bounds of the colormap or None for autoscale - (default) - """ - if self.isEditable() is False: - raise NotEditableError('Colormap is not editable') - if vmax is not None: - if self._vmin is not None and vmax < self._vmin: - err = "Can't set vmax because vmax <= vmin. " \ - "vmin = %s, vmax = %s" % (self._vmin, vmax) - raise ValueError(err) - - self._vmax = vmax - self.sigChanged.emit() - - def isEditable(self): - """ Return if the colormap is editable or not - - :return: editable state of the colormap - :rtype: bool - """ - return self._editable - - def setEditable(self, editable): - """ - Set the editable state of the colormap - - :param bool editable: is the colormap editable - """ - assert type(editable) is bool - self._editable = editable - self.sigChanged.emit() - - def _getNormalizer(self): - """Returns normalizer object""" - normalization = self.getNormalization() - if normalization == self.GAMMA: - return _GammaNormalization(self.getGammaNormalizationParameter()) - else: - return self._BASIC_NORMALIZATIONS[normalization] - - def _computeAutoscaleRange(self, data): - """Compute the data range which will be used in autoscale mode. - - :param numpy.ndarray data: The data for which to compute the range - :return: (vmin, vmax) range - """ - return self._getNormalizer().autoscale( - data, mode=self.getAutoscaleMode()) - - def getColormapRange(self, data=None): - """Return (vmin, vmax) the range of the colormap for the given data or item. - - :param Union[numpy.ndarray,~silx.gui.plot.items.ColormapMixIn] data: - The data or item to use for autoscale bounds. - :return: (vmin, vmax) corresponding to the colormap applied to data if provided. - :rtype: tuple - """ - vmin = self._vmin - vmax = self._vmax - assert vmin is None or vmax is None or vmin <= vmax # TODO handle this in setters - - normalizer = self._getNormalizer() - - # Handle invalid bounds as autoscale - if vmin is not None and not normalizer.isValid(vmin): - _logger.info( - 'Invalid vmin, switching to autoscale for lower bound') - vmin = None - if vmax is not None and not normalizer.isValid(vmax): - _logger.info( - 'Invalid vmax, switching to autoscale for upper bound') - vmax = None - - if vmin is None or vmax is None: # Handle autoscale - from .plot.items.core import ColormapMixIn # avoid cyclic import - if isinstance(data, ColormapMixIn): - min_, max_ = data._getColormapAutoscaleRange(self) - # Make sure min_, max_ are not None - min_ = normalizer.DEFAULT_RANGE[0] if min_ is None else min_ - max_ = normalizer.DEFAULT_RANGE[1] if max_ is None else max_ - else: - min_, max_ = normalizer.autoscale( - data, mode=self.getAutoscaleMode()) - - if vmin is None: # Set vmin respecting provided vmax - vmin = min_ if vmax is None else min(min_, vmax) - - if vmax is None: - vmax = max(max_, vmin) # Handle max_ <= 0 for log scale - - return vmin, vmax - - def getVRange(self): - """Get the bounds of the colormap - - :rtype: Tuple(Union[float,None],Union[float,None]) - :returns: A tuple of 2 values for min and max. Or None instead of float - for autoscale - """ - return self.getVMin(), self.getVMax() - - def setVRange(self, vmin, vmax): - """Set the bounds of the colormap - - :param vmin: Lower bound of the colormap or None for autoscale - (default) - :param vmax: Upper bounds of the colormap or None for autoscale - (default) - """ - if self.isEditable() is False: - raise NotEditableError('Colormap is not editable') - if vmin is not None and vmax is not None: - if vmin > vmax: - err = "Can't set vmin and vmax because vmin >= vmax " \ - "vmin = %s, vmax = %s" % (vmin, vmax) - raise ValueError(err) - - if self._vmin == vmin and self._vmax == vmax: - return - - self._vmin = vmin - self._vmax = vmax - self.sigChanged.emit() - - def __getitem__(self, item): - if item == 'autoscale': - return self.isAutoscale() - elif item == 'name': - return self.getName() - elif item == 'normalization': - return self.getNormalization() - elif item == 'vmin': - return self.getVMin() - elif item == 'vmax': - return self.getVMax() - elif item == 'colors': - return self.getColormapLUT() - elif item == 'autoscaleMode': - return self.getAutoscaleMode() - else: - raise KeyError(item) - - def _toDict(self): - """Return the equivalent colormap as a dictionary - (old colormap representation) - - :return: the representation of the Colormap as a dictionary - :rtype: dict - """ - return { - 'name': self._name, - 'colors': self.getColormapLUT(), - 'vmin': self._vmin, - 'vmax': self._vmax, - 'autoscale': self.isAutoscale(), - 'normalization': self.getNormalization(), - 'autoscaleMode': self.getAutoscaleMode(), - } - - def _setFromDict(self, dic): - """Set values to the colormap from a dictionary - - :param dict dic: the colormap as a dictionary - """ - if self.isEditable() is False: - raise NotEditableError('Colormap is not editable') - name = dic['name'] if 'name' in dic else None - colors = dic['colors'] if 'colors' in dic else None - if name is not None and colors is not None: - if isinstance(colors, int): - # Filter out argument which was supported but never used - _logger.info("Unused 'colors' from colormap dictionary filterer.") - colors = None - vmin = dic['vmin'] if 'vmin' in dic else None - vmax = dic['vmax'] if 'vmax' in dic else None - if 'normalization' in dic: - normalization = dic['normalization'] - else: - warn = 'Normalization not given in the dictionary, ' - warn += 'set by default to ' + Colormap.LINEAR - _logger.warning(warn) - normalization = Colormap.LINEAR - - if name is None and colors is None: - err = 'The colormap should have a name defined or a tuple of colors' - raise ValueError(err) - if normalization not in Colormap.NORMALIZATIONS: - err = 'Given normalization is not recognized (%s)' % normalization - raise ValueError(err) - - autoscaleMode = dic.get('autoscaleMode', Colormap.MINMAX) - if autoscaleMode not in Colormap.AUTOSCALE_MODES: - err = 'Given autoscale mode is not recognized (%s)' % autoscaleMode - raise ValueError(err) - - # If autoscale, then set boundaries to None - if dic.get('autoscale', False): - vmin, vmax = None, None - - if name is not None: - self.setName(name) - else: - self.setColormapLUT(colors) - self._vmin = vmin - self._vmax = vmax - self._autoscale = True if (vmin is None and vmax is None) else False - self._normalization = normalization - self._autoscaleMode = autoscaleMode - - self.sigChanged.emit() - - @staticmethod - def _fromDict(dic): - colormap = Colormap() - colormap._setFromDict(dic) - return colormap - - def copy(self): - """Return a copy of the Colormap. - - :rtype: silx.gui.colors.Colormap - """ - colormap = Colormap(name=self._name, - colors=self.getColormapLUT(), - vmin=self._vmin, - vmax=self._vmax, - normalization=self.getNormalization(), - autoscaleMode=self.getAutoscaleMode()) - colormap.setNaNColor(self.getNaNColor()) - colormap.setGammaNormalizationParameter( - self.getGammaNormalizationParameter()) - colormap.setEditable(self.isEditable()) - return colormap - - def applyToData(self, data, reference=None): - """Apply the colormap to the data - - :param Union[numpy.ndarray,~silx.gui.plot.item.ColormapMixIn] data: - The data to convert or the item for which to apply the colormap. - :param Union[numpy.ndarray,~silx.gui.plot.item.ColormapMixIn,None] reference: - The data or item to use as reference to compute autoscale - """ - if reference is None: - reference = data - vmin, vmax = self.getColormapRange(reference) - - if hasattr(data, "getColormappedData"): # Use item's data - data = data.getColormappedData(copy=False) - - return _colormap.cmap( - data, - self._colors, - vmin, - vmax, - self._getNormalizer(), - self.__nanColor) - - @staticmethod - def getSupportedColormaps(): - """Get the supported colormap names as a tuple of str. - - The list should at least contain and start by: - - ('gray', 'reversed gray', 'temperature', 'red', 'green', 'blue', - 'viridis', 'magma', 'inferno', 'plasma') - - :rtype: tuple - """ - colormaps = set() - if _matplotlib_colormaps is not None: - colormaps.update(_matplotlib_colormaps()) - colormaps.update(_AVAILABLE_LUTS.keys()) - - colormaps = tuple(cmap for cmap in sorted(colormaps) - if cmap not in _AVAILABLE_LUTS.keys()) - - return tuple(_AVAILABLE_LUTS.keys()) + colormaps - - def __str__(self): - return str(self._toDict()) - - def __eq__(self, other): - """Compare colormap values and not pointers""" - if other is None: - return False - if not isinstance(other, Colormap): - return False - if self.getNormalization() != other.getNormalization(): - return False - if self.getNormalization() == self.GAMMA: - delta = self.getGammaNormalizationParameter() - other.getGammaNormalizationParameter() - if abs(delta) > 0.001: - return False - return (self.getName() == other.getName() and - self.getAutoscaleMode() == other.getAutoscaleMode() and - self.getVMin() == other.getVMin() and - self.getVMax() == other.getVMax() and - numpy.array_equal(self.getColormapLUT(), other.getColormapLUT()) - ) - - _SERIAL_VERSION = 3 - - def restoreState(self, byteArray): - """ - Read the colormap state from a QByteArray. - - :param qt.QByteArray byteArray: Stream containing the state - :return: True if the restoration sussseed - :rtype: bool - """ - if self.isEditable() is False: - raise NotEditableError('Colormap is not editable') - stream = qt.QDataStream(byteArray, qt.QIODevice.ReadOnly) - - className = stream.readQString() - if className != self.__class__.__name__: - _logger.warning("Classname mismatch. Found %s." % className) - return False - - version = stream.readUInt32() - if version not in numpy.arange(1, self._SERIAL_VERSION+1): - _logger.warning("Serial version mismatch. Found %d." % version) - return False - - name = stream.readQString() - isNull = stream.readBool() - if not isNull: - vmin = stream.readQVariant() - else: - vmin = None - isNull = stream.readBool() - if not isNull: - vmax = stream.readQVariant() - else: - vmax = None - - normalization = stream.readQString() - if normalization == Colormap.GAMMA: - gamma = stream.readFloat() - else: - gamma = None - - if version == 1: - autoscaleMode = Colormap.MINMAX - else: - autoscaleMode = stream.readQString() - - if version <= 2: - nanColor = self._DEFAULT_NAN_COLOR - else: - nanColor = stream.readInt32(), stream.readInt32(), stream.readInt32(), stream.readInt32() - - # emit change event only once - old = self.blockSignals(True) - try: - self.setName(name) - self.setNormalization(normalization) - self.setAutoscaleMode(autoscaleMode) - self.setVRange(vmin, vmax) - if gamma is not None: - self.setGammaNormalizationParameter(gamma) - self.setNaNColor(nanColor) - finally: - self.blockSignals(old) - self.sigChanged.emit() - return True - - def saveState(self): - """ - Save state of the colomap into a QDataStream. - - :rtype: qt.QByteArray - """ - data = qt.QByteArray() - stream = qt.QDataStream(data, qt.QIODevice.WriteOnly) - - stream.writeQString(self.__class__.__name__) - stream.writeUInt32(self._SERIAL_VERSION) - stream.writeQString(self.getName()) - stream.writeBool(self.getVMin() is None) - if self.getVMin() is not None: - stream.writeQVariant(self.getVMin()) - stream.writeBool(self.getVMax() is None) - if self.getVMax() is not None: - stream.writeQVariant(self.getVMax()) - stream.writeQString(self.getNormalization()) - if self.getNormalization() == Colormap.GAMMA: - stream.writeFloat(self.getGammaNormalizationParameter()) - stream.writeQString(self.getAutoscaleMode()) - nanColor = self.getNaNColor() - stream.writeInt32(nanColor.red()) - stream.writeInt32(nanColor.green()) - stream.writeInt32(nanColor.blue()) - stream.writeInt32(nanColor.alpha()) - - return data - - -_PREFERRED_COLORMAPS = None -""" -Tuple of preferred colormap names accessed with :meth:`preferredColormaps`. -""" - - -def preferredColormaps(): - """Returns the name of the preferred colormaps. - - This list is used by widgets allowing to change the colormap - like the :class:`ColormapDialog` as a subset of colormap choices. - - :rtype: tuple of str - """ - global _PREFERRED_COLORMAPS - if _PREFERRED_COLORMAPS is None: - # Initialize preferred colormaps - default_preferred = [] - for name, info in _AVAILABLE_LUTS.items(): - if (info.preferred and - (info.source != 'matplotlib' or _matplotlib_cm is not None)): - default_preferred.append(name) - setPreferredColormaps(default_preferred) - return tuple(_PREFERRED_COLORMAPS) - - -def setPreferredColormaps(colormaps): - """Set the list of preferred colormap names. - - Warning: If a colormap name is not available - it will be removed from the list. - - :param colormaps: Not empty list of colormap names - :type colormaps: iterable of str - :raise ValueError: if the list of available preferred colormaps is empty. - """ - supportedColormaps = Colormap.getSupportedColormaps() - colormaps = [cmap for cmap in colormaps if cmap in supportedColormaps] - if len(colormaps) == 0: - raise ValueError("Cannot set preferred colormaps to an empty list") - - global _PREFERRED_COLORMAPS - _PREFERRED_COLORMAPS = colormaps - - -def registerLUT(name, colors, cursor_color='black', preferred=True): - """Register a custom LUT to be used with `Colormap` objects. - - It can override existing LUT names. - - :param str name: Name of the LUT as defined to configure colormaps - :param numpy.ndarray colors: The custom LUT to register. - Nx3 or Nx4 numpy array of RGB(A) colors, - either uint8 or float in [0, 1]. - :param bool preferred: If true, this LUT will be displayed as part of the - preferred colormaps in dialogs. - :param str cursor_color: Color used to display overlay over images using - colormap with this LUT. - """ - description = _LUT_DESCRIPTION('user', cursor_color, preferred=preferred) - colors = _arrayToRgba8888(colors) - _AVAILABLE_LUTS[name] = description - - if preferred: - # Invalidate the preferred cache - global _PREFERRED_COLORMAPS - if _PREFERRED_COLORMAPS is not None: - if name not in _PREFERRED_COLORMAPS: - _PREFERRED_COLORMAPS.append(name) - else: - # The cache is not yet loaded, it's fine - pass - - # Register the cache as the LUT was already loaded - _COLORMAP_CACHE[name] = colors diff --git a/silx/gui/console.py b/silx/gui/console.py deleted file mode 100644 index 5dc6336..0000000 --- a/silx/gui/console.py +++ /dev/null @@ -1,202 +0,0 @@ -# coding: utf-8 -# /*########################################################################## -# -# Copyright (c) 2004-2019 European Synchrotron Radiation Facility -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. -# -# ###########################################################################*/ -"""This module provides an IPython console widget. - -You can push variables - any python object - to the -console's interactive namespace. This provides users with an advanced way -of interacting with your program. For instance, if your program has a -:class:`PlotWidget` or a :class:`PlotWindow`, you can push a reference to -these widgets to allow your users to add curves, save data to files… by using -the widgets' methods from the console. - -.. note:: - - This module has a dependency on - `qtconsole <https://pypi.org/project/qtconsole/>`_. - An ``ImportError`` will be raised if it is - imported while the dependencies are not satisfied. - -Basic usage example:: - - from silx.gui import qt - from silx.gui.console import IPythonWidget - - app = qt.QApplication([]) - - hello_button = qt.QPushButton("Hello World!", None) - hello_button.show() - - console = IPythonWidget() - console.show() - console.pushVariables({"the_button": hello_button}) - - app.exec_() - -This program will display a console widget and a push button in two separate -windows. You will be able to interact with the button from the console, -for example change its text:: - - >>> the_button.setText("Spam spam") - -An IPython interactive console is a powerful tool that enables you to work -with data and plot it. -See `this tutorial <https://plot.ly/python/ipython-notebook-tutorial/>`_ -for more information on some of the rich features of IPython. -""" -__authors__ = ["Tim Rae", "V.A. Sole", "P. Knobel"] -__license__ = "MIT" -__date__ = "24/05/2016" - -import logging - -from . import qt - -_logger = logging.getLogger(__name__) - - -# This widget cannot be used inside an interactive IPython shell. -# It would raise MultipleInstanceError("Multiple incompatible subclass -# instances of InProcessInteractiveShell are being created"). -try: - __IPYTHON__ -except NameError: - pass # Not in IPython -else: - msg = "Module " + __name__ + " cannot be used within an IPython shell" - raise ImportError(msg) - -try: - from qtconsole.rich_jupyter_widget import RichJupyterWidget as \ - _RichJupyterWidget -except ImportError: - try: - from qtconsole.rich_ipython_widget import RichJupyterWidget as \ - _RichJupyterWidget - except ImportError: - from qtconsole.rich_ipython_widget import RichIPythonWidget as \ - _RichJupyterWidget - -from qtconsole.inprocess import QtInProcessKernelManager - -try: - from ipykernel import version_info as _ipykernel_version_info -except ImportError: - _ipykernel_version_info = None - - -class IPythonWidget(_RichJupyterWidget): - """Live IPython console widget. - - .. image:: img/IPythonWidget.png - - :param custom_banner: Custom welcome message to be printed at the top of - the console. - """ - - def __init__(self, parent=None, custom_banner=None, *args, **kwargs): - if parent is not None: - kwargs["parent"] = parent - super(IPythonWidget, self).__init__(*args, **kwargs) - if custom_banner is not None: - self.banner = custom_banner - self.setWindowTitle(self.banner) - self.kernel_manager = kernel_manager = QtInProcessKernelManager() - kernel_manager.start_kernel() - - # Monkey-patch to workaround issue: - # https://github.com/ipython/ipykernel/issues/370 - if (_ipykernel_version_info is not None and - _ipykernel_version_info[0] > 4 and - _ipykernel_version_info[:3] <= (5, 1, 0)): - def _abort_queues(*args, **kwargs): - pass - kernel_manager.kernel._abort_queues = _abort_queues - - self.kernel_client = kernel_client = self._kernel_manager.client() - kernel_client.start_channels() - - def stop(): - kernel_client.stop_channels() - kernel_manager.shutdown_kernel() - self.exit_requested.connect(stop) - - def sizeHint(self): - """Return a reasonable default size for usage in :class:`PlotWindow`""" - return qt.QSize(500, 300) - - def pushVariables(self, variable_dict): - """ Given a dictionary containing name / value pairs, push those - variables to the IPython console widget. - - :param variable_dict: Dictionary of variables to be pushed to the - console's interactive namespace (```{variable_name: object, …}```) - """ - self.kernel_manager.kernel.shell.push(variable_dict) - - -class IPythonDockWidget(qt.QDockWidget): - """Dock Widget including a :class:`IPythonWidget` inside - a vertical layout. - - .. image:: img/IPythonDockWidget.png - - :param available_vars: Dictionary of variables to be pushed to the - console's interactive namespace: ``{"variable_name": object, …}`` - :param custom_banner: Custom welcome message to be printed at the top of - the console - :param title: Dock widget title - :param parent: Parent :class:`qt.QMainWindow` containing this - :class:`qt.QDockWidget` - """ - def __init__(self, parent=None, available_vars=None, custom_banner=None, - title="Console"): - super(IPythonDockWidget, self).__init__(title, parent) - - self.ipyconsole = IPythonWidget(custom_banner=custom_banner) - - self.layout().setContentsMargins(0, 0, 0, 0) - self.setWidget(self.ipyconsole) - - if available_vars is not None: - self.ipyconsole.pushVariables(available_vars) - - def showEvent(self, event): - """Make sure this widget is raised when it is shown - (when it is first created as a tab in PlotWindow or when it is shown - again after hiding). - """ - self.raise_() - - -def main(): - """Run a Qt app with an IPython console""" - app = qt.QApplication([]) - widget = IPythonDockWidget() - widget.show() - app.exec_() - - -if __name__ == '__main__': - main() diff --git a/silx/gui/data/ArrayTableModel.py b/silx/gui/data/ArrayTableModel.py deleted file mode 100644 index b7bd9c4..0000000 --- a/silx/gui/data/ArrayTableModel.py +++ /dev/null @@ -1,670 +0,0 @@ -# coding: utf-8 -# /*########################################################################## -# -# Copyright (c) 2016-2021 European Synchrotron Radiation Facility -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. -# -# ###########################################################################*/ -""" -This module defines a data model for displaying and editing arrays of any -number of dimensions in a table view. -""" -from __future__ import division -import numpy -import logging -from silx.gui import qt -from silx.gui.data.TextFormatter import TextFormatter - -__authors__ = ["V.A. Sole"] -__license__ = "MIT" -__date__ = "27/09/2017" - - -_logger = logging.getLogger(__name__) - - -def _is_array(data): - """Return True if object implements all necessary attributes to be used - as a numpy array. - - :param object data: Array-like object (numpy array, h5py dataset...) - :return: boolean - """ - # add more required attribute if necessary - for attr in ("shape", "dtype"): - if not hasattr(data, attr): - return False - return True - - -class ArrayTableModel(qt.QAbstractTableModel): - """This data model provides access to 2D slices in a N-dimensional - array. - - A slice for a 3-D array is characterized by a perspective (the number of - the axis orthogonal to the slice) and an index at which the slice - intersects the orthogonal axis. - - In the n-D case, only slices parallel to the last two axes are handled. A - slice is therefore characterized by a list of indices locating the - slice on all the :math:`n - 2` orthogonal axes. - - :param parent: Parent QObject - :param data: Numpy array, or object implementing a similar interface - (e.g. h5py dataset) - :param str fmt: Format string for representing numerical values. - Default is ``"%g"``. - :param sequence[int] perspective: See documentation - of :meth:`setPerspective`. - """ - - MAX_NUMBER_OF_SECTIONS = 10e6 - """Maximum number of displayed rows and columns""" - - def __init__(self, parent=None, data=None, perspective=None): - qt.QAbstractTableModel.__init__(self, parent) - - self._array = None - """n-dimensional numpy array""" - - self._bgcolors = None - """(n+1)-dimensional numpy array containing RGB(A) color data - for the background color - """ - - self._fgcolors = None - """(n+1)-dimensional numpy array containing RGB(A) color data - for the foreground color - """ - - self._formatter = None - """Formatter for text representation of data""" - - formatter = TextFormatter(self) - formatter.setUseQuoteForText(False) - self.setFormatter(formatter) - - self._index = None - """This attribute stores the slice index, as a list of indices - where the frame intersects orthogonal axis.""" - - self._perspective = None - """Sequence of dimensions orthogonal to the frame to be viewed. - For an array with ``n`` dimensions, this is a sequence of ``n-2`` - integers. the first dimension is numbered ``0``. - By default, the data frames use the last two dimensions as their axes - and therefore the perspective is a sequence of the first ``n-2`` - dimensions. - For example, for a 5-D array, the default perspective is ``(0, 1, 2)`` - and the default frames axes are ``(3, 4)``.""" - - # set _data and _perspective - self.setArrayData(data, perspective=perspective) - - def _getRowDim(self): - """The row axis is the first axis parallel to the frames - (lowest dimension number) - - Return None for 0-D (scalar) or 1-D arrays - """ - n_dimensions = len(self._array.shape) - if n_dimensions < 2: - # scalar or 1D array: no row index - return None - # take all dimensions and remove the orthogonal ones - frame_axes = set(range(0, n_dimensions)) - set(self._perspective) - # sanity check - assert len(frame_axes) == 2 - return min(frame_axes) - - def _getColumnDim(self): - """The column axis is the second (highest dimension) axis parallel - to the frames - - Return None for 0-D (scalar) - """ - n_dimensions = len(self._array.shape) - if n_dimensions < 1: - # scalar: no column index - return None - frame_axes = set(range(0, n_dimensions)) - set(self._perspective) - # sanity check - assert (len(frame_axes) == 2) if n_dimensions > 1 else (len(frame_axes) == 1) - return max(frame_axes) - - def _getIndexTuple(self, table_row, table_col): - """Return the n-dimensional index of a value in the original array, - based on its row and column indices in the table view - - :param table_row: Row index (0-based) of a table cell - :param table_col: Column index (0-based) of a table cell - :return: Tuple of indices of the element in the numpy array - """ - row_dim = self._getRowDim() - col_dim = self._getColumnDim() - - # get indices on all orthogonal axes - selection = list(self._index) - # insert indices on parallel axes - if row_dim is not None: - selection.insert(row_dim, table_row) - if col_dim is not None: - selection.insert(col_dim, table_col) - return tuple(selection) - - # Methods to be implemented to subclass QAbstractTableModel - def rowCount(self, parent_idx=None): - """QAbstractTableModel method - Return number of rows to be displayed in table""" - row_dim = self._getRowDim() - if row_dim is None: - # 0-D and 1-D arrays - return 1 - return min(self._array.shape[row_dim], self.MAX_NUMBER_OF_SECTIONS) - - def columnCount(self, parent_idx=None): - """QAbstractTableModel method - Return number of columns to be displayed in table""" - col_dim = self._getColumnDim() - if col_dim is None: - # 0-D array - return 1 - return min(self._array.shape[col_dim], self.MAX_NUMBER_OF_SECTIONS) - - def __isClipped(self, orientation=qt.Qt.Vertical) -> bool: - """Returns whether or not array is clipped in a given orientation""" - if orientation == qt.Qt.Vertical: - dim = self._getRowDim() - else: - dim = self._getColumnDim() - return (dim is not None and - self._array.shape[dim] > self.MAX_NUMBER_OF_SECTIONS) - - def __isClippedIndex(self, index) -> bool: - """Returns whether or not index's cell represents clipped data.""" - if not index.isValid(): - return False - if index.row() == self.MAX_NUMBER_OF_SECTIONS - 2: - return self.__isClipped(qt.Qt.Vertical) - if index.column() == self.MAX_NUMBER_OF_SECTIONS - 2: - return self.__isClipped(qt.Qt.Horizontal) - return False - - def __clippedData(self, role=qt.Qt.DisplayRole): - """Return data for cells representing clipped data""" - if role == qt.Qt.DisplayRole: - return "..." - elif role == qt.Qt.ToolTipRole: - return "Dataset is too large: display is clipped" - else: - return None - - def data(self, index, role=qt.Qt.DisplayRole): - """QAbstractTableModel method to access data values - in the format ready to be displayed""" - if index.isValid(): - if self.__isClippedIndex(index): # Special displayed for clipped data - return self.__clippedData(role) - - row, column = index.row(), index.column() - - # When clipped, display last data of the array in last column of the table - if (self.__isClipped(qt.Qt.Vertical) and - row == self.MAX_NUMBER_OF_SECTIONS - 1): - row = self._array.shape[self._getRowDim()] - 1 - if (self.__isClipped(qt.Qt.Horizontal) and - column == self.MAX_NUMBER_OF_SECTIONS - 1): - column = self._array.shape[self._getColumnDim()] - 1 - - selection = self._getIndexTuple(row, column) - - if role == qt.Qt.DisplayRole: - return self._formatter.toString(self._array[selection], self._array.dtype) - - if role == qt.Qt.BackgroundRole and self._bgcolors is not None: - r, g, b = self._bgcolors[selection][0:3] - if self._bgcolors.shape[-1] == 3: - return qt.QColor(r, g, b) - if self._bgcolors.shape[-1] == 4: - a = self._bgcolors[selection][3] - return qt.QColor(r, g, b, a) - - if role == qt.Qt.ForegroundRole: - if self._fgcolors is not None: - r, g, b = self._fgcolors[selection][0:3] - if self._fgcolors.shape[-1] == 3: - return qt.QColor(r, g, b) - if self._fgcolors.shape[-1] == 4: - a = self._fgcolors[selection][3] - return qt.QColor(r, g, b, a) - - # no fg color given, use black or white - # based on luminosity threshold - elif self._bgcolors is not None: - r, g, b = self._bgcolors[selection][0:3] - lum = 0.21 * r + 0.72 * g + 0.07 * b - if lum < 128: - return qt.QColor(qt.Qt.white) - else: - return qt.QColor(qt.Qt.black) - - def headerData(self, section, orientation, role=qt.Qt.DisplayRole): - """QAbstractTableModel method - Return the 0-based row or column index, for display in the - horizontal and vertical headers""" - if self.__isClipped(orientation): # Header is clipped - if section == self.MAX_NUMBER_OF_SECTIONS - 2: - # Represent clipped data - return self.__clippedData(role) - - elif section == self.MAX_NUMBER_OF_SECTIONS - 1: - # Display last index from data not table - if role == qt.Qt.DisplayRole: - if orientation == qt.Qt.Vertical: - dim = self._getRowDim() - else: - dim = self._getColumnDim() - return str(self._array.shape[dim] - 1) - else: - return None - - if role == qt.Qt.DisplayRole: - return "%d" % section - return None - - def flags(self, index): - """QAbstractTableModel method to inform the view whether data - is editable or not.""" - if not self._editable or self.__isClippedIndex(index): - return qt.QAbstractTableModel.flags(self, index) - return qt.QAbstractTableModel.flags(self, index) | qt.Qt.ItemIsEditable - - def setData(self, index, value, role=None): - """QAbstractTableModel method to handle editing data. - Cast the new value into the same format as the array before editing - the array value.""" - if index.isValid() and role == qt.Qt.EditRole: - try: - # cast value to same type as array - v = numpy.array(value, dtype=self._array.dtype).item() - except ValueError: - return False - - selection = self._getIndexTuple(index.row(), - index.column()) - self._array[selection] = v - self.dataChanged.emit(index, index) - return True - else: - return False - - # Public methods - def setArrayData(self, data, copy=True, - perspective=None, editable=False): - """Set the data array and the viewing perspective. - - You can set ``copy=False`` if you need more performances, when dealing - with a large numpy array. In this case, a simple reference to the data - is used to access the data, rather than a copy of the array. - - .. warning:: - - Any change to the data model will affect your original data - array, when using a reference rather than a copy.. - - :param data: n-dimensional numpy array, or any object that can be - converted to a numpy array using ``numpy.array(data)`` (e.g. - a nested sequence). - :param bool copy: If *True* (default), a copy of the array is stored - and the original array is not modified if the table is edited. - If *False*, then the behavior depends on the data type: - if possible (if the original array is a proper numpy array) - a reference to the original array is used. - :param perspective: See documentation of :meth:`setPerspective`. - If None, the default perspective is the list of the first ``n-2`` - dimensions, to view frames parallel to the last two axes. - :param bool editable: Flag to enable editing data. Default *False*. - """ - if qt.qVersion() > "4.6": - self.beginResetModel() - else: - self.reset() - - if data is None: - # empty array - self._array = numpy.array([]) - elif copy: - # copy requested (default) - self._array = numpy.array(data, copy=True) - if hasattr(data, "dtype"): - # Avoid to lose the monkey-patched h5py dtype - self._array.dtype = data.dtype - elif not _is_array(data): - raise TypeError("data is not a proper array. Try setting" + - " copy=True to convert it into a numpy array" + - " (this will cause the data to be copied!)") - # # copy not requested, but necessary - # _logger.warning( - # "data is not an array-like object. " + - # "Data must be copied.") - # self._array = numpy.array(data, copy=True) - else: - # Copy explicitly disabled & data implements required attributes. - # We can use a reference. - self._array = data - - # reset colors to None if new data shape is inconsistent - valid_color_shapes = (self._array.shape + (3,), - self._array.shape + (4,)) - if self._bgcolors is not None: - if self._bgcolors.shape not in valid_color_shapes: - self._bgcolors = None - if self._fgcolors is not None: - if self._fgcolors.shape not in valid_color_shapes: - self._fgcolors = None - - self.setEditable(editable) - - self._index = [0 for _i in range((len(self._array.shape) - 2))] - self._perspective = tuple(perspective) if perspective is not None else\ - tuple(range(0, len(self._array.shape) - 2)) - - if qt.qVersion() > "4.6": - self.endResetModel() - - def setArrayColors(self, bgcolors=None, fgcolors=None): - """Set the colors for all table cells by passing an array - of RGB or RGBA values (integers between 0 and 255). - - The shape of the colors array must be consistent with the data shape. - - If the data array is n-dimensional, the colors array must be - (n+1)-dimensional, with the first n-dimensions identical to the data - array dimensions, and the last dimension length-3 (RGB) or - length-4 (RGBA). - - :param bgcolors: RGB or RGBA colors array, defining the background color - for each cell in the table. - :param fgcolors: RGB or RGBA colors array, defining the foreground color - (text color) for each cell in the table. - """ - # array must be RGB or RGBA - valid_shapes = (self._array.shape + (3,), self._array.shape + (4,)) - errmsg = "Inconsistent shape for color array, should be %s or %s" % valid_shapes - - if bgcolors is not None: - if not _is_array(bgcolors): - bgcolors = numpy.array(bgcolors) - assert bgcolors.shape in valid_shapes, errmsg - - self._bgcolors = bgcolors - - if fgcolors is not None: - if not _is_array(fgcolors): - fgcolors = numpy.array(fgcolors) - assert fgcolors.shape in valid_shapes, errmsg - - self._fgcolors = fgcolors - - def setEditable(self, editable): - """Set flags to make the data editable. - - .. warning:: - - If the data is a reference to a h5py dataset open in read-only - mode, setting *editable=True* will fail and print a warning. - - .. warning:: - - Making the data editable means that the underlying data structure - in this data model will be modified. - If the data is a reference to a public object (open with - ``copy=False``), this could have side effects. If it is a - reference to an HDF5 dataset, this means the file will be - modified. - - :param bool editable: Flag to enable editing data. - :return: True if setting desired flag succeeded, False if it failed. - """ - self._editable = editable - if hasattr(self._array, "file"): - if hasattr(self._array.file, "mode"): - if editable and self._array.file.mode == "r": - _logger.warning( - "Data is a HDF5 dataset open in read-only " + - "mode. Editing must be disabled.") - self._editable = False - return False - return True - - def getData(self, copy=True): - """Return a copy of the data array, or a reference to it - if *copy=False* is passed as parameter. - - In case the shape was modified, to convert 0-D or 1-D data - into 2-D data, the original shape is restored in the returned data. - - :param bool copy: If *True* (default), return a copy of the data. If - *False*, return a reference. - :return: numpy array of data, or reference to original data object - if *copy=False* - """ - data = self._array if not copy else numpy.array(self._array, copy=True) - return data - - def setFrameIndex(self, index): - """Set the active slice index. - - This method is only relevant to arrays with at least 3 dimensions. - - :param index: Index of the active slice in the array. - In the general n-D case, this is a sequence of :math:`n - 2` - indices where the slice intersects the respective orthogonal axes. - :raise IndexError: If any index in the index sequence is out of bound - on its respective axis. - """ - shape = self._array.shape - if len(shape) < 3: - # index is ignored - return - - if qt.qVersion() > "4.6": - self.beginResetModel() - else: - self.reset() - - if len(shape) == 3: - len_ = shape[self._perspective[0]] - # accept integers as index in the case of 3-D arrays - if not hasattr(index, "__len__"): - self._index = [index] - else: - self._index = index - if not 0 <= self._index[0] < len_: - raise ValueError("Index must be a positive integer " + - "lower than %d" % len_) - else: - # general n-D case - for i_, idx in enumerate(index): - if not 0 <= idx < shape[self._perspective[i_]]: - raise IndexError("Invalid index %d " % idx + - "not in range 0-%d" % (shape[i_] - 1)) - self._index = index - - if qt.qVersion() > "4.6": - self.endResetModel() - - def setFormatter(self, formatter): - """Set the formatter object to be used to display data from the model - - :param TextFormatter formatter: Formatter to use - """ - if formatter is self._formatter: - return - - if qt.qVersion() > "4.6": - self.beginResetModel() - - if self._formatter is not None: - self._formatter.formatChanged.disconnect(self.__formatChanged) - - self._formatter = formatter - if self._formatter is not None: - self._formatter.formatChanged.connect(self.__formatChanged) - - if qt.qVersion() > "4.6": - self.endResetModel() - else: - self.reset() - - def getFormatter(self): - """Returns the text formatter used. - - :rtype: TextFormatter - """ - return self._formatter - - def __formatChanged(self): - """Called when the format changed. - """ - self.reset() - - def setPerspective(self, perspective): - """Set the perspective by defining a sequence listing all axes - orthogonal to the frame or 2-D slice to be visualized. - - Alternatively, you can use :meth:`setFrameAxes` for the complementary - approach of specifying the two axes parallel to the frame. - - In the 1-D or 2-D case, this parameter is irrelevant. - - In the 3-D case, if the unit vectors describing - your axes are :math:`\vec{x}, \vec{y}, \vec{z}`, a perspective of 0 - means you slices are parallel to :math:`\vec{y}\vec{z}`, 1 means they - are parallel to :math:`\vec{x}\vec{z}` and 2 means they - are parallel to :math:`\vec{x}\vec{y}`. - - In the n-D case, this parameter is a sequence of :math:`n-2` axes - numbers. - For instance if you want to display 2-D frames whose axes are the - second and third dimensions of a 5-D array, set the perspective to - ``(0, 3, 4)``. - - :param perspective: Sequence of dimensions/axes orthogonal to the - frames. - :raise: IndexError if any value in perspective is higher than the - number of dimensions minus one (first dimension is 0), or - if the number of values is different from the number of dimensions - minus two. - """ - n_dimensions = len(self._array.shape) - if n_dimensions < 3: - _logger.warning( - "perspective is not relevant for 1D and 2D arrays") - return - - if not hasattr(perspective, "__len__"): - # we can tolerate an integer for 3-D array - if n_dimensions == 3: - perspective = [perspective] - else: - raise ValueError("perspective must be a sequence of integers") - - # ensure unicity of dimensions in perspective - perspective = tuple(set(perspective)) - - if len(perspective) != n_dimensions - 2 or\ - min(perspective) < 0 or max(perspective) >= n_dimensions: - raise IndexError( - "Invalid perspective " + str(perspective) + - " for %d-D array " % n_dimensions + - "with shape " + str(self._array.shape)) - - if qt.qVersion() > "4.6": - self.beginResetModel() - else: - self.reset() - - self._perspective = perspective - - # reset index - self._index = [0 for _i in range(n_dimensions - 2)] - - if qt.qVersion() > "4.6": - self.endResetModel() - - def setFrameAxes(self, row_axis, col_axis): - """Set the perspective by specifying the two axes parallel to the frame - to be visualised. - - The complementary approach of defining the orthogonal axes can be used - with :meth:`setPerspective`. - - :param int row_axis: Index (0-based) of the first dimension used as a frame - axis - :param int col_axis: Index (0-based) of the 2nd dimension used as a frame - axis - :raise: IndexError if axes are invalid - """ - if row_axis > col_axis: - _logger.warning("The dimension of the row axis must be lower " + - "than the dimension of the column axis. Swapping.") - row_axis, col_axis = min(row_axis, col_axis), max(row_axis, col_axis) - - n_dimensions = len(self._array.shape) - if n_dimensions < 3: - _logger.warning( - "Frame axes cannot be changed for 1D and 2D arrays") - return - - perspective = tuple(set(range(0, n_dimensions)) - {row_axis, col_axis}) - - if len(perspective) != n_dimensions - 2 or\ - min(perspective) < 0 or max(perspective) >= n_dimensions: - raise IndexError( - "Invalid perspective " + str(perspective) + - " for %d-D array " % n_dimensions + - "with shape " + str(self._array.shape)) - - if qt.qVersion() > "4.6": - self.beginResetModel() - else: - self.reset() - - self._perspective = perspective - # reset index - self._index = [0 for _i in range(n_dimensions - 2)] - - if qt.qVersion() > "4.6": - self.endResetModel() - - -if __name__ == "__main__": - app = qt.QApplication([]) - w = qt.QTableView() - d = numpy.random.normal(0, 1, (5, 1000, 1000)) - for i in range(5): - d[i, :, :] += i * 10 - m = ArrayTableModel(data=d) - w.setModel(m) - m.setFrameIndex(3) - # m.setArrayData(numpy.ones((100,))) - w.show() - app.exec_() diff --git a/silx/gui/data/ArrayTableWidget.py b/silx/gui/data/ArrayTableWidget.py deleted file mode 100644 index cb8e915..0000000 --- a/silx/gui/data/ArrayTableWidget.py +++ /dev/null @@ -1,492 +0,0 @@ -# coding: utf-8 -# /*########################################################################## -# -# Copyright (c) 2016-2017 European Synchrotron Radiation Facility -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. -# -# ###########################################################################*/ -"""This module defines a widget designed to display data arrays with any -number of dimensions as 2D frames (images, slices) in a table view. -The dimensions not displayed in the table can be browsed using improved -sliders. - -The widget uses a TableView that relies on a custom abstract item -model: :class:`silx.gui.data.ArrayTableModel`. -""" -from __future__ import division -import sys - -from silx.gui import qt -from silx.gui.widgets.TableWidget import TableView -from .ArrayTableModel import ArrayTableModel -from silx.gui.widgets.FrameBrowser import HorizontalSliderWithBrowser - -__authors__ = ["V.A. Sole", "P. Knobel"] -__license__ = "MIT" -__date__ = "24/01/2017" - - -class AxesSelector(qt.QWidget): - """Widget with two combo-boxes to select two dimensions among - all possible dimensions of an n-dimensional array. - - The first combobox contains values from :math:`0` to :math:`n-2`. - - The choices in the 2nd CB depend on the value selected in the first one. - If the value selected in the first CB is :math:`m`, the second one lets you - select values from :math:`m+1` to :math:`n-1`. - - The two axes can be used to select the row axis and the column axis t - display a slice of the array data in a table view. - """ - sigDimensionsChanged = qt.Signal(int, int) - """Signal emitted whenever one of the comboboxes is changed. - The signal carries the two selected dimensions.""" - - def __init__(self, parent=None, n=None): - qt.QWidget.__init__(self, parent) - self.layout = qt.QHBoxLayout(self) - self.layout.setContentsMargins(0, 2, 0, 2) - self.layout.setSpacing(10) - - self.rowsCB = qt.QComboBox(self) - self.columnsCB = qt.QComboBox(self) - - self.layout.addWidget(qt.QLabel("Rows dimension", self)) - self.layout.addWidget(self.rowsCB) - self.layout.addWidget(qt.QLabel(" ", self)) - self.layout.addWidget(qt.QLabel("Columns dimension", self)) - self.layout.addWidget(self.columnsCB) - self.layout.addStretch(1) - - self._slotsAreConnected = False - if n is not None: - self.setNDimensions(n) - - def setNDimensions(self, n): - """Initialize combo-boxes depending on number of dimensions of array. - Initially, the rows dimension is the second-to-last one, and the - columns dimension is the last one. - - Link the CBs together. MAke them emit a signal when their value is - changed. - - :param int n: Number of dimensions of array - """ - # remember the number of dimensions and the rows dimension - self.n = n - self._rowsDim = n - 2 - - # ensure slots are disconnected before (re)initializing widget - if self._slotsAreConnected: - self.rowsCB.currentIndexChanged.disconnect(self._rowDimChanged) - self.columnsCB.currentIndexChanged.disconnect(self._colDimChanged) - - self._clear() - self.rowsCB.addItems([str(i) for i in range(n - 1)]) - self.rowsCB.setCurrentIndex(n - 2) - if n >= 1: - self.columnsCB.addItem(str(n - 1)) - self.columnsCB.setCurrentIndex(0) - - # reconnect slots - self.rowsCB.currentIndexChanged.connect(self._rowDimChanged) - self.columnsCB.currentIndexChanged.connect(self._colDimChanged) - self._slotsAreConnected = True - - # emit new dimensions - if n > 2: - self.sigDimensionsChanged.emit(n - 2, n - 1) - - def setDimensions(self, row_dim, col_dim): - """Set the rows and columns dimensions. - - The rows dimension must be lower than the columns dimension. - - :param int row_dim: Rows dimension - :param int col_dim: Columns dimension - """ - if row_dim >= col_dim: - raise IndexError("Row dimension must be lower than column dimension") - if not (0 <= row_dim < self.n - 1): - raise IndexError("Row dimension must be between 0 and %d" % (self.n - 2)) - if not (row_dim < col_dim <= self.n - 1): - raise IndexError("Col dimension must be between %d and %d" % (row_dim + 1, self.n - 1)) - - # set the rows dimension; this triggers an update of columnsCB - self.rowsCB.setCurrentIndex(row_dim) - # columnsCB first item is "row_dim + 1". So index of "col_dim" is - # col_dim - (row_dim + 1) - self.columnsCB.setCurrentIndex(col_dim - row_dim - 1) - - def getDimensions(self): - """Return a 2-tuple of the rows dimension and the columns dimension. - - :return: 2-tuple of axes numbers (row_dimension, col_dimension) - """ - return self._getRowDim(), self._getColDim() - - def _clear(self): - """Empty the combo-boxes""" - self.rowsCB.clear() - self.columnsCB.clear() - - def _getRowDim(self): - """Get rows dimension, selected in :attr:`rowsCB` - """ - # rows combobox contains elements "0", ..."n-2", - # so the selected dim is always equal to the index - return self.rowsCB.currentIndex() - - def _getColDim(self): - """Get columns dimension, selected in :attr:`columnsCB`""" - # columns combobox contains elements "row_dim+1", "row_dim+2", ..., "n-1" - # so the selected dim is equal to row_dim + 1 + index - return self._rowsDim + 1 + self.columnsCB.currentIndex() - - def _rowDimChanged(self): - """Update columns combobox when the rows dimension is changed. - - Emit :attr:`sigDimensionsChanged`""" - old_col_dim = self._getColDim() - new_row_dim = self._getRowDim() - - # clear cols CB - self.columnsCB.currentIndexChanged.disconnect(self._colDimChanged) - self.columnsCB.clear() - # refill cols CB - for i in range(new_row_dim + 1, self.n): - self.columnsCB.addItem(str(i)) - - # keep previous col dimension if possible - new_col_cb_idx = old_col_dim - (new_row_dim + 1) - if new_col_cb_idx < 0: - # if row_dim is now greater than the previous col_dim, - # we select a new col_dim = row_dim + 1 (first element in cols CB) - new_col_cb_idx = 0 - self.columnsCB.setCurrentIndex(new_col_cb_idx) - - # reconnect slot - self.columnsCB.currentIndexChanged.connect(self._colDimChanged) - - self._rowsDim = new_row_dim - - self.sigDimensionsChanged.emit(self._getRowDim(), self._getColDim()) - - def _colDimChanged(self): - """Emit :attr:`sigDimensionsChanged`""" - self.sigDimensionsChanged.emit(self._getRowDim(), self._getColDim()) - - -def _get_shape(array_like): - """Return shape of an array like object. - - In case the object is a nested sequence (list of lists, tuples...), - the size of each dimension is assumed to be uniform, and is deduced from - the length of the first sequence. - - :param array_like: Array like object: numpy array, hdf5 dataset, - multi-dimensional sequence - :return: Shape of array, as a tuple of integers - """ - if hasattr(array_like, "shape"): - return array_like.shape - - shape = [] - subsequence = array_like - while hasattr(subsequence, "__len__"): - shape.append(len(subsequence)) - subsequence = subsequence[0] - - return tuple(shape) - - -class ArrayTableWidget(qt.QWidget): - """This widget is designed to display data of 2D frames (images, slices) - in a table view. The widget can load any n-dimensional array, and display - any 2-D frame/slice in the array. - - The index of the dimensions orthogonal to the displayed frame can be set - interactively using a browser widget (sliders, buttons and text entries). - - To set the data, use :meth:`setArrayData`. - To select the perspective, use :meth:`setPerspective` or - use :meth:`setFrameAxes`. - To select the frame, use :meth:`setFrameIndex`. - - .. image:: img/ArrayTableWidget.png - """ - def __init__(self, parent=None): - """ - - :param parent: parent QWidget - :param labels: list of labels for each dimension of the array - """ - qt.QWidget.__init__(self, parent) - self.mainLayout = qt.QVBoxLayout(self) - self.mainLayout.setContentsMargins(0, 0, 0, 0) - self.mainLayout.setSpacing(0) - - self.browserContainer = qt.QWidget(self) - self.browserLayout = qt.QGridLayout(self.browserContainer) - self.browserLayout.setContentsMargins(0, 0, 0, 0) - self.browserLayout.setSpacing(0) - - self._dimensionLabelsText = [] - """List of text labels sorted in the increasing order of the dimension - they apply to.""" - self._browserLabels = [] - """List of QLabel widgets.""" - self._browserWidgets = [] - """List of HorizontalSliderWithBrowser widgets.""" - - self.axesSelector = AxesSelector(self) - - self.view = TableView(self) - - self.mainLayout.addWidget(self.browserContainer) - self.mainLayout.addWidget(self.axesSelector) - self.mainLayout.addWidget(self.view) - - self.model = ArrayTableModel(self) - self.view.setModel(self.model) - - def setArrayData(self, data, labels=None, copy=True, editable=False): - """Set the data array. Update frame browsers and labels. - - :param data: Numpy array or similar object (e.g. nested sequence, - h5py dataset...) - :param labels: list of labels for each dimension of the array, or - boolean ``True`` to use default labels ("dimension 0", - "dimension 1", ...). `None` to disable labels (default). - :param bool copy: If *True*, store a copy of *data* in the model. If - *False*, store a reference to *data* if possible (only possible if - *data* is a proper numpy array or an object that implements the - same methods). - :param bool editable: Flag to enable editing data. Default is *False* - """ - self._data_shape = _get_shape(data) - - n_widgets = len(self._browserWidgets) - n_dimensions = len(self._data_shape) - - # Reset text of labels - self._dimensionLabelsText = [] - for i in range(n_dimensions): - if labels in [True, 1]: - label_text = "Dimension %d" % i - elif labels is None or i >= len(labels): - label_text = "" - else: - label_text = labels[i] - self._dimensionLabelsText.append(label_text) - - # not enough widgets, create new ones (we need n_dim - 2) - for i in range(n_widgets, n_dimensions - 2): - browser = HorizontalSliderWithBrowser(self.browserContainer) - self.browserLayout.addWidget(browser, i, 1) - self._browserWidgets.append(browser) - browser.valueChanged.connect(self._browserSlot) - browser.setEnabled(False) - browser.hide() - - label = qt.QLabel(self.browserContainer) - self._browserLabels.append(label) - self.browserLayout.addWidget(label, i, 0) - label.hide() - - n_widgets = len(self._browserWidgets) - for i in range(n_widgets): - label = self._browserLabels[i] - browser = self._browserWidgets[i] - - if (i + 2) < n_dimensions: - label.setText(self._dimensionLabelsText[i]) - browser.setRange(0, self._data_shape[i] - 1) - browser.setEnabled(True) - browser.show() - if labels is not None: - label.show() - else: - label.hide() - else: - browser.setEnabled(False) - browser.hide() - label.hide() - - # set model - self.model.setArrayData(data, copy=copy, editable=editable) - # some linux distributions need this call - self.view.setModel(self.model) - if editable: - self.view.enableCut() - self.view.enablePaste() - - # initialize & connect axesSelector - self.axesSelector.setNDimensions(n_dimensions) - self.axesSelector.sigDimensionsChanged.connect(self.setFrameAxes) - - def setArrayColors(self, bgcolors=None, fgcolors=None): - """Set the colors for all table cells by passing an array - of RGB or RGBA values (integers between 0 and 255). - - The shape of the colors array must be consistent with the data shape. - - If the data array is n-dimensional, the colors array must be - (n+1)-dimensional, with the first n-dimensions identical to the data - array dimensions, and the last dimension length-3 (RGB) or - length-4 (RGBA). - - :param bgcolors: RGB or RGBA colors array, defining the background color - for each cell in the table. - :param fgcolors: RGB or RGBA colors array, defining the foreground color - (text color) for each cell in the table. - """ - self.model.setArrayColors(bgcolors, fgcolors) - - def displayAxesSelector(self, isVisible): - """Allow to display or hide the axes selector. - - :param bool isVisible: True to display the axes selector. - """ - self.axesSelector.setVisible(isVisible) - - def setFrameIndex(self, index): - """Set the active slice/image index in the n-dimensional array. - - A frame is a 2D array extracted from an array. This frame is - necessarily parallel to 2 axes, and orthogonal to all other axes. - - The index of a frame is a sequence of indices along the orthogonal - axes, where the frame intersects the respective axis. The indices - are listed in the same order as the corresponding dimensions of the - data array. - - For example, it the data array has 5 dimensions, and we are - considering frames whose parallel axes are the 2nd and 4th dimensions - of the array, the frame index will be a sequence of length 3 - corresponding to the indices where the frame intersects the 1st, 3rd - and 5th axes. - - :param index: Sequence of indices defining the active data slice in - a n-dimensional array. The sequence length is :math:`n-2` - :raise: IndexError if any index in the index sequence is out of bound - on its respective axis. - """ - self.model.setFrameIndex(index) - - def _resetBrowsers(self, perspective): - """Adjust limits for browsers based on the perspective and the - size of the corresponding dimensions. Reset the index to 0. - Update the dimension in the labels. - - :param perspective: Sequence of axes/dimensions numbers (0-based) - defining the axes orthogonal to the frame. - """ - # for 3D arrays we can accept an int rather than a 1-tuple - if not hasattr(perspective, "__len__"): - perspective = [perspective] - - # perspective must be sorted - perspective = sorted(perspective) - - n_dimensions = len(self._data_shape) - for i in range(n_dimensions - 2): - browser = self._browserWidgets[i] - label = self._browserLabels[i] - browser.setRange(0, self._data_shape[perspective[i]] - 1) - browser.setValue(0) - label.setText(self._dimensionLabelsText[perspective[i]]) - - def setPerspective(self, perspective): - """Set the *perspective* by specifying which axes are orthogonal - to the frame. - - For the opposite approach (defining parallel axes), use - :meth:`setFrameAxes` instead. - - :param perspective: Sequence of unique axes numbers (0-based) defining - the orthogonal axes. For a n-dimensional array, the sequence - length is :math:`n-2`. The order is of the sequence is not taken - into account (the dimensions are displayed in increasing order - in the widget). - """ - self.model.setPerspective(perspective) - self._resetBrowsers(perspective) - - def setFrameAxes(self, row_axis, col_axis): - """Set the *perspective* by specifying which axes are parallel - to the frame. - - For the opposite approach (defining orthogonal axes), use - :meth:`setPerspective` instead. - - :param int row_axis: Index (0-based) of the first dimension used as a frame - axis - :param int col_axis: Index (0-based) of the 2nd dimension used as a frame - axis - """ - self.model.setFrameAxes(row_axis, col_axis) - n_dimensions = len(self._data_shape) - perspective = tuple(set(range(0, n_dimensions)) - {row_axis, col_axis}) - self._resetBrowsers(perspective) - - def _browserSlot(self, value): - index = [] - for browser in self._browserWidgets: - if browser.isEnabled(): - index.append(browser.value()) - self.setFrameIndex(index) - self.view.reset() - - def getData(self, copy=True): - """Return a copy of the data array, or a reference to it if - *copy=False* is passed as parameter. - - :param bool copy: If *True* (default), return a copy of the data. If - *False*, return a reference. - :return: Numpy array of data, or reference to original data object - if *copy=False* - """ - return self.model.getData(copy=copy) - - -def main(): - import numpy - a = qt.QApplication([]) - d = numpy.random.normal(0, 1, (4, 5, 1000, 1000)) - for j in range(4): - for i in range(5): - d[j, i, :, :] += i + 10 * j - w = ArrayTableWidget() - if "2" in sys.argv: - print("sending a single image") - w.setArrayData(d[0, 0]) - elif "3" in sys.argv: - print("sending 5 images") - w.setArrayData(d[0]) - else: - print("sending 4 * 5 images ") - w.setArrayData(d, labels=True) - w.show() - a.exec_() - -if __name__ == "__main__": - main() diff --git a/silx/gui/data/DataViewer.py b/silx/gui/data/DataViewer.py deleted file mode 100644 index 2e51439..0000000 --- a/silx/gui/data/DataViewer.py +++ /dev/null @@ -1,593 +0,0 @@ -# coding: utf-8 -# /*########################################################################## -# -# Copyright (c) 2016-2019 European Synchrotron Radiation Facility -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. -# -# ###########################################################################*/ -"""This module defines a widget designed to display data using the most adapted -view from the ones provided by silx. -""" -from __future__ import division - -import logging -import os.path -import collections -from silx.gui import qt -from silx.gui.data import DataViews -from silx.gui.data.DataViews import _normalizeData -from silx.gui.utils import blockSignals -from silx.gui.data.NumpyAxesSelector import NumpyAxesSelector - - -__authors__ = ["V. Valls"] -__license__ = "MIT" -__date__ = "12/02/2019" - - -_logger = logging.getLogger(__name__) - - -DataSelection = collections.namedtuple("DataSelection", - ["filename", "datapath", - "slice", "permutation"]) - - -class DataViewer(qt.QFrame): - """Widget to display any kind of data - - .. image:: img/DataViewer.png - - The method :meth:`setData` allows to set any data to the widget. Mostly - `numpy.array` and `h5py.Dataset` are supported with adapted views. Other - data types are displayed using a text viewer. - - A default view is automatically selected when a data is set. The method - :meth:`setDisplayMode` allows to change the view. To have a graphical tool - to select the view, prefer using the widget :class:`DataViewerFrame`. - - The dimension of the input data and the expected dimension of the selected - view can differ. For example you can display an image (2D) from 4D - data. In this case a :class:`NumpyAxesSelector` is displayed to allow the - user to select the axis mapping and the slicing of other axes. - - .. code-block:: python - - import numpy - data = numpy.random.rand(500,500) - viewer = DataViewer() - viewer.setData(data) - viewer.setVisible(True) - """ - - displayedViewChanged = qt.Signal(object) - """Emitted when the displayed view changes""" - - dataChanged = qt.Signal() - """Emitted when the data changes""" - - currentAvailableViewsChanged = qt.Signal() - """Emitted when the current available views (which support the current - data) change""" - - def __init__(self, parent=None): - """Constructor - - :param QWidget parent: The parent of the widget - """ - super(DataViewer, self).__init__(parent) - - self.__stack = qt.QStackedWidget(self) - self.__numpySelection = NumpyAxesSelector(self) - self.__numpySelection.selectedAxisChanged.connect(self.__numpyAxisChanged) - self.__numpySelection.selectionChanged.connect(self.__numpySelectionChanged) - self.__numpySelection.customAxisChanged.connect(self.__numpyCustomAxisChanged) - - self.setLayout(qt.QVBoxLayout(self)) - self.layout().addWidget(self.__stack, 1) - - group = qt.QGroupBox(self) - group.setLayout(qt.QVBoxLayout()) - group.layout().addWidget(self.__numpySelection) - group.setTitle("Axis selection") - self.__axisSelection = group - - self.layout().addWidget(self.__axisSelection) - - self.__currentAvailableViews = [] - self.__currentView = None - self.__data = None - self.__info = None - self.__useAxisSelection = False - self.__userSelectedView = None - self.__hooks = None - - self.__views = [] - self.__index = {} - """store stack index for each views""" - - self._initializeViews() - - def _initializeViews(self): - """Inisialize the available views""" - views = self.createDefaultViews(self.__stack) - self.__views = list(views) - self.setDisplayMode(DataViews.EMPTY_MODE) - - def setGlobalHooks(self, hooks): - """Set a data view hooks for all the views - - :param DataViewHooks context: The hooks to use - """ - self.__hooks = hooks - for v in self.__views: - v.setHooks(hooks) - - def createDefaultViews(self, parent=None): - """Create and returns available views which can be displayed by default - by the data viewer. It is called internally by the widget. It can be - overwriten to provide a different set of viewers. - - :param QWidget parent: QWidget parent of the views - :rtype: List[silx.gui.data.DataViews.DataView] - """ - viewClasses = [ - DataViews._EmptyView, - DataViews._Hdf5View, - DataViews._NXdataView, - DataViews._Plot1dView, - DataViews._ImageView, - DataViews._Plot3dView, - DataViews._RawView, - DataViews._StackView, - DataViews._Plot2dRecordView, - ] - views = [] - for viewClass in viewClasses: - try: - view = viewClass(parent) - views.append(view) - except Exception: - _logger.warning("%s instantiation failed. View is ignored" % viewClass.__name__) - _logger.debug("Backtrace", exc_info=True) - - return views - - def clear(self): - """Clear the widget""" - self.setData(None) - - def normalizeData(self, data): - """Returns a normalized data if the embed a numpy or a dataset. - Else returns the data.""" - return _normalizeData(data) - - def __getStackIndex(self, view): - """Get the stack index containing the view. - - :param silx.gui.data.DataViews.DataView view: The view - """ - if view not in self.__index: - widget = view.getWidget() - index = self.__stack.addWidget(widget) - self.__index[view] = index - else: - index = self.__index[view] - return index - - def __clearCurrentView(self): - """Clear the current selected view""" - view = self.__currentView - if view is not None: - view.clear() - - def __numpyCustomAxisChanged(self, name, value): - view = self.__currentView - if view is not None: - view.setCustomAxisValue(name, value) - - def __updateNumpySelectionAxis(self): - """ - Update the numpy-selector according to the needed axis names - """ - with blockSignals(self.__numpySelection): - previousPermutation = self.__numpySelection.permutation() - previousSelection = self.__numpySelection.selection() - - self.__numpySelection.clear() - - info = self._getInfo() - axisNames = self.__currentView.axesNames(self.__data, info) - if (info.isArray and info.size != 0 and - self.__data is not None and axisNames is not None): - self.__useAxisSelection = True - self.__numpySelection.setAxisNames(axisNames) - self.__numpySelection.setCustomAxis( - self.__currentView.customAxisNames()) - data = self.normalizeData(self.__data) - self.__numpySelection.setData(data) - - # Try to restore previous permutation and selection - try: - self.__numpySelection.setSelection( - previousSelection, previousPermutation) - except ValueError as e: - _logger.info("Not restoring selection because: %s", e) - - if hasattr(data, "shape"): - isVisible = not (len(axisNames) == 1 and len(data.shape) == 1) - else: - isVisible = True - self.__axisSelection.setVisible(isVisible) - else: - self.__useAxisSelection = False - self.__axisSelection.setVisible(False) - - def __updateDataInView(self): - """ - Update the views using the current data - """ - if self.__useAxisSelection: - self.__displayedData = self.__numpySelection.selectedData() - - permutation = self.__numpySelection.permutation() - normal = tuple(range(len(permutation))) - if permutation == normal: - permutation = None - slicing = self.__numpySelection.selection() - normal = tuple([slice(None)] * len(slicing)) - if slicing == normal: - slicing = None - else: - self.__displayedData = self.__data - permutation = None - slicing = None - - try: - filename = os.path.abspath(self.__data.file.filename) - except: - filename = None - - try: - datapath = self.__data.name - except: - datapath = None - - # FIXME: maybe use DataUrl, with added support of permutation - self.__displayedSelection = DataSelection(filename, datapath, slicing, permutation) - - # TODO: would be good to avoid that, it should be synchonous - qt.QTimer.singleShot(10, self.__setDataInView) - - def __setDataInView(self): - self.__currentView.setData(self.__displayedData) - self.__currentView.setDataSelection(self.__displayedSelection) - - def setDisplayedView(self, view): - """Set the displayed view. - - Change the displayed view according to the view itself. - - :param silx.gui.data.DataViews.DataView view: The DataView to use to display the data - """ - self.__userSelectedView = view - self._setDisplayedView(view) - - def _setDisplayedView(self, view): - """Internal set of the displayed view. - - Change the displayed view according to the view itself. - - :param silx.gui.data.DataViews.DataView view: The DataView to use to display the data - """ - if self.__currentView is view: - return - self.__clearCurrentView() - self.__currentView = view - self.__updateNumpySelectionAxis() - self.__updateDataInView() - stackIndex = self.__getStackIndex(self.__currentView) - if self.__currentView is not None: - self.__currentView.select() - self.__stack.setCurrentIndex(stackIndex) - self.displayedViewChanged.emit(view) - - def getViewFromModeId(self, modeId): - """Returns the first available view which have the requested modeId. - Return None if modeId does not correspond to an existing view. - - :param int modeId: Requested mode id - :rtype: silx.gui.data.DataViews.DataView - """ - for view in self.__views: - if view.modeId() == modeId: - return view - return None - - def setDisplayMode(self, modeId): - """Set the displayed view using display mode. - - Change the displayed view according to the requested mode. - - :param int modeId: Display mode, one of - - - `DataViews.EMPTY_MODE`: display nothing - - `DataViews.PLOT1D_MODE`: display the data as a curve - - `DataViews.IMAGE_MODE`: display the data as an image - - `DataViews.PLOT3D_MODE`: display the data as an isosurface - - `DataViews.RAW_MODE`: display the data as a table - - `DataViews.STACK_MODE`: display the data as a stack of images - - `DataViews.HDF5_MODE`: display the data as a table of HDF5 info - - `DataViews.NXDATA_MODE`: display the data as NXdata - """ - try: - view = self.getViewFromModeId(modeId) - except KeyError: - raise ValueError("Display mode %s is unknown" % modeId) - self._setDisplayedView(view) - - def displayedView(self): - """Returns the current displayed view. - - :rtype: silx.gui.data.DataViews.DataView - """ - return self.__currentView - - def addView(self, view): - """Allow to add a view to the dataview. - - If the current data support this view, it will be displayed. - - :param DataView view: A dataview - """ - if self.__hooks is not None: - view.setHooks(self.__hooks) - self.__views.append(view) - # TODO It can be skipped if the view do not support the data - self.__updateAvailableViews() - - def removeView(self, view): - """Allow to remove a view which was available from the dataview. - - If the view was displayed, the widget will be updated. - - :param DataView view: A dataview - """ - self.__views.remove(view) - self.__stack.removeWidget(view.getWidget()) - # invalidate the full index. It will be updated as expected - self.__index = {} - - if self.__userSelectedView is view: - self.__userSelectedView = None - - if view is self.__currentView: - self.__updateView() - else: - # TODO It can be skipped if the view is not part of the - # available views - self.__updateAvailableViews() - - def __updateAvailableViews(self): - """ - Update available views from the current data. - """ - data = self.__data - info = self._getInfo() - # sort available views according to priority - views = [] - for v in self.__views: - views.extend(v.getMatchingViews(data, info)) - views = [(v.getCachedDataPriority(data, info), v) for v in views] - views = filter(lambda t: t[0] > DataViews.DataView.UNSUPPORTED, views) - views = sorted(views, reverse=True) - views = [v[1] for v in views] - - # store available views - self.__setCurrentAvailableViews(views) - - def __updateView(self): - """Display the data using the widget which fit the best""" - data = self.__data - - # update available views for this data - self.__updateAvailableViews() - available = self.__currentAvailableViews - - # display the view with the most priority (the default view) - view = self.getDefaultViewFromAvailableViews(data, available) - self.__clearCurrentView() - try: - self._setDisplayedView(view) - except Exception as e: - # in case there is a problem to read the data, try to use a safe - # view - view = self.getSafeViewFromAvailableViews(data, available) - self._setDisplayedView(view) - raise e - - def getSafeViewFromAvailableViews(self, data, available): - """Returns a view which is sure to display something without failing - on rendering. - - :param object data: data which will be displayed - :param List[view] available: List of available views, from highest - priority to lowest. - :rtype: DataView - """ - hdf5View = self.getViewFromModeId(DataViews.HDF5_MODE) - if hdf5View in available: - return hdf5View - return self.getViewFromModeId(DataViews.EMPTY_MODE) - - def getDefaultViewFromAvailableViews(self, data, available): - """Returns the default view which will be used according to available - views. - - :param object data: data which will be displayed - :param List[view] available: List of available views, from highest - priority to lowest. - :rtype: DataView - """ - if len(available) > 0: - # returns the view with the highest priority - if self.__userSelectedView in available: - return self.__userSelectedView - self.__userSelectedView = None - view = available[0] - else: - # else returns the empty view - view = self.getViewFromModeId(DataViews.EMPTY_MODE) - return view - - def __setCurrentAvailableViews(self, availableViews): - """Set the current available viewa - - :param List[DataView] availableViews: Current available viewa - """ - self.__currentAvailableViews = availableViews - self.currentAvailableViewsChanged.emit() - - def currentAvailableViews(self): - """Returns the list of available views for the current data - - :rtype: List[DataView] - """ - return self.__currentAvailableViews - - def getReachableViews(self): - """Returns the list of reachable views from the registred available - views. - - :rtype: List[DataView] - """ - views = [] - for v in self.availableViews(): - views.extend(v.getReachableViews()) - return views - - def availableViews(self): - """Returns the list of registered views - - :rtype: List[DataView] - """ - return self.__views - - def setData(self, data): - """Set the data to view. - - It mostly can be a h5py.Dataset or a numpy.ndarray. Other kind of - objects will be displayed as text rendering. - - :param numpy.ndarray data: The data. - """ - self.__data = data - self._invalidateInfo() - self.__displayedData = None - self.__displayedSelection = None - self.__updateView() - self.__updateNumpySelectionAxis() - self.__updateDataInView() - self.dataChanged.emit() - - def __numpyAxisChanged(self): - """ - Called when axis selection of the numpy-selector changed - """ - self.__clearCurrentView() - - def __numpySelectionChanged(self): - """ - Called when data selection of the numpy-selector changed - """ - self.__updateDataInView() - - def data(self): - """Returns the data""" - return self.__data - - def _invalidateInfo(self): - """Invalidate DataInfo cache.""" - self.__info = None - - def _getInfo(self): - """Returns the DataInfo of the current selected data. - - This value is cached. - - :rtype: DataInfo - """ - if self.__info is None: - self.__info = DataViews.DataInfo(self.__data) - return self.__info - - def displayMode(self): - """Returns the current display mode""" - return self.__currentView.modeId() - - def replaceView(self, modeId, newView): - """Replace one of the builtin data views with a custom view. - Return True in case of success, False in case of failure. - - .. note:: - - This method must be called just after instantiation, before - the viewer is used. - - :param int modeId: Unique mode ID identifying the DataView to - be replaced. One of: - - - `DataViews.EMPTY_MODE` - - `DataViews.PLOT1D_MODE` - - `DataViews.IMAGE_MODE` - - `DataViews.PLOT2D_MODE` - - `DataViews.COMPLEX_IMAGE_MODE` - - `DataViews.PLOT3D_MODE` - - `DataViews.RAW_MODE` - - `DataViews.STACK_MODE` - - `DataViews.HDF5_MODE` - - `DataViews.NXDATA_MODE` - - `DataViews.NXDATA_INVALID_MODE` - - `DataViews.NXDATA_SCALAR_MODE` - - `DataViews.NXDATA_CURVE_MODE` - - `DataViews.NXDATA_XYVSCATTER_MODE` - - `DataViews.NXDATA_IMAGE_MODE` - - `DataViews.NXDATA_STACK_MODE` - - :param DataViews.DataView newView: New data view - :return: True if replacement was successful, else False - """ - assert isinstance(newView, DataViews.DataView) - isReplaced = False - for idx, view in enumerate(self.__views): - if view.modeId() == modeId: - if self.__hooks is not None: - newView.setHooks(self.__hooks) - self.__views[idx] = newView - isReplaced = True - break - elif isinstance(view, DataViews.CompositeDataView): - isReplaced = view.replaceView(modeId, newView) - if isReplaced: - break - - if isReplaced: - self.__updateAvailableViews() - return isReplaced diff --git a/silx/gui/data/DataViewerFrame.py b/silx/gui/data/DataViewerFrame.py deleted file mode 100644 index 9bfb95b..0000000 --- a/silx/gui/data/DataViewerFrame.py +++ /dev/null @@ -1,217 +0,0 @@ -# coding: utf-8 -# /*########################################################################## -# -# Copyright (c) 2016-2018 European Synchrotron Radiation Facility -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. -# -# ###########################################################################*/ -"""This module contains a DataViewer with a view selector. -""" - -__authors__ = ["V. Valls"] -__license__ = "MIT" -__date__ = "12/02/2019" - -from silx.gui import qt -from .DataViewer import DataViewer -from .DataViewerSelector import DataViewerSelector - - -class DataViewerFrame(qt.QWidget): - """ - A :class:`DataViewer` with a view selector. - - .. image:: img/DataViewerFrame.png - - This widget provides the same API as :class:`DataViewer`. Therefore, for more - documentation, take a look at the documentation of the class - :class:`DataViewer`. - - .. code-block:: python - - import numpy - data = numpy.random.rand(500,500) - viewer = DataViewerFrame() - viewer.setData(data) - viewer.setVisible(True) - - """ - - displayedViewChanged = qt.Signal(object) - """Emitted when the displayed view changes""" - - dataChanged = qt.Signal() - """Emitted when the data changes""" - - def __init__(self, parent=None): - """ - Constructor - - :param qt.QWidget parent: - """ - super(DataViewerFrame, self).__init__(parent) - - class _DataViewer(DataViewer): - """Overwrite methods to avoid to create views while the instance - is not created. `initializeViews` have to be called manually.""" - - def _initializeViews(self): - pass - - def initializeViews(self): - """Avoid to create views while the instance is not created.""" - super(_DataViewer, self)._initializeViews() - - def _createDefaultViews(self, parent): - """Expose the original `createDefaultViews` function""" - return super(_DataViewer, self).createDefaultViews() - - def createDefaultViews(self, parent=None): - """Allow the DataViewerFrame to override this function""" - return self.parent().createDefaultViews(parent) - - self.__dataViewer = _DataViewer(self) - # initialize views when `self.__dataViewer` is set - self.__dataViewer.initializeViews() - self.__dataViewer.setFrameShape(qt.QFrame.StyledPanel) - self.__dataViewer.setFrameShadow(qt.QFrame.Sunken) - self.__dataViewerSelector = DataViewerSelector(self, self.__dataViewer) - self.__dataViewerSelector.setFlat(True) - - layout = qt.QVBoxLayout(self) - layout.setContentsMargins(0, 0, 0, 0) - layout.setSpacing(0) - layout.addWidget(self.__dataViewer, 1) - layout.addWidget(self.__dataViewerSelector) - self.setLayout(layout) - - self.__dataViewer.dataChanged.connect(self.__dataChanged) - self.__dataViewer.displayedViewChanged.connect(self.__displayedViewChanged) - - def __dataChanged(self): - """Called when the data is changed""" - self.dataChanged.emit() - - def __displayedViewChanged(self, view): - """Called when the displayed view changes""" - self.displayedViewChanged.emit(view) - - def setGlobalHooks(self, hooks): - """Set a data view hooks for all the views - - :param DataViewHooks context: The hooks to use - """ - self.__dataViewer.setGlobalHooks(hooks) - - def getReachableViews(self): - return self.__dataViewer.getReachableViews() - - def availableViews(self): - """Returns the list of registered views - - :rtype: List[DataView] - """ - return self.__dataViewer.availableViews() - - def currentAvailableViews(self): - """Returns the list of available views for the current data - - :rtype: List[DataView] - """ - return self.__dataViewer.currentAvailableViews() - - def createDefaultViews(self, parent=None): - """Create and returns available views which can be displayed by default - by the data viewer. It is called internally by the widget. It can be - overwriten to provide a different set of viewers. - - :param QWidget parent: QWidget parent of the views - :rtype: List[silx.gui.data.DataViews.DataView] - """ - return self.__dataViewer._createDefaultViews(parent) - - def addView(self, view): - """Allow to add a view to the dataview. - - If the current data support this view, it will be displayed. - - :param DataView view: A dataview - """ - return self.__dataViewer.addView(view) - - def removeView(self, view): - """Allow to remove a view which was available from the dataview. - - If the view was displayed, the widget will be updated. - - :param DataView view: A dataview - """ - return self.__dataViewer.removeView(view) - - def setData(self, data): - """Set the data to view. - - It mostly can be a h5py.Dataset or a numpy.ndarray. Other kind of - objects will be displayed as text rendering. - - :param numpy.ndarray data: The data. - """ - self.__dataViewer.setData(data) - - def data(self): - """Returns the data""" - return self.__dataViewer.data() - - def setDisplayedView(self, view): - self.__dataViewer.setDisplayedView(view) - - def displayedView(self): - return self.__dataViewer.displayedView() - - def displayMode(self): - return self.__dataViewer.displayMode() - - def setDisplayMode(self, modeId): - """Set the displayed view using display mode. - - Change the displayed view according to the requested mode. - - :param int modeId: Display mode, one of - - - `EMPTY_MODE`: display nothing - - `PLOT1D_MODE`: display the data as a curve - - `PLOT2D_MODE`: display the data as an image - - `TEXT_MODE`: display the data as a text - - `ARRAY_MODE`: display the data as a table - """ - return self.__dataViewer.setDisplayMode(modeId) - - def getViewFromModeId(self, modeId): - """See :meth:`DataViewer.getViewFromModeId`""" - return self.__dataViewer.getViewFromModeId(modeId) - - def replaceView(self, modeId, newView): - """Replace one of the builtin data views with a custom view. - See :meth:`DataViewer.replaceView` for more documentation. - - :param DataViews.DataView newView: New data view - :return: True if replacement was successful, else False - """ - return self.__dataViewer.replaceView(modeId, newView) diff --git a/silx/gui/data/DataViewerSelector.py b/silx/gui/data/DataViewerSelector.py deleted file mode 100644 index a1e9947..0000000 --- a/silx/gui/data/DataViewerSelector.py +++ /dev/null @@ -1,175 +0,0 @@ -# coding: utf-8 -# /*########################################################################## -# -# Copyright (c) 2016-2017 European Synchrotron Radiation Facility -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. -# -# ###########################################################################*/ -"""This module defines a widget to be able to select the available view -of the DataViewer. -""" -from __future__ import division - -__authors__ = ["V. Valls"] -__license__ = "MIT" -__date__ = "12/02/2019" - -import weakref -import functools -from silx.gui import qt -import silx.utils.weakref - - -class DataViewerSelector(qt.QWidget): - """Widget to be able to select a custom view from the DataViewer""" - - def __init__(self, parent=None, dataViewer=None): - """Constructor - - :param QWidget parent: The parent of the widget - :param DataViewer dataViewer: The connected `DataViewer` - """ - super(DataViewerSelector, self).__init__(parent) - - self.__group = None - self.__buttons = {} - self.__buttonLayout = None - self.__buttonDummy = None - self.__dataViewer = None - - # Create the fixed layout - self.setLayout(qt.QHBoxLayout()) - layout = self.layout() - layout.setContentsMargins(0, 0, 0, 0) - self.__buttonLayout = qt.QHBoxLayout() - self.__buttonLayout.setContentsMargins(0, 0, 0, 0) - layout.addLayout(self.__buttonLayout) - layout.addStretch(1) - - if dataViewer is not None: - self.setDataViewer(dataViewer) - - def __updateButtons(self): - if self.__group is not None: - self.__group.deleteLater() - - # Clean up - for _, b in self.__buttons.items(): - b.deleteLater() - if self.__buttonDummy is not None: - self.__buttonDummy.deleteLater() - self.__buttonDummy = None - self.__buttons = {} - self.__buttonDummy = None - - self.__group = qt.QButtonGroup(self) - if self.__dataViewer is None: - return - - iconSize = qt.QSize(16, 16) - - for view in self.__dataViewer.getReachableViews(): - label = view.label() - icon = view.icon() - button = qt.QPushButton(label) - button.setIcon(icon) - button.setIconSize(iconSize) - button.setCheckable(True) - # the weak objects are needed to be able to destroy the widget safely - weakView = weakref.ref(view) - weakMethod = silx.utils.weakref.WeakMethodProxy(self.__setDisplayedView) - callback = functools.partial(weakMethod, weakView) - button.clicked.connect(callback) - self.__buttonLayout.addWidget(button) - self.__group.addButton(button) - self.__buttons[view] = button - - button = qt.QPushButton("Dummy") - button.setCheckable(True) - button.setVisible(False) - self.__buttonLayout.addWidget(button) - self.__group.addButton(button) - self.__buttonDummy = button - - self.__updateButtonsVisibility() - self.__displayedViewChanged(self.__dataViewer.displayedView()) - - def setDataViewer(self, dataViewer): - """Define the dataviewer connected to this status bar - - :param DataViewer dataViewer: The connected `DataViewer` - """ - if self.__dataViewer is dataViewer: - return - if self.__dataViewer is not None: - self.__dataViewer.dataChanged.disconnect(self.__updateButtonsVisibility) - self.__dataViewer.displayedViewChanged.disconnect(self.__displayedViewChanged) - self.__dataViewer = dataViewer - if self.__dataViewer is not None: - self.__dataViewer.dataChanged.connect(self.__updateButtonsVisibility) - self.__dataViewer.displayedViewChanged.connect(self.__displayedViewChanged) - self.__updateButtons() - - def setFlat(self, isFlat): - """Set the flat state of all the buttons. - - :param bool isFlat: True to display the buttons flatten. - """ - for b in self.__buttons.values(): - b.setFlat(isFlat) - self.__buttonDummy.setFlat(isFlat) - - def __displayedViewChanged(self, view): - """Called on displayed view changes""" - selectedButton = self.__buttons.get(view, self.__buttonDummy) - selectedButton.setChecked(True) - - def __setDisplayedView(self, refView, clickEvent=None): - """Display a data using the requested view - - :param DataView view: Requested view - :param clickEvent: Event sent by the clicked event - """ - if self.__dataViewer is None: - return - view = refView() - if view is None: - return - self.__dataViewer.setDisplayedView(view) - - def __checkAvailableButtons(self): - views = set(self.__dataViewer.getReachableViews()) - if views == set(self.__buttons.keys()): - return - # Recreate all the buttons - # TODO: We dont have to create everything again - # We expect the views stay quite stable - self.__updateButtons() - - def __updateButtonsVisibility(self): - """Called on data changed""" - if self.__dataViewer is None: - for b in self.__buttons.values(): - b.setVisible(False) - else: - self.__checkAvailableButtons() - availableViews = set(self.__dataViewer.currentAvailableViews()) - for view, button in self.__buttons.items(): - button.setVisible(view in availableViews) diff --git a/silx/gui/data/DataViews.py b/silx/gui/data/DataViews.py deleted file mode 100644 index b18a813..0000000 --- a/silx/gui/data/DataViews.py +++ /dev/null @@ -1,2059 +0,0 @@ -# coding: utf-8 -# /*########################################################################## -# -# Copyright (c) 2016-2020 European Synchrotron Radiation Facility -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. -# -# ###########################################################################*/ -"""This module defines a views used by :class:`silx.gui.data.DataViewer`. -""" - -from collections import OrderedDict -import logging -import numbers -import numpy -import os - -import silx.io -from silx.utils import deprecation -from silx.gui import qt, icons -from silx.gui.data.TextFormatter import TextFormatter -from silx.io import nxdata -from silx.gui.hdf5 import H5Node -from silx.io.nxdata import get_attr_as_unicode -from silx.gui.colors import Colormap -from silx.gui.dialog.ColormapDialog import ColormapDialog - -__authors__ = ["V. Valls", "P. Knobel"] -__license__ = "MIT" -__date__ = "19/02/2019" - -_logger = logging.getLogger(__name__) - - -# DataViewer modes -EMPTY_MODE = 0 -PLOT1D_MODE = 10 -RECORD_PLOT_MODE = 15 -IMAGE_MODE = 20 -PLOT2D_MODE = 21 -COMPLEX_IMAGE_MODE = 22 -PLOT3D_MODE = 30 -RAW_MODE = 40 -RAW_ARRAY_MODE = 41 -RAW_RECORD_MODE = 42 -RAW_SCALAR_MODE = 43 -RAW_HEXA_MODE = 44 -STACK_MODE = 50 -HDF5_MODE = 60 -NXDATA_MODE = 70 -NXDATA_INVALID_MODE = 71 -NXDATA_SCALAR_MODE = 72 -NXDATA_CURVE_MODE = 73 -NXDATA_XYVSCATTER_MODE = 74 -NXDATA_IMAGE_MODE = 75 -NXDATA_STACK_MODE = 76 -NXDATA_VOLUME_MODE = 77 -NXDATA_VOLUME_AS_STACK_MODE = 78 - - -def _normalizeData(data): - """Returns a normalized data. - - If the data embed a numpy data or a dataset it is returned. - Else returns the input data.""" - if isinstance(data, H5Node): - if data.is_broken: - return None - return data.h5py_object - return data - - -def _normalizeComplex(data): - """Returns a normalized complex data. - - If the data is a numpy data with complex, returns the - absolute value. - Else returns the input data.""" - if hasattr(data, "dtype"): - isComplex = numpy.issubdtype(data.dtype, numpy.complexfloating) - else: - isComplex = isinstance(data, numbers.Complex) - if isComplex: - data = numpy.absolute(data) - return data - - -class DataInfo(object): - """Store extracted information from a data""" - - def __init__(self, data): - self.__priorities = {} - data = self.normalizeData(data) - self.isArray = False - self.interpretation = None - self.isNumeric = False - self.isVoid = False - self.isComplex = False - self.isBoolean = False - self.isRecord = False - self.hasNXdata = False - self.isInvalidNXdata = False - self.countNumericColumns = 0 - self.shape = tuple() - self.dim = 0 - self.size = 0 - - if data is None: - return - - if silx.io.is_group(data): - nxd = nxdata.get_default(data) - nx_class = get_attr_as_unicode(data, "NX_class") - if nxd is not None: - self.hasNXdata = True - # can we plot it? - is_scalar = nxd.signal_is_0d or nxd.interpretation in ["scalar", "scaler"] - if not (is_scalar or nxd.is_curve or nxd.is_x_y_value_scatter or - nxd.is_image or nxd.is_stack): - # invalid: cannot be plotted by any widget - self.isInvalidNXdata = True - elif nx_class == "NXdata": - # group claiming to be NXdata could not be parsed - self.isInvalidNXdata = True - elif nx_class == "NXroot" or silx.io.is_file(data): - # root claiming to have a default entry - if "default" in data.attrs: - def_entry = data.attrs["default"] - if def_entry in data and "default" in data[def_entry].attrs: - # and entry claims to have default NXdata - self.isInvalidNXdata = True - elif "default" in data.attrs: - # group claiming to have a default NXdata could not be parsed - self.isInvalidNXdata = True - - if isinstance(data, numpy.ndarray): - self.isArray = True - elif silx.io.is_dataset(data) and data.shape != tuple(): - self.isArray = True - else: - self.isArray = False - - if silx.io.is_dataset(data): - if "interpretation" in data.attrs: - self.interpretation = get_attr_as_unicode(data, "interpretation") - else: - self.interpretation = None - elif self.hasNXdata: - self.interpretation = nxd.interpretation - else: - self.interpretation = None - - if hasattr(data, "dtype"): - if numpy.issubdtype(data.dtype, numpy.void): - # That's a real opaque type, else it is a structured type - self.isVoid = data.dtype.fields is None - self.isNumeric = numpy.issubdtype(data.dtype, numpy.number) - self.isRecord = data.dtype.fields is not None - self.isComplex = numpy.issubdtype(data.dtype, numpy.complexfloating) - self.isBoolean = numpy.issubdtype(data.dtype, numpy.bool_) - elif self.hasNXdata: - self.isNumeric = numpy.issubdtype(nxd.signal.dtype, - numpy.number) - self.isComplex = numpy.issubdtype(nxd.signal.dtype, numpy.complexfloating) - self.isBoolean = numpy.issubdtype(nxd.signal.dtype, numpy.bool_) - else: - self.isNumeric = isinstance(data, numbers.Number) - self.isComplex = isinstance(data, numbers.Complex) - self.isBoolean = isinstance(data, bool) - self.isRecord = False - - if hasattr(data, "shape"): - self.shape = data.shape - elif self.hasNXdata: - self.shape = nxd.signal.shape - else: - self.shape = tuple() - if self.shape is not None: - self.dim = len(self.shape) - - if hasattr(data, "shape") and data.shape is None: - # This test is expected to avoid to fall done on the h5py issue - # https://github.com/h5py/h5py/issues/1044 - self.size = 0 - elif hasattr(data, "size"): - self.size = int(data.size) - else: - self.size = 1 - - if hasattr(data, "dtype"): - if data.dtype.fields is not None: - for field in data.dtype.fields: - if numpy.issubdtype(data.dtype[field], numpy.number): - self.countNumericColumns += 1 - - def normalizeData(self, data): - """Returns a normalized data if the embed a numpy or a dataset. - Else returns the data.""" - return _normalizeData(data) - - def cachePriority(self, view, priority): - self.__priorities[view] = priority - - def getPriority(self, view): - return self.__priorities[view] - - -class DataViewHooks(object): - """A set of hooks defined to custom the behaviour of the data views.""" - - def getColormap(self, view): - """Returns a colormap for this view.""" - return None - - def getColormapDialog(self, view): - """Returns a color dialog for this view.""" - return None - - def viewWidgetCreated(self, view, plot): - """Called when the widget of the view was created""" - return - -class DataView(object): - """Holder for the data view.""" - - UNSUPPORTED = -1 - """Priority returned when the requested data can't be displayed by the - view.""" - - TITLE_PATTERN = "{datapath}{slicing} {permuted}" - """Pattern used to format the title of the plot. - - Supported fields: `{directory}`, `{filename}`, `{datapath}`, `{slicing}`, `{permuted}`. - """ - - def __init__(self, parent, modeId=None, icon=None, label=None): - """Constructor - - :param qt.QWidget parent: Parent of the hold widget - """ - self.__parent = parent - self.__widget = None - self.__modeId = modeId - if label is None: - label = self.__class__.__name__ - self.__label = label - if icon is None: - icon = qt.QIcon() - self.__icon = icon - self.__hooks = None - - def getHooks(self): - """Returns the data viewer hooks used by this view. - - :rtype: DataViewHooks - """ - return self.__hooks - - def setHooks(self, hooks): - """Set the data view hooks to use with this view. - - :param DataViewHooks hooks: The data view hooks to use - """ - self.__hooks = hooks - - def defaultColormap(self): - """Returns a default colormap. - - :rtype: Colormap - """ - colormap = None - if self.__hooks is not None: - colormap = self.__hooks.getColormap(self) - if colormap is None: - colormap = Colormap(name="viridis") - return colormap - - def defaultColorDialog(self): - """Returns a default color dialog. - - :rtype: ColormapDialog - """ - dialog = None - if self.__hooks is not None: - dialog = self.__hooks.getColormapDialog(self) - if dialog is None: - dialog = ColormapDialog() - dialog.setModal(False) - return dialog - - def icon(self): - """Returns the default icon""" - return self.__icon - - def label(self): - """Returns the default label""" - return self.__label - - def modeId(self): - """Returns the mode id""" - return self.__modeId - - def normalizeData(self, data): - """Returns a normalized data if the embed a numpy or a dataset. - Else returns the data.""" - return _normalizeData(data) - - def customAxisNames(self): - """Returns names of axes which can be custom by the user and provided - to the view.""" - return [] - - def setCustomAxisValue(self, name, value): - """ - Set the value of a custom axis - - :param str name: Name of the custom axis - :param int value: Value of the custom axis - """ - pass - - def isWidgetInitialized(self): - """Returns true if the widget is already initialized. - """ - return self.__widget is not None - - def select(self): - """Called when the view is selected to display the data. - """ - return - - def getWidget(self): - """Returns the widget hold in the view and displaying the data. - - :returns: qt.QWidget - """ - if self.__widget is None: - self.__widget = self.createWidget(self.__parent) - hooks = self.getHooks() - if hooks is not None: - hooks.viewWidgetCreated(self, self.__widget) - return self.__widget - - def createWidget(self, parent): - """Create the the widget displaying the data - - :param qt.QWidget parent: Parent of the widget - :returns: qt.QWidget - """ - raise NotImplementedError() - - def clear(self): - """Clear the data from the view""" - return None - - def setData(self, data): - """Set the data displayed by the view - - :param data: Data to display - :type data: numpy.ndarray or h5py.Dataset - """ - return None - - def __formatSlices(self, indices): - """Format an iterable of slice objects - - :param indices: The slices to format - :type indices: Union[None,List[Union[slice,int]]] - :rtype: str - """ - if indices is None: - return '' - - def formatSlice(slice_): - start, stop, step = slice_.start, slice_.stop, slice_.step - string = ('' if start is None else str(start)) + ':' - if stop is not None: - string += str(stop) - if step not in (None, 1): - string += ':' + step - return string - - return '[' + ', '.join( - formatSlice(index) if isinstance(index, slice) else str(index) - for index in indices) + ']' - - def titleForSelection(self, selection): - """Build title from given selection information. - - :param NamedTuple selection: Data selected - :rtype: str - """ - if selection is None or selection.filename is None: - return None - else: - directory, filename = os.path.split(selection.filename) - try: - slicing = self.__formatSlices(selection.slice) - except Exception: - _logger.debug("Error while formatting slices", exc_info=True) - slicing = '[sliced]' - - permuted = '(permuted)' if selection.permutation is not None else '' - - try: - title = self.TITLE_PATTERN.format( - directory=directory, - filename=filename, - datapath=selection.datapath, - slicing=slicing, - permuted=permuted) - except Exception: - _logger.debug("Error while formatting title", exc_info=True) - title = selection.datapath + slicing - - return title - - def setDataSelection(self, selection): - """Set the data selection displayed by the view - - If called, it have to be called directly after `setData`. - - :param selection: Data selected - :type selection: NamedTuple - """ - pass - - def axesNames(self, data, info): - """Returns names of the expected axes of the view, according to the - input data. A none value will disable the default axes selectior. - - :param data: Data to display - :type data: numpy.ndarray or h5py.Dataset - :param DataInfo info: Pre-computed information on the data - :rtype: list[str] or None - """ - return [] - - def getReachableViews(self): - """Returns the views that can be returned by `getMatchingViews`. - - :param object data: Any object to be displayed - :param DataInfo info: Information cached about this data - :rtype: List[DataView] - """ - return [self] - - def getMatchingViews(self, data, info): - """Returns the views according to data and info from the data. - - :param object data: Any object to be displayed - :param DataInfo info: Information cached about this data - :rtype: List[DataView] - """ - priority = self.getCachedDataPriority(data, info) - if priority == DataView.UNSUPPORTED: - return [] - return [self] - - def getCachedDataPriority(self, data, info): - try: - priority = info.getPriority(self) - except KeyError: - priority = self.getDataPriority(data, info) - info.cachePriority(self, priority) - return priority - - def getDataPriority(self, data, info): - """ - Returns the priority of using this view according to a data. - - - `UNSUPPORTED` means this view can't display this data - - `1` means this view can display the data - - `100` means this view should be used for this data - - `1000` max value used by the views provided by silx - - ... - - :param object data: The data to check - :param DataInfo info: Pre-computed information on the data - :rtype: int - """ - return DataView.UNSUPPORTED - - def __lt__(self, other): - return str(self) < str(other) - - -class _CompositeDataView(DataView): - """Contains sub views""" - - def getViews(self): - """Returns the direct sub views registered in this view. - - :rtype: List[DataView] - """ - raise NotImplementedError() - - def getReachableViews(self): - """Returns all views that can be reachable at on point. - - This method return any sub view provided (recursivly). - - :rtype: List[DataView] - """ - raise NotImplementedError() - - def getMatchingViews(self, data, info): - """Returns sub views matching this data and info. - - This method return any sub view provided (recursivly). - - :param object data: Any object to be displayed - :param DataInfo info: Information cached about this data - :rtype: List[DataView] - """ - raise NotImplementedError() - - @deprecation.deprecated(replacement="getReachableViews", since_version="0.10") - def availableViews(self): - return self.getViews() - - def isSupportedData(self, data, info): - """If true, the composite view allow sub views to access to this data. - Else this this data is considered as not supported by any of sub views - (incliding this composite view). - - :param object data: Any object to be displayed - :param DataInfo info: Information cached about this data - :rtype: bool - """ - return True - - -class SelectOneDataView(_CompositeDataView): - """Data view which can display a data using different view according to - the kind of the data.""" - - def __init__(self, parent, modeId=None, icon=None, label=None): - """Constructor - - :param qt.QWidget parent: Parent of the hold widget - """ - super(SelectOneDataView, self).__init__(parent, modeId, icon, label) - self.__views = OrderedDict() - self.__currentView = None - - def setHooks(self, hooks): - """Set the data context to use with this view. - - :param DataViewHooks hooks: The data view hooks to use - """ - super(SelectOneDataView, self).setHooks(hooks) - if hooks is not None: - for v in self.__views: - v.setHooks(hooks) - - def addView(self, dataView): - """Add a new dataview to the available list.""" - hooks = self.getHooks() - if hooks is not None: - dataView.setHooks(hooks) - self.__views[dataView] = None - - def getReachableViews(self): - views = [] - addSelf = False - for v in self.__views: - if isinstance(v, SelectManyDataView): - views.extend(v.getReachableViews()) - else: - addSelf = True - if addSelf: - # Single views are hidden by this view - views.insert(0, self) - return views - - def getMatchingViews(self, data, info): - if not self.isSupportedData(data, info): - return [] - view = self.__getBestView(data, info) - if isinstance(view, SelectManyDataView): - return view.getMatchingViews(data, info) - else: - return [self] - - def getViews(self): - """Returns the list of registered views - - :rtype: List[DataView] - """ - return list(self.__views.keys()) - - def __getBestView(self, data, info): - """Returns the best view according to priorities.""" - if not self.isSupportedData(data, info): - return None - views = [(v.getCachedDataPriority(data, info), v) for v in self.__views.keys()] - views = filter(lambda t: t[0] > DataView.UNSUPPORTED, views) - views = sorted(views, key=lambda t: t[0], reverse=True) - - if len(views) == 0: - return None - elif views[0][0] == DataView.UNSUPPORTED: - return None - else: - return views[0][1] - - def customAxisNames(self): - if self.__currentView is None: - return - return self.__currentView.customAxisNames() - - def setCustomAxisValue(self, name, value): - if self.__currentView is None: - return - self.__currentView.setCustomAxisValue(name, value) - - def __updateDisplayedView(self): - widget = self.getWidget() - if self.__currentView is None: - return - - # load the widget if it is not yet done - index = self.__views[self.__currentView] - if index is None: - w = self.__currentView.getWidget() - index = widget.addWidget(w) - self.__views[self.__currentView] = index - if widget.currentIndex() != index: - widget.setCurrentIndex(index) - self.__currentView.select() - - def select(self): - self.__updateDisplayedView() - if self.__currentView is not None: - self.__currentView.select() - - def createWidget(self, parent): - return qt.QStackedWidget() - - def clear(self): - for v in self.__views.keys(): - v.clear() - - def setData(self, data): - if self.__currentView is None: - return - self.__updateDisplayedView() - self.__currentView.setData(data) - - def setDataSelection(self, selection): - if self.__currentView is None: - return - self.__currentView.setDataSelection(selection) - - def axesNames(self, data, info): - view = self.__getBestView(data, info) - self.__currentView = view - return view.axesNames(data, info) - - def getDataPriority(self, data, info): - view = self.__getBestView(data, info) - self.__currentView = view - if view is None: - return DataView.UNSUPPORTED - else: - return view.getCachedDataPriority(data, info) - - def replaceView(self, modeId, newView): - """Replace a data view with a custom view. - Return True in case of success, False in case of failure. - - .. note:: - - This method must be called just after instantiation, before - the viewer is used. - - :param int modeId: Unique mode ID identifying the DataView to - be replaced. - :param DataViews.DataView newView: New data view - :return: True if replacement was successful, else False - """ - oldView = None - for view in self.__views: - if view.modeId() == modeId: - oldView = view - break - elif isinstance(view, _CompositeDataView): - # recurse - hooks = self.getHooks() - if hooks is not None: - newView.setHooks(hooks) - if view.replaceView(modeId, newView): - return True - if oldView is None: - return False - - # replace oldView with new view in dict - self.__views = OrderedDict( - (newView, None) if view is oldView else (view, idx) for - view, idx in self.__views.items()) - return True - - -# NOTE: SelectOneDataView was introduced with silx 0.10 -CompositeDataView = SelectOneDataView - - -class SelectManyDataView(_CompositeDataView): - """Data view which can select a set of sub views according to - the kind of the data. - - This view itself is abstract and is not exposed. - """ - - def __init__(self, parent, views=None): - """Constructor - - :param qt.QWidget parent: Parent of the hold widget - """ - super(SelectManyDataView, self).__init__(parent, modeId=None, icon=None, label=None) - if views is None: - views = [] - self.__views = views - - def setHooks(self, hooks): - """Set the data context to use with this view. - - :param DataViewHooks hooks: The data view hooks to use - """ - super(SelectManyDataView, self).setHooks(hooks) - if hooks is not None: - for v in self.__views: - v.setHooks(hooks) - - def addView(self, dataView): - """Add a new dataview to the available list.""" - hooks = self.getHooks() - if hooks is not None: - dataView.setHooks(hooks) - self.__views.append(dataView) - - def getViews(self): - """Returns the list of registered views - - :rtype: List[DataView] - """ - return list(self.__views) - - def getReachableViews(self): - views = [] - for v in self.__views: - views.extend(v.getReachableViews()) - return views - - def getMatchingViews(self, data, info): - """Returns the views according to data and info from the data. - - :param object data: Any object to be displayed - :param DataInfo info: Information cached about this data - """ - if not self.isSupportedData(data, info): - return [] - views = [v for v in self.__views if v.getCachedDataPriority(data, info) != DataView.UNSUPPORTED] - return views - - def customAxisNames(self): - raise RuntimeError("Abstract view") - - def setCustomAxisValue(self, name, value): - raise RuntimeError("Abstract view") - - def select(self): - raise RuntimeError("Abstract view") - - def createWidget(self, parent): - raise RuntimeError("Abstract view") - - def clear(self): - for v in self.__views: - v.clear() - - def setData(self, data): - raise RuntimeError("Abstract view") - - def axesNames(self, data, info): - raise RuntimeError("Abstract view") - - def getDataPriority(self, data, info): - if not self.isSupportedData(data, info): - return DataView.UNSUPPORTED - priorities = [v.getCachedDataPriority(data, info) for v in self.__views] - priorities = [v for v in priorities if v != DataView.UNSUPPORTED] - priorities = sorted(priorities) - if len(priorities) == 0: - return DataView.UNSUPPORTED - return priorities[-1] - - def replaceView(self, modeId, newView): - """Replace a data view with a custom view. - Return True in case of success, False in case of failure. - - .. note:: - - This method must be called just after instantiation, before - the viewer is used. - - :param int modeId: Unique mode ID identifying the DataView to - be replaced. - :param DataViews.DataView newView: New data view - :return: True if replacement was successful, else False - """ - oldView = None - for iview, view in enumerate(self.__views): - if view.modeId() == modeId: - oldView = view - break - elif isinstance(view, CompositeDataView): - # recurse - hooks = self.getHooks() - if hooks is not None: - newView.setHooks(hooks) - if view.replaceView(modeId, newView): - return True - - if oldView is None: - return False - - # replace oldView with new view in dict - self.__views[iview] = newView - return True - - -class _EmptyView(DataView): - """Dummy view to display nothing""" - - def __init__(self, parent): - DataView.__init__(self, parent, modeId=EMPTY_MODE) - - def axesNames(self, data, info): - return None - - def createWidget(self, parent): - return qt.QLabel(parent) - - def getDataPriority(self, data, info): - return DataView.UNSUPPORTED - - -class _Plot1dView(DataView): - """View displaying data using a 1d plot""" - - def __init__(self, parent): - super(_Plot1dView, self).__init__( - parent=parent, - modeId=PLOT1D_MODE, - label="Curve", - icon=icons.getQIcon("view-1d")) - self.__resetZoomNextTime = True - - def createWidget(self, parent): - from silx.gui import plot - return plot.Plot1D(parent=parent) - - def clear(self): - self.getWidget().clear() - self.__resetZoomNextTime = True - - def normalizeData(self, data): - data = DataView.normalizeData(self, data) - data = _normalizeComplex(data) - return data - - def setData(self, data): - data = self.normalizeData(data) - plotWidget = self.getWidget() - legend = "data" - plotWidget.addCurve(legend=legend, - x=range(len(data)), - y=data, - resetzoom=self.__resetZoomNextTime) - plotWidget.setActiveCurve(legend) - self.__resetZoomNextTime = True - - def setDataSelection(self, selection): - self.getWidget().setGraphTitle(self.titleForSelection(selection)) - - def axesNames(self, data, info): - return ["y"] - - def getDataPriority(self, data, info): - if info.size <= 0: - return DataView.UNSUPPORTED - if data is None or not info.isArray or not info.isNumeric: - return DataView.UNSUPPORTED - if info.dim < 1: - return DataView.UNSUPPORTED - if info.interpretation == "spectrum": - return 1000 - if info.dim == 2 and info.shape[0] == 1: - return 210 - if info.dim == 1: - return 100 - else: - return 10 - - -class _Plot2dRecordView(DataView): - def __init__(self, parent): - super(_Plot2dRecordView, self).__init__( - parent=parent, - modeId=RECORD_PLOT_MODE, - label="Curve", - icon=icons.getQIcon("view-1d")) - self.__resetZoomNextTime = True - self._data = None - self._xAxisDropDown = None - self._yAxisDropDown = None - self.__fields = None - - def createWidget(self, parent): - from ._RecordPlot import RecordPlot - return RecordPlot(parent=parent) - - def clear(self): - self.getWidget().clear() - self.__resetZoomNextTime = True - - def normalizeData(self, data): - data = DataView.normalizeData(self, data) - data = _normalizeComplex(data) - return data - - def setData(self, data): - self._data = self.normalizeData(data) - - all_fields = sorted(self._data.dtype.fields.items(), key=lambda e: e[1][1]) - numeric_fields = [f[0] for f in all_fields if numpy.issubdtype(f[1][0], numpy.number)] - if numeric_fields == self.__fields: # Reuse previously selected fields - fieldNameX = self.getWidget().getXAxisFieldName() - fieldNameY = self.getWidget().getYAxisFieldName() - else: - self.__fields = numeric_fields - - self.getWidget().setSelectableXAxisFieldNames(numeric_fields) - self.getWidget().setSelectableYAxisFieldNames(numeric_fields) - fieldNameX = None - fieldNameY = numeric_fields[0] - - # If there is a field called time, use it for the x-axis by default - if "time" in numeric_fields: - fieldNameX = "time" - # Use the first field that is not "time" for the y-axis - if fieldNameY == "time" and len(numeric_fields) >= 2: - fieldNameY = numeric_fields[1] - - self._plotData(fieldNameX, fieldNameY) - - if not self._xAxisDropDown: - self._xAxisDropDown = self.getWidget().getAxesSelectionToolBar().getXAxisDropDown() - self._yAxisDropDown = self.getWidget().getAxesSelectionToolBar().getYAxisDropDown() - self._xAxisDropDown.activated.connect(self._onAxesSelectionChaned) - self._yAxisDropDown.activated.connect(self._onAxesSelectionChaned) - - def setDataSelection(self, selection): - self.getWidget().setGraphTitle(self.titleForSelection(selection)) - - def _onAxesSelectionChaned(self): - fieldNameX = self._xAxisDropDown.currentData() - self._plotData(fieldNameX, self._yAxisDropDown.currentText()) - - def _plotData(self, fieldNameX, fieldNameY): - self.clear() - ydata = self._data[fieldNameY] - if fieldNameX is None: - xdata = numpy.arange(len(ydata)) - else: - xdata = self._data[fieldNameX] - self.getWidget().addCurve(legend="data", - x=xdata, - y=ydata, - resetzoom=self.__resetZoomNextTime) - self.getWidget().setXAxisFieldName(fieldNameX) - self.getWidget().setYAxisFieldName(fieldNameY) - self.__resetZoomNextTime = True - - def axesNames(self, data, info): - return ["data"] - - def getDataPriority(self, data, info): - if info.size <= 0: - return DataView.UNSUPPORTED - if data is None or not info.isRecord: - return DataView.UNSUPPORTED - if info.dim < 1: - return DataView.UNSUPPORTED - if info.countNumericColumns < 2: - return DataView.UNSUPPORTED - if info.interpretation == "spectrum": - return 1000 - if info.dim == 2 and info.shape[0] == 1: - return 210 - if info.dim == 1: - return 40 - else: - return 10 - - -class _Plot2dView(DataView): - """View displaying data using a 2d plot""" - - def __init__(self, parent): - super(_Plot2dView, self).__init__( - parent=parent, - modeId=PLOT2D_MODE, - label="Image", - icon=icons.getQIcon("view-2d")) - self.__resetZoomNextTime = True - - def createWidget(self, parent): - from silx.gui import plot - widget = plot.Plot2D(parent=parent) - widget.setDefaultColormap(self.defaultColormap()) - widget.getColormapAction().setColorDialog(self.defaultColorDialog()) - widget.getIntensityHistogramAction().setVisible(True) - widget.setKeepDataAspectRatio(True) - widget.getXAxis().setLabel('X') - widget.getYAxis().setLabel('Y') - maskToolsWidget = widget.getMaskToolsDockWidget().widget() - maskToolsWidget.setItemMaskUpdated(True) - return widget - - def clear(self): - self.getWidget().clear() - self.__resetZoomNextTime = True - - def normalizeData(self, data): - data = DataView.normalizeData(self, data) - data = _normalizeComplex(data) - return data - - def setData(self, data): - data = self.normalizeData(data) - self.getWidget().addImage(legend="data", - data=data, - resetzoom=self.__resetZoomNextTime) - self.__resetZoomNextTime = False - - def setDataSelection(self, selection): - self.getWidget().setGraphTitle(self.titleForSelection(selection)) - - def axesNames(self, data, info): - return ["y", "x"] - - def getDataPriority(self, data, info): - if info.size <= 0: - return DataView.UNSUPPORTED - if (data is None or - not info.isArray or - not (info.isNumeric or info.isBoolean)): - return DataView.UNSUPPORTED - if info.dim < 2: - return DataView.UNSUPPORTED - if info.interpretation == "image": - return 1000 - if info.dim == 2: - return 200 - else: - return 190 - - -class _Plot3dView(DataView): - """View displaying data using a 3d plot""" - - def __init__(self, parent): - super(_Plot3dView, self).__init__( - parent=parent, - modeId=PLOT3D_MODE, - label="Cube", - icon=icons.getQIcon("view-3d")) - try: - from ._VolumeWindow import VolumeWindow # noqa - except ImportError: - _logger.warning("3D visualization is not available") - _logger.debug("Backtrace", exc_info=True) - raise - self.__resetZoomNextTime = True - - def createWidget(self, parent): - from ._VolumeWindow import VolumeWindow - - plot = VolumeWindow(parent) - plot.setAxesLabels(*reversed(self.axesNames(None, None))) - return plot - - def clear(self): - self.getWidget().clear() - self.__resetZoomNextTime = True - - def setData(self, data): - data = self.normalizeData(data) - self.getWidget().setData(data) - self.__resetZoomNextTime = False - - def axesNames(self, data, info): - return ["z", "y", "x"] - - def getDataPriority(self, data, info): - if info.size <= 0: - return DataView.UNSUPPORTED - if data is None or not info.isArray or not info.isNumeric: - return DataView.UNSUPPORTED - if info.dim < 3: - return DataView.UNSUPPORTED - if min(data.shape) < 2: - return DataView.UNSUPPORTED - if info.dim == 3: - return 100 - else: - return 10 - - -class _ComplexImageView(DataView): - """View displaying data using a ComplexImageView""" - - def __init__(self, parent): - super(_ComplexImageView, self).__init__( - parent=parent, - modeId=COMPLEX_IMAGE_MODE, - label="Complex Image", - icon=icons.getQIcon("view-2d")) - - def createWidget(self, parent): - from silx.gui.plot.ComplexImageView import ComplexImageView - widget = ComplexImageView(parent=parent) - widget.setColormap(self.defaultColormap(), mode=ComplexImageView.ComplexMode.ABSOLUTE) - widget.setColormap(self.defaultColormap(), mode=ComplexImageView.ComplexMode.SQUARE_AMPLITUDE) - widget.setColormap(self.defaultColormap(), mode=ComplexImageView.ComplexMode.REAL) - widget.setColormap(self.defaultColormap(), mode=ComplexImageView.ComplexMode.IMAGINARY) - widget.getPlot().getColormapAction().setColorDialog(self.defaultColorDialog()) - widget.getPlot().getIntensityHistogramAction().setVisible(True) - widget.getPlot().setKeepDataAspectRatio(True) - widget.getXAxis().setLabel('X') - widget.getYAxis().setLabel('Y') - maskToolsWidget = widget.getPlot().getMaskToolsDockWidget().widget() - maskToolsWidget.setItemMaskUpdated(True) - return widget - - def clear(self): - self.getWidget().setData(None) - - def normalizeData(self, data): - data = DataView.normalizeData(self, data) - return data - - def setData(self, data): - data = self.normalizeData(data) - self.getWidget().setData(data) - - def setDataSelection(self, selection): - self.getWidget().getPlot().setGraphTitle( - self.titleForSelection(selection)) - - def axesNames(self, data, info): - return ["y", "x"] - - def getDataPriority(self, data, info): - if info.size <= 0: - return DataView.UNSUPPORTED - if data is None or not info.isArray or not info.isComplex: - return DataView.UNSUPPORTED - if info.dim < 2: - return DataView.UNSUPPORTED - if info.interpretation == "image": - return 1000 - if info.dim == 2: - return 200 - else: - return 190 - - -class _ArrayView(DataView): - """View displaying data using a 2d table""" - - def __init__(self, parent): - DataView.__init__(self, parent, modeId=RAW_ARRAY_MODE) - - def createWidget(self, parent): - from silx.gui.data.ArrayTableWidget import ArrayTableWidget - widget = ArrayTableWidget(parent) - widget.displayAxesSelector(False) - return widget - - def clear(self): - self.getWidget().setArrayData(numpy.array([[]])) - - def setData(self, data): - data = self.normalizeData(data) - self.getWidget().setArrayData(data) - - def axesNames(self, data, info): - return ["col", "row"] - - def getDataPriority(self, data, info): - if info.size <= 0: - return DataView.UNSUPPORTED - if data is None or not info.isArray or info.isRecord: - return DataView.UNSUPPORTED - if info.dim < 2: - return DataView.UNSUPPORTED - if info.interpretation in ["scalar", "scaler"]: - return 1000 - return 500 - - -class _StackView(DataView): - """View displaying data using a stack of images""" - - def __init__(self, parent): - super(_StackView, self).__init__( - parent=parent, - modeId=STACK_MODE, - label="Image stack", - icon=icons.getQIcon("view-2d-stack")) - self.__resetZoomNextTime = True - - def customAxisNames(self): - return ["depth"] - - def setCustomAxisValue(self, name, value): - if name == "depth": - self.getWidget().setFrameNumber(value) - else: - raise Exception("Unsupported axis") - - def createWidget(self, parent): - from silx.gui import plot - widget = plot.StackView(parent=parent) - widget.setColormap(self.defaultColormap()) - widget.getPlotWidget().getColormapAction().setColorDialog(self.defaultColorDialog()) - widget.setKeepDataAspectRatio(True) - widget.setLabels(self.axesNames(None, None)) - # hide default option panel - widget.setOptionVisible(False) - maskToolWidget = widget.getPlotWidget().getMaskToolsDockWidget().widget() - maskToolWidget.setItemMaskUpdated(True) - return widget - - def clear(self): - self.getWidget().clear() - self.__resetZoomNextTime = True - - def normalizeData(self, data): - data = DataView.normalizeData(self, data) - data = _normalizeComplex(data) - return data - - def setData(self, data): - data = self.normalizeData(data) - self.getWidget().setStack(stack=data, reset=self.__resetZoomNextTime) - # Override the colormap, while setStack overwrite it - self.getWidget().setColormap(self.defaultColormap()) - self.__resetZoomNextTime = False - - def setDataSelection(self, selection): - title = self.titleForSelection(selection) - self.getWidget().setTitleCallback( - lambda idx: "%s z=%d" % (title, idx)) - - def axesNames(self, data, info): - return ["depth", "y", "x"] - - def getDataPriority(self, data, info): - if info.size <= 0: - return DataView.UNSUPPORTED - if data is None or not info.isArray or not info.isNumeric: - return DataView.UNSUPPORTED - if info.dim < 3: - return DataView.UNSUPPORTED - if info.interpretation == "image": - return 500 - return 90 - - -class _ScalarView(DataView): - """View displaying data using text""" - - def __init__(self, parent): - DataView.__init__(self, parent, modeId=RAW_SCALAR_MODE) - - def createWidget(self, parent): - widget = qt.QTextEdit(parent) - widget.setTextInteractionFlags(qt.Qt.TextSelectableByMouse) - widget.setAlignment(qt.Qt.AlignLeft | qt.Qt.AlignTop) - self.__formatter = TextFormatter(parent) - return widget - - def clear(self): - self.getWidget().setText("") - - def setData(self, data): - d = self.normalizeData(data) - if silx.io.is_dataset(d): - d = d[()] - dtype = None - if data is not None: - if hasattr(data, "dtype"): - dtype = data.dtype - text = self.__formatter.toString(d, dtype) - self.getWidget().setText(text) - - def axesNames(self, data, info): - return [] - - def getDataPriority(self, data, info): - if info.size <= 0: - return DataView.UNSUPPORTED - data = self.normalizeData(data) - if info.shape is None: - return DataView.UNSUPPORTED - if data is None: - return DataView.UNSUPPORTED - if silx.io.is_group(data): - return DataView.UNSUPPORTED - return 2 - - -class _RecordView(DataView): - """View displaying data using text""" - - def __init__(self, parent): - DataView.__init__(self, parent, modeId=RAW_RECORD_MODE) - - def createWidget(self, parent): - from .RecordTableView import RecordTableView - widget = RecordTableView(parent) - widget.setWordWrap(False) - return widget - - def clear(self): - self.getWidget().setArrayData(None) - - def setData(self, data): - data = self.normalizeData(data) - widget = self.getWidget() - widget.setArrayData(data) - if len(data) < 100: - widget.resizeRowsToContents() - widget.resizeColumnsToContents() - - def axesNames(self, data, info): - return ["data"] - - def getDataPriority(self, data, info): - if info.size <= 0: - return DataView.UNSUPPORTED - if info.isRecord: - return 40 - if data is None or not info.isArray: - return DataView.UNSUPPORTED - if info.dim == 1: - if info.interpretation in ["scalar", "scaler"]: - return 1000 - if info.shape[0] == 1: - return 510 - return 500 - elif info.isRecord: - return 40 - return DataView.UNSUPPORTED - - -class _HexaView(DataView): - """View displaying data using text""" - - def __init__(self, parent): - DataView.__init__(self, parent, modeId=RAW_HEXA_MODE) - - def createWidget(self, parent): - from .HexaTableView import HexaTableView - widget = HexaTableView(parent) - return widget - - def clear(self): - self.getWidget().setArrayData(None) - - def setData(self, data): - data = self.normalizeData(data) - widget = self.getWidget() - widget.setArrayData(data) - - def axesNames(self, data, info): - return [] - - def getDataPriority(self, data, info): - if info.size <= 0: - return DataView.UNSUPPORTED - if info.isVoid: - return 2000 - return DataView.UNSUPPORTED - - -class _Hdf5View(DataView): - """View displaying data using text""" - - def __init__(self, parent): - super(_Hdf5View, self).__init__( - parent=parent, - modeId=HDF5_MODE, - label="HDF5", - icon=icons.getQIcon("view-hdf5")) - - def createWidget(self, parent): - from .Hdf5TableView import Hdf5TableView - widget = Hdf5TableView(parent) - return widget - - def clear(self): - widget = self.getWidget() - widget.setData(None) - - def setData(self, data): - widget = self.getWidget() - widget.setData(data) - - def axesNames(self, data, info): - return None - - def getDataPriority(self, data, info): - widget = self.getWidget() - if widget.isSupportedData(data): - return 1 - else: - return DataView.UNSUPPORTED - - -class _RawView(CompositeDataView): - """View displaying data as raw data. - - This implementation use a 2d-array view, or a record array view, or a - raw text output. - """ - - def __init__(self, parent): - super(_RawView, self).__init__( - parent=parent, - modeId=RAW_MODE, - label="Raw", - icon=icons.getQIcon("view-raw")) - self.addView(_HexaView(parent)) - self.addView(_ScalarView(parent)) - self.addView(_ArrayView(parent)) - self.addView(_RecordView(parent)) - - -class _ImageView(CompositeDataView): - """View displaying data as 2D image - - It choose between Plot2D and ComplexImageView widgets - """ - - def __init__(self, parent): - super(_ImageView, self).__init__( - parent=parent, - modeId=IMAGE_MODE, - label="Image", - icon=icons.getQIcon("view-2d")) - self.addView(_ComplexImageView(parent)) - self.addView(_Plot2dView(parent)) - - -class _InvalidNXdataView(DataView): - """DataView showing a simple label with an error message - to inform that a group with @NX_class=NXdata cannot be - interpreted by any NXDataview.""" - def __init__(self, parent): - DataView.__init__(self, parent, - modeId=NXDATA_INVALID_MODE) - self._msg = "" - - def createWidget(self, parent): - widget = qt.QLabel(parent) - widget.setWordWrap(True) - widget.setStyleSheet("QLabel { color : red; }") - return widget - - def axesNames(self, data, info): - return [] - - def clear(self): - self.getWidget().setText("") - - def setData(self, data): - self.getWidget().setText(self._msg) - - def getDataPriority(self, data, info): - data = self.normalizeData(data) - - if not info.isInvalidNXdata: - return DataView.UNSUPPORTED - - if info.hasNXdata: - self._msg = "NXdata seems valid, but cannot be displayed " - self._msg += "by any existing plot widget." - else: - nx_class = get_attr_as_unicode(data, "NX_class") - if nx_class == "NXdata": - # invalid: could not even be parsed by NXdata - self._msg = "Group has @NX_class = NXdata, but could not be interpreted" - self._msg += " as valid NXdata." - elif nx_class == "NXroot" or silx.io.is_file(data): - default_entry = data[data.attrs["default"]] - default_nxdata_name = default_entry.attrs["default"] - self._msg = "NXroot group provides a @default attribute " - self._msg += "pointing to a NXentry which defines its own " - self._msg += "@default attribute, " - if default_nxdata_name not in default_entry: - self._msg += " but no corresponding NXdata group exists." - elif get_attr_as_unicode(default_entry[default_nxdata_name], - "NX_class") != "NXdata": - self._msg += " but the corresponding item is not a " - self._msg += "NXdata group." - else: - self._msg += " but the corresponding NXdata seems to be" - self._msg += " malformed." - else: - self._msg = "Group provides a @default attribute," - default_nxdata_name = data.attrs["default"] - if default_nxdata_name not in data: - self._msg += " but no corresponding NXdata group exists." - elif get_attr_as_unicode(data[default_nxdata_name], "NX_class") != "NXdata": - self._msg += " but the corresponding item is not a " - self._msg += "NXdata group." - else: - self._msg += " but the corresponding NXdata seems to be" - self._msg += " malformed." - return 100 - - -class _NXdataBaseDataView(DataView): - """Base class for NXdata DataView""" - - def __init__(self, *args, **kwargs): - DataView.__init__(self, *args, **kwargs) - - def _updateColormap(self, nxdata): - """Update used colormap according to nxdata's SILX_style""" - cmap_norm = nxdata.plot_style.signal_scale_type - if cmap_norm is not None: - self.defaultColormap().setNormalization( - 'log' if cmap_norm == 'log' else 'linear') - - -class _NXdataScalarView(_NXdataBaseDataView): - """DataView using a table view for displaying NXdata scalars: - 0-D signal or n-D signal with *@interpretation=scalar*""" - def __init__(self, parent): - _NXdataBaseDataView.__init__( - self, parent, modeId=NXDATA_SCALAR_MODE) - - def createWidget(self, parent): - from silx.gui.data.ArrayTableWidget import ArrayTableWidget - widget = ArrayTableWidget(parent) - # widget.displayAxesSelector(False) - return widget - - def axesNames(self, data, info): - return ["col", "row"] - - def clear(self): - self.getWidget().setArrayData(numpy.array([[]]), - labels=True) - - def setData(self, data): - data = self.normalizeData(data) - # data could be a NXdata or an NXentry - nxd = nxdata.get_default(data, validate=False) - signal = nxd.signal - self.getWidget().setArrayData(signal, - labels=True) - - def getDataPriority(self, data, info): - data = self.normalizeData(data) - - if info.hasNXdata and not info.isInvalidNXdata: - nxd = nxdata.get_default(data, validate=False) - if nxd.signal_is_0d or nxd.interpretation in ["scalar", "scaler"]: - return 100 - return DataView.UNSUPPORTED - - -class _NXdataCurveView(_NXdataBaseDataView): - """DataView using a Plot1D for displaying NXdata curves: - 1-D signal or n-D signal with *@interpretation=spectrum*. - - It also handles basic scatter plots: - a 1-D signal with one axis whose values are not monotonically increasing. - """ - def __init__(self, parent): - _NXdataBaseDataView.__init__( - self, parent, modeId=NXDATA_CURVE_MODE) - - def createWidget(self, parent): - from silx.gui.data.NXdataWidgets import ArrayCurvePlot - widget = ArrayCurvePlot(parent) - return widget - - def axesNames(self, data, info): - # disabled (used by default axis selector widget in Hdf5Viewer) - return None - - def clear(self): - self.getWidget().clear() - - def setData(self, data): - data = self.normalizeData(data) - nxd = nxdata.get_default(data, validate=False) - signals_names = [nxd.signal_name] + nxd.auxiliary_signals_names - if nxd.axes_dataset_names[-1] is not None: - x_errors = nxd.get_axis_errors(nxd.axes_dataset_names[-1]) - else: - x_errors = None - - # this fix is necessary until the next release of PyMca (5.2.3 or 5.3.0) - # see https://github.com/vasole/pymca/issues/144 and https://github.com/vasole/pymca/pull/145 - if not hasattr(self.getWidget(), "setCurvesData") and \ - hasattr(self.getWidget(), "setCurveData"): - _logger.warning("Using deprecated ArrayCurvePlot API, " - "without support of auxiliary signals") - self.getWidget().setCurveData(nxd.signal, nxd.axes[-1], - yerror=nxd.errors, xerror=x_errors, - ylabel=nxd.signal_name, xlabel=nxd.axes_names[-1], - title=nxd.title or nxd.signal_name) - return - - self.getWidget().setCurvesData([nxd.signal] + nxd.auxiliary_signals, nxd.axes[-1], - yerror=nxd.errors, xerror=x_errors, - ylabels=signals_names, xlabel=nxd.axes_names[-1], - title=nxd.title or signals_names[0], - xscale=nxd.plot_style.axes_scale_types[-1], - yscale=nxd.plot_style.signal_scale_type) - - def getDataPriority(self, data, info): - data = self.normalizeData(data) - if info.hasNXdata and not info.isInvalidNXdata: - if nxdata.get_default(data, validate=False).is_curve: - return 100 - return DataView.UNSUPPORTED - - -class _NXdataXYVScatterView(_NXdataBaseDataView): - """DataView using a Plot1D for displaying NXdata 3D scatters as - a scatter of coloured points (1-D signal with 2 axes)""" - def __init__(self, parent): - _NXdataBaseDataView.__init__( - self, parent, modeId=NXDATA_XYVSCATTER_MODE) - - def createWidget(self, parent): - from silx.gui.data.NXdataWidgets import XYVScatterPlot - widget = XYVScatterPlot(parent) - widget.getScatterView().setColormap(self.defaultColormap()) - widget.getScatterView().getScatterToolBar().getColormapAction().setColorDialog( - self.defaultColorDialog()) - return widget - - def axesNames(self, data, info): - # disabled (used by default axis selector widget in Hdf5Viewer) - return None - - def clear(self): - self.getWidget().clear() - - def setData(self, data): - data = self.normalizeData(data) - nxd = nxdata.get_default(data, validate=False) - - x_axis, y_axis = nxd.axes[-2:] - if x_axis is None: - x_axis = numpy.arange(nxd.signal.size) - if y_axis is None: - y_axis = numpy.arange(nxd.signal.size) - - x_label, y_label = nxd.axes_names[-2:] - if x_label is not None: - x_errors = nxd.get_axis_errors(x_label) - else: - x_errors = None - - if y_label is not None: - y_errors = nxd.get_axis_errors(y_label) - else: - y_errors = None - - self._updateColormap(nxd) - - self.getWidget().setScattersData(y_axis, x_axis, values=[nxd.signal] + nxd.auxiliary_signals, - yerror=y_errors, xerror=x_errors, - ylabel=y_label, xlabel=x_label, - title=nxd.title, - scatter_titles=[nxd.signal_name] + nxd.auxiliary_signals_names, - xscale=nxd.plot_style.axes_scale_types[-2], - yscale=nxd.plot_style.axes_scale_types[-1]) - - def getDataPriority(self, data, info): - data = self.normalizeData(data) - if info.hasNXdata and not info.isInvalidNXdata: - if nxdata.get_default(data, validate=False).is_x_y_value_scatter: - # It have to be a little more than a NX curve priority - return 110 - - return DataView.UNSUPPORTED - - -class _NXdataImageView(_NXdataBaseDataView): - """DataView using a Plot2D for displaying NXdata images: - 2-D signal or n-D signals with *@interpretation=image*.""" - def __init__(self, parent): - _NXdataBaseDataView.__init__( - self, parent, modeId=NXDATA_IMAGE_MODE) - - def createWidget(self, parent): - from silx.gui.data.NXdataWidgets import ArrayImagePlot - widget = ArrayImagePlot(parent) - widget.getPlot().setDefaultColormap(self.defaultColormap()) - widget.getPlot().getColormapAction().setColorDialog(self.defaultColorDialog()) - return widget - - def axesNames(self, data, info): - # disabled (used by default axis selector widget in Hdf5Viewer) - return None - - def clear(self): - self.getWidget().clear() - - def setData(self, data): - data = self.normalizeData(data) - nxd = nxdata.get_default(data, validate=False) - isRgba = nxd.interpretation == "rgba-image" - - self._updateColormap(nxd) - - # last two axes are Y & X - img_slicing = slice(-2, None) if not isRgba else slice(-3, -1) - 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] - - 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, isRgba=isRgba, - xscale=x_scale, yscale=y_scale) - - def getDataPriority(self, data, info): - data = self.normalizeData(data) - - if info.hasNXdata and not info.isInvalidNXdata: - if nxdata.get_default(data, validate=False).is_image: - return 100 - - return DataView.UNSUPPORTED - - -class _NXdataComplexImageView(_NXdataBaseDataView): - """DataView using a ComplexImageView for displaying NXdata complex images: - 2-D signal or n-D signals with *@interpretation=image*.""" - def __init__(self, parent): - _NXdataBaseDataView.__init__( - self, parent, modeId=NXDATA_IMAGE_MODE) - - def createWidget(self, parent): - from silx.gui.data.NXdataWidgets import ArrayComplexImagePlot - widget = ArrayComplexImagePlot(parent, colormap=self.defaultColormap()) - widget.getPlot().getColormapAction().setColorDialog(self.defaultColorDialog()) - return widget - - def clear(self): - self.getWidget().clear() - - def setData(self, data): - data = self.normalizeData(data) - nxd = nxdata.get_default(data, validate=False) - - self._updateColormap(nxd) - - # last two axes are Y & X - img_slicing = slice(-2, None) - y_axis, x_axis = nxd.axes[img_slicing] - y_label, x_label = nxd.axes_names[img_slicing] - - 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) - - def axesNames(self, data, info): - # disabled (used by default axis selector widget in Hdf5Viewer) - return None - - def getDataPriority(self, data, info): - data = self.normalizeData(data) - - if info.hasNXdata and not info.isInvalidNXdata: - nxd = nxdata.get_default(data, validate=False) - if nxd.is_image and numpy.iscomplexobj(nxd.signal): - return 100 - - return DataView.UNSUPPORTED - - -class _NXdataStackView(_NXdataBaseDataView): - def __init__(self, parent): - _NXdataBaseDataView.__init__( - self, parent, modeId=NXDATA_STACK_MODE) - - def createWidget(self, parent): - from silx.gui.data.NXdataWidgets import ArrayStackPlot - widget = ArrayStackPlot(parent) - widget.getStackView().setColormap(self.defaultColormap()) - widget.getStackView().getPlotWidget().getColormapAction().setColorDialog(self.defaultColorDialog()) - return widget - - def axesNames(self, data, info): - # disabled (used by default axis selector widget in Hdf5Viewer) - return None - - def clear(self): - self.getWidget().clear() - - def setData(self, data): - data = self.normalizeData(data) - nxd = nxdata.get_default(data, validate=False) - signal_name = nxd.signal_name - z_axis, y_axis, x_axis = nxd.axes[-3:] - z_label, y_label, x_label = nxd.axes_names[-3:] - title = nxd.title or signal_name - - self._updateColormap(nxd) - - widget = self.getWidget() - widget.setStackData( - nxd.signal, x_axis=x_axis, y_axis=y_axis, z_axis=z_axis, - signal_name=signal_name, - xlabel=x_label, ylabel=y_label, zlabel=z_label, - title=title) - # Override the colormap, while setStack overwrite it - widget.getStackView().setColormap(self.defaultColormap()) - - def getDataPriority(self, data, info): - data = self.normalizeData(data) - if info.hasNXdata and not info.isInvalidNXdata: - if nxdata.get_default(data, validate=False).is_stack: - return 100 - - return DataView.UNSUPPORTED - - -class _NXdataVolumeView(_NXdataBaseDataView): - def __init__(self, parent): - _NXdataBaseDataView.__init__( - self, parent, - label="NXdata (3D)", - icon=icons.getQIcon("view-nexus"), - modeId=NXDATA_VOLUME_MODE) - try: - import silx.gui.plot3d # noqa - except ImportError: - _logger.warning("Plot3dView is not available") - _logger.debug("Backtrace", exc_info=True) - raise - - def normalizeData(self, data): - data = super(_NXdataVolumeView, self).normalizeData(data) - data = _normalizeComplex(data) - return data - - def createWidget(self, parent): - from silx.gui.data.NXdataWidgets import ArrayVolumePlot - widget = ArrayVolumePlot(parent) - return widget - - def axesNames(self, data, info): - # disabled (used by default axis selector widget in Hdf5Viewer) - return None - - def clear(self): - self.getWidget().clear() - - def setData(self, data): - data = self.normalizeData(data) - nxd = nxdata.get_default(data, validate=False) - signal_name = nxd.signal_name - z_axis, y_axis, x_axis = nxd.axes[-3:] - z_label, y_label, x_label = nxd.axes_names[-3:] - title = nxd.title or signal_name - - widget = self.getWidget() - widget.setData( - nxd.signal, x_axis=x_axis, y_axis=y_axis, z_axis=z_axis, - signal_name=signal_name, - xlabel=x_label, ylabel=y_label, zlabel=z_label, - title=title) - - def getDataPriority(self, data, info): - data = self.normalizeData(data) - if info.hasNXdata and not info.isInvalidNXdata: - if nxdata.get_default(data, validate=False).is_volume: - return 150 - - return DataView.UNSUPPORTED - - -class _NXdataVolumeAsStackView(_NXdataBaseDataView): - def __init__(self, parent): - _NXdataBaseDataView.__init__( - self, parent, - label="NXdata (2D)", - icon=icons.getQIcon("view-nexus"), - modeId=NXDATA_VOLUME_AS_STACK_MODE) - - def createWidget(self, parent): - from silx.gui.data.NXdataWidgets import ArrayStackPlot - widget = ArrayStackPlot(parent) - widget.getStackView().setColormap(self.defaultColormap()) - widget.getStackView().getPlotWidget().getColormapAction().setColorDialog(self.defaultColorDialog()) - return widget - - def axesNames(self, data, info): - # disabled (used by default axis selector widget in Hdf5Viewer) - return None - - def clear(self): - self.getWidget().clear() - - def setData(self, data): - data = self.normalizeData(data) - nxd = nxdata.get_default(data, validate=False) - signal_name = nxd.signal_name - z_axis, y_axis, x_axis = nxd.axes[-3:] - z_label, y_label, x_label = nxd.axes_names[-3:] - title = nxd.title or signal_name - - self._updateColormap(nxd) - - widget = self.getWidget() - widget.setStackData( - nxd.signal, x_axis=x_axis, y_axis=y_axis, z_axis=z_axis, - signal_name=signal_name, - xlabel=x_label, ylabel=y_label, zlabel=z_label, - title=title) - # Override the colormap, while setStack overwrite it - widget.getStackView().setColormap(self.defaultColormap()) - - def getDataPriority(self, data, info): - data = self.normalizeData(data) - if info.isComplex: - return DataView.UNSUPPORTED - if info.hasNXdata and not info.isInvalidNXdata: - if nxdata.get_default(data, validate=False).is_volume: - return 200 - - return DataView.UNSUPPORTED - -class _NXdataComplexVolumeAsStackView(_NXdataBaseDataView): - def __init__(self, parent): - _NXdataBaseDataView.__init__( - self, parent, - label="NXdata (2D)", - icon=icons.getQIcon("view-nexus"), - modeId=NXDATA_VOLUME_AS_STACK_MODE) - self._is_complex_data = False - - def createWidget(self, parent): - from silx.gui.data.NXdataWidgets import ArrayComplexImagePlot - widget = ArrayComplexImagePlot(parent, colormap=self.defaultColormap()) - widget.getPlot().getColormapAction().setColorDialog(self.defaultColorDialog()) - return widget - - def axesNames(self, data, info): - # disabled (used by default axis selector widget in Hdf5Viewer) - return None - - def clear(self): - self.getWidget().clear() - - def setData(self, data): - data = self.normalizeData(data) - nxd = nxdata.get_default(data, validate=False) - signal_name = nxd.signal_name - z_axis, y_axis, x_axis = nxd.axes[-3:] - z_label, y_label, x_label = nxd.axes_names[-3:] - title = nxd.title or signal_name - - self._updateColormap(nxd) - - 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) - - def getDataPriority(self, data, info): - data = self.normalizeData(data) - if not info.isComplex: - return DataView.UNSUPPORTED - if info.hasNXdata and not info.isInvalidNXdata: - if nxdata.get_default(data, validate=False).is_volume: - return 200 - - return DataView.UNSUPPORTED - - -class _NXdataView(CompositeDataView): - """Composite view displaying NXdata groups using the most adequate - widget depending on the dimensionality.""" - def __init__(self, parent): - super(_NXdataView, self).__init__( - parent=parent, - label="NXdata", - modeId=NXDATA_MODE, - icon=icons.getQIcon("view-nexus")) - - self.addView(_InvalidNXdataView(parent)) - self.addView(_NXdataScalarView(parent)) - self.addView(_NXdataCurveView(parent)) - self.addView(_NXdataXYVScatterView(parent)) - self.addView(_NXdataComplexImageView(parent)) - self.addView(_NXdataImageView(parent)) - self.addView(_NXdataStackView(parent)) - - # The 3D view can be displayed using 2 ways - nx3dViews = SelectManyDataView(parent) - nx3dViews.addView(_NXdataVolumeAsStackView(parent)) - nx3dViews.addView(_NXdataComplexVolumeAsStackView(parent)) - try: - nx3dViews.addView(_NXdataVolumeView(parent)) - except Exception: - _logger.warning("NXdataVolumeView is not available") - _logger.debug("Backtrace", exc_info=True) - self.addView(nx3dViews) diff --git a/silx/gui/data/Hdf5TableView.py b/silx/gui/data/Hdf5TableView.py deleted file mode 100644 index 7749326..0000000 --- a/silx/gui/data/Hdf5TableView.py +++ /dev/null @@ -1,646 +0,0 @@ -# coding: utf-8 -# /*########################################################################## -# -# Copyright (c) 2017-2020 European Synchrotron Radiation Facility -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. -# -# ###########################################################################*/ -""" -This 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" -__date__ = "12/02/2019" - -import collections -import functools -import os.path -import logging -import h5py -import numpy - -from silx.gui import qt -import silx.io -from .TextFormatter import TextFormatter -import silx.gui.hdf5 -from silx.gui.widgets import HierarchicalTableView -from ..hdf5.Hdf5Formatter import Hdf5Formatter -from ..hdf5._utils import htmlFromDict - - -_logger = logging.getLogger(__name__) - - -class _CellData(object): - """Store a table item - """ - def __init__(self, value=None, isHeader=False, span=None, tooltip=None): - """ - Constructor - - :param str value: Label of this property - :param bool isHeader: True if the cell is an header - :param tuple span: Tuple of row, column span - """ - self.__value = value - self.__isHeader = isHeader - self.__span = span - self.__tooltip = tooltip - - def isHeader(self): - """Returns true if the property is a sub-header title. - - :rtype: bool - """ - return self.__isHeader - - def value(self): - """Returns the value of the item. - """ - return self.__value - - def span(self): - """Returns the span size of the cell. - - :rtype: tuple - """ - return self.__span - - def tooltip(self): - """Returns the tooltip of the item. - - :rtype: tuple - """ - return self.__tooltip - - def invalidateValue(self): - self.__value = None - - def invalidateToolTip(self): - self.__tooltip = None - - def data(self, role): - return None - - -class _TableData(object): - """Modelize a table with header, row and column span. - - It is mostly defined as a row based table. - """ - - def __init__(self, columnCount): - """Constructor. - - :param int columnCount: Define the number of column of the table - """ - self.__colCount = columnCount - self.__data = [] - - def rowCount(self): - """Returns the number of rows. - - :rtype: int - """ - return len(self.__data) - - def columnCount(self): - """Returns the number of columns. - - :rtype: int - """ - return self.__colCount - - def clear(self): - """Remove all the cells of the table""" - self.__data = [] - - def cellAt(self, row, column): - """Returns the cell at the row column location. Else None if there is - nothing. - - :rtype: _CellData - """ - if row < 0: - return None - if column < 0: - return None - if row >= len(self.__data): - return None - cells = self.__data[row] - if column >= len(cells): - return None - return cells[column] - - def addHeaderRow(self, headerLabel): - """Append the table with header on the full row. - - :param str headerLabel: label of the header. - """ - item = _CellData(value=headerLabel, isHeader=True, span=(1, self.__colCount)) - self.__data.append([item]) - - def addHeaderValueRow(self, headerLabel, value, tooltip=None): - """Append the table with a row using the first column as an header and - other cells as a single cell for the value. - - :param str headerLabel: label of the header. - :param object value: value to store. - """ - header = _CellData(value=headerLabel, isHeader=True) - value = _CellData(value=value, span=(1, self.__colCount), tooltip=tooltip) - self.__data.append([header, value]) - - def addRow(self, *args): - """Append the table with a row using arguments for each cells - - :param list[object] args: List of cell values for the row - """ - row = [] - for value in args: - if not isinstance(value, _CellData): - value = _CellData(value=value) - row.append(value) - self.__data.append(row) - - -class _CellFilterAvailableData(_CellData): - """Cell rendering for availability of a filter""" - - _states = { - True: ("Available", qt.QColor(0x000000), None, None), - False: ("Not available", qt.QColor(0xFFFFFF), qt.QColor(0xFF0000), - "You have to install this filter on your system to be able to read this dataset"), - "na": ("n.a.", qt.QColor(0x000000), None, - "This version of h5py/hdf5 is not able to display the information"), - } - - def __init__(self, filterId): - if h5py.version.hdf5_version_tuple >= (1, 10, 2): - # Previous versions only returns True if the filter was first used - # to decode a dataset - self.__availability = h5py.h5z.filter_avail(filterId) - else: - self.__availability = "na" - _CellData.__init__(self) - - def value(self): - state = self._states[self.__availability] - return state[0] - - def tooltip(self): - state = self._states[self.__availability] - return state[3] - - def data(self, role=qt.Qt.DisplayRole): - state = self._states[self.__availability] - if role == qt.Qt.TextColorRole: - return state[1] - elif role == qt.Qt.BackgroundColorRole: - return state[2] - else: - return None - - -class Hdf5TableModel(HierarchicalTableView.HierarchicalTableModel): - """This data model provides access to HDF5 node content (File, Group, - Dataset). Main info, like name, file, attributes... are displayed - """ - - def __init__(self, parent=None, data=None): - """ - Constructor - - :param qt.QObject parent: Parent object - :param object data: An h5py-like object (file, group or dataset) - """ - super(Hdf5TableModel, self).__init__(parent) - - self.__obj = None - self.__data = _TableData(columnCount=5) - self.__formatter = None - self.__hdf5Formatter = Hdf5Formatter(self) - formatter = TextFormatter(self) - self.setFormatter(formatter) - self.setObject(data) - - def rowCount(self, parent_idx=None): - """Returns number of rows to be displayed in table""" - return self.__data.rowCount() - - def columnCount(self, parent_idx=None): - """Returns number of columns to be displayed in table""" - return self.__data.columnCount() - - def data(self, index, role=qt.Qt.DisplayRole): - """QAbstractTableModel method to access data values - in the format ready to be displayed""" - if not index.isValid(): - return None - - cell = self.__data.cellAt(index.row(), index.column()) - if cell is None: - return None - - if role == self.SpanRole: - return cell.span() - elif role == self.IsHeaderRole: - return cell.isHeader() - elif role in (qt.Qt.DisplayRole, qt.Qt.EditRole): - value = cell.value() - if callable(value): - try: - value = value(self.__obj) - except Exception: - cell.invalidateValue() - raise - return value - elif role == qt.Qt.ToolTipRole: - value = cell.tooltip() - if callable(value): - try: - value = value(self.__obj) - except Exception: - cell.invalidateToolTip() - raise - return value - else: - return cell.data(role) - return None - - def isSupportedObject(self, h5pyObject): - """ - Returns true if the provided object can be modelized using this model. - """ - isSupported = False - isSupported = isSupported or silx.io.is_group(h5pyObject) - isSupported = isSupported or silx.io.is_dataset(h5pyObject) - isSupported = isSupported or isinstance(h5pyObject, silx.gui.hdf5.H5Node) - return isSupported - - def setObject(self, h5pyObject): - """Set the h5py-like object exposed by the model - - :param h5pyObject: A h5py-like object. It can be a `h5py.Dataset`, - a `h5py.File`, a `h5py.Group`. It also can be a, - `silx.gui.hdf5.H5Node` which is needed to display some local path - information. - """ - if qt.qVersion() > "4.6": - self.beginResetModel() - - if h5pyObject is None or self.isSupportedObject(h5pyObject): - self.__obj = h5pyObject - else: - _logger.warning("Object class %s unsupported. Object ignored.", type(h5pyObject)) - self.__initProperties() - - if qt.qVersion() > "4.6": - self.endResetModel() - else: - self.reset() - - def __formatHdf5Type(self, dataset): - """Format the HDF5 type""" - return self.__hdf5Formatter.humanReadableHdf5Type(dataset) - - def __attributeTooltip(self, attribute): - attributeDict = collections.OrderedDict() - if hasattr(attribute, "shape"): - attributeDict["Shape"] = self.__hdf5Formatter.humanReadableShape(attribute) - attributeDict["Data type"] = self.__hdf5Formatter.humanReadableType(attribute, full=True) - html = htmlFromDict(attributeDict, title="HDF5 Attribute") - return html - - def __formatDType(self, dataset): - """Format the numpy dtype""" - return self.__hdf5Formatter.humanReadableType(dataset, full=True) - - def __formatShape(self, dataset): - """Format the shape""" - if dataset.shape is None or len(dataset.shape) <= 1: - return self.__hdf5Formatter.humanReadableShape(dataset) - size = dataset.size - shape = self.__hdf5Formatter.humanReadableShape(dataset) - return u"%s = %s" % (shape, size) - - def __formatChunks(self, dataset): - """Format the shape""" - chunks = dataset.chunks - if chunks is None: - return "" - shape = " \u00D7 ".join([str(i) for i in chunks]) - sizes = numpy.product(chunks) - text = "%s = %s" % (shape, sizes) - return text - - def __initProperties(self): - """Initialize the list of available properties according to the defined - h5py-like object.""" - self.__data.clear() - if self.__obj is None: - return - - obj = self.__obj - - hdf5obj = obj - if isinstance(obj, silx.gui.hdf5.H5Node): - hdf5obj = obj.h5py_object - - if silx.io.is_file(hdf5obj): - objectType = "File" - elif silx.io.is_group(hdf5obj): - objectType = "Group" - elif silx.io.is_dataset(hdf5obj): - objectType = "Dataset" - else: - objectType = obj.__class__.__name__ - self.__data.addHeaderRow(headerLabel="HDF5 %s" % objectType) - - SEPARATOR = "::" - - self.__data.addHeaderRow(headerLabel="Path info") - showPhysicalLocation = True - if isinstance(obj, silx.gui.hdf5.H5Node): - # helpful informations if the object come from an HDF5 tree - self.__data.addHeaderValueRow("Basename", lambda x: x.local_basename) - self.__data.addHeaderValueRow("Name", lambda x: x.local_name) - local = lambda x: x.local_filename + SEPARATOR + x.local_name - self.__data.addHeaderValueRow("Local", local) - else: - # it's a real H5py object - self.__data.addHeaderValueRow("Basename", lambda x: os.path.basename(x.name)) - self.__data.addHeaderValueRow("Name", lambda x: x.name) - if obj.file is not None: - self.__data.addHeaderValueRow("File", lambda x: x.file.filename) - if hasattr(obj, "path"): - # That's a link - if hasattr(obj, "filename"): - # External link - link = lambda x: x.filename + SEPARATOR + x.path - else: - # Soft link - link = lambda x: x.path - self.__data.addHeaderValueRow("Link", link) - showPhysicalLocation = False - - # External data (nothing to do with external links) - nExtSources = 0 - firstExtSource = None - extType = None - if silx.io.is_dataset(hdf5obj): - if hasattr(hdf5obj, "is_virtual"): - if hdf5obj.is_virtual: - extSources = hdf5obj.virtual_sources() - if extSources: - firstExtSource = extSources[0].file_name + SEPARATOR + extSources[0].dset_name - extType = "Virtual" - nExtSources = len(extSources) - if hasattr(hdf5obj, "external"): - extSources = hdf5obj.external - if extSources: - firstExtSource = extSources[0][0] - extType = "Raw" - nExtSources = len(extSources) - - if showPhysicalLocation: - def _physical_location(x): - if isinstance(obj, silx.gui.hdf5.H5Node): - return x.physical_filename + SEPARATOR + x.physical_name - elif silx.io.is_file(obj): - return x.filename + SEPARATOR + x.name - elif obj.file is not None: - return x.file.filename + SEPARATOR + x.name - else: - # Guess it is a virtual node - return "No physical location" - - self.__data.addHeaderValueRow("Physical", _physical_location) - - if extType: - def _first_source(x): - # Absolute path - if os.path.isabs(firstExtSource): - return firstExtSource - - # Relative path with respect to the file directory - if isinstance(obj, silx.gui.hdf5.H5Node): - filename = x.physical_filename - elif silx.io.is_file(obj): - filename = x.filename - elif obj.file is not None: - filename = x.file.filename - else: - return firstExtSource - - if firstExtSource[0] == ".": - firstExtSource.pop(0) - return os.path.join(os.path.dirname(filename), firstExtSource) - - self.__data.addHeaderRow(headerLabel="External sources") - self.__data.addHeaderValueRow("Type", extType) - self.__data.addHeaderValueRow("Count", str(nExtSources)) - self.__data.addHeaderValueRow("First", _first_source) - - if hasattr(obj, "dtype"): - - self.__data.addHeaderRow(headerLabel="Data info") - - if hasattr(obj, "id") and hasattr(obj.id, "get_type"): - # display the HDF5 type - self.__data.addHeaderValueRow("HDF5 type", self.__formatHdf5Type) - self.__data.addHeaderValueRow("dtype", self.__formatDType) - if hasattr(obj, "shape"): - self.__data.addHeaderValueRow("shape", self.__formatShape) - if hasattr(obj, "chunks") and obj.chunks is not None: - self.__data.addHeaderValueRow("chunks", self.__formatChunks) - - # relative to compression - # h5py expose compression, compression_opts but are not initialized - # for external plugins, then we use id - # h5py also expose fletcher32 and shuffle attributes, but it is also - # part of the filters - if hasattr(obj, "shape") and hasattr(obj, "id"): - if hasattr(obj.id, "get_create_plist"): - dcpl = obj.id.get_create_plist() - if dcpl.get_nfilters() > 0: - self.__data.addHeaderRow(headerLabel="Compression info") - pos = _CellData(value="Position", isHeader=True) - hdf5id = _CellData(value="HDF5 ID", isHeader=True) - name = _CellData(value="Name", isHeader=True) - options = _CellData(value="Options", isHeader=True) - availability = _CellData(value="", isHeader=True) - self.__data.addRow(pos, hdf5id, name, options, availability) - for index in range(dcpl.get_nfilters()): - filterId, name, options = self.__getFilterInfo(obj, index) - pos = _CellData(value=str(index)) - hdf5id = _CellData(value=str(filterId)) - name = _CellData(value=name) - options = _CellData(value=options) - availability = _CellFilterAvailableData(filterId=filterId) - self.__data.addRow(pos, hdf5id, name, options, availability) - - if hasattr(obj, "attrs"): - if len(obj.attrs) > 0: - self.__data.addHeaderRow(headerLabel="Attributes") - for key in sorted(obj.attrs.keys()): - callback = lambda key, x: self.__formatter.toString(x.attrs[key]) - callbackTooltip = lambda key, x: self.__attributeTooltip(x.attrs[key]) - self.__data.addHeaderValueRow(headerLabel=key, - value=functools.partial(callback, key), - tooltip=functools.partial(callbackTooltip, key)) - - def __getFilterInfo(self, dataset, filterIndex): - """Get a tuple of readable info from dataset filters - - :param h5py.Dataset dataset: A h5py dataset - :param int filterId: - """ - try: - dcpl = dataset.id.get_create_plist() - info = dcpl.get_filter(filterIndex) - filterId, _flags, cdValues, name = info - name = self.__formatter.toString(name) - options = " ".join([self.__formatter.toString(i) for i in cdValues]) - return (filterId, name, options) - except Exception: - _logger.debug("Backtrace", exc_info=True) - return (None, None, None) - - def object(self): - """Returns the internal object modelized. - - :rtype: An h5py-like object - """ - return self.__obj - - def setFormatter(self, formatter): - """Set the formatter object to be used to display data from the model - - :param TextFormatter formatter: Formatter to use - """ - if formatter is self.__formatter: - return - - self.__hdf5Formatter.setTextFormatter(formatter) - - if qt.qVersion() > "4.6": - self.beginResetModel() - - if self.__formatter is not None: - self.__formatter.formatChanged.disconnect(self.__formatChanged) - - self.__formatter = formatter - if self.__formatter is not None: - self.__formatter.formatChanged.connect(self.__formatChanged) - - if qt.qVersion() > "4.6": - self.endResetModel() - else: - self.reset() - - def getFormatter(self): - """Returns the text formatter used. - - :rtype: TextFormatter - """ - return self.__formatter - - def __formatChanged(self): - """Called when the format changed. - """ - self.reset() - - -class Hdf5TableItemDelegate(HierarchicalTableView.HierarchicalItemDelegate): - """Item delegate the :class:`Hdf5TableView` with read-only text editor""" - - def createEditor(self, parent, option, index): - """See :meth:`QStyledItemDelegate.createEditor`""" - editor = super().createEditor(parent, option, index) - if isinstance(editor, qt.QLineEdit): - editor.setReadOnly(True) - editor.deselect() - editor.textChanged.connect(self.__textChanged, qt.Qt.QueuedConnection) - self.installEventFilter(editor) - return editor - - def __textChanged(self, text): - sender = self.sender() - if sender is not None: - sender.deselect() - - def eventFilter(self, watched, event): - eventType = event.type() - if eventType == qt.QEvent.FocusIn: - watched.selectAll() - qt.QTimer.singleShot(0, watched.selectAll) - elif eventType == qt.QEvent.FocusOut: - watched.deselect() - return super().eventFilter(watched, event) - - -class Hdf5TableView(HierarchicalTableView.HierarchicalTableView): - """A widget to display metadata about a HDF5 node using a table.""" - - def __init__(self, parent=None): - super(Hdf5TableView, self).__init__(parent) - self.setModel(Hdf5TableModel(self)) - self.setItemDelegate(Hdf5TableItemDelegate(self)) - self.setSelectionMode(qt.QAbstractItemView.NoSelection) - - def isSupportedData(self, data): - """ - Returns true if the provided object can be modelized using this model. - """ - return self.model().isSupportedObject(data) - - def setData(self, data): - """Set the h5py-like object exposed by the model - - :param data: A h5py-like object. It can be a `h5py.Dataset`, - a `h5py.File`, a `h5py.Group`. It also can be a, - `silx.gui.hdf5.H5Node` which is needed to display some local path - information. - """ - model = self.model() - - model.setObject(data) - header = self.horizontalHeader() - if qt.qVersion() < "5.0": - setResizeMode = header.setResizeMode - else: - setResizeMode = header.setSectionResizeMode - setResizeMode(0, qt.QHeaderView.Fixed) - setResizeMode(1, qt.QHeaderView.ResizeToContents) - setResizeMode(2, qt.QHeaderView.Stretch) - setResizeMode(3, qt.QHeaderView.ResizeToContents) - setResizeMode(4, qt.QHeaderView.ResizeToContents) - header.setStretchLastSection(False) - - for row in range(model.rowCount()): - for column in range(model.columnCount()): - index = model.index(row, column) - if (index.isValid() and index.data( - HierarchicalTableView.HierarchicalTableModel.IsHeaderRole) is False): - self.openPersistentEditor(index) diff --git a/silx/gui/data/HexaTableView.py b/silx/gui/data/HexaTableView.py deleted file mode 100644 index 1617f0a..0000000 --- a/silx/gui/data/HexaTableView.py +++ /dev/null @@ -1,286 +0,0 @@ -# coding: utf-8 -# /*########################################################################## -# -# Copyright (c) 2017-2018 European Synchrotron Radiation Facility -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. -# -# ###########################################################################*/ -""" -This module defines model and widget to display raw data using an -hexadecimal viewer. -""" -from __future__ import division - -import collections - -import numpy -import six - -from silx.gui import qt -import silx.io.utils -from silx.gui.widgets.TableWidget import CopySelectedCellsAction - -__authors__ = ["V. Valls"] -__license__ = "MIT" -__date__ = "23/05/2018" - - -class _VoidConnector(object): - """Byte connector to a numpy.void data. - - It uses a cache of 32 x 1KB and a direct read access API from HDF5. - """ - - def __init__(self, data): - self.__cache = collections.OrderedDict() - self.__len = data.itemsize - self.__data = data - - def __getBuffer(self, bufferId): - if bufferId not in self.__cache: - pos = bufferId << 10 - data = self.__data - if hasattr(data, "tobytes"): - data = data.tobytes()[pos:pos + 1024] - else: - # Old fashion - data = data.data[pos:pos + 1024] - - self.__cache[bufferId] = data - if len(self.__cache) > 32: - self.__cache.popitem() - else: - data = self.__cache[bufferId] - return data - - def __getitem__(self, pos): - """Returns the value of the byte at the given position. - - :param uint pos: Position of the byte - :rtype: int - """ - bufferId = pos >> 10 - bufferPos = pos & 0b1111111111 - data = self.__getBuffer(bufferId) - value = data[bufferPos] - if six.PY2: - return ord(value) - else: - return value - - def __len__(self): - """ - Returns the number of available bytes. - - :rtype: uint - """ - return self.__len - - -class HexaTableModel(qt.QAbstractTableModel): - """This data model provides access to a numpy void data. - - Bytes are displayed one by one as a hexadecimal viewer. - - The 16th first columns display bytes as hexadecimal, the last column - displays the same data as ASCII. - - :param qt.QObject parent: Parent object - :param data: A numpy array or a h5py dataset - """ - def __init__(self, parent=None, data=None): - qt.QAbstractTableModel.__init__(self, parent) - - self.__data = None - self.__connector = None - self.setArrayData(data) - - if hasattr(qt.QFontDatabase, "systemFont"): - self.__font = qt.QFontDatabase.systemFont(qt.QFontDatabase.FixedFont) - else: - self.__font = qt.QFont("Monospace") - self.__font.setStyleHint(qt.QFont.TypeWriter) - self.__palette = qt.QPalette() - - def rowCount(self, parent_idx=None): - """Returns number of rows to be displayed in table""" - if self.__connector is None: - return 0 - return ((len(self.__connector) - 1) >> 4) + 1 - - def columnCount(self, parent_idx=None): - """Returns number of columns to be displayed in table""" - return 0x10 + 1 - - def data(self, index, role=qt.Qt.DisplayRole): - """QAbstractTableModel method to access data values - in the format ready to be displayed""" - if not index.isValid(): - return None - - if self.__connector is None: - return None - - row = index.row() - column = index.column() - - if role == qt.Qt.DisplayRole: - if column == 0x10: - start = (row << 4) - text = "" - for i in range(0x10): - pos = start + i - if pos >= len(self.__connector): - break - value = self.__connector[pos] - if value > 0x20 and value < 0x7F: - text += chr(value) - else: - text += "." - return text - else: - pos = (row << 4) + column - if pos < len(self.__connector): - value = self.__connector[pos] - return "%02X" % value - else: - return "" - elif role == qt.Qt.FontRole: - return self.__font - - elif role == qt.Qt.BackgroundColorRole: - pos = (row << 4) + column - if column != 0x10 and pos >= len(self.__connector): - return self.__palette.color(qt.QPalette.Disabled, qt.QPalette.Background) - else: - return None - - return None - - def headerData(self, section, orientation, role=qt.Qt.DisplayRole): - """Returns the 0-based row or column index, for display in the - horizontal and vertical headers""" - if section == -1: - # PyQt4 send -1 when there is columns but no rows - return None - - if role == qt.Qt.DisplayRole: - if orientation == qt.Qt.Vertical: - return "%02X" % (section << 4) - if orientation == qt.Qt.Horizontal: - if section == 0x10: - return "ASCII" - else: - return "%02X" % section - elif role == qt.Qt.FontRole: - return self.__font - elif role == qt.Qt.TextAlignmentRole: - if orientation == qt.Qt.Vertical: - return qt.Qt.AlignRight - if orientation == qt.Qt.Horizontal: - if section == 0x10: - return qt.Qt.AlignLeft - else: - return qt.Qt.AlignCenter - return None - - def flags(self, index): - """QAbstractTableModel method to inform the view whether data - is editable or not. - """ - row = index.row() - column = index.column() - pos = (row << 4) + column - if column != 0x10 and pos >= len(self.__connector): - return qt.Qt.NoItemFlags - return qt.QAbstractTableModel.flags(self, index) - - def setArrayData(self, data): - """Set the data array. - - :param data: A numpy object or a dataset. - """ - if qt.qVersion() > "4.6": - self.beginResetModel() - - self.__connector = None - self.__data = data - if self.__data is not None: - if silx.io.utils.is_dataset(self.__data): - data = data[()] - elif isinstance(self.__data, numpy.ndarray): - data = data[()] - self.__connector = _VoidConnector(data) - - if qt.qVersion() > "4.6": - self.endResetModel() - else: - self.reset() - - def arrayData(self): - """Returns the internal data. - - :rtype: numpy.ndarray of h5py.Dataset - """ - return self.__data - - -class HexaTableView(qt.QTableView): - """TableView using HexaTableModel as default model. - - It customs the column size to provide a better layout. - """ - def __init__(self, parent=None): - """ - Constructor - - :param qt.QWidget parent: parent QWidget - """ - qt.QTableView.__init__(self, parent) - - model = HexaTableModel(self) - self.setModel(model) - self._copyAction = CopySelectedCellsAction(self) - self.addAction(self._copyAction) - - def copy(self): - self._copyAction.trigger() - - def setArrayData(self, data): - """Set the data array. - - :param data: A numpy object or a dataset. - """ - self.model().setArrayData(data) - self.__fixHeader() - - def __fixHeader(self): - """Update the view according to the state of the auto-resize""" - header = self.horizontalHeader() - if qt.qVersion() < "5.0": - setResizeMode = header.setResizeMode - else: - setResizeMode = header.setSectionResizeMode - - header.setDefaultSectionSize(30) - header.setStretchLastSection(True) - for i in range(0x10): - setResizeMode(i, qt.QHeaderView.Fixed) - setResizeMode(0x10, qt.QHeaderView.Stretch) diff --git a/silx/gui/data/NXdataWidgets.py b/silx/gui/data/NXdataWidgets.py deleted file mode 100644 index be7d0e3..0000000 --- a/silx/gui/data/NXdataWidgets.py +++ /dev/null @@ -1,1081 +0,0 @@ -# coding: utf-8 -# /*########################################################################## -# -# Copyright (c) 2017-2020 European Synchrotron Radiation Facility -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. -# -# ###########################################################################*/ -"""This module defines widgets used by _NXdataView. -""" -__authors__ = ["P. Knobel"] -__license__ = "MIT" -__date__ = "12/11/2018" - -import logging -import numpy - -from silx.gui import qt -from silx.gui.data.NumpyAxesSelector import NumpyAxesSelector -from silx.gui.plot import Plot1D, Plot2D, StackView, ScatterView -from silx.gui.plot.ComplexImageView import ComplexImageView -from silx.gui.colors import Colormap -from silx.gui.widgets.FrameBrowser import HorizontalSliderWithBrowser - -from silx.math.calibration import ArrayCalibration, NoCalibration, LinearCalibration - - -_logger = logging.getLogger(__name__) - - -class ArrayCurvePlot(qt.QWidget): - """ - Widget for plotting a curve from a multi-dimensional signal array - and a 1D axis array. - - The signal array can have an arbitrary number of dimensions, the only - limitation being that the last dimension must have the same length as - the axis array. - - The widget provides sliders to select indices on the first (n - 1) - dimensions of the signal array, and buttons to add/replace selected - curves to the plot. - - This widget also handles simple 2D or 3D scatter plots (third dimension - displayed as colour of points). - """ - def __init__(self, parent=None): - """ - - :param parent: Parent QWidget - """ - super(ArrayCurvePlot, self).__init__(parent) - - self.__signals = None - self.__signals_names = None - self.__signal_errors = None - self.__axis = None - self.__axis_name = None - self.__x_axis_errors = None - self.__values = None - - self._plot = Plot1D(self) - - self._selector = NumpyAxesSelector(self) - self._selector.setNamedAxesSelectorVisibility(False) - self.__selector_is_connected = False - - self._plot.sigActiveCurveChanged.connect(self._setYLabelFromActiveLegend) - - layout = qt.QVBoxLayout() - layout.setContentsMargins(0, 0, 0, 0) - layout.addWidget(self._plot) - layout.addWidget(self._selector) - - self.setLayout(layout) - - def getPlot(self): - """Returns the plot used for the display - - :rtype: Plot1D - """ - return self._plot - - def setCurvesData(self, ys, x=None, - yerror=None, xerror=None, - ylabels=None, xlabel=None, title=None, - xscale=None, yscale=None): - """ - - :param List[ndarray] ys: List of arrays to be represented by the y (vertical) axis. - It can be multiple n-D array whose last dimension must - have the same length as x (and values must be None) - :param ndarray x: 1-D dataset used as the curve's x values. If provided, - its lengths must be equal to the length of the last dimension of - ``y`` (and equal to the length of ``value``, for a scatter plot). - :param ndarray yerror: Single array of errors for y (same shape), or None. - There can only be one array, and it applies to the first/main y - (no y errors for auxiliary_signals curves). - :param ndarray xerror: 1-D dataset of errors for x, or None - :param str ylabels: Labels for each curve's Y axis - :param str xlabel: Label for X axis - :param str title: Graph title - :param str xscale: Scale of X axis in (None, 'linear', 'log') - :param str yscale: Scale of Y axis in (None, 'linear', 'log') - """ - self.__signals = ys - self.__signals_names = ylabels or (["Y"] * len(ys)) - self.__signal_errors = yerror - self.__axis = x - self.__axis_name = xlabel - self.__x_axis_errors = xerror - - if self.__selector_is_connected: - self._selector.selectionChanged.disconnect(self._updateCurve) - self.__selector_is_connected = False - self._selector.setData(ys[0]) - self._selector.setAxisNames(["Y"]) - - if len(ys[0].shape) < 2: - self._selector.hide() - else: - self._selector.show() - - self._plot.setGraphTitle(title or "") - if xscale is not None: - self._plot.getXAxis().setScale( - 'log' if xscale == 'log' else 'linear') - if yscale is not None: - self._plot.getYAxis().setScale( - 'log' if yscale == 'log' else 'linear') - self._updateCurve() - - if not self.__selector_is_connected: - self._selector.selectionChanged.connect(self._updateCurve) - self.__selector_is_connected = True - - def _updateCurve(self): - selection = self._selector.selection() - ys = [sig[selection] for sig in self.__signals] - y0 = ys[0] - len_y = len(y0) - x = self.__axis - if x is None: - x = numpy.arange(len_y) - elif numpy.isscalar(x) or len(x) == 1: - # constant axis - x = x * numpy.ones_like(y0) - elif len(x) == 2 and len_y != 2: - # linear calibration a + b * x - x = x[0] + x[1] * numpy.arange(len_y) - - self._plot.remove(kind=("curve",)) - - for i in range(len(self.__signals)): - legend = self.__signals_names[i] - - # errors only supported for primary signal in NXdata - y_errors = None - if i == 0 and self.__signal_errors is not None: - y_errors = self.__signal_errors[self._selector.selection()] - self._plot.addCurve(x, ys[i], legend=legend, - xerror=self.__x_axis_errors, - yerror=y_errors) - if i == 0: - self._plot.setActiveCurve(legend) - - self._plot.resetZoom() - self._plot.getXAxis().setLabel(self.__axis_name) - self._plot.getYAxis().setLabel(self.__signals_names[0]) - - def _setYLabelFromActiveLegend(self, previous_legend, new_legend): - for ylabel in self.__signals_names: - if new_legend is not None and new_legend == ylabel: - self._plot.getYAxis().setLabel(ylabel) - break - - def clear(self): - old = self._selector.blockSignals(True) - self._selector.clear() - self._selector.blockSignals(old) - self._plot.clear() - - -class XYVScatterPlot(qt.QWidget): - """ - Widget for plotting one or more scatters - (with identical x, y coordinates). - """ - def __init__(self, parent=None): - """ - - :param parent: Parent QWidget - """ - super(XYVScatterPlot, self).__init__(parent) - - self.__y_axis = None - """1D array""" - self.__y_axis_name = None - self.__values = None - """List of 1D arrays (for multiple scatters with identical - x, y coordinates)""" - - self.__x_axis = None - self.__x_axis_name = None - self.__x_axis_errors = None - self.__y_axis = None - self.__y_axis_name = None - self.__y_axis_errors = None - - self._plot = ScatterView(self) - self._plot.setColormap(Colormap(name="viridis", - vmin=None, vmax=None, - normalization=Colormap.LINEAR)) - - self._slider = HorizontalSliderWithBrowser(parent=self) - self._slider.setMinimum(0) - self._slider.setValue(0) - self._slider.valueChanged[int].connect(self._sliderIdxChanged) - self._slider.setToolTip("Select auxiliary signals") - - layout = qt.QGridLayout() - layout.setContentsMargins(0, 0, 0, 0) - layout.addWidget(self._plot, 0, 0) - layout.addWidget(self._slider, 1, 0) - - self.setLayout(layout) - - def _sliderIdxChanged(self, value): - self._updateScatter() - - def getScatterView(self): - """Returns the :class:`ScatterView` used for the display - - :rtype: ScatterView - """ - return self._plot - - def getPlot(self): - """Returns the plot used for the display - - :rtype: PlotWidget - """ - return self._plot.getPlotWidget() - - def setScattersData(self, y, x, values, - yerror=None, xerror=None, - ylabel=None, xlabel=None, - title="", scatter_titles=None, - xscale=None, yscale=None): - """ - - :param ndarray y: 1D array for y (vertical) coordinates. - :param ndarray x: 1D array for x coordinates. - :param List[ndarray] values: List of 1D arrays of values. - This will be used to compute the color map and assign colors - to the points. There should be as many arrays in the list as - scatters to be represented. - :param ndarray yerror: 1D array of errors for y (same shape), or None. - :param ndarray xerror: 1D array of errors for x, or None - :param str ylabel: Label for Y axis - :param str xlabel: Label for X axis - :param str title: Main graph title - :param List[str] scatter_titles: Subtitles (one per scatter) - :param str xscale: Scale of X axis in (None, 'linear', 'log') - :param str yscale: Scale of Y axis in (None, 'linear', 'log') - """ - self.__y_axis = y - self.__x_axis = x - self.__x_axis_name = xlabel or "X" - self.__y_axis_name = ylabel or "Y" - self.__x_axis_errors = xerror - self.__y_axis_errors = yerror - self.__values = values - - self.__graph_title = title or "" - self.__scatter_titles = scatter_titles - - self._slider.valueChanged[int].disconnect(self._sliderIdxChanged) - self._slider.setMaximum(len(values) - 1) - if len(values) > 1: - self._slider.show() - else: - self._slider.hide() - self._slider.setValue(0) - self._slider.valueChanged[int].connect(self._sliderIdxChanged) - - if xscale is not None: - self._plot.getXAxis().setScale( - 'log' if xscale == 'log' else 'linear') - if yscale is not None: - self._plot.getYAxis().setScale( - 'log' if yscale == 'log' else 'linear') - - self._updateScatter() - - def _updateScatter(self): - x = self.__x_axis - y = self.__y_axis - - idx = self._slider.value() - - if self.__graph_title: - title = self.__graph_title # main NXdata @title - if len(self.__scatter_titles) > 1: - # Append dataset name only when there is many datasets - title += '\n' + self.__scatter_titles[idx] - else: - title = self.__scatter_titles[idx] # scatter dataset name - - self._plot.setGraphTitle(title) - self._plot.setData(x, y, self.__values[idx], - xerror=self.__x_axis_errors, - yerror=self.__y_axis_errors) - self._plot.resetZoom() - self._plot.getXAxis().setLabel(self.__x_axis_name) - self._plot.getYAxis().setLabel(self.__y_axis_name) - - def clear(self): - self._plot.getPlotWidget().clear() - - -class ArrayImagePlot(qt.QWidget): - """ - Widget for plotting an image from a multi-dimensional signal array - and two 1D axes array. - - The signal array can have an arbitrary number of dimensions, the only - limitation being that the last two dimensions must have the same length as - the axes arrays. - - Sliders are provided to select indices on the first (n - 2) dimensions of - the signal array, and the plot is updated to show the image corresponding - to the selection. - - If one or both of the axes does not have regularly spaced values, the - the image is plotted as a coloured scatter plot. - """ - def __init__(self, parent=None): - """ - - :param parent: Parent QWidget - """ - super(ArrayImagePlot, self).__init__(parent) - - self.__signals = None - self.__signals_names = None - self.__x_axis = None - self.__x_axis_name = None - self.__y_axis = None - self.__y_axis_name = None - - self._plot = Plot2D(self) - self._plot.setDefaultColormap(Colormap(name="viridis", - vmin=None, vmax=None, - normalization=Colormap.LINEAR)) - self._plot.getIntensityHistogramAction().setVisible(True) - self._plot.setKeepDataAspectRatio(True) - maskToolWidget = self._plot.getMaskToolsDockWidget().widget() - maskToolWidget.setItemMaskUpdated(True) - - # not closable - self._selector = NumpyAxesSelector(self) - self._selector.setNamedAxesSelectorVisibility(False) - self._selector.selectionChanged.connect(self._updateImage) - - self._auxSigSlider = HorizontalSliderWithBrowser(parent=self) - self._auxSigSlider.setMinimum(0) - self._auxSigSlider.setValue(0) - self._auxSigSlider.valueChanged[int].connect(self._sliderIdxChanged) - self._auxSigSlider.setToolTip("Select auxiliary signals") - - layout = qt.QVBoxLayout() - layout.addWidget(self._plot) - layout.addWidget(self._selector) - layout.addWidget(self._auxSigSlider) - - self.setLayout(layout) - - def _sliderIdxChanged(self, value): - self._updateImage() - - def getPlot(self): - """Returns the plot used for the display - - :rtype: Plot2D - """ - return self._plot - - def setImageData(self, signals, - x_axis=None, y_axis=None, - signals_names=None, - xlabel=None, ylabel=None, - title=None, isRgba=False, - xscale=None, yscale=None): - """ - - :param signals: list of n-D datasets, whose last 2 dimensions are used as the - image's values, or list of 3D datasets interpreted as RGBA image. - :param x_axis: 1-D dataset used as the image's x coordinates. If - provided, its lengths must be equal to the length of the last - dimension of ``signal``. - :param y_axis: 1-D dataset used as the image's y. If provided, - its lengths must be equal to the length of the 2nd to last - dimension of ``signal``. - :param signals_names: Names for each image, used as subtitle and legend. - :param xlabel: Label for X axis - :param ylabel: Label for Y axis - :param title: Graph title - :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') - """ - self._selector.selectionChanged.disconnect(self._updateImage) - self._auxSigSlider.valueChanged.disconnect(self._sliderIdxChanged) - - self.__signals = signals - self.__signals_names = signals_names - self.__x_axis = x_axis - self.__x_axis_name = xlabel - self.__y_axis = y_axis - self.__y_axis_name = ylabel - self.__title = title - - self._selector.clear() - if not isRgba: - self._selector.setAxisNames(["Y", "X"]) - img_ndim = 2 - else: - self._selector.setAxisNames(["Y", "X", "RGB(A) channel"]) - img_ndim = 3 - self._selector.setData(signals[0]) - - if len(signals[0].shape) <= img_ndim: - self._selector.hide() - else: - self._selector.show() - - self._auxSigSlider.setMaximum(len(signals) - 1) - if len(signals) > 1: - self._auxSigSlider.show() - else: - self._auxSigSlider.hide() - self._auxSigSlider.setValue(0) - - self._axis_scales = xscale, yscale - self._updateImage() - self._plot.resetZoom() - - self._selector.selectionChanged.connect(self._updateImage) - self._auxSigSlider.valueChanged.connect(self._sliderIdxChanged) - - def _updateImage(self): - selection = self._selector.selection() - auxSigIdx = self._auxSigSlider.value() - - legend = self.__signals_names[auxSigIdx] - - images = [img[selection] for img in self.__signals] - image = images[auxSigIdx] - - x_axis = self.__x_axis - y_axis = self.__y_axis - - if x_axis is None and y_axis is None: - xcalib = NoCalibration() - ycalib = NoCalibration() - else: - if x_axis is None: - # no calibration - x_axis = numpy.arange(image.shape[1]) - elif numpy.isscalar(x_axis) or len(x_axis) == 1: - # constant axis - x_axis = x_axis * numpy.ones((image.shape[1], )) - elif len(x_axis) == 2: - # linear calibration - x_axis = x_axis[0] * numpy.arange(image.shape[1]) + x_axis[1] - - if y_axis is None: - y_axis = numpy.arange(image.shape[0]) - elif numpy.isscalar(y_axis) or len(y_axis) == 1: - y_axis = y_axis * numpy.ones((image.shape[0], )) - elif len(y_axis) == 2: - y_axis = y_axis[0] * numpy.arange(image.shape[0]) + y_axis[1] - - xcalib = ArrayCalibration(x_axis) - ycalib = ArrayCalibration(y_axis) - - self._plot.remove(kind=("scatter", "image",)) - if xcalib.is_affine() and ycalib.is_affine(): - # regular image - xorigin, xscale = xcalib(0), xcalib.get_slope() - yorigin, yscale = ycalib(0), ycalib.get_slope() - origin = (xorigin, yorigin) - scale = (xscale, yscale) - - self._plot.getXAxis().setScale('linear') - self._plot.getYAxis().setScale('linear') - self._plot.addImage(image, legend=legend, - origin=origin, scale=scale, - replace=True, resetzoom=False) - else: - xaxisscale, yaxisscale = self._axis_scales - - if xaxisscale is not None: - self._plot.getXAxis().setScale( - 'log' if xaxisscale == 'log' else 'linear') - if yaxisscale is not None: - self._plot.getYAxis().setScale( - 'log' if yaxisscale == 'log' else 'linear') - - scatterx, scattery = numpy.meshgrid(x_axis, y_axis) - # fixme: i don't think this can handle "irregular" RGBA images - self._plot.addScatter(numpy.ravel(scatterx), - numpy.ravel(scattery), - numpy.ravel(image), - legend=legend) - - if self.__title: - title = self.__title - if len(self.__signals_names) > 1: - # Append dataset name only when there is many datasets - title += '\n' + self.__signals_names[auxSigIdx] - else: - title = self.__signals_names[auxSigIdx] - self._plot.setGraphTitle(title) - self._plot.getXAxis().setLabel(self.__x_axis_name) - self._plot.getYAxis().setLabel(self.__y_axis_name) - - def clear(self): - old = self._selector.blockSignals(True) - self._selector.clear() - self._selector.blockSignals(old) - self._plot.clear() - - -class ArrayComplexImagePlot(qt.QWidget): - """ - Widget for plotting an image of complex from a multi-dimensional signal array - and two 1D axes array. - - The signal array can have an arbitrary number of dimensions, the only - limitation being that the last two dimensions must have the same length as - the axes arrays. - - Sliders are provided to select indices on the first (n - 2) dimensions of - the signal array, and the plot is updated to show the image corresponding - to the selection. - - If one or both of the axes does not have regularly spaced values, the - the image is plotted as a coloured scatter plot. - """ - def __init__(self, parent=None, colormap=None): - """ - - :param parent: Parent QWidget - """ - super(ArrayComplexImagePlot, self).__init__(parent) - - self.__signals = None - self.__signals_names = None - self.__x_axis = None - self.__x_axis_name = None - self.__y_axis = None - self.__y_axis_name = None - - self._plot = ComplexImageView(self) - if colormap is not None: - for mode in (ComplexImageView.ComplexMode.ABSOLUTE, - ComplexImageView.ComplexMode.SQUARE_AMPLITUDE, - ComplexImageView.ComplexMode.REAL, - ComplexImageView.ComplexMode.IMAGINARY): - self._plot.setColormap(colormap, mode) - - self._plot.getPlot().getIntensityHistogramAction().setVisible(True) - self._plot.setKeepDataAspectRatio(True) - maskToolWidget = self._plot.getPlot().getMaskToolsDockWidget().widget() - maskToolWidget.setItemMaskUpdated(True) - - # not closable - self._selector = NumpyAxesSelector(self) - self._selector.setNamedAxesSelectorVisibility(False) - self._selector.selectionChanged.connect(self._updateImage) - - self._auxSigSlider = HorizontalSliderWithBrowser(parent=self) - self._auxSigSlider.setMinimum(0) - self._auxSigSlider.setValue(0) - self._auxSigSlider.valueChanged[int].connect(self._sliderIdxChanged) - self._auxSigSlider.setToolTip("Select auxiliary signals") - - layout = qt.QVBoxLayout() - layout.addWidget(self._plot) - layout.addWidget(self._selector) - layout.addWidget(self._auxSigSlider) - - self.setLayout(layout) - - def _sliderIdxChanged(self, value): - self._updateImage() - - def getPlot(self): - """Returns the plot used for the display - - :rtype: PlotWidget - """ - return self._plot.getPlot() - - def setImageData(self, signals, - x_axis=None, y_axis=None, - signals_names=None, - xlabel=None, ylabel=None, - title=None): - """ - - :param signals: list of n-D datasets, whose last 2 dimensions are used as the - image's values, or list of 3D datasets interpreted as RGBA image. - :param x_axis: 1-D dataset used as the image's x coordinates. If - provided, its lengths must be equal to the length of the last - dimension of ``signal``. - :param y_axis: 1-D dataset used as the image's y. If provided, - its lengths must be equal to the length of the 2nd to last - dimension of ``signal``. - :param signals_names: Names for each image, used as subtitle and legend. - :param xlabel: Label for X axis - :param ylabel: Label for Y axis - :param title: Graph title - """ - self._selector.selectionChanged.disconnect(self._updateImage) - self._auxSigSlider.valueChanged.disconnect(self._sliderIdxChanged) - - self.__signals = signals - self.__signals_names = signals_names - self.__x_axis = x_axis - self.__x_axis_name = xlabel - self.__y_axis = y_axis - self.__y_axis_name = ylabel - self.__title = title - - self._selector.clear() - self._selector.setAxisNames(["Y", "X"]) - self._selector.setData(signals[0]) - - if len(signals[0].shape) <= 2: - self._selector.hide() - else: - self._selector.show() - - self._auxSigSlider.setMaximum(len(signals) - 1) - if len(signals) > 1: - self._auxSigSlider.show() - else: - self._auxSigSlider.hide() - self._auxSigSlider.setValue(0) - - self._updateImage() - self._plot.getPlot().resetZoom() - - self._selector.selectionChanged.connect(self._updateImage) - self._auxSigSlider.valueChanged.connect(self._sliderIdxChanged) - - def _updateImage(self): - selection = self._selector.selection() - auxSigIdx = self._auxSigSlider.value() - - images = [img[selection] for img in self.__signals] - image = images[auxSigIdx] - - x_axis = self.__x_axis - y_axis = self.__y_axis - - if x_axis is None and y_axis is None: - xcalib = NoCalibration() - ycalib = NoCalibration() - else: - if x_axis is None: - # no calibration - x_axis = numpy.arange(image.shape[1]) - elif numpy.isscalar(x_axis) or len(x_axis) == 1: - # constant axis - x_axis = x_axis * numpy.ones((image.shape[1], )) - elif len(x_axis) == 2: - # linear calibration - x_axis = x_axis[0] * numpy.arange(image.shape[1]) + x_axis[1] - - if y_axis is None: - y_axis = numpy.arange(image.shape[0]) - elif numpy.isscalar(y_axis) or len(y_axis) == 1: - y_axis = y_axis * numpy.ones((image.shape[0], )) - elif len(y_axis) == 2: - y_axis = y_axis[0] * numpy.arange(image.shape[0]) + y_axis[1] - - xcalib = ArrayCalibration(x_axis) - ycalib = ArrayCalibration(y_axis) - - self._plot.setData(image) - if xcalib.is_affine(): - xorigin, xscale = xcalib(0), xcalib.get_slope() - else: - _logger.warning("Unsupported complex image X axis calibration") - xorigin, xscale = 0., 1. - - if ycalib.is_affine(): - yorigin, yscale = ycalib(0), ycalib.get_slope() - else: - _logger.warning("Unsupported complex image Y axis calibration") - yorigin, yscale = 0., 1. - - self._plot.setOrigin((xorigin, yorigin)) - self._plot.setScale((xscale, yscale)) - - if self.__title: - title = self.__title - if len(self.__signals_names) > 1: - # Append dataset name only when there is many datasets - title += '\n' + self.__signals_names[auxSigIdx] - else: - title = self.__signals_names[auxSigIdx] - self._plot.setGraphTitle(title) - self._plot.getXAxis().setLabel(self.__x_axis_name) - self._plot.getYAxis().setLabel(self.__y_axis_name) - - def clear(self): - old = self._selector.blockSignals(True) - self._selector.clear() - self._selector.blockSignals(old) - self._plot.setData(None) - - -class ArrayStackPlot(qt.QWidget): - """ - Widget for plotting a n-D array (n >= 3) as a stack of images. - Three axis arrays can be provided to calibrate the axes. - - The signal array can have an arbitrary number of dimensions, the only - limitation being that the last 3 dimensions must have the same length as - the axes arrays. - - Sliders are provided to select indices on the first (n - 3) dimensions of - the signal array, and the plot is updated to load the stack corresponding - to the selection. - """ - def __init__(self, parent=None): - """ - - :param parent: Parent QWidget - """ - super(ArrayStackPlot, self).__init__(parent) - - self.__signal = None - self.__signal_name = None - # the Z, Y, X axes apply to the last three dimensions of the signal - # (in that order) - self.__z_axis = None - self.__z_axis_name = None - self.__y_axis = None - self.__y_axis_name = None - self.__x_axis = None - self.__x_axis_name = None - - self._stack_view = StackView(self) - maskToolWidget = self._stack_view.getPlotWidget().getMaskToolsDockWidget().widget() - maskToolWidget.setItemMaskUpdated(True) - - self._hline = qt.QFrame(self) - self._hline.setFrameStyle(qt.QFrame.HLine) - self._hline.setFrameShadow(qt.QFrame.Sunken) - self._legend = qt.QLabel(self) - self._selector = NumpyAxesSelector(self) - self._selector.setNamedAxesSelectorVisibility(False) - self.__selector_is_connected = False - - layout = qt.QVBoxLayout() - layout.addWidget(self._stack_view) - layout.addWidget(self._hline) - layout.addWidget(self._legend) - layout.addWidget(self._selector) - - self.setLayout(layout) - - def getStackView(self): - """Returns the plot used for the display - - :rtype: StackView - """ - return self._stack_view - - def setStackData(self, signal, - x_axis=None, y_axis=None, z_axis=None, - signal_name=None, - xlabel=None, ylabel=None, zlabel=None, - title=None): - """ - - :param signal: n-D dataset, whose last 3 dimensions are used as the - 3D stack values. - :param x_axis: 1-D dataset used as the image's x coordinates. If - provided, its lengths must be equal to the length of the last - dimension of ``signal``. - :param y_axis: 1-D dataset used as the image's y. If provided, - its lengths must be equal to the length of the 2nd to last - dimension of ``signal``. - :param z_axis: 1-D dataset used as the image's z. If provided, - its lengths must be equal to the length of the 3rd to last - dimension of ``signal``. - :param signal_name: Label used in the legend - :param xlabel: Label for X axis - :param ylabel: Label for Y axis - :param zlabel: Label for Z axis - :param title: Graph title - """ - if self.__selector_is_connected: - self._selector.selectionChanged.disconnect(self._updateStack) - self.__selector_is_connected = False - - self.__signal = signal - self.__signal_name = signal_name or "" - self.__x_axis = x_axis - self.__x_axis_name = xlabel - self.__y_axis = y_axis - self.__y_axis_name = ylabel - self.__z_axis = z_axis - self.__z_axis_name = zlabel - - self._selector.setData(signal) - self._selector.setAxisNames(["Y", "X", "Z"]) - - self._stack_view.setGraphTitle(title or "") - # by default, the z axis is the image position (dimension not plotted) - self._stack_view.getPlotWidget().getXAxis().setLabel(self.__x_axis_name or "X") - self._stack_view.getPlotWidget().getYAxis().setLabel(self.__y_axis_name or "Y") - - self._updateStack() - - ndims = len(signal.shape) - self._stack_view.setFirstStackDimension(ndims - 3) - - # the legend label shows the selection slice producing the volume - # (only interesting for ndim > 3) - if ndims > 3: - self._selector.setVisible(True) - self._legend.setVisible(True) - self._hline.setVisible(True) - else: - self._selector.setVisible(False) - self._legend.setVisible(False) - self._hline.setVisible(False) - - if not self.__selector_is_connected: - self._selector.selectionChanged.connect(self._updateStack) - self.__selector_is_connected = True - - @staticmethod - def _get_origin_scale(axis): - """Assuming axis is a regularly spaced 1D array, - return a tuple (origin, scale) where: - - origin = axis[0] - - scale = (axis[n-1] - axis[0]) / (n -1) - :param axis: 1D numpy array - :return: Tuple (axis[0], (axis[-1] - axis[0]) / (len(axis) - 1)) - """ - return axis[0], (axis[-1] - axis[0]) / (len(axis) - 1) - - def _updateStack(self): - """Update displayed stack according to the current axes selector - data.""" - stk = self._selector.selectedData() - x_axis = self.__x_axis - y_axis = self.__y_axis - z_axis = self.__z_axis - - calibrations = [] - for axis in [z_axis, y_axis, x_axis]: - - if axis is None: - calibrations.append(NoCalibration()) - elif len(axis) == 2: - calibrations.append( - LinearCalibration(y_intercept=axis[0], - slope=axis[1])) - else: - calibrations.append(ArrayCalibration(axis)) - - legend = self.__signal_name + "[" - for sl in self._selector.selection(): - if sl == slice(None): - legend += ":, " - else: - legend += str(sl) + ", " - legend = legend[:-2] + "]" - self._legend.setText("Displayed data: " + legend) - - self._stack_view.setStack(stk, calibrations=calibrations) - self._stack_view.setLabels( - labels=[self.__z_axis_name, - self.__y_axis_name, - self.__x_axis_name]) - - def clear(self): - old = self._selector.blockSignals(True) - self._selector.clear() - self._selector.blockSignals(old) - self._stack_view.clear() - - -class ArrayVolumePlot(qt.QWidget): - """ - Widget for plotting a n-D array (n >= 3) as a 3D scalar field. - Three axis arrays can be provided to calibrate the axes. - - The signal array can have an arbitrary number of dimensions, the only - limitation being that the last 3 dimensions must have the same length as - the axes arrays. - - Sliders are provided to select indices on the first (n - 3) dimensions of - the signal array, and the plot is updated to load the stack corresponding - to the selection. - """ - def __init__(self, parent=None): - """ - - :param parent: Parent QWidget - """ - super(ArrayVolumePlot, self).__init__(parent) - - self.__signal = None - self.__signal_name = None - # the Z, Y, X axes apply to the last three dimensions of the signal - # (in that order) - self.__z_axis = None - self.__z_axis_name = None - self.__y_axis = None - self.__y_axis_name = None - self.__x_axis = None - self.__x_axis_name = None - - from ._VolumeWindow import VolumeWindow - - self._view = VolumeWindow(self) - - self._hline = qt.QFrame(self) - self._hline.setFrameStyle(qt.QFrame.HLine) - self._hline.setFrameShadow(qt.QFrame.Sunken) - self._legend = qt.QLabel(self) - self._selector = NumpyAxesSelector(self) - self._selector.setNamedAxesSelectorVisibility(False) - self.__selector_is_connected = False - - layout = qt.QVBoxLayout() - layout.addWidget(self._view) - layout.addWidget(self._hline) - layout.addWidget(self._legend) - layout.addWidget(self._selector) - - self.setLayout(layout) - - def getVolumeView(self): - """Returns the plot used for the display - - :rtype: SceneWindow - """ - return self._view - - def setData(self, signal, - x_axis=None, y_axis=None, z_axis=None, - signal_name=None, - xlabel=None, ylabel=None, zlabel=None, - title=None): - """ - - :param signal: n-D dataset, whose last 3 dimensions are used as the - 3D stack values. - :param x_axis: 1-D dataset used as the image's x coordinates. If - provided, its lengths must be equal to the length of the last - dimension of ``signal``. - :param y_axis: 1-D dataset used as the image's y. If provided, - its lengths must be equal to the length of the 2nd to last - dimension of ``signal``. - :param z_axis: 1-D dataset used as the image's z. If provided, - its lengths must be equal to the length of the 3rd to last - dimension of ``signal``. - :param signal_name: Label used in the legend - :param xlabel: Label for X axis - :param ylabel: Label for Y axis - :param zlabel: Label for Z axis - :param title: Graph title - """ - if self.__selector_is_connected: - self._selector.selectionChanged.disconnect(self._updateVolume) - self.__selector_is_connected = False - - self.__signal = signal - self.__signal_name = signal_name or "" - self.__x_axis = x_axis - self.__x_axis_name = xlabel - self.__y_axis = y_axis - self.__y_axis_name = ylabel - self.__z_axis = z_axis - self.__z_axis_name = zlabel - - self._selector.setData(signal) - self._selector.setAxisNames(["Y", "X", "Z"]) - - self._updateVolume() - - # the legend label shows the selection slice producing the volume - # (only interesting for ndim > 3) - if signal.ndim > 3: - self._selector.setVisible(True) - self._legend.setVisible(True) - self._hline.setVisible(True) - else: - self._selector.setVisible(False) - self._legend.setVisible(False) - self._hline.setVisible(False) - - if not self.__selector_is_connected: - self._selector.selectionChanged.connect(self._updateVolume) - self.__selector_is_connected = True - - def _updateVolume(self): - """Update displayed stack according to the current axes selector - data.""" - x_axis = self.__x_axis - y_axis = self.__y_axis - z_axis = self.__z_axis - - offset = [] - scale = [] - for axis in [x_axis, y_axis, z_axis]: - if axis is None: - calibration = NoCalibration() - elif len(axis) == 2: - calibration = LinearCalibration( - y_intercept=axis[0], slope=axis[1]) - else: - calibration = ArrayCalibration(axis) - if not calibration.is_affine(): - _logger.warning("Axis has not linear values, ignored") - offset.append(0.) - scale.append(1.) - else: - offset.append(calibration(0)) - scale.append(calibration.get_slope()) - - legend = self.__signal_name + "[" - for sl in self._selector.selection(): - if sl == slice(None): - legend += ":, " - else: - legend += str(sl) + ", " - legend = legend[:-2] + "]" - self._legend.setText("Displayed data: " + legend) - - # Update SceneWidget - data = self._selector.selectedData() - - volumeView = self.getVolumeView() - volumeView.setData(data, offset=offset, scale=scale) - volumeView.setAxesLabels( - self.__x_axis_name, self.__y_axis_name, self.__z_axis_name) - - def clear(self): - old = self._selector.blockSignals(True) - self._selector.clear() - self._selector.blockSignals(old) - self.getVolumeView().clear() diff --git a/silx/gui/data/NumpyAxesSelector.py b/silx/gui/data/NumpyAxesSelector.py deleted file mode 100644 index e6da0d4..0000000 --- a/silx/gui/data/NumpyAxesSelector.py +++ /dev/null @@ -1,578 +0,0 @@ -# coding: utf-8 -# /*########################################################################## -# -# Copyright (c) 2016-2019 European Synchrotron Radiation Facility -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. -# -# ###########################################################################*/ -"""This module defines a widget 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" -__date__ = "29/01/2018" - -import logging -import numpy -import functools -from silx.gui.widgets.FrameBrowser import HorizontalSliderWithBrowser -from silx.gui import qt -from silx.gui.utils import blockSignals -import silx.utils.weakref - - -_logger = logging.getLogger(__name__) - - -class _Axis(qt.QWidget): - """Widget displaying an axis. - - It allows to display and scroll in the axis, and provide a widget to - map the axis with a named axis (the one from the view). - """ - - valueChanged = qt.Signal(int) - """Emitted when the location on the axis change.""" - - axisNameChanged = qt.Signal(object) - """Emitted when the user change the name of the axis.""" - - def __init__(self, parent=None): - """Constructor - - :param parent: Parent of the widget - """ - super(_Axis, self).__init__(parent) - self.__axisNumber = None - self.__customAxisNames = set([]) - self.__label = qt.QLabel(self) - self.__axes = qt.QComboBox(self) - self.__axes.currentIndexChanged[int].connect(self.__axisMappingChanged) - self.__slider = HorizontalSliderWithBrowser(self) - self.__slider.valueChanged[int].connect(self.__sliderValueChanged) - layout = qt.QHBoxLayout(self) - layout.setContentsMargins(0, 0, 0, 0) - layout.addWidget(self.__label) - layout.addWidget(self.__axes) - layout.addWidget(self.__slider, 10000) - layout.addStretch(1) - self.setLayout(layout) - - def slider(self): - """Returns the slider used to display axes location. - - :rtype: HorizontalSliderWithBrowser - """ - return self.__slider - - def setAxis(self, number, position, size): - """Set axis information. - - :param int number: The number of the axis (from the original numpy - array) - :param int position: The current position in the axis (for a slicing) - :param int size: The size of this axis (0..n) - """ - self.__label.setText("Dimension %s" % number) - self.__axisNumber = number - self.__slider.setMaximum(size - 1) - - def axisNumber(self): - """Returns the axis number. - - :rtype: int - """ - return self.__axisNumber - - def setAxisName(self, axisName): - """Set the current used axis name. - - If this name is not available an exception is raised. An empty string - means that no name is selected. - - :param str axisName: The new name of the axis - :raise ValueError: When the name is not available - """ - if axisName == "" and self.__axes.count() == 0: - self.__axes.setCurrentIndex(-1) - self.__updateSliderVisibility() - return - - for index in range(self.__axes.count()): - name = self.__axes.itemData(index) - if name == axisName: - self.__axes.setCurrentIndex(index) - self.__updateSliderVisibility() - return - raise ValueError("Axis name '%s' not found", axisName) - - def axisName(self): - """Returns the selected axis name. - - If no name is selected, an empty string is returned. - - :rtype: str - """ - index = self.__axes.currentIndex() - if index == -1: - return "" - return self.__axes.itemData(index) - - def setAxisNames(self, axesNames): - """Set the available list of names for the axis. - - :param List[str] axesNames: List of available names - """ - self.__axes.clear() - with blockSignals(self.__axes): - self.__axes.addItem(" ", "") - for axis in axesNames: - self.__axes.addItem(axis, axis) - - self.__updateSliderVisibility() - - def setCustomAxis(self, axesNames): - """Set the available list of named axis which can be set to a value. - - :param List[str] axesNames: List of customable axis names - """ - self.__customAxisNames = set(axesNames) - self.__updateSliderVisibility() - - def __axisMappingChanged(self, index): - """Called when the selected name change. - - :param int index: Selected index - """ - self.__updateSliderVisibility() - name = self.axisName() - self.axisNameChanged.emit(name) - - def __updateSliderVisibility(self): - """Update the visibility of the slider according to axis names and - customable axis names.""" - name = self.axisName() - isVisible = name == "" or name in self.__customAxisNames - self.__slider.setVisible(isVisible) - - def value(self): - """Returns the currently selected position in the axis. - - :rtype: int - """ - return self.__slider.value() - - def setValue(self, value): - """Set the currently selected position in the axis. - - :param int value: - """ - self.__slider.setValue(value) - - def __sliderValueChanged(self, value): - """Called when the selected position in the axis change. - - :param int value: Position of the axis - """ - self.valueChanged.emit(value) - - def setNamedAxisSelectorVisibility(self, visible): - """Hide or show the named axis combobox. - - If both the selector and the slider are hidden, hide the entire widget. - - :param visible: boolean - """ - self.__axes.setVisible(visible) - name = self.axisName() - self.setVisible(visible or name == "") - - -class NumpyAxesSelector(qt.QWidget): - """Widget to select a view from a numpy array. - - .. image:: img/NumpyAxesSelector.png - - The widget is set with an input data using :meth:`setData`, and a requested - output dimension using :meth:`setAxisNames`. - - Widgets are provided to selected expected input axis, and a slice on the - non-selected axis. - - The final selected array can be reached using the getter - :meth:`selectedData`, and the event `selectionChanged`. - - If the input data is a HDF5 Dataset, the selected output data will be a - new numpy array. - """ - - dataChanged = qt.Signal() - """Emitted when the input data change""" - - selectedAxisChanged = qt.Signal() - """Emitted when the selected axis change""" - - selectionChanged = qt.Signal() - """Emitted when the selected data change""" - - customAxisChanged = qt.Signal(str, int) - """Emitted when a custom axis change""" - - def __init__(self, parent=None): - """Constructor - - :param parent: Parent of the widget - """ - super(NumpyAxesSelector, self).__init__(parent) - - self.__data = None - self.__selectedData = None - self.__axis = [] - self.__axisNames = [] - self.__customAxisNames = set([]) - self.__namedAxesVisibility = True - layout = qt.QVBoxLayout(self) - layout.setContentsMargins(0, 0, 0, 0) - layout.setSizeConstraint(qt.QLayout.SetMinAndMaxSize) - self.setLayout(layout) - - def clear(self): - """Clear the widget.""" - self.setData(None) - - def setAxisNames(self, axesNames): - """Set the axis names of the output selected data. - - Axis names are defined from slower to faster axis. - - The size of the list will constrain the dimension of the resulting - array. - - :param List[str] axesNames: List of distinct strings identifying axis names - """ - self.__axisNames = list(axesNames) - assert len(set(self.__axisNames)) == len(self.__axisNames),\ - "Non-unique axes names: %s" % self.__axisNames - - delta = len(self.__axis) - len(self.__axisNames) - if delta < 0: - delta = 0 - for index, axis in enumerate(self.__axis): - with blockSignals(axis): - axis.setAxisNames(self.__axisNames) - if index >= delta and index - delta < len(self.__axisNames): - axis.setAxisName(self.__axisNames[index - delta]) - else: - axis.setAxisName("") - self.__updateSelectedData() - - def setCustomAxis(self, axesNames): - """Set the available list of named axis which can be set to a value. - - :param List[str] axesNames: List of customable axis names - """ - self.__customAxisNames = set(axesNames) - for axis in self.__axis: - axis.setCustomAxis(self.__customAxisNames) - - def setData(self, data): - """Set the input data unsed by the widget. - - :param numpy.ndarray data: The input data - """ - if self.__data is not None: - # clean up - for widget in self.__axis: - self.layout().removeWidget(widget) - widget.deleteLater() - self.__axis = [] - - self.__data = data - - if data is not None: - # create expected axes - dimensionNumber = len(data.shape) - delta = dimensionNumber - len(self.__axisNames) - for index in range(dimensionNumber): - axis = _Axis(self) - axis.setAxis(index, 0, data.shape[index]) - axis.setAxisNames(self.__axisNames) - axis.setCustomAxis(self.__customAxisNames) - if index >= delta and index - delta < len(self.__axisNames): - axis.setAxisName(self.__axisNames[index - delta]) - # this weak method was expected to be able to delete sub widget - callback = functools.partial(silx.utils.weakref.WeakMethodProxy(self.__axisValueChanged), axis) - axis.valueChanged.connect(callback) - # this weak method was expected to be able to delete sub widget - callback = functools.partial(silx.utils.weakref.WeakMethodProxy(self.__axisNameChanged), axis) - axis.axisNameChanged.connect(callback) - axis.setNamedAxisSelectorVisibility(self.__namedAxesVisibility) - self.layout().addWidget(axis) - self.__axis.append(axis) - self.__normalizeAxisGeometry() - - self.dataChanged.emit() - self.__updateSelectedData() - - def __normalizeAxisGeometry(self): - """Update axes geometry to align all axes components together.""" - if len(self.__axis) <= 0: - return - lineEditWidth = max([a.slider().lineEdit().minimumSize().width() for a in self.__axis]) - limitWidth = max([a.slider().limitWidget().minimumSizeHint().width() for a in self.__axis]) - for a in self.__axis: - a.slider().lineEdit().setFixedWidth(lineEditWidth) - a.slider().limitWidget().setFixedWidth(limitWidth) - - def __axisValueChanged(self, axis, value): - name = axis.axisName() - if name in self.__customAxisNames: - self.customAxisChanged.emit(name, value) - else: - self.__updateSelectedData() - - def __axisNameChanged(self, axis, name): - """Called when an axis name change. - - :param _Axis axis: The changed axis - :param str name: The new name of the axis - """ - names = [x.axisName() for x in self.__axis] - missingName = set(self.__axisNames) - set(names) - set("") - if len(missingName) == 0: - missingName = None - elif len(missingName) == 1: - missingName = list(missingName)[0] - else: - raise Exception("Unexpected state") - - axisChanged = True - - if axis.axisName() == "": - # set the removed label to another widget if it is possible - availableWidget = None - for widget in self.__axis: - if widget is axis: - continue - if widget.axisName() == "": - availableWidget = widget - break - if availableWidget is None: - # If there is no other solution we set the name at the same place - axisChanged = False - availableWidget = axis - with blockSignals(availableWidget): - availableWidget.setAxisName(missingName) - else: - # there is a duplicated name somewhere - # we swap it with the missing name or with nothing - dupWidget = None - for widget in self.__axis: - if widget is axis: - continue - if widget.axisName() == axis.axisName(): - dupWidget = widget - break - if missingName is None: - missingName = "" - with blockSignals(dupWidget): - dupWidget.setAxisName(missingName) - - if self.__data is None: - return - if axisChanged: - self.selectedAxisChanged.emit() - self.__updateSelectedData() - - def __updateSelectedData(self): - """Update the selected data according to the state of the widget. - - It fires a `selectionChanged` event. - """ - permutation = self.permutation() - - if self.__data is None or permutation is None: - # No data or not all the expected axes are there - if self.__selectedData is not None: - self.__selectedData = None - self.selectionChanged.emit() - return - - # get a view with few fixed dimensions - # with a h5py dataset, it create a copy - # TODO we can reuse the same memory in case of a copy - self.__selectedData = numpy.transpose(self.__data[self.selection()], permutation) - self.selectionChanged.emit() - - def data(self): - """Returns the input data. - - :rtype: Union[numpy.ndarray,None] - """ - if self.__data is None: - return None - else: - return numpy.array(self.__data, copy=False) - - def selectedData(self): - """Returns the output data. - - This is equivalent to:: - - numpy.transpose(self.data()[self.selection()], self.permutation()) - - :rtype: Union[numpy.ndarray,None] - """ - if self.__selectedData is None: - return None - else: - return numpy.array(self.__selectedData, copy=False) - - def permutation(self): - """Returns the axes permutation to convert data subset to selected data. - - If permutation cannot be computer, it returns None. - - :rtype: Union[List[int],None] - """ - if self.__data is None: - return None - else: - indices = [] - for name in self.__axisNames: - index = 0 - for axis in self.__axis: - if axis.axisName() == name: - indices.append(index) - break - if axis.axisName() != "": - index += 1 - else: - _logger.warning("No axis corresponding to: %s", name) - return None - return tuple(indices) - - def selection(self): - """Returns the selection tuple used to slice the data. - - :rtype: tuple - """ - if self.__data is None: - return tuple() - else: - return tuple([axis.value() if axis.axisName() == "" else slice(None) - for axis in self.__axis]) - - def setSelection(self, selection, permutation=None): - """Set the selection along each dimension. - - tuple returned by :meth:`selection` can be provided as input, - provided that it is for the same the number of axes and - the same number of dimensions of the data. - - :param List[Union[int,slice,None]] selection: - The selection tuple with as one element for each dimension of the data. - If an element is None, then the whole dimension is selected. - :param Union[List[int],None] permutation: - The data axes indices to transpose. - If not given, no permutation is applied - :raise ValueError: - When the selection does not match current data shape and number of axes. - """ - data_shape = self.__data.shape if self.__data is not None else () - - # Check selection - if len(selection) != len(data_shape): - raise ValueError( - "Selection length (%d) and data ndim (%d) mismatch" % - (len(selection), len(data_shape))) - - # Check selection type - selectedDataNDim = 0 - for element, size in zip(selection, data_shape): - if isinstance(element, int): - if not 0 <= element < size: - raise ValueError( - "Selected index (%d) outside data dimension range [0-%d]" % - (element, size)) - elif element is None or element == slice(None): - selectedDataNDim += 1 - else: - raise ValueError("Unsupported element in selection: %s" % element) - - ndim = len(self.__axisNames) - if selectedDataNDim != ndim: - raise ValueError( - "Selection dimensions (%d) and number of axes (%d) mismatch" % - (selectedDataNDim, ndim)) - - # check permutation - if permutation is None: - permutation = tuple(range(ndim)) - - if set(permutation) != set(range(ndim)): - raise ValueError( - "Error in provided permutation: " - "Wrong size, elements out of range or duplicates") - - inversePermutation = numpy.argsort(permutation) - - axisNameChanged = False - customValueChanged = [] - with blockSignals(*self.__axis): - index = 0 - for element, axis in zip(selection, self.__axis): - if isinstance(element, int): - name = "" - else: - name = self.__axisNames[inversePermutation[index]] - index += 1 - - if axis.axisName() != name: - axis.setAxisName(name) - axisNameChanged = True - - for element, axis in zip(selection, self.__axis): - value = element if isinstance(element, int) else 0 - if axis.value() != value: - axis.setValue(value) - - name = axis.axisName() - if name in self.__customAxisNames: - customValueChanged.append((name, value)) - - # Send signals that where disabled - if axisNameChanged: - self.selectedAxisChanged.emit() - for name, value in customValueChanged: - self.customAxisChanged.emit(name, value) - self.__updateSelectedData() - - def setNamedAxesSelectorVisibility(self, visible): - """Show or hide the combo-boxes allowing to map the plot axes - to the data dimension. - - :param visible: Boolean - """ - self.__namedAxesVisibility = visible - for axis in self.__axis: - axis.setNamedAxisSelectorVisibility(visible) diff --git a/silx/gui/data/RecordTableView.py b/silx/gui/data/RecordTableView.py deleted file mode 100644 index 2c0011a..0000000 --- a/silx/gui/data/RecordTableView.py +++ /dev/null @@ -1,447 +0,0 @@ -# coding: utf-8 -# /*########################################################################## -# -# Copyright (c) 2017-2021 European Synchrotron Radiation Facility -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. -# -# ###########################################################################*/ -""" -This module 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 -from silx.gui import qt -import silx.io -from .TextFormatter import TextFormatter -from silx.gui.widgets.TableWidget import CopySelectedCellsAction - -__authors__ = ["V. Valls"] -__license__ = "MIT" -__date__ = "29/08/2018" - - -class _MultiLineItem(qt.QItemDelegate): - """Draw a multiline text without hiding anything. - - The paint method display a cell without any wrap. And an editor is - available to scroll into the selected cell. - """ - - def __init__(self, parent=None): - """ - Constructor - - :param qt.QWidget parent: Parent of the widget - """ - qt.QItemDelegate.__init__(self, parent) - self.__textOptions = qt.QTextOption() - self.__textOptions.setFlags(qt.QTextOption.IncludeTrailingSpaces | - qt.QTextOption.ShowTabsAndSpaces) - self.__textOptions.setWrapMode(qt.QTextOption.NoWrap) - self.__textOptions.setAlignment(qt.Qt.AlignTop | qt.Qt.AlignLeft) - - def paint(self, painter, option, index): - """ - Write multiline text without using any wrap or any alignment according - to the cell size. - - :param qt.QPainter painter: Painter context used to displayed the cell - :param qt.QStyleOptionViewItem option: Control how the editor is shown - :param qt.QIndex index: Index of the data to display - """ - painter.save() - - # set colors - painter.setPen(qt.QPen(qt.Qt.NoPen)) - if option.state & qt.QStyle.State_Selected: - brush = option.palette.highlight() - painter.setBrush(brush) - else: - brush = index.data(qt.Qt.BackgroundRole) - if brush is None: - # default background color for a cell - brush = qt.Qt.white - painter.setBrush(brush) - painter.drawRect(option.rect) - - if index.isValid(): - if option.state & qt.QStyle.State_Selected: - brush = option.palette.highlightedText() - else: - brush = index.data(qt.Qt.ForegroundRole) - if brush is None: - brush = option.palette.text() - painter.setPen(qt.QPen(brush.color())) - text = index.data(qt.Qt.DisplayRole) - painter.drawText(qt.QRectF(option.rect), text, self.__textOptions) - - painter.restore() - - def createEditor(self, parent, option, index): - """ - Returns the widget used to edit the item specified by index for editing. - - We use it not to edit the content but to show the content with a - convenient scroll bar. - - :param qt.QWidget parent: Parent of the widget - :param qt.QStyleOptionViewItem option: Control how the editor is shown - :param qt.QIndex index: Index of the data to display - """ - if not index.isValid(): - return super(_MultiLineItem, self).createEditor(parent, option, index) - - editor = qt.QTextEdit(parent) - editor.setReadOnly(True) - return editor - - def setEditorData(self, editor, index): - """ - Read data from the model and feed the editor. - - :param qt.QWidget editor: Editor widget - :param qt.QIndex index: Index of the data to display - """ - text = index.model().data(index, qt.Qt.EditRole) - editor.setText(text) - - def updateEditorGeometry(self, editor, option, index): - """ - Update the geometry of the editor according to the changes of the view. - - :param qt.QWidget editor: Editor widget - :param qt.QStyleOptionViewItem option: Control how the editor is shown - :param qt.QIndex index: Index of the data to display - """ - editor.setGeometry(option.rect) - - -class RecordTableModel(qt.QAbstractTableModel): - """This data model provides access to 1D slices from numpy array using - compound data types or hdf5 databases. - - Each entries are displayed in a single row, and each columns contain a - specific field of the compound type. - - It also allows to display 1D arrays of simple data types. - array. - - :param qt.QObject parent: Parent object - :param numpy.ndarray data: A numpy array or a h5py dataset - """ - - MAX_NUMBER_OF_ROWS = 10e6 - """Maximum number of display values of the dataset""" - - def __init__(self, parent=None, data=None): - qt.QAbstractTableModel.__init__(self, parent) - - self.__data = None - self.__is_array = False - self.__fields = None - self.__formatter = None - self.__editFormatter = None - self.setFormatter(TextFormatter(self)) - - # set _data - self.setArrayData(data) - - # Methods to be implemented to subclass QAbstractTableModel - def rowCount(self, parent_idx=None): - """Returns number of rows to be displayed in table""" - if self.__data is None: - return 0 - elif not self.__is_array: - return 1 - else: - return min(len(self.__data), self.MAX_NUMBER_OF_ROWS) - - def columnCount(self, parent_idx=None): - """Returns number of columns to be displayed in table""" - if self.__fields is None: - return 1 - else: - return len(self.__fields) - - def __clippedData(self, role=qt.Qt.DisplayRole): - """Return data for cells representing clipped data""" - if role == qt.Qt.DisplayRole: - return "..." - elif role == qt.Qt.ToolTipRole: - return "Dataset is too large: display is clipped" - else: - return None - - def data(self, index, role=qt.Qt.DisplayRole): - """QAbstractTableModel method to access data values - in the format ready to be displayed""" - if not index.isValid(): - return None - - if self.__data is None: - return None - - # Special display of one before last data for clipped table - if self.__isClipped() and index.row() == self.rowCount() - 2: - return self.__clippedData(role) - - if self.__is_array: - row = index.row() - if row >= self.rowCount(): - return None - elif self.__isClipped() and row == self.rowCount() - 1: - # Clipped array, display last value at the end - data = self.__data[-1] - else: - data = self.__data[row] - else: - if index.row() > 0: - return None - data = self.__data - - if self.__fields is not None: - if index.column() >= len(self.__fields): - return None - key = self.__fields[index.column()][1] - data = data[key[0]] - if len(key) > 1: - data = data[key[1]] - - # no dtype in case of 1D array of unicode objects (#2093) - dtype = getattr(data, "dtype", None) - - if role == qt.Qt.DisplayRole: - return self.__formatter.toString(data, dtype=dtype) - elif role == qt.Qt.EditRole: - return self.__editFormatter.toString(data, dtype=dtype) - return None - - def headerData(self, section, orientation, role=qt.Qt.DisplayRole): - """Returns the 0-based row or column index, for display in the - horizontal and vertical headers""" - if section == -1: - # PyQt4 send -1 when there is columns but no rows - return None - - # Handle clipping of huge tables - if (self.__isClipped() and - orientation == qt.Qt.Vertical and - section == self.rowCount() - 2): - return self.__clippedData(role) - - if role == qt.Qt.DisplayRole: - if orientation == qt.Qt.Vertical: - if not self.__is_array: - return "Scalar" - elif section == self.MAX_NUMBER_OF_ROWS - 1: - return str(len(self.__data) - 1) - else: - return str(section) - if orientation == qt.Qt.Horizontal: - if self.__fields is None: - if section == 0: - return "Data" - else: - return None - else: - if section < len(self.__fields): - return self.__fields[section][0] - else: - return None - return None - - def flags(self, index): - """QAbstractTableModel method to inform the view whether data - is editable or not. - """ - return qt.QAbstractTableModel.flags(self, index) - - def __isClipped(self) -> bool: - """Returns whether the displayed array is clipped or not""" - return self.__data is not None and self.__is_array and len(self.__data) > self.MAX_NUMBER_OF_ROWS - - def setArrayData(self, data): - """Set the data array and the viewing perspective. - - You can set ``copy=False`` if you need more performances, when dealing - with a large numpy array. In this case, a simple reference to the data - is used to access the data, rather than a copy of the array. - - .. warning:: - - Any change to the data model will affect your original data - array, when using a reference rather than a copy.. - - :param data: 1D numpy array, or any object that can be - converted to a numpy array using ``numpy.array(data)`` (e.g. - a nested sequence). - """ - if qt.qVersion() > "4.6": - self.beginResetModel() - - self.__data = data - if isinstance(data, numpy.ndarray): - self.__is_array = True - elif silx.io.is_dataset(data) and data.shape != tuple(): - self.__is_array = True - else: - self.__is_array = False - - self.__fields = [] - if data is not None: - if data.dtype.fields is not None: - fields = sorted(data.dtype.fields.items(), key=lambda e: e[1][1]) - for name, (dtype, _index) in fields: - if dtype.shape != tuple(): - keys = itertools.product(*[range(x) for x in dtype.shape]) - for key in keys: - label = "%s%s" % (name, list(key)) - array_key = (name, key) - self.__fields.append((label, array_key)) - else: - self.__fields.append((name, (name,))) - else: - self.__fields = None - - if qt.qVersion() > "4.6": - self.endResetModel() - else: - self.reset() - - def arrayData(self): - """Returns the internal data. - - :rtype: numpy.ndarray of h5py.Dataset - """ - return self.__data - - def setFormatter(self, formatter): - """Set the formatter object to be used to display data from the model - - :param TextFormatter formatter: Formatter to use - """ - if formatter is self.__formatter: - return - - if qt.qVersion() > "4.6": - self.beginResetModel() - - if self.__formatter is not None: - self.__formatter.formatChanged.disconnect(self.__formatChanged) - - self.__formatter = formatter - self.__editFormatter = TextFormatter(formatter) - self.__editFormatter.setUseQuoteForText(False) - - if self.__formatter is not None: - self.__formatter.formatChanged.connect(self.__formatChanged) - - if qt.qVersion() > "4.6": - self.endResetModel() - else: - self.reset() - - def getFormatter(self): - """Returns the text formatter used. - - :rtype: TextFormatter - """ - return self.__formatter - - def __formatChanged(self): - """Called when the format changed. - """ - self.__editFormatter = TextFormatter(self, self.getFormatter()) - self.__editFormatter.setUseQuoteForText(False) - self.reset() - - -class _ShowEditorProxyModel(qt.QIdentityProxyModel): - """ - Allow to custom the flag edit of the model - """ - - def __init__(self, parent=None): - """ - Constructor - - :param qt.QObject arent: parent object - """ - super(_ShowEditorProxyModel, self).__init__(parent) - self.__forceEditable = False - - def flags(self, index): - flag = qt.QIdentityProxyModel.flags(self, index) - if self.__forceEditable: - flag = flag | qt.Qt.ItemIsEditable - return flag - - def forceCellEditor(self, show): - """ - Enable the editable flag to allow to display cell editor. - """ - if self.__forceEditable == show: - return - self.beginResetModel() - self.__forceEditable = show - self.endResetModel() - - -class RecordTableView(qt.QTableView): - """TableView using DatabaseTableModel as default model. - """ - def __init__(self, parent=None): - """ - Constructor - - :param qt.QWidget parent: parent QWidget - """ - qt.QTableView.__init__(self, parent) - - model = _ShowEditorProxyModel(self) - self._model = RecordTableModel() - model.setSourceModel(self._model) - self.setModel(model) - - self.__multilineView = _MultiLineItem(self) - self.setEditTriggers(qt.QAbstractItemView.AllEditTriggers) - self._copyAction = CopySelectedCellsAction(self) - self.addAction(self._copyAction) - - def copy(self): - self._copyAction.trigger() - - def setArrayData(self, data): - model = self.model() - sourceModel = model.sourceModel() - sourceModel.setArrayData(data) - - if data is not None: - if issubclass(data.dtype.type, (numpy.string_, numpy.unicode_)): - # TODO it would be nice to also fix fields - # but using it only for string array is already very useful - self.setItemDelegateForColumn(0, self.__multilineView) - model.forceCellEditor(True) - else: - self.setItemDelegateForColumn(0, None) - model.forceCellEditor(False) diff --git a/silx/gui/data/TextFormatter.py b/silx/gui/data/TextFormatter.py deleted file mode 100644 index 8fd7c7c..0000000 --- a/silx/gui/data/TextFormatter.py +++ /dev/null @@ -1,395 +0,0 @@ -# coding: utf-8 -# /*########################################################################## -# -# Copyright (c) 2017-2018 European Synchrotron Radiation Facility -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. -# -# ###########################################################################*/ -"""This package provides a class sharred by widget from the -data module to format data as text in the same way.""" - -__authors__ = ["V. Valls"] -__license__ = "MIT" -__date__ = "24/07/2018" - -import logging -import numbers - -import numpy -import six - -from silx.gui import qt - -import h5py - - -_logger = logging.getLogger(__name__) - - -class TextFormatter(qt.QObject): - """Formatter to convert data to string. - - The method :meth:`toString` returns a formatted string from an input data - using parameters set to this object. - - It support most python and numpy data, expecting dictionary. Unsupported - data are displayed using the string representation of the object (`str`). - - It provides a set of parameters to custom the formatting of integer and - float values (:meth:`setIntegerFormat`, :meth:`setFloatFormat`). - - It also allows to custom the use of quotes to display text data - (:meth:`setUseQuoteForText`), and custom unit used to display imaginary - numbers (:meth:`setImaginaryUnit`). - - The object emit an event `formatChanged` every time a parametter is - changed. - """ - - formatChanged = qt.Signal() - """Emitted when properties of the formatter change.""" - - def __init__(self, parent=None, formatter=None): - """ - Constructor - - :param qt.QObject parent: Owner of the object - :param TextFormatter formatter: Instantiate this object from the - formatter - """ - qt.QObject.__init__(self, parent) - if formatter is not None: - self.__integerFormat = formatter.integerFormat() - self.__floatFormat = formatter.floatFormat() - self.__useQuoteForText = formatter.useQuoteForText() - self.__imaginaryUnit = formatter.imaginaryUnit() - self.__enumFormat = formatter.enumFormat() - else: - self.__integerFormat = "%d" - self.__floatFormat = "%g" - self.__useQuoteForText = True - self.__imaginaryUnit = u"j" - self.__enumFormat = u"%(name)s(%(value)d)" - - def integerFormat(self): - """Returns the format string controlling how the integer data - are formated by this object. - - This is the C-style format string used by python when formatting - strings with the modulus operator. - - :rtype: str - """ - return self.__integerFormat - - def setIntegerFormat(self, value): - """Set format string controlling how the integer data are - formated by this object. - - :param str value: Format string (e.g. "%d", "%i", "%08i"). - This is the C-style format string used by python when formatting - strings with the modulus operator. - """ - if self.__integerFormat == value: - return - self.__integerFormat = value - self.formatChanged.emit() - - def floatFormat(self): - """Returns the format string controlling how the floating-point data - are formated by this object. - - This is the C-style format string used by python when formatting - strings with the modulus operator. - - :rtype: str - """ - return self.__floatFormat - - def setFloatFormat(self, value): - """Set format string controlling how the floating-point data are - formated by this object. - - :param str value: Format string (e.g. "%.3f", "%d", "%-10.2f", - "%10.3e"). - This is the C-style format string used by python when formatting - strings with the modulus operator. - """ - if self.__floatFormat == value: - return - self.__floatFormat = value - self.formatChanged.emit() - - def useQuoteForText(self): - """Returns true if the string data are formatted using double quotes. - - Else, no quotes are used. - """ - return self.__integerFormat - - def setUseQuoteForText(self, useQuote): - """Set the use of quotes to delimit string data. - - :param bool useQuote: True to use quotes. - """ - if self.__useQuoteForText == useQuote: - return - self.__useQuoteForText = useQuote - self.formatChanged.emit() - - def imaginaryUnit(self): - """Returns the unit display for imaginary numbers. - - :rtype: str - """ - return self.__imaginaryUnit - - def setImaginaryUnit(self, imaginaryUnit): - """Set the unit display for imaginary numbers. - - :param str imaginaryUnit: Unit displayed after imaginary numbers - """ - if self.__imaginaryUnit == imaginaryUnit: - return - self.__imaginaryUnit = imaginaryUnit - self.formatChanged.emit() - - def setEnumFormat(self, value): - """Set format string controlling how the enum data are - formated by this object. - - :param str value: Format string (e.g. "%(name)s(%(value)d)"). - This is the C-style format string used by python when formatting - strings with the modulus operator. - """ - if self.__enumFormat == value: - return - self.__enumFormat = value - self.formatChanged.emit() - - def enumFormat(self): - """Returns the format string controlling how the enum data - are formated by this object. - - This is the C-style format string used by python when formatting - strings with the modulus operator. - - :rtype: str - """ - return self.__enumFormat - - def __formatText(self, text): - if self.__useQuoteForText: - text = "\"%s\"" % text.replace("\\", "\\\\").replace("\"", "\\\"") - return text - - def __formatBinary(self, data): - if isinstance(data, numpy.void): - if six.PY2: - data = [ord(d) for d in data.data] - else: - data = data.item() - if isinstance(data, numpy.ndarray): - # Before numpy 1.15.0 the item API was returning a numpy array - data = data.astype(numpy.uint8) - else: - # Now it is supposed to be a bytes type - pass - elif six.PY2: - data = [ord(d) for d in data] - # In python3 data is already a bytes array - data = ["\\x%02X" % d for d in data] - if self.__useQuoteForText: - return "b\"%s\"" % "".join(data) - else: - return "".join(data) - - def __formatSafeAscii(self, data): - if six.PY2: - data = [ord(d) for d in data] - data = [chr(d) if (d > 0x20 and d < 0x7F) else "\\x%02X" % d for d in data] - if self.__useQuoteForText: - data = [c if c != '"' else "\\" + c for c in data] - return "b\"%s\"" % "".join(data) - else: - return "".join(data) - - def __formatCharString(self, data): - """Format text of char. - - From the specifications we expect to have ASCII, but we also allow - CP1252 in some ceases as fallback. - - If no encoding fits, it will display a readable ASCII chars, with - escaped chars (using the python syntax) for non decoded characters. - - :param data: A binary string of char expected in ASCII - :rtype: str - """ - try: - text = "%s" % data.decode("ascii") - return self.__formatText(text) - except UnicodeDecodeError: - # Here we can spam errors, this is definitly a badly - # generated file - _logger.error("Invalid ASCII string %s.", data) - if data == b"\xB0": - _logger.error("Fallback using cp1252 encoding") - return self.__formatText(u"\u00B0") - return self.__formatSafeAscii(data) - - def __formatH5pyObject(self, data, dtype): - # That's an HDF5 object - ref = h5py.check_dtype(ref=dtype) - if ref is not None: - if bool(data): - return "REF" - else: - return "NULL_REF" - vlen = h5py.check_dtype(vlen=dtype) - if vlen is not None: - if vlen == six.text_type: - # HDF5 UTF8 - # With h5py>=3 reading dataset returns bytes - if isinstance(data, (bytes, numpy.bytes_)): - try: - data = data.decode("utf-8") - except UnicodeDecodeError: - self.__formatSafeAscii(data) - return self.__formatText(data) - elif vlen == six.binary_type: - # HDF5 ASCII - return self.__formatCharString(data) - elif isinstance(vlen, numpy.dtype): - return self.toString(data, vlen) - return None - - def toString(self, data, dtype=None): - """Format a data into a string using formatter options - - :param object data: Data to render - :param dtype: enforce a dtype (mostly used to remember the h5py dtype, - special h5py dtypes are not propagated from array to items) - :rtype: str - """ - if isinstance(data, tuple): - text = [self.toString(d) for d in data] - return "(" + " ".join(text) + ")" - elif isinstance(data, list): - text = [self.toString(d) for d in data] - return "[" + " ".join(text) + "]" - elif isinstance(data, numpy.ndarray): - if dtype is None: - dtype = data.dtype - if data.shape == (): - # it is a scaler - return self.toString(data[()], dtype) - else: - text = [self.toString(d, dtype) for d in data] - return "[" + " ".join(text) + "]" - if dtype is not None and dtype.kind == 'O': - text = self.__formatH5pyObject(data, dtype) - if text is not None: - return text - elif isinstance(data, numpy.void): - if dtype is None: - dtype = data.dtype - if dtype.fields is not None: - text = [] - for index, field in enumerate(dtype.fields.items()): - text.append(field[0] + ":" + self.toString(data[index], field[1][0])) - return "(" + " ".join(text) + ")" - return self.__formatBinary(data) - elif isinstance(data, (numpy.unicode_, six.text_type)): - return self.__formatText(data) - elif isinstance(data, (numpy.string_, six.binary_type)): - if dtype is None and hasattr(data, "dtype"): - dtype = data.dtype - if dtype is not None: - # Maybe a sub item from HDF5 - if dtype.kind == 'S': - return self.__formatCharString(data) - elif dtype.kind == 'O': - text = self.__formatH5pyObject(data, dtype) - if text is not None: - return text - try: - # Try ascii/utf-8 - text = "%s" % data.decode("utf-8") - return self.__formatText(text) - except UnicodeDecodeError: - pass - return self.__formatBinary(data) - elif isinstance(data, six.string_types): - text = "%s" % data - return self.__formatText(text) - elif isinstance(data, (numpy.integer)): - if dtype is None: - dtype = data.dtype - enumType = h5py.check_dtype(enum=dtype) - if enumType is not None: - for key, value in enumType.items(): - if value == data: - result = {} - result["name"] = key - result["value"] = data - return self.__enumFormat % result - return self.__integerFormat % data - elif isinstance(data, (numbers.Integral)): - return self.__integerFormat % data - elif isinstance(data, (numbers.Real, numpy.floating)): - # It have to be done before complex checking - return self.__floatFormat % data - elif isinstance(data, (numpy.complexfloating, numbers.Complex)): - text = "" - if data.real != 0: - text += self.__floatFormat % data.real - if data.real != 0 and data.imag != 0: - if data.imag < 0: - template = self.__floatFormat + " - " + self.__floatFormat + self.__imaginaryUnit - params = (data.real, -data.imag) - else: - template = self.__floatFormat + " + " + self.__floatFormat + self.__imaginaryUnit - params = (data.real, data.imag) - else: - if data.imag != 0: - template = self.__floatFormat + self.__imaginaryUnit - params = (data.imag) - else: - template = self.__floatFormat - params = (data.real) - return template % params - elif isinstance(data, h5py.h5r.Reference): - dtype = h5py.special_dtype(ref=h5py.Reference) - text = self.__formatH5pyObject(data, dtype) - return text - elif isinstance(data, h5py.h5r.RegionReference): - dtype = h5py.special_dtype(ref=h5py.RegionReference) - text = self.__formatH5pyObject(data, dtype) - return text - elif isinstance(data, numpy.object_) or dtype is not None: - if dtype is None: - dtype = data.dtype - text = self.__formatH5pyObject(data, dtype) - if text is not None: - return text - # That's a numpy object - return str(data) - return str(data) diff --git a/silx/gui/data/_RecordPlot.py b/silx/gui/data/_RecordPlot.py deleted file mode 100644 index 5be792f..0000000 --- a/silx/gui/data/_RecordPlot.py +++ /dev/null @@ -1,92 +0,0 @@ -from silx.gui.plot.PlotWindow import PlotWindow -from silx.gui.plot.PlotWidget import PlotWidget -from .. import qt - - -class RecordPlot(PlotWindow): - def __init__(self, parent=None, backend=None): - super(RecordPlot, self).__init__(parent=parent, backend=backend, - resetzoom=True, autoScale=True, - logScale=True, grid=True, - curveStyle=True, colormap=False, - aspectRatio=False, yInverted=False, - copy=True, save=True, print_=True, - control=True, position=True, - roi=True, mask=False, fit=True) - if parent is None: - self.setWindowTitle('RecordPlot') - self._axesSelectionToolBar = AxesSelectionToolBar(parent=self, plot=self) - self.addToolBar(qt.Qt.BottomToolBarArea, self._axesSelectionToolBar) - - def setXAxisFieldName(self, value): - """Set the current selected field for the X axis. - - :param Union[str,None] value: - """ - label = '' if value is None else value - index = self._axesSelectionToolBar.getXAxisDropDown().findData(value) - - if index >= 0: - self.getXAxis().setLabel(label) - self._axesSelectionToolBar.getXAxisDropDown().setCurrentIndex(index) - - def getXAxisFieldName(self): - """Returns currently selected field for the X axis or None. - - rtype: Union[str,None] - """ - return self._axesSelectionToolBar.getXAxisDropDown().currentData() - - def setYAxisFieldName(self, value): - self.getYAxis().setLabel(value) - index = self._axesSelectionToolBar.getYAxisDropDown().findText(value) - if index >= 0: - self._axesSelectionToolBar.getYAxisDropDown().setCurrentIndex(index) - - def getYAxisFieldName(self): - return self._axesSelectionToolBar.getYAxisDropDown().currentText() - - def setSelectableXAxisFieldNames(self, fieldNames): - """Add list of field names to X axis - - :param List[str] fieldNames: - """ - comboBox = self._axesSelectionToolBar.getXAxisDropDown() - comboBox.clear() - comboBox.addItem('-', None) - comboBox.insertSeparator(1) - for name in fieldNames: - comboBox.addItem(name, name) - - def setSelectableYAxisFieldNames(self, fieldNames): - self._axesSelectionToolBar.getYAxisDropDown().clear() - self._axesSelectionToolBar.getYAxisDropDown().addItems(fieldNames) - - def getAxesSelectionToolBar(self): - return self._axesSelectionToolBar - -class AxesSelectionToolBar(qt.QToolBar): - def __init__(self, parent=None, plot=None, title='Plot Axes Selection'): - super(AxesSelectionToolBar, self).__init__(title, parent) - - assert isinstance(plot, PlotWidget) - - self.addWidget(qt.QLabel("Field selection: ")) - - self._labelXAxis = qt.QLabel(" X: ") - self.addWidget(self._labelXAxis) - - self._selectXAxisDropDown = qt.QComboBox() - self.addWidget(self._selectXAxisDropDown) - - self._labelYAxis = qt.QLabel(" Y: ") - self.addWidget(self._labelYAxis) - - self._selectYAxisDropDown = qt.QComboBox() - self.addWidget(self._selectYAxisDropDown) - - def getXAxisDropDown(self): - return self._selectXAxisDropDown - - def getYAxisDropDown(self): - return self._selectYAxisDropDown
\ No newline at end of file diff --git a/silx/gui/data/_VolumeWindow.py b/silx/gui/data/_VolumeWindow.py deleted file mode 100644 index 03b6876..0000000 --- a/silx/gui/data/_VolumeWindow.py +++ /dev/null @@ -1,148 +0,0 @@ -# coding: utf-8 -# /*########################################################################## -# -# Copyright (c) 2019 European Synchrotron Radiation Facility -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. -# -# ###########################################################################*/ -"""This module provides a widget to visualize 3D arrays""" - -__authors__ = ["T. Vincent"] -__license__ = "MIT" -__date__ = "22/03/2019" - - -import numpy - -from .. import qt -from ..plot3d.SceneWindow import SceneWindow -from ..plot3d.items import ScalarField3D, ComplexField3D, ItemChangedType - - -class VolumeWindow(SceneWindow): - """Extends SceneWindow with a convenient API for 3D array - - :param QWidget: parent - """ - - def __init__(self, parent): - super(VolumeWindow, self).__init__(parent) - self.__firstData = True - # Hide global parameter dock - self.getGroupResetWidget().parent().setVisible(False) - - def setAxesLabels(self, xlabel=None, ylabel=None, zlabel=None): - """Set the text labels of the axes. - - :param Union[str,None] xlabel: Label of the X axis - :param Union[str,None] ylabel: Label of the Y axis - :param Union[str,None] zlabel: Label of the Z axis - """ - sceneWidget = self.getSceneWidget() - sceneWidget.getSceneGroup().setAxesLabels( - 'X' if xlabel is None else xlabel, - 'Y' if ylabel is None else ylabel, - 'Z' if zlabel is None else zlabel) - - def clear(self): - """Clear any currently displayed data""" - sceneWidget = self.getSceneWidget() - items = sceneWidget.getItems() - if (len(items) == 1 and - isinstance(items[0], (ScalarField3D, ComplexField3D))): - items[0].setData(None) - else: # Safety net - sceneWidget.clearItems() - - @staticmethod - def __computeIsolevel(data): - """Returns a suitable isolevel value for data - - :param numpy.ndarray data: - :rtype: float - """ - data = data[numpy.isfinite(data)] - if len(data) == 0: - return 0 - else: - return numpy.mean(data) + numpy.std(data) - - def setData(self, data, offset=(0., 0., 0.), scale=(1., 1., 1.)): - """Set the 3D array data to display. - - :param numpy.ndarray data: 3D array of float or complex - :param List[float] offset: (tx, ty, tz) coordinates of the origin - :param List[float] scale: (sx, sy, sz) scale for each dimension - """ - sceneWidget = self.getSceneWidget() - dataMaxCoords = numpy.array(list(reversed(data.shape))) - 1 - - previousItems = sceneWidget.getItems() - if (len(previousItems) == 1 and - isinstance(previousItems[0], (ScalarField3D, ComplexField3D)) and - numpy.iscomplexobj(data) == isinstance(previousItems[0], ComplexField3D)): - # Reuse existing volume item - volume = sceneWidget.getItems()[0] - volume.setData(data, copy=False) - # Make sure the plane goes through the dataset - for plane in volume.getCutPlanes(): - point = numpy.array(plane.getPoint()) - if numpy.any(point < (0, 0, 0)) or numpy.any(point > dataMaxCoords): - plane.setPoint(dataMaxCoords // 2) - else: - # Add a new volume - sceneWidget.clearItems() - volume = sceneWidget.addVolume(data, copy=False) - volume.setLabel('Volume') - for plane in volume.getCutPlanes(): - # Make plane going through the center of the data - plane.setPoint(dataMaxCoords // 2) - plane.setVisible(False) - plane.sigItemChanged.connect(self.__cutPlaneUpdated) - volume.addIsosurface(self.__computeIsolevel, '#FF0000FF') - - # Expand the parameter tree - model = self.getParamTreeView().model() - index = qt.QModelIndex() # Invalid index for top level - while 1: - rowCount = model.rowCount(parent=index) - if rowCount == 0: - break - index = model.index(rowCount - 1, 0, parent=index) - self.getParamTreeView().setExpanded(index, True) - if not index.isValid(): - break - - volume.setTranslation(*offset) - volume.setScale(*scale) - - if self.__firstData: # Only center for first dataset - self.__firstData = False - sceneWidget.centerScene() - - def __cutPlaneUpdated(self, event): - """Handle the change of visibility of the cut plane - - :param event: Kind of update - """ - if event == ItemChangedType.VISIBLE: - plane = self.sender() - if plane.isVisible(): - self.getSceneWidget().selection().setCurrentItem(plane) diff --git a/silx/gui/data/__init__.py b/silx/gui/data/__init__.py deleted file mode 100644 index 560062d..0000000 --- a/silx/gui/data/__init__.py +++ /dev/null @@ -1,35 +0,0 @@ -# coding: utf-8 -# /*########################################################################## -# -# Copyright (c) 2016-2017 European Synchrotron Radiation Facility -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. -# -# ###########################################################################*/ -"""This package provides a set of Qt widgets for displaying data arrays using -table views and plot widgets. - -.. note:: - - Widgets in this package may rely on additional dependencies that are - not mandatory for *silx*. - :class:`DataViewer.DataViewer` relies on :mod:`silx.gui.plot` which - depends on *matplotlib*. It also optionally depends on *PyOpenGL* for 3D - visualization. -""" diff --git a/silx/gui/data/setup.py b/silx/gui/data/setup.py deleted file mode 100644 index 23ccbdd..0000000 --- a/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/silx/gui/data/test/__init__.py b/silx/gui/data/test/__init__.py deleted file mode 100644 index 08c044b..0000000 --- a/silx/gui/data/test/__init__.py +++ /dev/null @@ -1,45 +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. -# -# ###########################################################################*/ -import unittest - -from . import test_arraywidget -from . import test_numpyaxesselector -from . import test_dataviewer -from . import test_textformatter - -__authors__ = ["V. Valls", "P. Knobel"] -__license__ = "MIT" -__date__ = "24/01/2017" - - -def suite(): - test_suite = unittest.TestSuite() - test_suite.addTests( - [test_arraywidget.suite(), - test_numpyaxesselector.suite(), - test_dataviewer.suite(), - test_textformatter.suite(), - ]) - return test_suite diff --git a/silx/gui/data/test/test_arraywidget.py b/silx/gui/data/test/test_arraywidget.py deleted file mode 100644 index 87081ed..0000000 --- a/silx/gui/data/test/test_arraywidget.py +++ /dev/null @@ -1,329 +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__ = ["P. Knobel"] -__license__ = "MIT" -__date__ = "05/12/2016" - -import os -import tempfile -import unittest - -import numpy - -from silx.gui import qt -from silx.gui.data import ArrayTableWidget -from silx.gui.data.ArrayTableModel import ArrayTableModel -from silx.gui.utils.testutils import TestCaseQt - -import h5py - - -class TestArrayWidget(TestCaseQt): - """Basic test for ArrayTableWidget with a numpy array""" - def setUp(self): - super(TestArrayWidget, self).setUp() - self.aw = ArrayTableWidget.ArrayTableWidget() - - def tearDown(self): - del self.aw - super(TestArrayWidget, self).tearDown() - - def testShow(self): - """test for errors""" - self.aw.show() - self.qWaitForWindowExposed(self.aw) - - def testSetData0D(self): - a = 1 - self.aw.setArrayData(a) - b = self.aw.getData(copy=True) - - self.assertTrue(numpy.array_equal(a, b)) - - # scalar/0D data has no frame index - self.assertEqual(len(self.aw.model._index), 0) - # and no perspective - self.assertEqual(len(self.aw.model._perspective), 0) - - def testSetData1D(self): - a = [1, 2] - self.aw.setArrayData(a) - b = self.aw.getData(copy=True) - - self.assertTrue(numpy.array_equal(a, b)) - - # 1D data has no frame index - self.assertEqual(len(self.aw.model._index), 0) - # and no perspective - self.assertEqual(len(self.aw.model._perspective), 0) - - def testSetData4D(self): - a = numpy.reshape(numpy.linspace(0.213, 1.234, 1250), - (5, 5, 5, 10)) - self.aw.setArrayData(a) - - # default perspective (0, 1) - self.assertEqual(list(self.aw.model._perspective), - [0, 1]) - self.aw.setPerspective((1, 3)) - self.assertEqual(list(self.aw.model._perspective), - [1, 3]) - - b = self.aw.getData(copy=True) - self.assertTrue(numpy.array_equal(a, b)) - - # 4D data has a 2-tuple as frame index - self.assertEqual(len(self.aw.model._index), 2) - # default index is (0, 0) - self.assertEqual(list(self.aw.model._index), - [0, 0]) - self.aw.setFrameIndex((3, 1)) - - self.assertEqual(list(self.aw.model._index), - [3, 1]) - - def testColors(self): - a = numpy.arange(256, dtype=numpy.uint8) - self.aw.setArrayData(a) - - bgcolor = numpy.empty(a.shape + (3,), dtype=numpy.uint8) - # Black & white palette - bgcolor[..., 0] = a - bgcolor[..., 1] = a - bgcolor[..., 2] = a - - fgcolor = numpy.bitwise_xor(bgcolor, 255) - - self.aw.setArrayColors(bgcolor, fgcolor) - - # test colors are as expected in model - for i in range(256): - # all RGB channels for BG equal to data value - self.assertEqual( - self.aw.model.data(self.aw.model.index(0, i), - role=qt.Qt.BackgroundRole), - qt.QColor(i, i, i), - "Unexpected background color" - ) - - # all RGB channels for FG equal to XOR(data value, 255) - self.assertEqual( - self.aw.model.data(self.aw.model.index(0, i), - role=qt.Qt.ForegroundRole), - qt.QColor(i ^ 255, i ^ 255, i ^ 255), - "Unexpected text color" - ) - - # test colors are reset to None when a new data array is loaded - # with different shape - self.aw.setArrayData(numpy.arange(300)) - - for i in range(300): - # all RGB channels for BG equal to data value - self.assertIsNone( - self.aw.model.data(self.aw.model.index(0, i), - role=qt.Qt.BackgroundRole)) - - def testDefaultFlagNotEditable(self): - """editable should be False by default, in setArrayData""" - self.aw.setArrayData([[0]]) - idx = self.aw.model.createIndex(0, 0) - # model is editable - self.assertFalse( - self.aw.model.flags(idx) & qt.Qt.ItemIsEditable) - - def testFlagEditable(self): - self.aw.setArrayData([[0]], editable=True) - idx = self.aw.model.createIndex(0, 0) - # model is editable - self.assertTrue( - self.aw.model.flags(idx) & qt.Qt.ItemIsEditable) - - def testFlagNotEditable(self): - self.aw.setArrayData([[0]], editable=False) - idx = self.aw.model.createIndex(0, 0) - # model is editable - self.assertFalse( - self.aw.model.flags(idx) & qt.Qt.ItemIsEditable) - - def testReferenceReturned(self): - """when setting the data with copy=False and - retrieving it with getData(copy=False), we should recover - the same original object. - """ - # n-D (n >=2) - a0 = numpy.reshape(numpy.linspace(0.213, 1.234, 1000), - (10, 10, 10)) - self.aw.setArrayData(a0, copy=False) - a1 = self.aw.getData(copy=False) - - self.assertIs(a0, a1) - - # 1D - b0 = numpy.linspace(0.213, 1.234, 1000) - self.aw.setArrayData(b0, copy=False) - b1 = self.aw.getData(copy=False) - self.assertIs(b0, b1) - - def testClipping(self): - """Test clipping of large arrays""" - self.aw.show() - self.qWaitForWindowExposed(self.aw) - - data = numpy.arange(ArrayTableModel.MAX_NUMBER_OF_SECTIONS + 10) - - for shape in [(1, -1), (-1, 1)]: - with self.subTest(shape=shape): - self.aw.setArrayData(data.reshape(shape), editable=True) - self.qapp.processEvents() - - -class TestH5pyArrayWidget(TestCaseQt): - """Basic test for ArrayTableWidget with a dataset. - - Test flags, for dataset open in read-only or read-write modes""" - def setUp(self): - super(TestH5pyArrayWidget, self).setUp() - self.aw = ArrayTableWidget.ArrayTableWidget() - self.data = numpy.reshape(numpy.linspace(0.213, 1.234, 1000), - (10, 10, 10)) - # create an h5py file with a dataset - self.tempdir = tempfile.mkdtemp() - self.h5_fname = os.path.join(self.tempdir, "array.h5") - h5f = h5py.File(self.h5_fname, mode='w') - h5f["my_array"] = self.data - h5f["my_scalar"] = 3.14 - h5f["my_1D_array"] = numpy.array(numpy.arange(1000)) - h5f.close() - - def tearDown(self): - del self.aw - os.unlink(self.h5_fname) - os.rmdir(self.tempdir) - super(TestH5pyArrayWidget, self).tearDown() - - def testShow(self): - self.aw.show() - self.qWaitForWindowExposed(self.aw) - - def testReadOnly(self): - """Open H5 dataset in read-only mode, ensure the model is not editable.""" - h5f = h5py.File(self.h5_fname, "r") - a = h5f["my_array"] - # ArrayTableModel relies on following condition - self.assertTrue(a.file.mode == "r") - - self.aw.setArrayData(a, copy=False, editable=True) - - self.assertIsInstance(a, h5py.Dataset) # simple sanity check - # internal representation must be a reference to original data (copy=False) - self.assertIsInstance(self.aw.model._array, h5py.Dataset) - self.assertTrue(self.aw.model._array.file.mode == "r") - - b = self.aw.getData() - self.assertTrue(numpy.array_equal(self.data, b)) - - # model must have detected read-only dataset and disabled editing - self.assertFalse(self.aw.model._editable) - idx = self.aw.model.createIndex(0, 0) - self.assertFalse( - self.aw.model.flags(idx) & qt.Qt.ItemIsEditable) - - # force editing read-only datasets raises IOError - self.assertRaises(IOError, self.aw.model.setData, - idx, 123.4, role=qt.Qt.EditRole) - h5f.close() - - def testReadWrite(self): - h5f = h5py.File(self.h5_fname, "r+") - a = h5f["my_array"] - self.assertTrue(a.file.mode == "r+") - - self.aw.setArrayData(a, copy=False, editable=True) - b = self.aw.getData(copy=False) - self.assertTrue(numpy.array_equal(self.data, b)) - - idx = self.aw.model.createIndex(0, 0) - # model is editable - self.assertTrue( - self.aw.model.flags(idx) & qt.Qt.ItemIsEditable) - h5f.close() - - def testSetData0D(self): - h5f = h5py.File(self.h5_fname, "r+") - a = h5f["my_scalar"] - self.aw.setArrayData(a) - b = self.aw.getData(copy=True) - - self.assertTrue(numpy.array_equal(a, b)) - - h5f.close() - - def testSetData1D(self): - h5f = h5py.File(self.h5_fname, "r+") - a = h5f["my_1D_array"] - self.aw.setArrayData(a) - b = self.aw.getData(copy=True) - - self.assertTrue(numpy.array_equal(a, b)) - - h5f.close() - - def testReferenceReturned(self): - """when setting the data with copy=False and - retrieving it with getData(copy=False), we should recover - the same original object. - - This only works for array with at least 2D. For 1D and 0D - arrays, a view is created at some point, which in the case - of an hdf5 dataset creates a copy.""" - h5f = h5py.File(self.h5_fname, "r+") - - # n-D - a0 = h5f["my_array"] - self.aw.setArrayData(a0, copy=False) - a1 = self.aw.getData(copy=False) - self.assertIs(a0, a1) - - # 1D - b0 = h5f["my_1D_array"] - self.aw.setArrayData(b0, copy=False) - b1 = self.aw.getData(copy=False) - self.assertIs(b0, b1) - - h5f.close() - - -def suite(): - test_suite = unittest.TestSuite() - test_suite.addTest( - unittest.defaultTestLoader.loadTestsFromTestCase(TestArrayWidget)) - test_suite.addTest( - unittest.defaultTestLoader.loadTestsFromTestCase(TestH5pyArrayWidget)) - return test_suite - - -if __name__ == '__main__': - unittest.main(defaultTest='suite') diff --git a/silx/gui/data/test/test_dataviewer.py b/silx/gui/data/test/test_dataviewer.py deleted file mode 100644 index dd01dd6..0000000 --- a/silx/gui/data/test/test_dataviewer.py +++ /dev/null @@ -1,314 +0,0 @@ -# coding: utf-8 -# /*########################################################################## -# -# Copyright (c) 2016-2020 European Synchrotron Radiation Facility -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. -# -# ###########################################################################*/ -__authors__ = ["V. Valls"] -__license__ = "MIT" -__date__ = "19/02/2019" - -import os -import tempfile -import unittest -from contextlib import contextmanager - -import numpy -from ..DataViewer import DataViewer -from ..DataViews import DataView -from .. import DataViews - -from silx.gui import qt - -from silx.gui.data.DataViewerFrame import DataViewerFrame -from silx.gui.utils.testutils import SignalListener -from silx.gui.utils.testutils import TestCaseQt - -import h5py - - -class _DataViewMock(DataView): - """Dummy view to display nothing""" - - def __init__(self, parent): - DataView.__init__(self, parent) - - def axesNames(self, data, info): - return [] - - def createWidget(self, parent): - return qt.QLabel(parent) - - def getDataPriority(self, data, info): - return 0 - - -class AbstractDataViewerTests(TestCaseQt): - - def create_widget(self): - # Avoid to raise an error when testing the full module - self.skipTest("Not implemented") - - @contextmanager - def h5_temporary_file(self): - # create tmp file - fd, tmp_name = tempfile.mkstemp(suffix=".h5") - os.close(fd) - data = numpy.arange(3 * 3 * 3) - data.shape = 3, 3, 3 - # create h5 data - h5file = h5py.File(tmp_name, "w") - h5file["data"] = data - yield h5file - # clean up - h5file.close() - os.unlink(tmp_name) - - def test_text_data(self): - data_list = ["aaa", int, 8, self] - widget = self.create_widget() - for data in data_list: - widget.setData(data) - self.assertEqual(DataViews.RAW_MODE, widget.displayMode()) - - def test_plot_1d_data(self): - data = numpy.arange(3 ** 1) - data.shape = [3] * 1 - widget = self.create_widget() - widget.setData(data) - availableModes = set([v.modeId() for v in widget.currentAvailableViews()]) - self.assertEqual(DataViews.RAW_MODE, widget.displayMode()) - self.assertIn(DataViews.PLOT1D_MODE, availableModes) - - def test_image_data(self): - data = numpy.arange(3 ** 2) - data.shape = [3] * 2 - widget = self.create_widget() - widget.setData(data) - availableModes = set([v.modeId() for v in widget.currentAvailableViews()]) - self.assertEqual(DataViews.RAW_MODE, widget.displayMode()) - self.assertIn(DataViews.IMAGE_MODE, availableModes) - - def test_image_bool(self): - data = numpy.zeros((10, 10), dtype=bool) - data[::2, ::2] = True - widget = self.create_widget() - widget.setData(data) - availableModes = set([v.modeId() for v in widget.currentAvailableViews()]) - self.assertEqual(DataViews.RAW_MODE, widget.displayMode()) - self.assertIn(DataViews.IMAGE_MODE, availableModes) - - def test_image_complex_data(self): - data = numpy.arange(3 ** 2, dtype=numpy.complex64) - data.shape = [3] * 2 - widget = self.create_widget() - widget.setData(data) - availableModes = set([v.modeId() for v in widget.currentAvailableViews()]) - self.assertEqual(DataViews.RAW_MODE, widget.displayMode()) - self.assertIn(DataViews.IMAGE_MODE, availableModes) - - def test_plot_3d_data(self): - data = numpy.arange(3 ** 3) - data.shape = [3] * 3 - widget = self.create_widget() - widget.setData(data) - availableModes = set([v.modeId() for v in widget.currentAvailableViews()]) - try: - import silx.gui.plot3d # noqa - self.assertIn(DataViews.PLOT3D_MODE, availableModes) - except ImportError: - self.assertIn(DataViews.STACK_MODE, availableModes) - self.assertEqual(DataViews.RAW_MODE, widget.displayMode()) - - def test_array_1d_data(self): - data = numpy.array(["aaa"] * (3 ** 1)) - data.shape = [3] * 1 - widget = self.create_widget() - widget.setData(data) - self.assertEqual(DataViews.RAW_MODE, widget.displayedView().modeId()) - - def test_array_2d_data(self): - data = numpy.array(["aaa"] * (3 ** 2)) - data.shape = [3] * 2 - widget = self.create_widget() - widget.setData(data) - self.assertEqual(DataViews.RAW_MODE, widget.displayedView().modeId()) - - def test_array_4d_data(self): - data = numpy.array(["aaa"] * (3 ** 4)) - data.shape = [3] * 4 - widget = self.create_widget() - widget.setData(data) - self.assertEqual(DataViews.RAW_MODE, widget.displayedView().modeId()) - - def test_record_4d_data(self): - data = numpy.zeros(3 ** 4, dtype='3int8, float32, (2,3)float64') - data.shape = [3] * 4 - widget = self.create_widget() - widget.setData(data) - self.assertEqual(DataViews.RAW_MODE, widget.displayedView().modeId()) - - def test_3d_h5_dataset(self): - with self.h5_temporary_file() as h5file: - dataset = h5file["data"] - widget = self.create_widget() - widget.setData(dataset) - - def test_data_event(self): - listener = SignalListener() - widget = self.create_widget() - widget.dataChanged.connect(listener) - widget.setData(10) - widget.setData(None) - self.assertEqual(listener.callCount(), 2) - - def test_display_mode_event(self): - listener = SignalListener() - widget = self.create_widget() - widget.displayedViewChanged.connect(listener) - widget.setData(10) - widget.setData(None) - modes = [v.modeId() for v in listener.arguments(argumentIndex=0)] - self.assertEqual(modes, [DataViews.RAW_MODE, DataViews.EMPTY_MODE]) - listener.clear() - - def test_change_display_mode(self): - data = numpy.arange(10 ** 4) - data.shape = [10] * 4 - widget = self.create_widget() - widget.setData(data) - widget.setDisplayMode(DataViews.PLOT1D_MODE) - self.assertEqual(widget.displayedView().modeId(), DataViews.PLOT1D_MODE) - widget.setDisplayMode(DataViews.IMAGE_MODE) - self.assertEqual(widget.displayedView().modeId(), DataViews.IMAGE_MODE) - widget.setDisplayMode(DataViews.RAW_MODE) - self.assertEqual(widget.displayedView().modeId(), DataViews.RAW_MODE) - widget.setDisplayMode(DataViews.EMPTY_MODE) - self.assertEqual(widget.displayedView().modeId(), DataViews.EMPTY_MODE) - - def test_create_default_views(self): - widget = self.create_widget() - views = widget.createDefaultViews() - self.assertTrue(len(views) > 0) - - def test_add_view(self): - widget = self.create_widget() - view = _DataViewMock(widget) - widget.addView(view) - self.assertTrue(view in widget.availableViews()) - self.assertTrue(view in widget.currentAvailableViews()) - - def test_remove_view(self): - widget = self.create_widget() - widget.setData("foobar") - view = widget.currentAvailableViews()[0] - widget.removeView(view) - self.assertTrue(view not in widget.availableViews()) - self.assertTrue(view not in widget.currentAvailableViews()) - - def test_replace_view(self): - widget = self.create_widget() - view = _DataViewMock(widget) - widget.replaceView(DataViews.RAW_MODE, - view) - self.assertIsNone(widget.getViewFromModeId(DataViews.RAW_MODE)) - self.assertTrue(view in widget.availableViews()) - self.assertTrue(view in widget.currentAvailableViews()) - - def test_replace_view_in_composite(self): - # replace a view that is a child of a composite view - widget = self.create_widget() - view = _DataViewMock(widget) - replaced = widget.replaceView(DataViews.NXDATA_INVALID_MODE, - view) - self.assertTrue(replaced) - nxdata_view = widget.getViewFromModeId(DataViews.NXDATA_MODE) - self.assertNotIn(DataViews.NXDATA_INVALID_MODE, - [v.modeId() for v in nxdata_view.getViews()]) - self.assertTrue(view in nxdata_view.getViews()) - - -class TestDataViewer(AbstractDataViewerTests): - def create_widget(self): - return DataViewer() - - -class TestDataViewerFrame(AbstractDataViewerTests): - def create_widget(self): - return DataViewerFrame() - - -class TestDataView(TestCaseQt): - - def createComplexData(self): - line = [1, 2j, 3 + 3j, 4] - image = [line, line, line, line] - cube = [image, image, image, image] - data = numpy.array(cube, dtype=numpy.complex64) - return data - - def createDataViewWithData(self, dataViewClass, data): - viewer = dataViewClass(None) - widget = viewer.getWidget() - viewer.setData(data) - return widget - - def testCurveWithComplex(self): - data = self.createComplexData() - dataViewClass = DataViews._Plot1dView - widget = self.createDataViewWithData(dataViewClass, data[0, 0]) - self.qWaitForWindowExposed(widget) - - def testImageWithComplex(self): - data = self.createComplexData() - dataViewClass = DataViews._Plot2dView - widget = self.createDataViewWithData(dataViewClass, data[0]) - self.qWaitForWindowExposed(widget) - - def testCubeWithComplex(self): - self.skipTest("OpenGL widget not yet tested") - try: - import silx.gui.plot3d # noqa - except ImportError: - self.skipTest("OpenGL not available") - data = self.createComplexData() - dataViewClass = DataViews._Plot3dView - widget = self.createDataViewWithData(dataViewClass, data) - self.qWaitForWindowExposed(widget) - - def testImageStackWithComplex(self): - data = self.createComplexData() - dataViewClass = DataViews._StackView - widget = self.createDataViewWithData(dataViewClass, data) - self.qWaitForWindowExposed(widget) - - -def suite(): - test_suite = unittest.TestSuite() - loadTestsFromTestCase = unittest.defaultTestLoader.loadTestsFromTestCase - test_suite.addTest(loadTestsFromTestCase(TestDataViewer)) - test_suite.addTest(loadTestsFromTestCase(TestDataViewerFrame)) - test_suite.addTest(loadTestsFromTestCase(TestDataView)) - return test_suite - - -if __name__ == '__main__': - unittest.main(defaultTest='suite') diff --git a/silx/gui/data/test/test_numpyaxesselector.py b/silx/gui/data/test/test_numpyaxesselector.py deleted file mode 100644 index d37cff7..0000000 --- a/silx/gui/data/test/test_numpyaxesselector.py +++ /dev/null @@ -1,161 +0,0 @@ -# coding: utf-8 -# /*########################################################################## -# -# Copyright (c) 2016-2019 European Synchrotron Radiation Facility -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. -# -# ###########################################################################*/ -__authors__ = ["V. Valls"] -__license__ = "MIT" -__date__ = "29/01/2018" - -import os -import tempfile -import unittest -from contextlib import contextmanager - -import numpy - -from silx.gui.data.NumpyAxesSelector import NumpyAxesSelector -from silx.gui.utils.testutils import SignalListener -from silx.gui.utils.testutils import TestCaseQt - -import h5py - - -class TestNumpyAxesSelector(TestCaseQt): - - def test_creation(self): - data = numpy.arange(3 * 3 * 3) - data.shape = 3, 3, 3 - widget = NumpyAxesSelector() - widget.setVisible(True) - - def test_none(self): - data = numpy.arange(3 * 3 * 3) - widget = NumpyAxesSelector() - widget.setData(data) - widget.setData(None) - result = widget.selectedData() - self.assertIsNone(result) - - def test_output_samedim(self): - data = numpy.arange(3 * 3 * 3) - data.shape = 3, 3, 3 - expectedResult = data - - widget = NumpyAxesSelector() - widget.setAxisNames(["x", "y", "z"]) - widget.setData(data) - result = widget.selectedData() - self.assertTrue(numpy.array_equal(result, expectedResult)) - - def test_output_moredim(self): - data = numpy.arange(3 * 3 * 3 * 3) - data.shape = 3, 3, 3, 3 - expectedResult = data - - widget = NumpyAxesSelector() - widget.setAxisNames(["x", "y", "z", "boum"]) - widget.setData(data[0]) - result = widget.selectedData() - self.assertIsNone(result) - widget.setData(data) - result = widget.selectedData() - self.assertTrue(numpy.array_equal(result, expectedResult)) - - def test_output_lessdim(self): - data = numpy.arange(3 * 3 * 3) - data.shape = 3, 3, 3 - expectedResult = data[0] - - widget = NumpyAxesSelector() - widget.setAxisNames(["y", "x"]) - widget.setData(data) - result = widget.selectedData() - self.assertTrue(numpy.array_equal(result, expectedResult)) - - def test_output_1dim(self): - data = numpy.arange(3 * 3 * 3) - data.shape = 3, 3, 3 - expectedResult = data[0, 0, 0] - - widget = NumpyAxesSelector() - widget.setData(data) - result = widget.selectedData() - self.assertTrue(numpy.array_equal(result, expectedResult)) - - @contextmanager - def h5_temporary_file(self): - # create tmp file - fd, tmp_name = tempfile.mkstemp(suffix=".h5") - os.close(fd) - data = numpy.arange(3 * 3 * 3) - data.shape = 3, 3, 3 - # create h5 data - h5file = h5py.File(tmp_name, "w") - h5file["data"] = data - yield h5file - # clean up - h5file.close() - os.unlink(tmp_name) - - def test_h5py_dataset(self): - with self.h5_temporary_file() as h5file: - dataset = h5file["data"] - expectedResult = dataset[0] - - widget = NumpyAxesSelector() - widget.setData(dataset) - widget.setAxisNames(["y", "x"]) - result = widget.selectedData() - self.assertTrue(numpy.array_equal(result, expectedResult)) - - def test_data_event(self): - data = numpy.arange(3 * 3 * 3) - widget = NumpyAxesSelector() - listener = SignalListener() - widget.dataChanged.connect(listener) - widget.setData(data) - widget.setData(None) - self.assertEqual(listener.callCount(), 2) - - def test_selected_data_event(self): - data = numpy.arange(3 * 3 * 3) - data.shape = 3, 3, 3 - widget = NumpyAxesSelector() - listener = SignalListener() - widget.selectionChanged.connect(listener) - widget.setData(data) - widget.setAxisNames(["x"]) - widget.setData(None) - self.assertEqual(listener.callCount(), 3) - listener.clear() - - -def suite(): - test_suite = unittest.TestSuite() - test_suite.addTest( - unittest.defaultTestLoader.loadTestsFromTestCase(TestNumpyAxesSelector)) - return test_suite - - -if __name__ == '__main__': - unittest.main(defaultTest='suite') diff --git a/silx/gui/data/test/test_textformatter.py b/silx/gui/data/test/test_textformatter.py deleted file mode 100644 index d3050bf..0000000 --- a/silx/gui/data/test/test_textformatter.py +++ /dev/null @@ -1,212 +0,0 @@ -# coding: utf-8 -# /*########################################################################## -# -# Copyright (c) 2016-2019 European Synchrotron Radiation Facility -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. -# -# ###########################################################################*/ -__authors__ = ["V. Valls"] -__license__ = "MIT" -__date__ = "12/12/2017" - -import unittest -import shutil -import tempfile - -import numpy -import six - -from silx.gui.utils.testutils import TestCaseQt -from silx.gui.utils.testutils import SignalListener -from ..TextFormatter import TextFormatter -from silx.io.utils import h5py_read_dataset - -import h5py - - -class TestTextFormatter(TestCaseQt): - - def test_copy(self): - formatter = TextFormatter() - copy = TextFormatter(formatter=formatter) - self.assertIsNot(formatter, copy) - copy.setFloatFormat("%.3f") - self.assertEqual(formatter.integerFormat(), copy.integerFormat()) - self.assertNotEqual(formatter.floatFormat(), copy.floatFormat()) - self.assertEqual(formatter.useQuoteForText(), copy.useQuoteForText()) - self.assertEqual(formatter.imaginaryUnit(), copy.imaginaryUnit()) - - def test_event(self): - listener = SignalListener() - formatter = TextFormatter() - formatter.formatChanged.connect(listener) - formatter.setFloatFormat("%.3f") - formatter.setIntegerFormat("%03i") - formatter.setUseQuoteForText(False) - formatter.setImaginaryUnit("z") - self.assertEqual(listener.callCount(), 4) - - def test_int(self): - formatter = TextFormatter() - formatter.setIntegerFormat("%05i") - result = formatter.toString(512) - self.assertEqual(result, "00512") - - def test_float(self): - formatter = TextFormatter() - formatter.setFloatFormat("%.3f") - result = formatter.toString(1.3) - self.assertEqual(result, "1.300") - - def test_complex(self): - formatter = TextFormatter() - formatter.setFloatFormat("%.1f") - formatter.setImaginaryUnit("i") - result = formatter.toString(1.0 + 5j) - result = result.replace(" ", "") - self.assertEqual(result, "1.0+5.0i") - - def test_string(self): - formatter = TextFormatter() - formatter.setIntegerFormat("%.1f") - formatter.setImaginaryUnit("z") - result = formatter.toString("toto") - self.assertEqual(result, '"toto"') - - def test_numpy_void(self): - formatter = TextFormatter() - result = formatter.toString(numpy.void(b"\xFF")) - self.assertEqual(result, 'b"\\xFF"') - - def test_char_cp1252(self): - # degree character in cp1252 - formatter = TextFormatter() - result = formatter.toString(numpy.bytes_(b"\xB0")) - self.assertEqual(result, u'"\u00B0"') - - -class TestTextFormatterWithH5py(TestCaseQt): - - @classmethod - def setUpClass(cls): - super(TestTextFormatterWithH5py, cls).setUpClass() - - cls.tmpDirectory = tempfile.mkdtemp() - cls.h5File = h5py.File("%s/formatter.h5" % cls.tmpDirectory, mode="w") - cls.formatter = TextFormatter() - - @classmethod - def tearDownClass(cls): - super(TestTextFormatterWithH5py, cls).tearDownClass() - cls.h5File.close() - cls.h5File = None - shutil.rmtree(cls.tmpDirectory) - - def create_dataset(self, data, dtype=None): - testName = "%s" % self.id() - dataset = self.h5File.create_dataset(testName, data=data, dtype=dtype) - return dataset - - def read_dataset(self, d): - return self.formatter.toString(d[()], dtype=d.dtype) - - def testAscii(self): - d = self.create_dataset(data=b"abc") - result = self.read_dataset(d) - self.assertEqual(result, '"abc"') - - def testUnicode(self): - d = self.create_dataset(data=u"i\u2661cookies") - result = self.read_dataset(d) - self.assertEqual(len(result), 11) - self.assertEqual(result, u'"i\u2661cookies"') - - def testBadAscii(self): - d = self.create_dataset(data=b"\xF0\x9F\x92\x94") - result = self.read_dataset(d) - self.assertEqual(result, 'b"\\xF0\\x9F\\x92\\x94"') - - def testVoid(self): - d = self.create_dataset(data=numpy.void(b"abc\xF0")) - result = self.read_dataset(d) - self.assertEqual(result, 'b"\\x61\\x62\\x63\\xF0"') - - def testEnum(self): - dtype = h5py.special_dtype(enum=('i', {"RED": 0, "GREEN": 1, "BLUE": 42})) - d = numpy.array(42, dtype=dtype) - d = self.create_dataset(data=d) - result = self.read_dataset(d) - self.assertEqual(result, 'BLUE(42)') - - def testRef(self): - dtype = h5py.special_dtype(ref=h5py.Reference) - d = numpy.array(self.h5File.ref, dtype=dtype) - d = self.create_dataset(data=d) - result = self.read_dataset(d) - self.assertEqual(result, 'REF') - - def testArrayAscii(self): - d = self.create_dataset(data=[b"abc"]) - result = self.read_dataset(d) - self.assertEqual(result, '["abc"]') - - def testArrayUnicode(self): - dtype = h5py.special_dtype(vlen=six.text_type) - d = numpy.array([u"i\u2661cookies"], dtype=dtype) - d = self.create_dataset(data=d) - result = self.read_dataset(d) - self.assertEqual(len(result), 13) - self.assertEqual(result, u'["i\u2661cookies"]') - - def testArrayBadAscii(self): - d = self.create_dataset(data=[b"\xF0\x9F\x92\x94"]) - result = self.read_dataset(d) - self.assertEqual(result, '[b"\\xF0\\x9F\\x92\\x94"]') - - def testArrayVoid(self): - d = self.create_dataset(data=numpy.void([b"abc\xF0"])) - result = self.read_dataset(d) - self.assertEqual(result, '[b"\\x61\\x62\\x63\\xF0"]') - - def testArrayEnum(self): - dtype = h5py.special_dtype(enum=('i', {"RED": 0, "GREEN": 1, "BLUE": 42})) - d = numpy.array([42, 1, 100], dtype=dtype) - d = self.create_dataset(data=d) - result = self.read_dataset(d) - self.assertEqual(result, '[BLUE(42) GREEN(1) 100]') - - def testArrayRef(self): - dtype = h5py.special_dtype(ref=h5py.Reference) - d = numpy.array([self.h5File.ref, None], dtype=dtype) - d = self.create_dataset(data=d) - result = self.read_dataset(d) - self.assertEqual(result, '[REF NULL_REF]') - - -def suite(): - loadTests = unittest.defaultTestLoader.loadTestsFromTestCase - test_suite = unittest.TestSuite() - test_suite.addTest(loadTests(TestTextFormatter)) - test_suite.addTest(loadTests(TestTextFormatterWithH5py)) - return test_suite - - -if __name__ == '__main__': - unittest.main(defaultTest='suite') diff --git a/silx/gui/dialog/AbstractDataFileDialog.py b/silx/gui/dialog/AbstractDataFileDialog.py deleted file mode 100644 index 29e7bb5..0000000 --- a/silx/gui/dialog/AbstractDataFileDialog.py +++ /dev/null @@ -1,1742 +0,0 @@ -# coding: utf-8 -# /*########################################################################## -# -# Copyright (c) 2016-2018 European Synchrotron Radiation Facility -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. -# -# ###########################################################################*/ -""" -This module contains an :class:`AbstractDataFileDialog`. -""" - -__authors__ = ["V. Valls"] -__license__ = "MIT" -__date__ = "05/03/2019" - - -import sys -import os -import logging -import functools -from distutils.version import LooseVersion - -import numpy -import six - -import silx.io.url -from silx.gui import qt -from silx.gui.hdf5.Hdf5TreeModel import Hdf5TreeModel -from . import utils -from .FileTypeComboBox import FileTypeComboBox - -import fabio - - -_logger = logging.getLogger(__name__) - - -DEFAULT_SIDEBAR_URL = True -"""Set it to false to disable initilializing of the sidebar urls with the -default Qt list. This could allow to disable a behaviour known to segfault on -some version of PyQt.""" - - -class _IconProvider(object): - - FileDialogToParentDir = qt.QStyle.SP_CustomBase + 1 - - FileDialogToParentFile = qt.QStyle.SP_CustomBase + 2 - - def __init__(self): - self.__iconFileDialogToParentDir = None - self.__iconFileDialogToParentFile = None - - def _createIconToParent(self, standardPixmap): - """ - - FIXME: It have to be tested for some OS (arrow icon do not have always - the same direction) - """ - style = qt.QApplication.style() - baseIcon = style.standardIcon(qt.QStyle.SP_FileDialogToParent) - backgroundIcon = style.standardIcon(standardPixmap) - icon = qt.QIcon() - - sizes = baseIcon.availableSizes() - sizes = sorted(sizes, key=lambda s: s.height()) - sizes = filter(lambda s: s.height() < 100, sizes) - sizes = list(sizes) - if len(sizes) > 0: - baseSize = sizes[-1] - else: - baseSize = baseIcon.availableSizes()[0] - size = qt.QSize(baseSize.width(), baseSize.height() * 3 // 2) - - modes = [qt.QIcon.Normal, qt.QIcon.Disabled] - for mode in modes: - pixmap = qt.QPixmap(size) - pixmap.fill(qt.Qt.transparent) - painter = qt.QPainter(pixmap) - painter.drawPixmap(0, 0, backgroundIcon.pixmap(baseSize, mode=mode)) - painter.drawPixmap(0, size.height() // 3, baseIcon.pixmap(baseSize, mode=mode)) - painter.end() - icon.addPixmap(pixmap, mode=mode) - - return icon - - def getFileDialogToParentDir(self): - if self.__iconFileDialogToParentDir is None: - self.__iconFileDialogToParentDir = self._createIconToParent(qt.QStyle.SP_DirIcon) - return self.__iconFileDialogToParentDir - - def getFileDialogToParentFile(self): - if self.__iconFileDialogToParentFile is None: - self.__iconFileDialogToParentFile = self._createIconToParent(qt.QStyle.SP_FileIcon) - return self.__iconFileDialogToParentFile - - def icon(self, kind): - if kind == self.FileDialogToParentDir: - return self.getFileDialogToParentDir() - elif kind == self.FileDialogToParentFile: - return self.getFileDialogToParentFile() - else: - style = qt.QApplication.style() - icon = style.standardIcon(kind) - return icon - - -class _SideBar(qt.QListView): - """Sidebar containing shortcuts for common directories""" - - def __init__(self, parent=None): - super(_SideBar, self).__init__(parent) - self.__iconProvider = qt.QFileIconProvider() - self.setUniformItemSizes(True) - model = qt.QStandardItemModel(self) - self.setModel(model) - self._initModel() - self.setEditTriggers(qt.QAbstractItemView.NoEditTriggers) - - def iconProvider(self): - return self.__iconProvider - - def _initModel(self): - urls = self._getDefaultUrls() - self.setUrls(urls) - - def _getDefaultUrls(self): - """Returns the default shortcuts. - - It uses the default QFileDialog shortcuts if it is possible, else - provides a link to the computer's root and the user's home. - - :rtype: List[str] - """ - urls = [] - version = LooseVersion(qt.qVersion()) - feed_sidebar = True - - if not DEFAULT_SIDEBAR_URL: - _logger.debug("Skip default sidebar URLs (from setted variable)") - feed_sidebar = False - elif version.version[0] == 4 and sys.platform in ["win32"]: - # Avoid locking the GUI 5min in case of use of network driver - _logger.debug("Skip default sidebar URLs (avoid lock when using network drivers)") - feed_sidebar = False - elif version < LooseVersion("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 - - if feed_sidebar: - # Get default shortcut - # There is no other way - d = qt.QFileDialog(self) - # Needed to be able to reach the sidebar urls - d.setOption(qt.QFileDialog.DontUseNativeDialog, True) - urls = d.sidebarUrls() - d.deleteLater() - d = None - - if len(urls) == 0: - urls.append(qt.QUrl("file://")) - urls.append(qt.QUrl.fromLocalFile(qt.QDir.homePath())) - - return urls - - def setSelectedPath(self, path): - selected = None - model = self.model() - for i in range(model.rowCount()): - index = model.index(i, 0) - url = model.data(index, qt.Qt.UserRole) - urlPath = url.toLocalFile() - if path == urlPath: - selected = index - - selectionModel = self.selectionModel() - if selected is not None: - selectionModel.setCurrentIndex(selected, qt.QItemSelectionModel.ClearAndSelect) - else: - selectionModel.clear() - - def setUrls(self, urls): - model = self.model() - model.clear() - - names = {} - names[qt.QDir.rootPath()] = "Computer" - names[qt.QDir.homePath()] = "Home" - - style = qt.QApplication.style() - iconProvider = self.iconProvider() - for url in urls: - path = url.toLocalFile() - if path == "": - if sys.platform != "win32": - url = qt.QUrl(qt.QDir.rootPath()) - name = "Computer" - icon = style.standardIcon(qt.QStyle.SP_ComputerIcon) - else: - fileInfo = qt.QFileInfo(path) - name = names.get(path, fileInfo.fileName()) - icon = iconProvider.icon(fileInfo) - - if icon.isNull(): - icon = style.standardIcon(qt.QStyle.SP_MessageBoxCritical) - - item = qt.QStandardItem() - item.setText(name) - item.setIcon(icon) - item.setData(url, role=qt.Qt.UserRole) - model.appendRow(item) - - def urls(self): - result = [] - model = self.model() - for i in range(model.rowCount()): - index = model.index(i, 0) - url = model.data(index, qt.Qt.UserRole) - result.append(url) - return result - - def sizeHint(self): - index = self.model().index(0, 0) - return self.sizeHintForIndex(index) + qt.QSize(2 * self.frameWidth(), 2 * self.frameWidth()) - - -class _Browser(qt.QStackedWidget): - - activated = qt.Signal(qt.QModelIndex) - selected = qt.Signal(qt.QModelIndex) - rootIndexChanged = qt.Signal(qt.QModelIndex) - - def __init__(self, parent, listView, detailView): - qt.QStackedWidget.__init__(self, parent) - self.__listView = listView - self.__detailView = detailView - self.insertWidget(0, self.__listView) - self.insertWidget(1, self.__detailView) - - self.__listView.activated.connect(self.__emitActivated) - self.__detailView.activated.connect(self.__emitActivated) - - def __emitActivated(self, index): - self.activated.emit(index) - - def __emitSelected(self, selected, deselected): - index = self.selectedIndex() - if index is not None: - self.selected.emit(index) - - def selectedIndex(self): - if self.currentIndex() == 0: - selectionModel = self.__listView.selectionModel() - else: - selectionModel = self.__detailView.selectionModel() - - if selectionModel is None: - return None - - indexes = selectionModel.selectedIndexes() - # Filter non-main columns - indexes = [i for i in indexes if i.column() == 0] - if len(indexes) == 1: - index = indexes[0] - return index - return None - - def model(self): - """Returns the current model.""" - if self.currentIndex() == 0: - return self.__listView.model() - else: - return self.__detailView.model() - - def selectIndex(self, index): - if self.currentIndex() == 0: - selectionModel = self.__listView.selectionModel() - else: - selectionModel = self.__detailView.selectionModel() - if selectionModel is None: - return - selectionModel.setCurrentIndex(index, qt.QItemSelectionModel.ClearAndSelect) - - def viewMode(self): - """Returns the current view mode. - - :rtype: qt.QFileDialog.ViewMode - """ - if self.currentIndex() == 0: - return qt.QFileDialog.List - elif self.currentIndex() == 1: - return qt.QFileDialog.Detail - else: - assert(False) - - def setViewMode(self, mode): - """Set the current view mode. - - :param qt.QFileDialog.ViewMode mode: The new view mode - """ - if mode == qt.QFileDialog.Detail: - self.showDetails() - elif mode == qt.QFileDialog.List: - self.showList() - else: - assert(False) - - def showList(self): - self.__listView.show() - self.__detailView.hide() - self.setCurrentIndex(0) - - def showDetails(self): - self.__listView.hide() - self.__detailView.show() - self.setCurrentIndex(1) - self.__detailView.updateGeometry() - - def clear(self): - self.__listView.setRootIndex(qt.QModelIndex()) - self.__detailView.setRootIndex(qt.QModelIndex()) - selectionModel = self.__listView.selectionModel() - if selectionModel is not None: - selectionModel.selectionChanged.disconnect() - selectionModel.clear() - selectionModel = self.__detailView.selectionModel() - if selectionModel is not None: - selectionModel.selectionChanged.disconnect() - selectionModel.clear() - self.__listView.setModel(None) - self.__detailView.setModel(None) - - def setRootIndex(self, index, model=None): - """Sets the root item to the item at the given index. - """ - rootIndex = self.__listView.rootIndex() - newModel = model or index.model() - assert(newModel is not None) - - if rootIndex is None or rootIndex.model() is not newModel: - # update the model - selectionModel = self.__listView.selectionModel() - if selectionModel is not None: - selectionModel.selectionChanged.disconnect() - selectionModel.clear() - selectionModel = self.__detailView.selectionModel() - if selectionModel is not None: - selectionModel.selectionChanged.disconnect() - selectionModel.clear() - pIndex = qt.QPersistentModelIndex(index) - self.__listView.setModel(newModel) - # changing the model of the tree view change the index mapping - # that is why we are using a persistance model index - self.__detailView.setModel(newModel) - index = newModel.index(pIndex.row(), pIndex.column(), pIndex.parent()) - selectionModel = self.__listView.selectionModel() - selectionModel.selectionChanged.connect(self.__emitSelected) - selectionModel = self.__detailView.selectionModel() - selectionModel.selectionChanged.connect(self.__emitSelected) - - self.__listView.setRootIndex(index) - self.__detailView.setRootIndex(index) - self.rootIndexChanged.emit(index) - - def rootIndex(self): - """Returns the model index of the model's root item. The root item is - the parent item to the view's toplevel items. The root can be invalid. - """ - return self.__listView.rootIndex() - - __serialVersion = 1 - """Store the current version of the serialized data""" - - def visualRect(self, index): - """Returns the rectangle on the viewport occupied by the item at index. - - :param qt.QModelIndex index: An index - :rtype: QRect - """ - if self.currentIndex() == 0: - return self.__listView.visualRect(index) - else: - return self.__detailView.visualRect(index) - - def viewport(self): - """Returns the viewport widget. - - :param qt.QModelIndex index: An index - :rtype: QRect - """ - if self.currentIndex() == 0: - return self.__listView.viewport() - else: - return self.__detailView.viewport() - - def restoreState(self, state): - """Restores the dialogs's layout, history and current directory to the - state specified. - - :param qt.QByeArray state: Stream containing the new state - :rtype: bool - """ - stream = qt.QDataStream(state, qt.QIODevice.ReadOnly) - - nameId = stream.readQString() - if nameId != "Browser": - _logger.warning("Stored state contains an invalid name id. Browser restoration cancelled.") - return False - - version = stream.readInt32() - if version != self.__serialVersion: - _logger.warning("Stored state contains an invalid version. Browser restoration cancelled.") - return False - - headerData = stream.readQVariant() - self.__detailView.header().restoreState(headerData) - - viewMode = stream.readInt32() - self.setViewMode(viewMode) - return True - - def saveState(self): - """Saves the state of the dialog's layout. - - :rtype: qt.QByteArray - """ - data = qt.QByteArray() - stream = qt.QDataStream(data, qt.QIODevice.WriteOnly) - - nameId = u"Browser" - stream.writeQString(nameId) - stream.writeInt32(self.__serialVersion) - stream.writeQVariant(self.__detailView.header().saveState()) - stream.writeInt32(self.viewMode()) - - return data - - -class _FabioData(object): - - def __init__(self, fabioFile): - self.__fabioFile = fabioFile - - @property - def dtype(self): - # Let say it is a valid type - return numpy.dtype("float") - - @property - def shape(self): - if self.__fabioFile.nframes == 0: - return None - if self.__fabioFile.nframes == 1: - return [slice(None), slice(None)] - return [self.__fabioFile.nframes, slice(None), slice(None)] - - def __getitem__(self, selector): - if self.__fabioFile.nframes == 1 and selector == tuple(): - return self.__fabioFile.data - if isinstance(selector, tuple) and len(selector) == 1: - selector = selector[0] - - if isinstance(selector, six.integer_types): - if 0 <= selector < self.__fabioFile.nframes: - if self.__fabioFile.nframes == 1: - return self.__fabioFile.data - else: - frame = self.__fabioFile.getframe(selector) - return frame.data - else: - raise ValueError("Invalid selector %s" % selector) - else: - raise TypeError("Unsupported selector type %s" % type(selector)) - - -class _PathEdit(qt.QLineEdit): - pass - - -class _CatchResizeEvent(qt.QObject): - - resized = qt.Signal(qt.QResizeEvent) - - def __init__(self, parent, target): - super(_CatchResizeEvent, self).__init__(parent) - self.__target = target - self.__target_oldResizeEvent = self.__target.resizeEvent - self.__target.resizeEvent = self.__resizeEvent - - def __resizeEvent(self, event): - result = self.__target_oldResizeEvent(event) - self.resized.emit(event) - return result - - -class AbstractDataFileDialog(qt.QDialog): - """The `AbstractFileDialog` provides a generic GUI to create a custom dialog - allowing to access to file resources like HDF5 files or HDF5 datasets. - - .. image:: img/abstractdatafiledialog.png - - The dialog contains: - - - Shortcuts: It provides few links to have a fast access of browsing - locations. - - Browser: It provides a display to browse throw the file system and inside - HDF5 files or fabio files. A file format selector is provided. - - URL: Display the URL available to reach the data using - :meth:`silx.io.get_data`, :meth:`silx.io.open`. - - Data selector: A widget to apply a sub selection of the browsed dataset. - This widget can be provided, else nothing will be used. - - Data preview: A widget to preview the selected data, which is the result - of the filter from the data selector. - This widget can be provided, else nothing will be used. - - Preview's toolbar: Provides tools used to custom data preview or data - selector. - This widget can be provided, else nothing will be used. - - Buttons to validate the dialog - """ - - _defaultIconProvider = None - """Lazy loaded default icon provider""" - - def __init__(self, parent=None): - super(AbstractDataFileDialog, self).__init__(parent) - self._init() - - def _init(self): - self.setWindowTitle("Open") - - self.__openedFiles = [] - """Store the list of files opened by the model itself.""" - # FIXME: It should be managed one by one by Hdf5Item itself - - self.__directory = None - self.__directoryLoadedFilter = None - self.__errorWhileLoadingFile = None - self.__selectedFile = None - self.__selectedData = None - self.__currentHistory = [] - """Store history of URLs, last index one is the latest one""" - self.__currentHistoryLocation = -1 - """Store the location in the history. Bigger is older""" - - self.__processing = 0 - """Number of asynchronous processing tasks""" - self.__h5 = None - self.__fabio = None - - if qt.qVersion() < "5.0": - # On Qt4 it is needed to provide a safe file system model - _logger.debug("Uses SafeFileSystemModel") - from .SafeFileSystemModel import SafeFileSystemModel - self.__fileModel = SafeFileSystemModel(self) - else: - # On Qt5 a safe icon provider is still needed to avoid freeze - _logger.debug("Uses default QFileSystemModel with a SafeFileIconProvider") - self.__fileModel = qt.QFileSystemModel(self) - from .SafeFileIconProvider import SafeFileIconProvider - iconProvider = SafeFileIconProvider() - self.__fileModel.setIconProvider(iconProvider) - - # The common file dialog filter only on Mac OS X - self.__fileModel.setNameFilterDisables(sys.platform == "darwin") - self.__fileModel.setReadOnly(True) - self.__fileModel.directoryLoaded.connect(self.__directoryLoaded) - - self.__dataModel = Hdf5TreeModel(self) - - self.__createWidgets() - self.__initLayout() - self.__showAsListView() - - path = os.getcwd() - self.__fileModel_setRootPath(path) - - self.__clearData() - self.__updatePath() - - # Update the file model filter - self.__fileTypeCombo.setCurrentIndex(0) - self.__filterSelected(0) - - # It is not possible to override the QObject destructor nor - # to access to the content of the Python object with the `destroyed` - # signal cause the Python method was already removed with the QWidget, - # while the QObject still exists. - # We use a static method plus explicit references to objects to - # release. The callback do not use any ref to self. - onDestroy = functools.partial(self._closeFileList, self.__openedFiles) - self.destroyed.connect(onDestroy) - - @staticmethod - def _closeFileList(fileList): - """Static method to close explicit references to internal objects.""" - _logger.debug("Clear AbstractDataFileDialog") - for obj in fileList: - _logger.debug("Close file %s", obj.filename) - obj.close() - fileList[:] = [] - - def done(self, result): - self._clear() - super(AbstractDataFileDialog, self).done(result) - - def _clear(self): - """Explicit method to clear data stored in the dialog. - After this call it is not anymore possible to use the widget. - - This method is triggered by the destruction of the object and the - QDialog :meth:`done`. Then it can be triggered more than once. - """ - _logger.debug("Clear dialog") - self.__errorWhileLoadingFile = None - self.__clearData() - if self.__fileModel is not None: - # Cache the directory before cleaning the model - self.__directory = self.directory() - self.__browser.clear() - self.__closeFile() - self.__fileModel = None - self.__dataModel = None - - def hasPendingEvents(self): - """Returns true if the dialog have asynchronous tasks working on the - background.""" - return self.__processing > 0 - - # User interface - - def __createWidgets(self): - self.__sidebar = self._createSideBar() - if self.__sidebar is not None: - sideBarModel = self.__sidebar.selectionModel() - sideBarModel.selectionChanged.connect(self.__shortcutSelected) - self.__sidebar.setSelectionMode(qt.QAbstractItemView.SingleSelection) - - listView = qt.QListView(self) - listView.setSelectionBehavior(qt.QAbstractItemView.SelectRows) - listView.setSelectionMode(qt.QAbstractItemView.SingleSelection) - listView.setResizeMode(qt.QListView.Adjust) - listView.setWrapping(True) - listView.setEditTriggers(qt.QAbstractItemView.NoEditTriggers) - listView.setContextMenuPolicy(qt.Qt.CustomContextMenu) - utils.patchToConsumeReturnKey(listView) - - treeView = qt.QTreeView(self) - treeView.setSelectionBehavior(qt.QAbstractItemView.SelectRows) - treeView.setSelectionMode(qt.QAbstractItemView.SingleSelection) - treeView.setRootIsDecorated(False) - treeView.setItemsExpandable(False) - treeView.setSortingEnabled(True) - treeView.header().setSortIndicator(0, qt.Qt.AscendingOrder) - treeView.header().setStretchLastSection(False) - treeView.setTextElideMode(qt.Qt.ElideMiddle) - treeView.setEditTriggers(qt.QAbstractItemView.NoEditTriggers) - treeView.setContextMenuPolicy(qt.Qt.CustomContextMenu) - treeView.setDragDropMode(qt.QAbstractItemView.InternalMove) - utils.patchToConsumeReturnKey(treeView) - - self.__browser = _Browser(self, listView, treeView) - self.__browser.activated.connect(self.__browsedItemActivated) - self.__browser.selected.connect(self.__browsedItemSelected) - self.__browser.rootIndexChanged.connect(self.__rootIndexChanged) - self.__browser.setObjectName("browser") - - self.__previewWidget = self._createPreviewWidget(self) - - self.__fileTypeCombo = FileTypeComboBox(self) - self.__fileTypeCombo.setObjectName("fileTypeCombo") - self.__fileTypeCombo.setDuplicatesEnabled(False) - self.__fileTypeCombo.setSizeAdjustPolicy(qt.QComboBox.AdjustToMinimumContentsLength) - self.__fileTypeCombo.setSizePolicy(qt.QSizePolicy.Expanding, qt.QSizePolicy.Fixed) - self.__fileTypeCombo.activated[int].connect(self.__filterSelected) - self.__fileTypeCombo.setFabioUrlSupproted(self._isFabioFilesSupported()) - - self.__pathEdit = _PathEdit(self) - self.__pathEdit.setSizePolicy(qt.QSizePolicy.Expanding, qt.QSizePolicy.Fixed) - self.__pathEdit.textChanged.connect(self.__textChanged) - self.__pathEdit.setObjectName("url") - utils.patchToConsumeReturnKey(self.__pathEdit) - - self.__buttons = qt.QDialogButtonBox(self) - self.__buttons.setSizePolicy(qt.QSizePolicy.Fixed, qt.QSizePolicy.Fixed) - types = qt.QDialogButtonBox.Open | qt.QDialogButtonBox.Cancel - self.__buttons.setStandardButtons(types) - self.__buttons.button(qt.QDialogButtonBox.Cancel).setObjectName("cancel") - self.__buttons.button(qt.QDialogButtonBox.Open).setObjectName("open") - - self.__buttons.accepted.connect(self.accept) - self.__buttons.rejected.connect(self.reject) - - self.__browseToolBar = self._createBrowseToolBar() - self.__backwardAction.setEnabled(False) - self.__forwardAction.setEnabled(False) - self.__fileDirectoryAction.setEnabled(False) - self.__parentFileDirectoryAction.setEnabled(False) - - self.__selectorWidget = self._createSelectorWidget(self) - if self.__selectorWidget is not None: - self.__selectorWidget.selectionChanged.connect(self.__selectorWidgetChanged) - - self.__previewToolBar = self._createPreviewToolbar(self, self.__previewWidget, self.__selectorWidget) - - self.__dataIcon = qt.QLabel(self) - self.__dataIcon.setSizePolicy(qt.QSizePolicy.Fixed, qt.QSizePolicy.Fixed) - self.__dataIcon.setScaledContents(True) - self.__dataIcon.setMargin(2) - self.__dataIcon.setAlignment(qt.Qt.AlignCenter) - - self.__dataInfo = qt.QLabel(self) - self.__dataInfo.setSizePolicy(qt.QSizePolicy.Expanding, qt.QSizePolicy.Fixed) - - def _createSideBar(self): - sidebar = _SideBar(self) - sidebar.setObjectName("sidebar") - return sidebar - - def iconProvider(self): - iconProvider = self.__class__._defaultIconProvider - if iconProvider is None: - iconProvider = _IconProvider() - self.__class__._defaultIconProvider = iconProvider - return iconProvider - - def _createBrowseToolBar(self): - toolbar = qt.QToolBar(self) - toolbar.setIconSize(qt.QSize(16, 16)) - iconProvider = self.iconProvider() - - backward = qt.QAction(toolbar) - backward.setText("Back") - backward.setObjectName("backwardAction") - backward.setIcon(iconProvider.icon(qt.QStyle.SP_ArrowBack)) - backward.triggered.connect(self.__navigateBackward) - self.__backwardAction = backward - - forward = qt.QAction(toolbar) - forward.setText("Forward") - forward.setObjectName("forwardAction") - forward.setIcon(iconProvider.icon(qt.QStyle.SP_ArrowForward)) - forward.triggered.connect(self.__navigateForward) - self.__forwardAction = forward - - parentDirectory = qt.QAction(toolbar) - parentDirectory.setText("Go to parent") - parentDirectory.setObjectName("toParentAction") - parentDirectory.setIcon(iconProvider.icon(qt.QStyle.SP_FileDialogToParent)) - parentDirectory.triggered.connect(self.__navigateToParent) - self.__toParentAction = parentDirectory - - fileDirectory = qt.QAction(toolbar) - fileDirectory.setText("Root of the file") - fileDirectory.setObjectName("toRootFileAction") - fileDirectory.setIcon(iconProvider.icon(iconProvider.FileDialogToParentFile)) - fileDirectory.triggered.connect(self.__navigateToParentFile) - self.__fileDirectoryAction = fileDirectory - - parentFileDirectory = qt.QAction(toolbar) - parentFileDirectory.setText("Parent directory of the file") - parentFileDirectory.setObjectName("toDirectoryAction") - parentFileDirectory.setIcon(iconProvider.icon(iconProvider.FileDialogToParentDir)) - parentFileDirectory.triggered.connect(self.__navigateToParentDir) - self.__parentFileDirectoryAction = parentFileDirectory - - listView = qt.QAction(toolbar) - listView.setText("List view") - listView.setObjectName("listModeAction") - listView.setIcon(iconProvider.icon(qt.QStyle.SP_FileDialogListView)) - listView.triggered.connect(self.__showAsListView) - listView.setCheckable(True) - - detailView = qt.QAction(toolbar) - detailView.setText("Detail view") - detailView.setObjectName("detailModeAction") - detailView.setIcon(iconProvider.icon(qt.QStyle.SP_FileDialogDetailedView)) - detailView.triggered.connect(self.__showAsDetailedView) - detailView.setCheckable(True) - - self.__listViewAction = listView - self.__detailViewAction = detailView - - toolbar.addAction(backward) - toolbar.addAction(forward) - toolbar.addSeparator() - toolbar.addAction(parentDirectory) - toolbar.addAction(fileDirectory) - toolbar.addAction(parentFileDirectory) - toolbar.addSeparator() - toolbar.addAction(listView) - toolbar.addAction(detailView) - - toolbar.setStyleSheet("QToolBar { border: 0px }") - - return toolbar - - def __initLayout(self): - sideBarLayout = qt.QVBoxLayout() - sideBarLayout.setContentsMargins(0, 0, 0, 0) - dummyToolBar = qt.QWidget(self) - dummyToolBar.setSizePolicy(qt.QSizePolicy.Expanding, qt.QSizePolicy.Fixed) - dummyCombo = qt.QWidget(self) - dummyCombo.setSizePolicy(qt.QSizePolicy.Expanding, qt.QSizePolicy.Fixed) - sideBarLayout.addWidget(dummyToolBar) - if self.__sidebar is not None: - sideBarLayout.addWidget(self.__sidebar) - sideBarLayout.addWidget(dummyCombo) - sideBarWidget = qt.QWidget(self) - sideBarWidget.setLayout(sideBarLayout) - - dummyCombo.setFixedHeight(self.__fileTypeCombo.height()) - self.__resizeCombo = _CatchResizeEvent(self, self.__fileTypeCombo) - self.__resizeCombo.resized.connect(lambda e: dummyCombo.setFixedHeight(e.size().height())) - - dummyToolBar.setFixedHeight(self.__browseToolBar.height()) - self.__resizeToolbar = _CatchResizeEvent(self, self.__browseToolBar) - self.__resizeToolbar.resized.connect(lambda e: dummyToolBar.setFixedHeight(e.size().height())) - - datasetSelection = qt.QWidget(self) - layoutLeft = qt.QVBoxLayout() - layoutLeft.setContentsMargins(0, 0, 0, 0) - layoutLeft.addWidget(self.__browseToolBar) - layoutLeft.addWidget(self.__browser) - layoutLeft.addWidget(self.__fileTypeCombo) - datasetSelection.setLayout(layoutLeft) - datasetSelection.setSizePolicy(qt.QSizePolicy.MinimumExpanding, qt.QSizePolicy.Expanding) - - infoLayout = qt.QHBoxLayout() - infoLayout.setContentsMargins(0, 0, 0, 0) - infoLayout.addWidget(self.__dataIcon) - infoLayout.addWidget(self.__dataInfo) - - dataFrame = qt.QFrame(self) - dataFrame.setFrameShape(qt.QFrame.StyledPanel) - layout = qt.QVBoxLayout() - layout.setContentsMargins(0, 0, 0, 0) - layout.setSpacing(0) - layout.addWidget(self.__previewWidget) - layout.addLayout(infoLayout) - dataFrame.setLayout(layout) - - dataSelection = qt.QWidget(self) - dataLayout = qt.QVBoxLayout() - dataLayout.setContentsMargins(0, 0, 0, 0) - if self.__previewToolBar is not None: - dataLayout.addWidget(self.__previewToolBar) - else: - # Add dummy space - dummyToolbar2 = qt.QWidget(self) - dummyToolbar2.setSizePolicy(qt.QSizePolicy.Expanding, qt.QSizePolicy.Fixed) - dummyToolbar2.setFixedHeight(self.__browseToolBar.height()) - self.__resizeToolbar = _CatchResizeEvent(self, self.__browseToolBar) - self.__resizeToolbar.resized.connect(lambda e: dummyToolbar2.setFixedHeight(e.size().height())) - dataLayout.addWidget(dummyToolbar2) - - dataLayout.addWidget(dataFrame) - if self.__selectorWidget is not None: - dataLayout.addWidget(self.__selectorWidget) - else: - # Add dummy space - dummyCombo2 = qt.QWidget(self) - dummyCombo2.setSizePolicy(qt.QSizePolicy.Expanding, qt.QSizePolicy.Fixed) - dummyCombo2.setFixedHeight(self.__fileTypeCombo.height()) - self.__resizeToolbar = _CatchResizeEvent(self, self.__fileTypeCombo) - self.__resizeToolbar.resized.connect(lambda e: dummyCombo2.setFixedHeight(e.size().height())) - dataLayout.addWidget(dummyCombo2) - dataSelection.setLayout(dataLayout) - - self.__splitter = qt.QSplitter(self) - self.__splitter.setContentsMargins(0, 0, 0, 0) - self.__splitter.addWidget(sideBarWidget) - self.__splitter.addWidget(datasetSelection) - self.__splitter.addWidget(dataSelection) - self.__splitter.setStretchFactor(1, 10) - - bottomLayout = qt.QHBoxLayout() - bottomLayout.setContentsMargins(0, 0, 0, 0) - bottomLayout.addWidget(self.__pathEdit) - bottomLayout.addWidget(self.__buttons) - - layout = qt.QVBoxLayout(self) - layout.addWidget(self.__splitter) - layout.addLayout(bottomLayout) - - self.setLayout(layout) - self.updateGeometry() - - # Logic - - def __navigateBackward(self): - """Navigate through the history one step backward.""" - if len(self.__currentHistory) > 0 and self.__currentHistoryLocation > 0: - self.__currentHistoryLocation -= 1 - url = self.__currentHistory[self.__currentHistoryLocation] - self.selectUrl(url) - - def __navigateForward(self): - """Navigate through the history one step forward.""" - if len(self.__currentHistory) > 0 and self.__currentHistoryLocation < len(self.__currentHistory) - 1: - self.__currentHistoryLocation += 1 - url = self.__currentHistory[self.__currentHistoryLocation] - self.selectUrl(url) - - def __navigateToParent(self): - index = self.__browser.rootIndex() - if index.model() is self.__fileModel: - # browse throw the file system - index = index.parent() - path = self.__fileModel.filePath(index) - self.__fileModel_setRootPath(path) - self.__browser.selectIndex(qt.QModelIndex()) - self.__updatePath() - elif index.model() is self.__dataModel: - index = index.parent() - if index.isValid(): - # browse throw the hdf5 - self.__browser.setRootIndex(index) - self.__browser.selectIndex(qt.QModelIndex()) - self.__updatePath() - else: - # go back to the file system - self.__navigateToParentDir() - else: - # Root of the file system (my computer) - pass - - def __navigateToParentFile(self): - index = self.__browser.rootIndex() - if index.model() is self.__dataModel: - index = self.__dataModel.indexFromH5Object(self.__h5) - self.__browser.setRootIndex(index) - self.__browser.selectIndex(qt.QModelIndex()) - self.__updatePath() - - def __navigateToParentDir(self): - index = self.__browser.rootIndex() - if index.model() is self.__dataModel: - path = os.path.dirname(self.__h5.file.filename) - index = self.__fileModel.index(path) - self.__browser.setRootIndex(index) - self.__browser.selectIndex(qt.QModelIndex()) - self.__closeFile() - self.__updatePath() - - def viewMode(self): - """Returns the current view mode. - - :rtype: qt.QFileDialog.ViewMode - """ - return self.__browser.viewMode() - - def setViewMode(self, mode): - """Set the current view mode. - - :param qt.QFileDialog.ViewMode mode: The new view mode - """ - if mode == qt.QFileDialog.Detail: - self.__browser.showDetails() - self.__listViewAction.setChecked(False) - self.__detailViewAction.setChecked(True) - elif mode == qt.QFileDialog.List: - self.__browser.showList() - self.__listViewAction.setChecked(True) - self.__detailViewAction.setChecked(False) - else: - assert(False) - - def __showAsListView(self): - self.setViewMode(qt.QFileDialog.List) - - def __showAsDetailedView(self): - self.setViewMode(qt.QFileDialog.Detail) - - def __shortcutSelected(self): - self.__browser.selectIndex(qt.QModelIndex()) - self.__clearData() - self.__updatePath() - selectionModel = self.__sidebar.selectionModel() - indexes = selectionModel.selectedIndexes() - if len(indexes) == 1: - index = indexes[0] - url = self.__sidebar.model().data(index, role=qt.Qt.UserRole) - path = url.toLocalFile() - self.__fileModel_setRootPath(path) - - def __browsedItemActivated(self, index): - if not index.isValid(): - return - if index.model() is self.__fileModel: - path = self.__fileModel.filePath(index) - if self.__fileModel.isDir(index): - self.__fileModel_setRootPath(path) - if os.path.isfile(path): - self.__fileActivated(index) - elif index.model() is self.__dataModel: - obj = self.__dataModel.data(index, role=Hdf5TreeModel.H5PY_OBJECT_ROLE) - if silx.io.is_group(obj): - self.__browser.setRootIndex(index) - else: - assert(False) - - def __browsedItemSelected(self, index): - self.__dataSelected(index) - self.__updatePath() - - def __fileModel_setRootPath(self, path): - """Set the root path of the fileModel with a filter on the - directoryLoaded event. - - Without this filter an extra event is received (at least with PyQt4) - when we use for the first time the sidebar. - - :param str path: Path to load - """ - assert(path is not None) - if path != "" and not os.path.exists(path): - return - if self.hasPendingEvents(): - # Make sure the asynchronous fileModel setRootPath is finished - qt.QApplication.instance().processEvents() - - if self.__directoryLoadedFilter is not None: - if utils.samefile(self.__directoryLoadedFilter, path): - return - self.__directoryLoadedFilter = path - self.__processing += 1 - if self.__fileModel is None: - return - index = self.__fileModel.setRootPath(path) - if not index.isValid(): - # There is a problem with this path - # No asynchronous process will be waked up - self.__processing -= 1 - self.__browser.setRootIndex(index, model=self.__fileModel) - self.__clearData() - self.__updatePath() - - def __directoryLoaded(self, path): - if self.__directoryLoadedFilter is not None: - if not utils.samefile(self.__directoryLoadedFilter, path): - # Filter event which should not arrive in PyQt4 - # The first click on the sidebar sent 2 events - self.__processing -= 1 - return - if self.__fileModel is None: - return - index = self.__fileModel.index(path) - self.__browser.setRootIndex(index, model=self.__fileModel) - self.__updatePath() - self.__processing -= 1 - - def __closeFile(self): - self.__openedFiles[:] = [] - self.__fileDirectoryAction.setEnabled(False) - self.__parentFileDirectoryAction.setEnabled(False) - if self.__h5 is not None: - self.__dataModel.removeH5pyObject(self.__h5) - self.__h5.close() - self.__h5 = None - if self.__fabio is not None: - if hasattr(self.__fabio, "close"): - self.__fabio.close() - self.__fabio = None - - def __openFabioFile(self, filename): - self.__closeFile() - try: - self.__fabio = fabio.open(filename) - self.__openedFiles.append(self.__fabio) - self.__selectedFile = filename - except Exception as e: - _logger.error("Error while loading file %s: %s", filename, e.args[0]) - _logger.debug("Backtrace", exc_info=True) - self.__errorWhileLoadingFile = filename, e.args[0] - return False - else: - return True - - def __openSilxFile(self, filename): - self.__closeFile() - try: - self.__h5 = silx.io.open(filename) - self.__openedFiles.append(self.__h5) - self.__selectedFile = filename - except IOError as e: - _logger.error("Error while loading file %s: %s", filename, e.args[0]) - _logger.debug("Backtrace", exc_info=True) - self.__errorWhileLoadingFile = filename, e.args[0] - return False - else: - self.__fileDirectoryAction.setEnabled(True) - self.__parentFileDirectoryAction.setEnabled(True) - self.__dataModel.insertH5pyObject(self.__h5) - return True - - def __isSilxHavePriority(self, filename): - """Silx have priority when there is a specific decoder - """ - _, ext = os.path.splitext(filename) - ext = "*%s" % ext - formats = silx.io.supported_extensions(flat_formats=False) - for extensions in formats.values(): - if ext in extensions: - return True - return False - - def __openFile(self, filename): - codec = self.__fileTypeCombo.currentCodec() - openners = [] - if codec.is_autodetect(): - if self.__isSilxHavePriority(filename): - openners.append(self.__openSilxFile) - if self._isFabioFilesSupported(): - openners.append(self.__openFabioFile) - else: - if self._isFabioFilesSupported(): - openners.append(self.__openFabioFile) - openners.append(self.__openSilxFile) - elif codec.is_silx_codec(): - openners.append(self.__openSilxFile) - elif self._isFabioFilesSupported() and codec.is_fabio_codec(): - # It is requested to use fabio, anyway fabio is here or not - openners.append(self.__openFabioFile) - - for openner in openners: - ref = openner(filename) - if ref is not None: - return True - return False - - def __fileActivated(self, index): - self.__selectedFile = None - path = self.__fileModel.filePath(index) - if os.path.isfile(path): - loaded = self.__openFile(path) - if loaded: - if self.__h5 is not None: - index = self.__dataModel.indexFromH5Object(self.__h5) - self.__browser.setRootIndex(index) - elif self.__fabio is not None: - data = _FabioData(self.__fabio) - self.__setData(data) - self.__updatePath() - else: - self.__clearData() - - def __dataSelected(self, index): - selectedData = None - if index is not None: - if index.model() is self.__dataModel: - obj = self.__dataModel.data(index, self.__dataModel.H5PY_OBJECT_ROLE) - if self._isDataSupportable(obj): - selectedData = obj - elif index.model() is self.__fileModel: - self.__closeFile() - if self._isFabioFilesSupported(): - path = self.__fileModel.filePath(index) - if os.path.isfile(path): - codec = self.__fileTypeCombo.currentCodec() - is_fabio_decoder = codec.is_fabio_codec() - is_fabio_have_priority = not codec.is_silx_codec() and not self.__isSilxHavePriority(path) - if is_fabio_decoder or is_fabio_have_priority: - # Then it's flat frame container - self.__openFabioFile(path) - if self.__fabio is not None: - selectedData = _FabioData(self.__fabio) - else: - assert(False) - - self.__setData(selectedData) - - def __filterSelected(self, index): - filters = self.__fileTypeCombo.itemExtensions(index) - self.__fileModel.setNameFilters(list(filters)) - - def __setData(self, data): - self.__data = data - - if data is not None and self._isDataSupportable(data): - if self.__selectorWidget is not None: - self.__selectorWidget.setData(data) - if not self.__selectorWidget.isUsed(): - # Needed to fake the fact we have to reset the zoom in preview - self.__selectedData = None - self.__setSelectedData(data) - self.__selectorWidget.hide() - else: - self.__selectorWidget.setVisible(self.__selectorWidget.hasVisibleSelectors()) - # Needed to fake the fact we have to reset the zoom in preview - self.__selectedData = None - self.__selectorWidget.selectionChanged.emit() - else: - # Needed to fake the fact we have to reset the zoom in preview - self.__selectedData = None - self.__setSelectedData(data) - else: - self.__clearData() - self.__updatePath() - - def _isDataSupported(self, data): - """Check if the data can be returned by the dialog. - - If true, this data can be returned by the dialog and the open button - while be enabled. If false the button will be disabled. - - :rtype: bool - """ - raise NotImplementedError() - - def _isDataSupportable(self, data): - """Check if the selected data can be supported at one point. - - If true, the data selector will be checked and it will update the data - preview. Else the selecting is disabled. - - :rtype: bool - """ - raise NotImplementedError() - - def __clearData(self): - """Clear the data part of the GUI""" - if self.__previewWidget is not None: - self.__previewWidget.setData(None) - if self.__selectorWidget is not None: - self.__selectorWidget.setData(None) - self.__selectorWidget.hide() - self.__selectedData = None - self.__data = None - self.__updateDataInfo() - button = self.__buttons.button(qt.QDialogButtonBox.Open) - button.setEnabled(False) - - def __selectorWidgetChanged(self): - data = self.__selectorWidget.getSelectedData(self.__data) - self.__setSelectedData(data) - - def __setSelectedData(self, data): - """Set the data selected by the dialog. - - If :meth:`_isDataSupported` returns false, this function will be - inhibited and no data will be selected. - """ - if isinstance(data, _FabioData): - data = data[()] - if self.__previewWidget is not None: - fromDataSelector = self.__selectedData is not None - self.__previewWidget.setData(data, fromDataSelector=fromDataSelector) - if self._isDataSupported(data): - self.__selectedData = data - else: - self.__clearData() - return - self.__updateDataInfo() - self.__updatePath() - button = self.__buttons.button(qt.QDialogButtonBox.Open) - button.setEnabled(True) - - def __updateDataInfo(self): - if self.__errorWhileLoadingFile is not None: - filename, message = self.__errorWhileLoadingFile - message = "<b>Error while loading file '%s'</b><hr/>%s" % (filename, message) - size = self.__dataInfo.height() - icon = self.style().standardIcon(qt.QStyle.SP_MessageBoxCritical) - pixmap = icon.pixmap(size, size) - - self.__dataInfo.setText("Error while loading file") - self.__dataInfo.setToolTip(message) - self.__dataIcon.setToolTip(message) - self.__dataIcon.setVisible(True) - self.__dataIcon.setPixmap(pixmap) - - self.__errorWhileLoadingFile = None - return - - self.__dataIcon.setVisible(False) - self.__dataInfo.setToolTip("") - if self.__selectedData is None: - self.__dataInfo.setText("No data selected") - else: - text = self._displayedDataInfo(self.__data, self.__selectedData) - self.__dataInfo.setVisible(text is not None) - if text is not None: - self.__dataInfo.setText(text) - - def _displayedDataInfo(self, dataBeforeSelection, dataAfterSelection): - """Returns the text displayed under the data preview. - - This zone is used to display error in case or problem of data selection - or problems with IO. - - :param numpy.ndarray dataAfterSelection: Data as it is after the - selection widget (basically the data from the preview widget) - :param numpy.ndarray dataAfterSelection: Data as it is before the - selection widget (basically the data from the browsing widget) - :rtype: bool - """ - return None - - def __createUrlFromIndex(self, index, useSelectorWidget=True): - if index.model() is self.__fileModel: - filename = self.__fileModel.filePath(index) - dataPath = None - elif index.model() is self.__dataModel: - obj = self.__dataModel.data(index, role=Hdf5TreeModel.H5PY_OBJECT_ROLE) - filename = obj.file.filename - dataPath = obj.name - else: - # root of the computer - filename = "" - dataPath = None - - if useSelectorWidget and self.__selectorWidget is not None and self.__selectorWidget.isUsed(): - slicing = self.__selectorWidget.slicing() - if slicing == tuple(): - slicing = None - else: - slicing = None - - if self.__fabio is not None: - scheme = "fabio" - elif self.__h5 is not None: - scheme = "silx" - else: - if os.path.isfile(filename): - codec = self.__fileTypeCombo.currentCodec() - if codec.is_fabio_codec(): - scheme = "fabio" - elif codec.is_silx_codec(): - scheme = "silx" - else: - scheme = None - else: - scheme = None - - url = silx.io.url.DataUrl(file_path=filename, data_path=dataPath, data_slice=slicing, scheme=scheme) - return url - - def __updatePath(self): - index = self.__browser.selectedIndex() - if index is None: - index = self.__browser.rootIndex() - url = self.__createUrlFromIndex(index) - if url.path() != self.__pathEdit.text(): - old = self.__pathEdit.blockSignals(True) - self.__pathEdit.setText(url.path()) - self.__pathEdit.blockSignals(old) - - def __rootIndexChanged(self, index): - url = self.__createUrlFromIndex(index, useSelectorWidget=False) - - currentUrl = None - if 0 <= self.__currentHistoryLocation < len(self.__currentHistory): - currentUrl = self.__currentHistory[self.__currentHistoryLocation] - - if currentUrl is None or currentUrl != url.path(): - # clean up the forward history - self.__currentHistory = self.__currentHistory[0:self.__currentHistoryLocation + 1] - self.__currentHistory.append(url.path()) - self.__currentHistoryLocation += 1 - - if index.model() != self.__dataModel: - if sys.platform == "win32": - # path == "" - isRoot = not index.isValid() - else: - # path in ["", "/"] - isRoot = not index.isValid() or not index.parent().isValid() - else: - isRoot = False - - if index.isValid(): - self.__dataSelected(index) - self.__toParentAction.setEnabled(not isRoot) - self.__updateActionHistory() - self.__updateSidebar() - - def __updateSidebar(self): - """Called when the current directory location change""" - if self.__sidebar is None: - return - selectionModel = self.__sidebar.selectionModel() - selectionModel.selectionChanged.disconnect(self.__shortcutSelected) - index = self.__browser.rootIndex() - if index.model() == self.__fileModel: - path = self.__fileModel.filePath(index) - self.__sidebar.setSelectedPath(path) - elif index.model() is None: - path = "" - self.__sidebar.setSelectedPath(path) - else: - selectionModel.clear() - selectionModel.selectionChanged.connect(self.__shortcutSelected) - - def __updateActionHistory(self): - self.__forwardAction.setEnabled(len(self.__currentHistory) - 1 > self.__currentHistoryLocation) - self.__backwardAction.setEnabled(self.__currentHistoryLocation > 0) - - def __textChanged(self, text): - self.__pathChanged() - - def _isFabioFilesSupported(self): - """Returns true fabio files can be loaded. - """ - return True - - def _isLoadableUrl(self, url): - """Returns true if the URL is loadable by this dialog. - - :param DataUrl url: The requested URL - """ - return True - - def __pathChanged(self): - url = silx.io.url.DataUrl(path=self.__pathEdit.text()) - if url.is_valid() or url.path() == "": - if url.path() in ["", "/"] or url.file_path() in ["", "/"]: - self.__fileModel_setRootPath(qt.QDir.rootPath()) - elif os.path.exists(url.file_path()): - rootIndex = None - if os.path.isdir(url.file_path()): - self.__fileModel_setRootPath(url.file_path()) - index = self.__fileModel.index(url.file_path()) - elif os.path.isfile(url.file_path()): - if self._isLoadableUrl(url): - if url.scheme() == "silx": - loaded = self.__openSilxFile(url.file_path()) - elif url.scheme() == "fabio" and self._isFabioFilesSupported(): - loaded = self.__openFabioFile(url.file_path()) - else: - loaded = self.__openFile(url.file_path()) - else: - loaded = False - if loaded: - if self.__h5 is not None: - rootIndex = self.__dataModel.indexFromH5Object(self.__h5) - elif self.__fabio is not None: - index = self.__fileModel.index(url.file_path()) - rootIndex = index - if rootIndex is None: - index = self.__fileModel.index(url.file_path()) - index = index.parent() - - if rootIndex is not None: - if rootIndex.model() == self.__dataModel: - if url.data_path() is not None: - dataPath = url.data_path() - if dataPath in self.__h5: - obj = self.__h5[dataPath] - else: - path = utils.findClosestSubPath(self.__h5, dataPath) - if path is None: - path = "/" - obj = self.__h5[path] - - if silx.io.is_file(obj): - self.__browser.setRootIndex(rootIndex) - elif silx.io.is_group(obj): - index = self.__dataModel.indexFromH5Object(obj) - self.__browser.setRootIndex(index) - else: - index = self.__dataModel.indexFromH5Object(obj) - self.__browser.setRootIndex(index.parent()) - self.__browser.selectIndex(index) - else: - self.__browser.setRootIndex(rootIndex) - self.__clearData() - elif rootIndex.model() == self.__fileModel: - # that's a fabio file - self.__browser.setRootIndex(rootIndex.parent()) - self.__browser.selectIndex(rootIndex) - # data = _FabioData(self.__fabio) - # self.__setData(data) - else: - assert(False) - else: - self.__browser.setRootIndex(index, model=self.__fileModel) - self.__clearData() - - if self.__selectorWidget is not None: - self.__selectorWidget.selectSlicing(url.data_slice()) - else: - self.__errorWhileLoadingFile = (url.file_path(), "File not found") - self.__clearData() - else: - self.__errorWhileLoadingFile = (url.file_path(), "Path invalid") - self.__clearData() - - def previewToolbar(self): - return self.__previewToolbar - - def previewWidget(self): - return self.__previewWidget - - def selectorWidget(self): - return self.__selectorWidget - - def _createPreviewToolbar(self, parent, dataPreviewWidget, dataSelectorWidget): - return None - - def _createPreviewWidget(self, parent): - return None - - def _createSelectorWidget(self, parent): - return None - - # Selected file - - def setDirectory(self, path): - """Sets the data dialog's current directory.""" - self.__fileModel_setRootPath(path) - - def selectedFile(self): - """Returns the file path containing the selected data. - - :rtype: str - """ - return self.__selectedFile - - def selectFile(self, filename): - """Sets the data dialog's current file.""" - self.__directoryLoadedFilter = "" - old = self.__pathEdit.blockSignals(True) - try: - self.__pathEdit.setText(filename) - finally: - self.__pathEdit.blockSignals(old) - self.__pathChanged() - - # Selected data - - def selectUrl(self, url): - """Sets the data dialog's current data url. - - :param Union[str,DataUrl] url: URL identifying a data (it can be a - `DataUrl` object) - """ - if isinstance(url, silx.io.url.DataUrl): - url = url.path() - self.__directoryLoadedFilter = "" - old = self.__pathEdit.blockSignals(True) - try: - self.__pathEdit.setText(url) - finally: - self.__pathEdit.blockSignals(old) - self.__pathChanged() - - def selectedUrl(self): - """Returns the URL from the file system to the data. - - If the dialog is not validated, the path can be an intermediat - selected path, or an invalid path. - - :rtype: str - """ - return self.__pathEdit.text() - - def selectedDataUrl(self): - """Returns the URL as a :class:`DataUrl` from the file system to the - data. - - If the dialog is not validated, the path can be an intermediat - selected path, or an invalid path. - - :rtype: DataUrl - """ - url = self.selectedUrl() - return silx.io.url.DataUrl(url) - - def directory(self): - """Returns the path from the current browsed directory. - - :rtype: str - """ - if self.__directory is not None: - # At post execution, returns the cache - return self.__directory - - index = self.__browser.rootIndex() - if index.model() is self.__fileModel: - path = self.__fileModel.filePath(index) - return path - elif index.model() is self.__dataModel: - path = os.path.dirname(self.__h5.file.filename) - return path - else: - return "" - - def _selectedData(self): - """Returns the internal selected data - - :rtype: numpy.ndarray - """ - return self.__selectedData - - # Filters - - def selectedNameFilter(self): - """Returns the filter that the user selected in the file dialog.""" - return self.__fileTypeCombo.currentText() - - # History - - def history(self): - """Returns the browsing history of the filedialog as a list of paths. - - :rtype: List<str> - """ - if len(self.__currentHistory) <= 1: - return [] - history = self.__currentHistory[0:self.__currentHistoryLocation] - return list(history) - - def setHistory(self, history): - self.__currentHistory = [] - self.__currentHistory.extend(history) - self.__currentHistoryLocation = len(self.__currentHistory) - 1 - self.__updateActionHistory() - - # Colormap - - def colormap(self): - if self.__previewWidget is None: - return None - return self.__previewWidget.colormap() - - def setColormap(self, colormap): - if self.__previewWidget is None: - raise RuntimeError("No preview widget defined") - self.__previewWidget.setColormap(colormap) - - # Sidebar - - def setSidebarUrls(self, urls): - """Sets the urls that are located in the sidebar.""" - if self.__sidebar is None: - return - self.__sidebar.setUrls(urls) - - def sidebarUrls(self): - """Returns a list of urls that are currently in the sidebar.""" - if self.__sidebar is None: - return [] - return self.__sidebar.urls() - - # State - - __serialVersion = 1 - """Store the current version of the serialized data""" - - @classmethod - def qualifiedName(cls): - return "%s.%s" % (cls.__module__, cls.__name__) - - def restoreState(self, state): - """Restores the dialogs's layout, history and current directory to the - state specified. - - :param qt.QByteArray state: Stream containing the new state - :rtype: bool - """ - stream = qt.QDataStream(state, qt.QIODevice.ReadOnly) - - qualifiedName = stream.readQString() - if qualifiedName != self.qualifiedName(): - _logger.warning("Stored state contains an invalid qualified name. %s restoration cancelled.", self.__class__.__name__) - return False - - version = stream.readInt32() - if version != self.__serialVersion: - _logger.warning("Stored state contains an invalid version. %s restoration cancelled.", self.__class__.__name__) - return False - - result = True - - splitterData = stream.readQVariant() - sidebarUrls = stream.readQStringList() - history = stream.readQStringList() - workingDirectory = stream.readQString() - browserData = stream.readQVariant() - viewMode = stream.readInt32() - colormapData = stream.readQVariant() - - result &= self.__splitter.restoreState(splitterData) - sidebarUrls = [qt.QUrl(s) for s in sidebarUrls] - self.setSidebarUrls(list(sidebarUrls)) - history = [s for s in history] - self.setHistory(list(history)) - if workingDirectory is not None: - self.setDirectory(workingDirectory) - result &= self.__browser.restoreState(browserData) - self.setViewMode(viewMode) - colormap = self.colormap() - if colormap is not None: - result &= self.colormap().restoreState(colormapData) - - return result - - def saveState(self): - """Saves the state of the dialog's layout, history and current - directory. - - :rtype: qt.QByteArray - """ - data = qt.QByteArray() - stream = qt.QDataStream(data, qt.QIODevice.WriteOnly) - - s = self.qualifiedName() - stream.writeQString(u"%s" % s) - stream.writeInt32(self.__serialVersion) - stream.writeQVariant(self.__splitter.saveState()) - strings = [u"%s" % s.toString() for s in self.sidebarUrls()] - stream.writeQStringList(strings) - strings = [u"%s" % s for s in self.history()] - stream.writeQStringList(strings) - stream.writeQString(u"%s" % self.directory()) - stream.writeQVariant(self.__browser.saveState()) - stream.writeInt32(self.viewMode()) - colormap = self.colormap() - if colormap is not None: - stream.writeQVariant(self.colormap().saveState()) - else: - stream.writeQVariant(None) - - return data diff --git a/silx/gui/dialog/ColormapDialog.py b/silx/gui/dialog/ColormapDialog.py deleted file mode 100644 index ca7ee97..0000000 --- a/silx/gui/dialog/ColormapDialog.py +++ /dev/null @@ -1,1771 +0,0 @@ -# coding: utf-8 -# /*########################################################################## -# -# Copyright (c) 2004-2020 European Synchrotron Radiation Facility -# -# Permission is hereby granted, free of charge, to any person obtaining a copy -# of this software and associated documentation files (the "Software"), to deal -# in the Software without restriction, including without limitation the rights -# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell -# copies of the Software, and to permit persons to whom the Software is -# furnished to do so, subject to the following conditions: -# -# The above copyright notice and this permission notice shall be included in -# all copies or substantial portions of the Software. -# -# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN -# THE SOFTWARE. -# -# ###########################################################################*/ -"""A QDialog widget to set-up the colormap. - -It uses a description of colormaps as dict compatible with :class:`Plot`. - -To run the following sample code, a QApplication must be initialized. - -Create the colormap dialog and set the colormap description and data range: - ->>> from silx.gui.dialog.ColormapDialog import ColormapDialog ->>> from silx.gui.colors import Colormap - ->>> dialog = ColormapDialog() ->>> colormap = Colormap(name='red', normalization='log', -... vmin=1., vmax=2.) - ->>> dialog.setColormap(colormap) ->>> colormap.setVRange(1., 100.) # This scale the width of the plot area ->>> dialog.show() - -Get the colormap description (compatible with :class:`Plot`) from the dialog: - ->>> cmap = dialog.getColormap() ->>> cmap.getName() -'red' - -It is also possible to display an histogram of the image in the dialog. -This updates the data range with the range of the bins. - ->>> import numpy ->>> image = numpy.random.normal(size=512 * 512).reshape(512, -1) ->>> hist, bin_edges = numpy.histogram(image, bins=10) ->>> dialog.setHistogram(hist, bin_edges) - -The updates of the colormap description are also available through the signal: -:attr:`ColormapDialog.sigColormapChanged`. -""" # noqa - -__authors__ = ["V.A. Sole", "T. Vincent", "H. Payno"] -__license__ = "MIT" -__date__ = "08/12/2020" - -import enum -import logging - -import numpy - -from .. import qt -from .. import utils -from ..colors import Colormap, cursorColorForColormap -from ..plot import PlotWidget -from ..plot.items.axis import Axis -from ..plot.items import BoundingRect -from silx.gui.widgets.FloatEdit import FloatEdit -import weakref -from silx.math.combo import min_max -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.math.histogram import Histogramnd -from silx.utils import deprecation -from silx.gui.plot.items.roi import RectangleROI -from silx.gui.plot.tools.roi import RegionOfInterestManager - -_logger = logging.getLogger(__name__) - -_colormapIconPreview = {} - - -class _DataRefHolder(items.Item, items.ColormapMixIn): - """Holder for a weakref of a numpy array. - - It provides features from `ColormapMixIn`. - """ - - def __init__(self, dataRef): - items.Item.__init__(self) - items.ColormapMixIn.__init__(self) - self.__dataRef = dataRef - self._updated(items.ItemChangedType.DATA) - - def getColormappedData(self, copy=True): - return self.__dataRef() - - -class _BoundaryWidget(qt.QWidget): - """Widget to edit a boundary of the colormap (vmin or vmax)""" - - sigAutoScaleChanged = qt.Signal(object) - """Signal emitted when the autoscale was changed - - True is sent as an argument if autoscale is set to true. - """ - - sigValueChanged = qt.Signal(object) - """Signal emitted when value is changed - - The new value is sent as an argument. - """ - - def __init__(self, parent=None, value=0.0): - qt.QWidget.__init__(self, parent=parent) - self.setLayout(qt.QHBoxLayout()) - self.layout().setContentsMargins(0, 0, 0, 0) - self._numVal = FloatEdit(parent=self, value=value) - self.layout().addWidget(self._numVal) - self._autoCB = qt.QCheckBox('auto', parent=self) - self.layout().addWidget(self._autoCB) - self._autoCB.setChecked(False) - self._autoCB.setVisible(False) - - self._autoCB.toggled.connect(self._autoToggled) - self._numVal.textEdited.connect(self.__textEdited) - self._numVal.editingFinished.connect(self.__editingFinished) - self.setFocusProxy(self._numVal) - - self.__textWasEdited = False - """True if the text was edited, in order to send an event - at the end of the user interaction""" - - self.__realValue = None - """Store the real value set by setValue, to avoid - rounding of the widget""" - - def __textEdited(self): - self.__textWasEdited = True - - def __editingFinished(self): - if self.__textWasEdited: - value = self._numVal.value() - self.__realValue = value - with utils.blockSignals(self._numVal): - # Fix the formatting - self._numVal.setValue(self.__realValue) - self.sigValueChanged.emit(value) - self.__textWasEdited = False - - def isAutoChecked(self): - return self._autoCB.isChecked() - - def getValue(self): - """Returns the stored range. If autoscale is - enabled, this returns None. - """ - if self._autoCB.isChecked(): - return None - if self.__realValue is not None: - return self.__realValue - return self._numVal.value() - - def _autoToggled(self, enabled): - self._numVal.setEnabled(not enabled) - self._updateDisplayedText() - self.sigAutoScaleChanged.emit(enabled) - - def _updateDisplayedText(self): - self.__textWasEdited = False - if self._autoCB.isChecked() and self.__realValue is not None: - with utils.blockSignals(self._numVal): - self._numVal.setValue(self.__realValue) - - def setValue(self, value, isAuto=False): - """Set the value of the boundary. - - :param float value: A finite value for the boundary - :param bool isAuto: If true, the finite value was automatically computed - from the data, else it is a fixed custom value. - """ - assert value is not None - self._autoCB.setChecked(isAuto) - with utils.blockSignals(self._numVal): - if isAuto or self.__realValue != value: - if not self.__textWasEdited: - self._numVal.setValue(value) - self.__realValue = value - self._numVal.setEnabled(not isAuto) - - -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"), - } - - def __init__(self, parent: qt.QWidget): - super(_AutoscaleModeComboBox, self).__init__(parent=parent) - self.currentIndexChanged.connect(self.__updateTooltip) - self._init() - - def _init(self): - for mode in Colormap.AUTOSCALE_MODES: - label, tooltip = self.DATA.get(mode, (mode, None)) - self.addItem(label, mode) - if tooltip is not None: - self.setItemData(self.count() - 1, tooltip, qt.Qt.ToolTipRole) - - def setCurrentIndex(self, index): - self.__updateTooltip(index) - super(_AutoscaleModeComboBox, self).setCurrentIndex(index) - - def __updateTooltip(self, index): - if index > -1: - tooltip = self.itemData(index, qt.Qt.ToolTipRole) - else: - tooltip = "" - self.setToolTip(tooltip) - - def currentMode(self): - index = self.currentIndex() - return self.itemData(index) - - def setCurrentMode(self, mode): - for index in range(self.count()): - if mode == self.itemData(index): - self.setCurrentIndex(index) - return - if mode is None: - # If None was not a value - self.setCurrentIndex(-1) - return - self.addItem(mode, mode) - self.setCurrentIndex(self.count() - 1) - - -class _AutoScaleButtons(qt.QWidget): - - 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): - 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() - - -@enum.unique -class _DataInPlotMode(enum.Enum): - """Enum for each mode of display of the data in the plot.""" - RANGE = 'range' - HISTOGRAM = 'histogram' - - -class _ColormapHistogram(qt.QWidget): - """Display the colormap and the data as a plot.""" - - sigRangeMoving = qt.Signal(object, object) - """Emitted when a mouse interaction moves the location - of the colormap range in the plot. - - 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 - """ - - sigRangeMoved = qt.Signal(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 - """ - - def __init__(self, parent): - qt.QWidget.__init__(self, parent=parent) - self._dataInPlotMode = _DataInPlotMode.RANGE - self._finiteRange = None, None - self._initPlot() - - self._histogramData = {} - """Histogram displayed in the plot""" - - self._dragging = False, False - """True, if the min or the max handle is dragging""" - - self._dataRange = {} - """Histogram displayed in the plot""" - - self._invalidated = False - - def paintEvent(self, event): - if self._invalidated: - self._updateDataInPlot() - self._invalidated = False - self._updateMarkerPosition() - return super(_ColormapHistogram, self).paintEvent(event) - - def getFiniteRange(self): - """Returns the colormap range as displayed in the plot.""" - return self._finiteRange - - def setFiniteRange(self, vRange): - """Set the colormap range to use in the plot. - - Here there is no concept of auto. The values should - not be None, except if there is no range or marker - to display. - """ - # Do not reset the limit for handle about to be dragged - if self._dragging[0]: - vRange = self._finiteRange[0], vRange[1] - if self._dragging[1]: - vRange = vRange[0], self._finiteRange[1] - - if vRange == self._finiteRange: - return - - self._finiteRange = vRange - self.update() - - def getColormap(self): - return self.parent().getColormap() - - def _getNormalizedHistogram(self): - """Return an histogram already normalized according to the colormap - normalization. - - Returns a tuple edges, counts - """ - norm = self._getNorm() - histogram = self._histogramData.get(norm, None) - if histogram is None: - histogram = self._computeNormalizedHistogram() - self._histogramData[norm] = histogram - return histogram - - def _computeNormalizedHistogram(self): - colormap = self.getColormap() - if colormap is None: - norm = Colormap.LINEAR - else: - norm = colormap.getNormalization() - - # Try to use the histogram defined in the dialog - histo = self.parent()._getHistogram() - if histo is not None: - counts, edges = histo - normalizer = Colormap(normalization=norm)._getNormalizer() - mask = normalizer.isValid(edges[:-1]) # Check lower bin edges only - firstValid = numpy.argmax(mask) # edges increases monotonically - if firstValid == 0: # Mask is all False or all True - return (counts, edges) if mask[0] else (None, None) - else: # Clip to valid values - return counts[firstValid:], edges[firstValid:] - - data = self.parent()._getArray() - if data is None: - return None, None - dataRange = self._getNormalizedDataRange() - if dataRange[0] is None or dataRange[1] is None: - return None, None - counts, edges = self.parent().computeHistogram(data, scale=norm, dataRange=dataRange) - return counts, edges - - def _getNormalizedDataRange(self): - """Return a data range already normalized according to the colormap - normalization. - - Returns a tuple with min and max - """ - norm = self._getNorm() - dataRange = self._dataRange.get(norm, None) - if dataRange is None: - dataRange = self._computeNormalizedDataRange() - self._dataRange[norm] = dataRange - return dataRange - - def _computeNormalizedDataRange(self): - colormap = self.getColormap() - if colormap is None: - norm = Colormap.LINEAR - else: - norm = colormap.getNormalization() - - # Try to use the one defined in the dialog - dataRange = self.parent()._getDataRange() - if dataRange is not None: - if norm in (Colormap.LINEAR, Colormap.GAMMA, Colormap.ARCSINH): - return dataRange[0], dataRange[2] - elif norm == Colormap.LOGARITHM: - return dataRange[1], dataRange[2] - elif norm == Colormap.SQRT: - return dataRange[1], dataRange[2] - else: - _logger.error("Undefined %s normalization", norm) - - # Try to use the histogram defined in the dialog - histo = self.parent()._getHistogram() - if histo is not None: - _histo, edges = histo - normalizer = Colormap(normalization=norm)._getNormalizer() - edges = edges[normalizer.isValid(edges)] - if edges.size == 0: - return None, None - else: - dataRange = min_max(edges, finite=True) - return dataRange.minimum, dataRange.maximum - - item = self.parent()._getItem() - if item is not None: - # Trick to reach data range using colormap cache - cm = Colormap() - cm.setVRange(None, None) - cm.setNormalization(norm) - dataRange = item._getColormapAutoscaleRange(cm) - return dataRange - - # If there is no item, there is no data - return None, None - - def _getDisplayableRange(self): - """Returns the selected min/max range to apply to the data, - according to the used scale. - - One or both limits can be None in case it is not displayable in the - current axes scale. - - :returns: Tuple{float, float} - """ - scale = self._plot.getXAxis().getScale() - - def isDisplayable(pos): - if pos is None: - return False - if scale == Axis.LOGARITHMIC: - return pos > 0.0 - return True - - posMin, posMax = self.getFiniteRange() - if not isDisplayable(posMin): - posMin = None - if not isDisplayable(posMax): - posMax = None - - return posMin, posMax - - 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.getXAxis().setLabel("Data Values") - self._plot.getYAxis().setLabel("") - self._plot.setInteractiveMode('select', zoomOnWheel=False) - self._plot.setActiveCurveHandling(False) - self._plot.setMinimumSize(qt.QSize(250, 200)) - self._plot.sigPlotSignal.connect(self._plotEventReceived) - palette = self.palette() - color = palette.color(qt.QPalette.Normal, qt.QPalette.Window) - self._plot.setBackgroundColor(color) - self._plot.setDataBackgroundColor("white") - - lut = numpy.arange(256) - lut.shape = 1, -1 - self._plot.addImage(lut, legend='lut') - self._lutItem = self._plot._getItem("image", "lut") - self._lutItem.setVisible(False) - - self._plot.addScatter(x=[], y=[], value=[], legend='lut2') - self._lutItem2 = self._plot._getItem("scatter", "lut2") - self._lutItem2.setVisible(False) - self.__lutY = numpy.array([-0.05] * 256) - self.__lutV = numpy.arange(256) - - self._bound = BoundingRect() - self._plot.addItem(self._bound) - self._bound.setVisible(True) - - # Add plot for histogram - self._plotToolbar = qt.QToolBar(self) - self._plotToolbar.setFloatable(False) - self._plotToolbar.setMovable(False) - self._plotToolbar.setIconSize(qt.QSize(8, 8)) - self._plotToolbar.setStyleSheet("QToolBar { border: 0px }") - self._plotToolbar.setOrientation(qt.Qt.Vertical) - - group = qt.QActionGroup(self._plotToolbar) - group.setExclusive(True) - - action = qt.QAction("Data range", self) - action.setToolTip("Display the data range within the colormap range. A fast data processing have to be done.") - action.setIcon(icons.getQIcon('colormap-range')) - action.setCheckable(True) - action.setData(_DataInPlotMode.RANGE) - action.setChecked(action.data() == self._dataInPlotMode) - self._plotToolbar.addAction(action) - group.addAction(action) - action = qt.QAction("Histogram", self) - action.setToolTip("Display the data histogram within the colormap range. A slow data processing have to be done. ") - action.setIcon(icons.getQIcon('colormap-histogram')) - action.setCheckable(True) - action.setData(_DataInPlotMode.HISTOGRAM) - action.setChecked(action.data() == self._dataInPlotMode) - self._plotToolbar.addAction(action) - group.addAction(action) - group.triggered.connect(self._displayDataInPlotModeChanged) - - plotBoxLayout = qt.QHBoxLayout() - plotBoxLayout.setContentsMargins(0, 0, 0, 0) - plotBoxLayout.setSpacing(2) - plotBoxLayout.addWidget(self._plotToolbar) - plotBoxLayout.addWidget(self._plot) - plotBoxLayout.setSizeConstraint(qt.QLayout.SetMinimumSize) - self.setLayout(plotBoxLayout) - - def _plotEventReceived(self, event): - """Handle events from the plot""" - kind = event['event'] - - if kind == 'markerMoving': - value = event['xdata'] - if event['label'] == 'Min': - self._dragging = True, False - self._finiteRange = value, self._finiteRange[1] - self._last = value, None - self.sigRangeMoving.emit(*self._last) - elif event['label'] == 'Max': - self._dragging = False, True - self._finiteRange = self._finiteRange[0], value - self._last = 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 - else: - pass - - def _updateMarkerPosition(self): - colormap = self.getColormap() - posMin, posMax = self._getDisplayableRange() - - if colormap is None: - isDraggable = False - else: - isDraggable = colormap.isEditable() - - with utils.blockSignals(self): - if posMin is not None and not self._dragging[0]: - self._plot.addXMarker( - posMin, - legend='Min', - text='Min', - draggable=isDraggable, - color="blue", - constraint=self._plotMinMarkerConstraint) - if posMax is not None and not self._dragging[1]: - self._plot.addXMarker( - posMax, - legend='Max', - text='Max', - draggable=isDraggable, - color="blue", - constraint=self._plotMaxMarkerConstraint) - - self._updateLutItem((posMin, posMax)) - self._plot.resetZoom() - - def _updateLutItem(self, vRange): - colormap = self.getColormap() - if colormap is None: - return - - if vRange is None: - posMin, posMax = self._getDisplayableRange() - else: - posMin, posMax = vRange - if posMin is None or posMax is None: - self._lutItem.setVisible(False) - pos = posMax if posMin is None else posMin - if pos is not None: - self._bound.setBounds((pos, pos, -0.1, 0)) - else: - self._bound.setBounds((0, 0, -0.1, 0)) - else: - norm = colormap.getNormalization() - normColormap = colormap.copy() - normColormap.setVRange(0, 255) - normColormap.setNormalization(Colormap.LINEAR) - if norm == Colormap.LINEAR: - scale = (posMax - posMin) / 256 - self._lutItem.setColormap(normColormap) - self._lutItem.setOrigin((posMin, -0.09)) - self._lutItem.setScale((scale, 0.08)) - self._lutItem.setVisible(True) - self._lutItem2.setVisible(False) - elif norm == Colormap.LOGARITHM: - self._lutItem2.setVisible(False) - self._lutItem2.setColormap(normColormap) - xx = numpy.geomspace(posMin, posMax, 256) - self._lutItem2.setData(x=xx, - y=self.__lutY, - value=self.__lutV, - copy=False) - self._lutItem2.setSymbol("|") - self._lutItem2.setVisible(True) - self._lutItem.setVisible(False) - else: - # Fallback: Display with linear axis and applied normalization - self._lutItem2.setVisible(False) - normColormap.setNormalization(norm) - self._lutItem2.setColormap(normColormap) - xx = numpy.linspace(posMin, posMax, 256, endpoint=True) - self._lutItem2.setData( - x=xx, - y=self.__lutY, - value=self.__lutV, - copy=False) - self._lutItem2.setSymbol("|") - self._lutItem2.setVisible(True) - self._lutItem.setVisible(False) - - self._bound.setBounds((posMin, posMax, -0.1, 1)) - - def _plotMinMarkerConstraint(self, x, y): - """Constraint of the min marker""" - _vmin, vmax = self.getFiniteRange() - if vmax is None: - return x, y - return min(x, vmax), y - - def _plotMaxMarkerConstraint(self, x, y): - """Constraint of the max marker""" - vmin, _vmax = self.getFiniteRange() - if vmin is None: - return x, y - return max(x, vmin), y - - def _setDataInPlotMode(self, mode): - if self._dataInPlotMode == mode: - return - self._dataInPlotMode = mode - self._updateDataInPlot() - - def _displayDataInPlotModeChanged(self, action): - mode = action.data() - self._setDataInPlotMode(mode) - - def invalidateData(self): - self._histogramData = {} - self._dataRange = {} - self._invalidated = True - self.update() - - def _updateDataInPlot(self): - mode = self._dataInPlotMode - - norm = self._getNorm() - if norm == Colormap.LINEAR: - scale = Axis.LINEAR - elif norm == Colormap.LOGARITHM: |