Files
2026-08-03 18:42:47 +08:00

271 lines
7.7 KiB
Python

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()