diff options
Diffstat (limited to 'silx/gui/_glutils/OpenGLWidget.py')
-rw-r--r-- | silx/gui/_glutils/OpenGLWidget.py | 409 |
1 files changed, 409 insertions, 0 deletions
diff --git a/silx/gui/_glutils/OpenGLWidget.py b/silx/gui/_glutils/OpenGLWidget.py new file mode 100644 index 0000000..6cbf8f0 --- /dev/null +++ b/silx/gui/_glutils/OpenGLWidget.py @@ -0,0 +1,409 @@ +# coding: utf-8 +# /*########################################################################## +# +# Copyright (c) 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 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__ = "26/07/2017" + + +import logging +import sys + +from .. import qt +from .._glutils import gl + + +_logger = logging.getLogger(__name__) + + +# Probe OpenGL availability and widget +ERROR = '' # Error message from probing Qt OpenGL support +_BaseOpenGLWidget = None # Qt OpenGL widget to use + +if hasattr(qt, 'QOpenGLWidget'): # PyQt>=5.4 + _logger.info('Using QOpenGLWidget') + _BaseOpenGLWidget = qt.QOpenGLWidget + +elif not qt.HAS_OPENGL: # QtOpenGL not installed + ERROR = '%s.QtOpenGL not available' % qt.BINDING + +elif qt.QApplication.instance() and not qt.QGLFormat.hasOpenGL(): + # qt.QGLFormat.hasOpenGL MUST be called with a QApplication created + # so this is only checked if the QApplication is already created + ERROR = 'Qt reports OpenGL not available' + +else: + _logger.info('Using QGLWidget') + _BaseOpenGLWidget = qt.QGLWidget + + +# Internal class wrapping Qt OpenGL widget +if _BaseOpenGLWidget is None: + _logger.error('OpenGL-based widget disabled: %s', ERROR) + _OpenGLWidget = None + +else: + 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_) + + + 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(): + version = gl.glGetString(gl.GL_VERSION) + 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 == 'PyQt5': + 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) + + if _OpenGLWidget is None: + self.__openGLWidget = None + label = self._createErrorQLabel(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 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: + return self.__openGLWidget.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 |