diff options
Diffstat (limited to 'src/silx/gui/_glutils/OpenGLWidget.py')
-rw-r--r-- | src/silx/gui/_glutils/OpenGLWidget.py | 445 |
1 files changed, 445 insertions, 0 deletions
diff --git a/src/silx/gui/_glutils/OpenGLWidget.py b/src/silx/gui/_glutils/OpenGLWidget.py new file mode 100644 index 0000000..59fa4f0 --- /dev/null +++ b/src/silx/gui/_glutils/OpenGLWidget.py @@ -0,0 +1,445 @@ +# /*########################################################################## +# +# Copyright (c) 2017-2023 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.Widget, + ): + # 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 + + 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.Widget, + ): + 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) + return + + 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.0 + 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 None: + return 96.0 * self.getDevicePixelRatio() + + physicalDPI = screen.physicalDotsPerInch() + if physicalDPI > 1000.0: + _logger.error( + "Reported screen DPI too high: %f, using default value instead", + physicalDPI, + ) + physicalDPI = 96.0 + return physicalDPI * self.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: + # 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 |