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