mirror of
https://github.com/schrodinger/pymol-open-source.git
synced 2026-06-03 19:54:24 +08:00
368 lines
9.5 KiB
Python
368 lines
9.5 KiB
Python
from pymol.Qt import *
|
|
|
|
import shutil
|
|
import subprocess
|
|
|
|
class UpdateLock:
|
|
"""
|
|
Locking mechanism to prevent circular signal/slot updates.
|
|
|
|
Decorator notation:
|
|
|
|
>>> @lock.skipIfCircular
|
|
>>> def updatesomething():
|
|
... dosomething()
|
|
|
|
Context manager notation:
|
|
|
|
>>> def updatesomething():
|
|
... with lock:
|
|
... lock.acquire() # exits block if already acquired
|
|
... dosomething()
|
|
|
|
"""
|
|
|
|
class LockFailed(Exception):
|
|
pass
|
|
|
|
def __init__(self, silent_exc_types=()):
|
|
self.primed = False
|
|
self.acquired = False
|
|
self.silent_exc_types = tuple(silent_exc_types)
|
|
|
|
def __enter__(self):
|
|
assert not self.primed, 'missing acquire()'
|
|
self.primed = True
|
|
|
|
def __exit__(self, exc_type, exc_val, exc_tb):
|
|
assert not self.primed, 'missing acquire()'
|
|
|
|
if exc_type == self.LockFailed:
|
|
return True
|
|
|
|
assert self.acquired, 'inconsistency!?'
|
|
self.acquired = False
|
|
|
|
if exc_type in self.silent_exc_types:
|
|
return True
|
|
|
|
def acquire(self):
|
|
assert self.primed, 'missing with ...():'
|
|
self.primed = False
|
|
|
|
if self.acquired:
|
|
raise self.LockFailed
|
|
|
|
self.acquired = True
|
|
|
|
def skipIfCircular(self, func):
|
|
def wrapper(*args, **kwargs):
|
|
with self:
|
|
self.acquire()
|
|
return func(*args, **kwargs)
|
|
return wrapper
|
|
|
|
|
|
class WidgetMenu(QtWidgets.QMenu):
|
|
"""
|
|
QMenu that represents a single widget that pops up under a push button.
|
|
|
|
>>> btn = QtWidgets.QPushButton()
|
|
>>> btn.setMenu(WidgetMenu().setSetupUi(setupUi))
|
|
"""
|
|
|
|
def focusNextPrevChild(self, next):
|
|
'''Overload which prevents menu-like tab action'''
|
|
return QtWidgets.QWidget.focusNextPrevChild(self, next)
|
|
|
|
def setWidget(self, widget):
|
|
self.clear()
|
|
action = QtWidgets.QWidgetAction(self)
|
|
action.setDefaultWidget(widget)
|
|
self.addAction(action)
|
|
return self
|
|
|
|
def setSetupUi(self, setupUi):
|
|
'''Use a setup function for the widget. The widget will be created
|
|
and initialized as "setupUi(widget)" before the menu is shown for
|
|
the first time.'''
|
|
|
|
@self.aboutToShow.connect
|
|
def _():
|
|
self.aboutToShow.disconnect()
|
|
self._widget = QtWidgets.QWidget()
|
|
form = setupUi(self._widget)
|
|
self.setWidget(form)
|
|
|
|
return self
|
|
|
|
|
|
class AsyncFunc(QtCore.QThread):
|
|
"""
|
|
Decorator to call a function asynchronous.
|
|
|
|
Signals:
|
|
- returned(result)
|
|
Emitted if the function successfully returned
|
|
- finished(result, exception)
|
|
Emitted on success or failure. On success, exception is None,
|
|
on failure result is None.
|
|
|
|
Example:
|
|
>>> asyncsquare = AsyncFunc(lambda x: x * x, print)
|
|
>>> asyncsquare(5)
|
|
25
|
|
|
|
"""
|
|
# Warning: PySide crashes if passing None to an "object" type signal
|
|
returned = QtCore.Signal(object)
|
|
finished = QtCore.Signal(tuple)
|
|
|
|
def __init__(self, func, returnslot=None, finishslot=None):
|
|
super(AsyncFunc, self).__init__()
|
|
self.func = func
|
|
if returnslot is not None:
|
|
self.returned.connect(returnslot)
|
|
if finishslot is not None:
|
|
self.finished.connect(finishslot)
|
|
|
|
def __call__(self, *args, **kwargs):
|
|
self.args = args
|
|
self.kwargs = kwargs
|
|
self.start()
|
|
|
|
def run(self):
|
|
result = None
|
|
exception = None
|
|
try:
|
|
result = self.func(*self.args, **self.kwargs)
|
|
self.returned.emit(result)
|
|
except Exception as e:
|
|
exception = e
|
|
except:
|
|
exception = Exception()
|
|
self.finished.emit((result, exception))
|
|
|
|
|
|
class MainThreadCaller(QtCore.QObject):
|
|
"""
|
|
Allows calling a GUI function from a non-main thread. Will pause
|
|
the current thread until the function has returned from the main
|
|
thread.
|
|
|
|
>>> # in main thread
|
|
>>> callInMainThread = MainThreadCaller()
|
|
|
|
>>> # in async thread
|
|
>>> callInMainThread(lambda: 123)
|
|
123
|
|
|
|
Note: QMetaObject.invokeMethod with BlockingQueuedConnection could
|
|
potentially be used to achieve the same goal.
|
|
"""
|
|
mainthreadrequested = QtCore.Signal(object)
|
|
|
|
RESULT_RETURN = 0
|
|
RESULT_EXCEPTION = 1
|
|
|
|
def __init__(self):
|
|
super(MainThreadCaller, self).__init__()
|
|
self.waitcondition = QtCore.QWaitCondition()
|
|
self.mutex = QtCore.QMutex()
|
|
self.results = {}
|
|
self.mainthreadrequested.connect(self._mainThreadAction)
|
|
|
|
def _mainThreadAction(self, func):
|
|
try:
|
|
self.results[func] = (self.RESULT_RETURN, func())
|
|
except Exception as ex:
|
|
self.results[func] = (self.RESULT_EXCEPTION, ex)
|
|
self.waitcondition.wakeAll()
|
|
|
|
def __call__(self, func):
|
|
if self.thread() is QtCore.QThread.currentThread():
|
|
return func()
|
|
|
|
self.mainthreadrequested.emit(func)
|
|
|
|
while True:
|
|
self.mutex.lock()
|
|
self.waitcondition.wait(self.mutex)
|
|
self.mutex.unlock()
|
|
try:
|
|
result = self.results.pop(func)
|
|
break
|
|
except KeyError:
|
|
print(type(self).__name__ + ': result was not ready')
|
|
|
|
if result[0] == self.RESULT_EXCEPTION:
|
|
raise result[1]
|
|
|
|
return result[1]
|
|
|
|
|
|
def connectFontContextMenu(widget):
|
|
"""
|
|
Connects a custom context menu with a "Select Font..." entry
|
|
to the given widget.
|
|
|
|
@type widget: QWidget
|
|
"""
|
|
widget.setContextMenuPolicy(QtCore.Qt.ContextMenuPolicy.CustomContextMenu)
|
|
|
|
@widget.customContextMenuRequested.connect
|
|
def _(pt):
|
|
menu = widget.createStandardContextMenu()
|
|
menu.addSeparator()
|
|
action = menu.addAction("Select Font...")
|
|
|
|
@action.triggered.connect
|
|
def _():
|
|
font, ok = QtWidgets.QFontDialog.getFont(widget.font(), widget,
|
|
"Select Font", QtWidgets.QFontDialog.FontDialogOption.DontUseNativeDialog)
|
|
if ok:
|
|
widget.setFont(font)
|
|
|
|
menu.exec(widget.mapToGlobal(pt))
|
|
|
|
|
|
def getSaveFileNameWithExt(*args, **kwargs):
|
|
"""
|
|
Return a file name, append extension from filter if no extension provided.
|
|
"""
|
|
import os, re
|
|
|
|
fname, filter = QtWidgets.QFileDialog.getSaveFileName(*args, **kwargs)
|
|
|
|
if not fname:
|
|
return ''
|
|
|
|
if '.' not in os.path.split(fname)[-1]:
|
|
m = re.search(r'\*(\.[\w\.]+)', filter)
|
|
if m:
|
|
# append first extension from filter
|
|
fname += m.group(1)
|
|
|
|
return fname
|
|
|
|
|
|
def getMonospaceFont(size=9):
|
|
"""
|
|
Get the best looking monospace font for the current platform
|
|
"""
|
|
import sys
|
|
|
|
if sys.platform == 'darwin':
|
|
family = 'Monaco'
|
|
size += 3
|
|
elif sys.platform == 'win32':
|
|
family = 'Consolas'
|
|
else:
|
|
family = 'Monospace'
|
|
|
|
font = QtGui.QFont(family, size)
|
|
font.setStyleHint(QtGui.QFont.StyleHint.Monospace)
|
|
|
|
return font
|
|
|
|
|
|
def loadUi(uifile, widget):
|
|
"""
|
|
Load .ui file into widget
|
|
|
|
@param uifile: filename
|
|
@type uifile: str
|
|
@type widget: QtWidgets.QWidget
|
|
"""
|
|
if PYQT_NAME == "PySide6":
|
|
from PySide6.QtUiTools import QUiLoader
|
|
return QUiLoader().load(uifile, widget)
|
|
elif PYQT_NAME.startswith('PyQt'):
|
|
m = __import__(PYQT_NAME + '.uic')
|
|
return m.uic.loadUi(uifile, widget)
|
|
elif PYQT_NAME == 'PySide2':
|
|
try:
|
|
import pyside2uic as pysideuic
|
|
except ImportError:
|
|
pysideuic = None
|
|
else:
|
|
import pysideuic
|
|
|
|
def find_valid_uic_exe(execs):
|
|
for exe in execs:
|
|
if shutil.which(exe):
|
|
return exe
|
|
|
|
if pysideuic is None:
|
|
uic_exec = find_valid_uic_exe(('uic', 'pyside2-uic'))
|
|
if uic_exec is None:
|
|
raise RuntimeError('uic not found')
|
|
p = subprocess.Popen([uic_exec, '-g', 'python', uifile],
|
|
stdout=subprocess.PIPE)
|
|
source, _ = p.communicate()
|
|
# workaround for empty retranslateUi bug
|
|
source += b'\n' + b' ' * 8 + b'pass'
|
|
else:
|
|
import io
|
|
stream = io.StringIO()
|
|
pysideuic.compileUi(uifile, stream)
|
|
source = stream.getvalue()
|
|
|
|
ns_locals = {}
|
|
exec(source, ns_locals)
|
|
|
|
if 'Ui_Form' in ns_locals:
|
|
form = ns_locals['Ui_Form']()
|
|
else:
|
|
form = ns_locals['Ui_Dialog']()
|
|
|
|
form.setupUi(widget)
|
|
return form
|
|
|
|
|
|
class PopupOnException:
|
|
"""
|
|
Context manager which shows a message box if an exception is raised.
|
|
|
|
>>> with PopupOnException():
|
|
... # do something
|
|
|
|
Decorator support:
|
|
|
|
>>> @PopupOnException.decorator
|
|
... def foo():
|
|
... # do something
|
|
|
|
"""
|
|
|
|
@classmethod
|
|
def decorator(cls, func):
|
|
def wrapper(*args, **kwargs):
|
|
with cls():
|
|
return func(*args, **kwargs)
|
|
return wrapper
|
|
|
|
def __enter__(self):
|
|
pass
|
|
|
|
def __exit__(self, exc_type, e, tb):
|
|
if e is not None:
|
|
import traceback
|
|
QMB = QtWidgets.QMessageBox
|
|
|
|
parent = QtWidgets.QApplication.focusWidget()
|
|
|
|
msg = str(e) or 'unknown error'
|
|
msgbox = QMB(QMB.Icon.Critical, 'Error', msg, QMB.StandardButton.Close, parent)
|
|
msgbox.setDetailedText(''.join(traceback.format_tb(tb)))
|
|
msgbox.exec()
|
|
|
|
return True
|
|
|
|
|
|
def conda_ask_install(packagespec, channel=None, msg="", parent=None, url=""):
|
|
"""
|
|
Install a conda package
|
|
"""
|
|
return True
|