import os import sys from pymol2 import SingletonPyMOL as PyMOL import pymol from pymol.Qt import QtCore from pymol.Qt import QtGui from pymol.Qt import QtWidgets Gesture = QtCore.QEvent.Gesture Qt = QtCore.Qt from .keymapping import get_modifiers from .keymapping import get_wheel_button # don't import the heavy OpenGL (PyOpenGL) module from pymol._cmd import glViewport # QOpenGLWidget is supposed to supersede QGLWidget, but has issues (e.g. # no stereo support) USE_QOPENGLWIDGET = int( os.getenv("PYMOL_USE_QOPENGLWIDGET") or (QtCore.QT_VERSION >= 0x50400 and pymol.IS_MACOS or QtCore.QT_VERSION >= 0x60000)) if USE_QOPENGLWIDGET: BaseGLWidget = QtWidgets.QOpenGLWidget AUTO_DETECT_STEREO = False else: from pymol.Qt import QtOpenGL BaseGLWidget = QtOpenGL.QGLWidget # only attempt stereo detection in Qt <= 5.6 (with 5.9+ on Linux I # get GL_DOUBLEBUFFER=0 with flickering when requesting stereo) AUTO_DETECT_STEREO = pymol.IS_WINDOWS or QtCore.QT_VERSION < 0x50700 class PyMOLGLWidget(BaseGLWidget): ''' PyMOL OpenGL Widget Can be used as a context manager to make OpenGL context current. ''' # mouse button map _buttonMap = { Qt.LeftButton: 0, Qt.MidButton: 1, Qt.RightButton: 2, } def __enter__(self): ''' Context manager to make the OpenGL context "current". Fixes depth-buffer issue with QOpenGLWidget https://github.com/schrodinger/pymol-open-source/issues/25 ''' self.makeCurrent() pymol._cmd._pushValidContext(self.cmd._COb) def __exit__(self, exc_type, exc_value, traceback): pymol._cmd._popValidContext(self.cmd._COb) def __init__(self, parent): self.gui = parent self.fb_scale = 1.0 # OpenGL context setup if USE_QOPENGLWIDGET: f = QtGui.QSurfaceFormat() else: f = QtOpenGL.QGLFormat() from pymol.invocation import options # logic equivalent to layer5/main.cpp:launch if options.multisample: f.setSamples(4) if options.force_stereo != -1: # See layer1/Setting.h for stereo modes if options.stereo_mode in (1, 12) or ( options.stereo_mode == 0 and AUTO_DETECT_STEREO): f.setStereo(True) if options.stereo_mode in (11, 12) and not USE_QOPENGLWIDGET: f.setAccum(True) if USE_QOPENGLWIDGET: super(PyMOLGLWidget, self).__init__(parent=parent) self.setFormat(f) self.setUpdateBehavior(QtWidgets.QOpenGLWidget.PartialUpdate) else: super(PyMOLGLWidget, self).__init__(f, parent=parent) # pymol instance self.pymol = PyMOL() self.pymol.start() self.cmd = self.pymol.cmd # capture python output for feedback import pcatch pcatch._install() # for passive move drag self.setMouseTracking(True) # for accepting keyboard input (command line, shortcuts) self.setFocusPolicy(Qt.ClickFocus) # for idle rendering self._timer = QtCore.QTimer() self._timer.setSingleShot(True) self._timer.timeout.connect(self._pymolProcess) # drag n drop self.setAcceptDrops(True) # pinch-zoom self.grabGesture(Qt.PinchGesture) def sizeHint(self): # default 640 + internal_gui, 480 + internal_feedback return QtCore.QSize(860, 498) ########################## # Input Events ########################## def event(self, ev): if ev.type() == Gesture: return self.gestureEvent(ev) return super(PyMOLGLWidget, self).event(ev) def gestureEvent(self, ev): gesture = ev.gesture(Qt.PinchGesture) if gesture is None: return False if gesture.state() == Qt.GestureStarted: self.pinch_start_z = self.cmd.get_view()[11] changeFlags = gesture.changeFlags() if changeFlags & QtWidgets.QPinchGesture.RotationAngleChanged: delta = gesture.lastRotationAngle() - gesture.rotationAngle() self.cmd.turn('z', delta) if changeFlags & QtWidgets.QPinchGesture.ScaleFactorChanged: view = list(self.cmd.get_view()) # best guess for https://bugreports.qt.io/browse/QTBUG-48138 totalscalefactor = gesture.totalScaleFactor() if totalscalefactor == 1.0: totalscalefactor = gesture.scaleFactor() z = self.pinch_start_z / totalscalefactor delta = z - view[11] view[11] = z view[15] -= delta view[16] -= delta self.cmd.set_view(view) return True def _event_x_y_mod(self, ev): pos = ev.position() if hasattr(ev, "position") else ev.pos() return ( int(self.fb_scale * pos.x()), int(self.fb_scale * (self.height() - pos.y())), get_modifiers(ev), ) def mouseMoveEvent(self, ev): self.pymol.drag(*self._event_x_y_mod(ev)) def mousePressEvent(self, ev, state=0): if ev.button() not in self._buttonMap: return self.pymol.button(self._buttonMap[ev.button()], state, *self._event_x_y_mod(ev)) def mouseReleaseEvent(self, ev): self.mousePressEvent(ev, 1) def wheelEvent(self, ev): button = get_wheel_button(ev) if not button: return args = self._event_x_y_mod(ev) self.pymol.button(button, 0, *args) self.pymol.button(button, 1, *args) ########################## # OpenGL ########################## def paintGL(self): if not USE_QOPENGLWIDGET: glViewport(0, 0, int(self.fb_scale * self.width()), int(self.fb_scale * self.height())) self.pymol.draw() self._timer.start(0) def resizeGL(self, w, h): if USE_QOPENGLWIDGET: w = int(w * self.fb_scale) h = int(h * self.fb_scale) self.pymol.reshape(w, h, True) def updateFbScale(self, context): '''Update PyMOL's display scale factor from the window or screen context @type context: QWindow or QScreen ''' self.fb_scale = context.devicePixelRatio() try: self.cmd.set('display_scale_factor', int(self.fb_scale)) except BaseException as e: # fails with modal draw (mpng ..., modal=1) print(e) def initializeGL(self): # Scale framebuffer for Retina displays try: window = self.windowHandle() # QOpenGLWidget workaround if window is None: window = self.parent().windowHandle() self.updateFbScale(window) window.screenChanged.connect(self.updateFbScale) window.screen().physicalDotsPerInchChanged.connect( lambda dpi: self.updateFbScale(window)) except AttributeError: # Fallback for Qt4 pass def _pymolProcess(self): idle = self.pymol.idle() if idle or self.pymol.getRedisplay(): self.update() self._timer.start(20) ########################## # drag n drop ########################## def dragEnterEvent(self, event): if event.mimeData().hasUrls: event.accept() else: event.ignore() def dropEvent(self, event): if event.mimeData().hasUrls: for url in event.mimeData().urls(): if url.isLocalFile(): url = url.toLocalFile() else: url = url.toString() self.gui.load_dialog(url) event.accept()