1from __future__ import absolute_import 2 3import sys 4from pymol2 import SingletonPyMOL as PyMOL 5 6import pymol 7 8from pymol.Qt import QtCore 9from pymol.Qt import QtGui 10from pymol.Qt import QtWidgets 11Gesture = QtCore.QEvent.Gesture 12Qt = QtCore.Qt 13 14from .keymapping import get_modifiers 15 16# don't import the heavy OpenGL (PyOpenGL) module 17from pymol._cmd import glViewport 18 19# QOpenGLWidget is supposed to supersede QGLWidget, but has issues (e.g. 20# no stereo support) 21USE_QOPENGLWIDGET = pymol.IS_MACOS and QtCore.QT_VERSION >= 0x50400 22 23if USE_QOPENGLWIDGET: 24 BaseGLWidget = QtWidgets.QOpenGLWidget 25 AUTO_DETECT_STEREO = False 26else: 27 from pymol.Qt import QtOpenGL 28 BaseGLWidget = QtOpenGL.QGLWidget 29 # only attempt stereo detection in Qt <= 5.6 (with 5.9+ on Linux I 30 # get GL_DOUBLEBUFFER=0 with flickering when requesting stereo) 31 AUTO_DETECT_STEREO = pymol.IS_WINDOWS or QtCore.QT_VERSION < 0x50700 32 33 34class PyMOLGLWidget(BaseGLWidget): 35 ''' 36 PyMOL OpenGL Widget 37 ''' 38 39 # mouse button map 40 _buttonMap = { 41 Qt.LeftButton: 0, 42 Qt.MidButton: 1, 43 Qt.RightButton: 2, 44 } 45 46 def __init__(self, parent): 47 self.gui = parent 48 self.fb_scale = 1.0 49 50 # OpenGL context setup 51 if USE_QOPENGLWIDGET: 52 f = QtGui.QSurfaceFormat() 53 else: 54 f = QtOpenGL.QGLFormat() 55 56 from pymol.invocation import options 57 58 # logic equivalent to layer5/main.cpp:launch 59 60 if options.multisample: 61 f.setSamples(4) 62 63 if options.force_stereo != -1: 64 # See layer1/Setting.h for stereo modes 65 66 if options.stereo_mode in (1, 12) or ( 67 options.stereo_mode == 0 and AUTO_DETECT_STEREO): 68 f.setStereo(True) 69 70 if options.stereo_mode in (11, 12) and not USE_QOPENGLWIDGET: 71 f.setAccum(True) 72 73 if USE_QOPENGLWIDGET: 74 super(PyMOLGLWidget, self).__init__(parent=parent) 75 self.setFormat(f) 76 self.setUpdateBehavior(QtWidgets.QOpenGLWidget.PartialUpdate) 77 else: 78 super(PyMOLGLWidget, self).__init__(f, parent=parent) 79 80 # pymol instance 81 self.pymol = PyMOL() 82 self.pymol.start() 83 self.cmd = self.pymol.cmd 84 85 # capture python output for feedback 86 import pcatch 87 pcatch._install() 88 89 # for passive move drag 90 self.setMouseTracking(True) 91 92 # for accepting keyboard input (command line, shortcuts) 93 self.setFocusPolicy(Qt.ClickFocus) 94 95 # for idle rendering 96 self._timer = QtCore.QTimer() 97 self._timer.setSingleShot(True) 98 self._timer.timeout.connect(self._pymolProcess) 99 100 # drag n drop 101 self.setAcceptDrops(True) 102 103 # pinch-zoom 104 self.grabGesture(Qt.PinchGesture) 105 106 def sizeHint(self): 107 # default 640 + internal_gui, 480 + internal_feedback 108 return QtCore.QSize(860, 498) 109 110 ########################## 111 # Input Events 112 ########################## 113 114 def event(self, ev): 115 if ev.type() == Gesture: 116 return self.gestureEvent(ev) 117 118 return super(PyMOLGLWidget, self).event(ev) 119 120 def gestureEvent(self, ev): 121 gesture = ev.gesture(Qt.PinchGesture) 122 123 if gesture is None: 124 return False 125 126 if gesture.state() == Qt.GestureStarted: 127 self.pinch_start_z = self.cmd.get_view()[11] 128 129 changeFlags = gesture.changeFlags() 130 131 if changeFlags & QtWidgets.QPinchGesture.RotationAngleChanged: 132 delta = gesture.lastRotationAngle() - gesture.rotationAngle() 133 self.cmd.turn('z', delta) 134 135 if changeFlags & QtWidgets.QPinchGesture.ScaleFactorChanged: 136 view = list(self.cmd.get_view()) 137 138 # best guess for https://bugreports.qt.io/browse/QTBUG-48138 139 totalscalefactor = gesture.totalScaleFactor() 140 if totalscalefactor == 1.0: 141 totalscalefactor = gesture.scaleFactor() 142 143 z = self.pinch_start_z / totalscalefactor 144 delta = z - view[11] 145 view[11] = z 146 view[15] -= delta 147 view[16] -= delta 148 self.cmd.set_view(view) 149 150 return True 151 152 def mouseMoveEvent(self, ev): 153 self.pymol.drag(int(self.fb_scale * ev.x()), 154 int(self.fb_scale * (self.height() - ev.y())), 155 get_modifiers(ev)) 156 157 def mousePressEvent(self, ev, state=0): 158 if ev.button() not in self._buttonMap: 159 return 160 self.pymol.button(self._buttonMap[ev.button()], state, 161 int(self.fb_scale * ev.x()), 162 int(self.fb_scale * (self.height() - ev.y())), 163 get_modifiers(ev)) 164 165 def mouseReleaseEvent(self, ev): 166 self.mousePressEvent(ev, 1) 167 168 def wheelEvent(self, ev): 169 pymolmod = get_modifiers(ev) 170 try: 171 delta = ev.delta() 172 except AttributeError: 173 # Qt5 174 angledelta = ev.angleDelta() 175 delta = angledelta.y() 176 if abs(delta) < abs(angledelta.x()): 177 # Shift+Wheel emulates horizontal scrolling 178 if not (ev.modifiers() & Qt.ShiftModifier): 179 return 180 delta = angledelta.x() 181 if not delta: 182 return 183 button = 3 if delta > 0 else 4 184 args = (int(self.fb_scale * ev.x()), 185 int(self.fb_scale * (self.height() - ev.y())), 186 pymolmod) 187 self.pymol.button(button, 0, *args) 188 self.pymol.button(button, 1, *args) 189 190 ########################## 191 # OpenGL 192 ########################## 193 194 def paintGL(self): 195 if not USE_QOPENGLWIDGET: 196 glViewport(0, 0, int(self.fb_scale * self.width()), 197 int(self.fb_scale * self.height())) 198 self.pymol.draw() 199 self._timer.start(0) 200 201 def resizeGL(self, w, h): 202 if USE_QOPENGLWIDGET: 203 w = int(w * self.fb_scale) 204 h = int(h * self.fb_scale) 205 206 self.pymol.reshape(w, h, True) 207 208 def updateFbScale(self, context): 209 '''Update PyMOL's display scale factor from the window or screen context 210 @type context: QWindow or QScreen 211 ''' 212 self.fb_scale = context.devicePixelRatio() 213 try: 214 self.cmd.set('display_scale_factor', int(self.fb_scale)) 215 except BaseException as e: 216 # fails with modal draw (mpng ..., modal=1) 217 print(e) 218 219 def initializeGL(self): 220 # Scale framebuffer for Retina displays 221 try: 222 window = self.windowHandle() 223 224 # QOpenGLWidget workaround 225 if window is None: 226 window = self.parent().windowHandle() 227 228 self.updateFbScale(window) 229 window.screenChanged.connect(self.updateFbScale) 230 window.screen().physicalDotsPerInchChanged.connect( 231 lambda dpi: self.updateFbScale(window)) 232 233 except AttributeError: 234 # Fallback for Qt4 235 pass 236 237 def _pymolProcess(self): 238 idle = self.pymol.idle() 239 if idle or self.pymol.getRedisplay(): 240 self.update() 241 242 self._timer.start(20) 243 244 ########################## 245 # drag n drop 246 ########################## 247 248 def dragEnterEvent(self, event): 249 if event.mimeData().hasUrls: 250 event.accept() 251 else: 252 event.ignore() 253 254 def dropEvent(self, event): 255 if event.mimeData().hasUrls: 256 for url in event.mimeData().urls(): 257 if url.isLocalFile(): 258 url = url.toLocalFile() 259 else: 260 url = url.toString() 261 self.gui.load_dialog(url) 262 event.accept() 263