From ac911539be112e4c689d10e001acfff5e5171e61 Mon Sep 17 00:00:00 2001 From: ye11owSub Date: Fri, 9 Aug 2024 02:16:21 +0300 Subject: [PATCH 1/2] using pyqt5 as the only ui lib --- modules/pymol/Qt/__init__.py | 97 ------------------------------------ modules/pymol/Qt/utils.py | 59 ++++------------------ pyproject.toml | 1 + 3 files changed, 12 insertions(+), 145 deletions(-) diff --git a/modules/pymol/Qt/__init__.py b/modules/pymol/Qt/__init__.py index bcc990a7b..e69de29bb 100644 --- a/modules/pymol/Qt/__init__.py +++ b/modules/pymol/Qt/__init__.py @@ -1,97 +0,0 @@ -""" -Wrapper for PyMOL scripts to get PySide or PyQt - -Useful link for PySide/PyQt4 differences: -https://deptinfo-ensip.univ-poitiers.fr/ENS/pyside-docs/pysideapi2.html - -PyQt5/PyQt4 differences: -http://pyqt.sourceforge.net/Docs/PyQt5/pyqt4_differences.html -""" - -DEBUG = False - -PYQT_NAME = None -QtWidgets = None - -try: - from pymol._Qt_pre import * -except ImportError: - if DEBUG: - print('import _Qt_pre failed') - -import os - -qt_api = os.environ.get('QT_API', '') - -if not PYQT_NAME and qt_api in ('', 'pyqt5'): - try: - from PyQt5 import QtGui, QtCore, QtOpenGL, QtWidgets - PYQT_NAME = 'PyQt5' - except ImportError: - if DEBUG: - print('import PyQt5 failed') - -if not PYQT_NAME and qt_api in ('', 'pyside2'): - try: - from PySide2 import QtGui, QtCore, QtOpenGL, QtWidgets - PYQT_NAME = 'PySide2' - except ImportError: - if DEBUG: - print('import PySide2 failed') - -if not PYQT_NAME and qt_api in ('', 'pyqt4'): - try: - try: - import PyQt4.sip as sip - except ImportError: - import sip - sip.setapi("QString", 2) - from PyQt4 import QtGui, QtCore, QtOpenGL - PYQT_NAME = 'PyQt4' - except ImportError: - if DEBUG: - print('import PyQt4 failed') - -if not PYQT_NAME and qt_api in ('', 'pyside'): - try: - from PySide import QtGui, QtCore, QtOpenGL - PYQT_NAME = 'PySide' - except ImportError: - if DEBUG: - print('import PySide failed') - -if not PYQT_NAME: - raise ImportError(__name__) - -# qtpy compatibility -os.environ['QT_API'] = PYQT_NAME.lower() - -if QtWidgets is None: - QtWidgets = QtGui - -if hasattr(QtCore, 'QAbstractProxyModel'): - QtCoreModels = QtCore -else: - QtCoreModels = QtGui - -if PYQT_NAME == 'PyQt4': - QFileDialog = QtWidgets.QFileDialog - QFileDialog.getOpenFileName = QFileDialog.getOpenFileNameAndFilter - QFileDialog.getOpenFileNames = QFileDialog.getOpenFileNamesAndFilter - QFileDialog.getSaveFileName = QFileDialog.getSaveFileNameAndFilter - del QFileDialog - -if PYQT_NAME[:4] == 'PyQt': - QtCore.Signal = QtCore.pyqtSignal - QtCore.Slot = QtCore.pyqtSlot -else: - QtCore.pyqtSignal = QtCore.Signal - QtCore.pyqtSlot = QtCore.Slot - QtCore.QT_VERSION_STR = QtCore.__version__ - QtCore.QT_VERSION = ( - 0x10000 * QtCore.__version_info__[0] + - 0x00100 * QtCore.__version_info__[1] + - 0x00001 * QtCore.__version_info__[2]) - -del qt_api -del os diff --git a/modules/pymol/Qt/utils.py b/modules/pymol/Qt/utils.py index db68f26ca..fde0e4e5f 100644 --- a/modules/pymol/Qt/utils.py +++ b/modules/pymol/Qt/utils.py @@ -1,4 +1,9 @@ -from pymol.Qt import * +import os +import re +import sys +import traceback + +from PyQt5 import QtGui, QtCore, QtWidgets, uic class UpdateLock: @@ -113,8 +118,8 @@ class AsyncFunc(QtCore.QThread): """ # Warning: PySide crashes if passing None to an "object" type signal - returned = QtCore.Signal(object) - finished = QtCore.Signal(tuple) + returned = QtCore.pyqtSignal(object) + finished = QtCore.pyqtSignal(tuple) def __init__(self, func, returnslot=None, finishslot=None): super(AsyncFunc, self).__init__() @@ -158,7 +163,7 @@ class MainThreadCaller(QtCore.QObject): Note: QMetaObject.invokeMethod with BlockingQueuedConnection could potentially be used to achieve the same goal. """ - mainthreadrequested = QtCore.Signal(object) + mainthreadrequested = QtCore.pyqtSignal(object) RESULT_RETURN = 0 RESULT_EXCEPTION = 1 @@ -206,7 +211,7 @@ def connectFontContextMenu(widget): @type widget: QWidget """ - widget.setContextMenuPolicy(QtCore.Qt.CustomContextMenu) + widget.setContextMenuPolicy(QtCore.Qt.ContextMenuPolicy.CustomContextMenu) @widget.customContextMenuRequested.connect def _(pt): @@ -228,7 +233,6 @@ 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) @@ -248,7 +252,6 @@ def getMonospaceFont(size=9): """ Get the best looking monospace font for the current platform """ - import sys if sys.platform == 'darwin': family = 'Monaco' @@ -272,40 +275,7 @@ def loadUi(uifile, widget): @type uifile: str @type widget: QtWidgets.QWidget """ - if 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 - - if pysideuic is None: - import subprocess - p = subprocess.Popen(['uic', '-g', 'python', uifile], - stdout=subprocess.PIPE) - source = p.communicate()[0] - # 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 + return uic.loadUi(uifile, widget) class PopupOnException: @@ -335,7 +305,6 @@ def __enter__(self): def __exit__(self, exc_type, e, tb): if e is not None: - import traceback QMB = QtWidgets.QMessageBox parent = QtWidgets.QApplication.focusWidget() @@ -347,9 +316,3 @@ def __exit__(self, exc_type, e, tb): return True - -def conda_ask_install(packagespec, channel=None, msg="", parent=None, url=""): - """ - Install a conda package - """ - return True diff --git a/pyproject.toml b/pyproject.toml index d8629e40a..70f453139 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -15,6 +15,7 @@ authors = [ ] dependencies = [ "numpy>=1.26.4,<2", + "pyqt5==5.15.11", ] [build-system] From b4f6363feba99e9856b0d17b0d724498f9fe13e8 Mon Sep 17 00:00:00 2001 From: ye11owSub Date: Fri, 9 Aug 2024 02:25:59 +0300 Subject: [PATCH 2/2] format of Qt.utils --- modules/pymol/Qt/utils.py | 61 ++++++++++++++++++++++----------------- 1 file changed, 34 insertions(+), 27 deletions(-) diff --git a/modules/pymol/Qt/utils.py b/modules/pymol/Qt/utils.py index fde0e4e5f..7ae41e310 100644 --- a/modules/pymol/Qt/utils.py +++ b/modules/pymol/Qt/utils.py @@ -3,7 +3,7 @@ import sys import traceback -from PyQt5 import QtGui, QtCore, QtWidgets, uic +from PyQt5 import QtCore, QtGui, QtWidgets, uic class UpdateLock: @@ -34,23 +34,23 @@ def __init__(self, silent_exc_types=()): self.silent_exc_types = tuple(silent_exc_types) def __enter__(self): - assert not self.primed, 'missing acquire()' + assert not self.primed, "missing acquire()" self.primed = True def __exit__(self, exc_type, exc_val, exc_tb): - assert not self.primed, 'missing acquire()' + assert not self.primed, "missing acquire()" if exc_type == self.LockFailed: return True - assert self.acquired, 'inconsistency!?' + 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 ...():' + assert self.primed, "missing with ...():" self.primed = False if self.acquired: @@ -63,6 +63,7 @@ def wrapper(*args, **kwargs): with self: self.acquire() return func(*args, **kwargs) + return wrapper @@ -75,7 +76,7 @@ class WidgetMenu(QtWidgets.QMenu): """ def focusNextPrevChild(self, next): - '''Overload which prevents menu-like tab action''' + """Overload which prevents menu-like tab action""" return QtWidgets.QWidget.focusNextPrevChild(self, next) def setWidget(self, widget): @@ -86,9 +87,9 @@ def setWidget(self, widget): return self def setSetupUi(self, setupUi): - '''Use a setup function for the widget. The widget will be created + """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.''' + the first time.""" @self.aboutToShow.connect def _(): @@ -117,6 +118,7 @@ class AsyncFunc(QtCore.QThread): 25 """ + # Warning: PySide crashes if passing None to an "object" type signal returned = QtCore.pyqtSignal(object) finished = QtCore.pyqtSignal(tuple) @@ -163,6 +165,7 @@ class MainThreadCaller(QtCore.QObject): Note: QMetaObject.invokeMethod with BlockingQueuedConnection could potentially be used to achieve the same goal. """ + mainthreadrequested = QtCore.pyqtSignal(object) RESULT_RETURN = 0 @@ -196,7 +199,7 @@ def __call__(self, func): result = self.results.pop(func) break except KeyError: - print(type(self).__name__ + ': result was not ready') + print(type(self).__name__ + ": result was not ready") if result[0] == self.RESULT_EXCEPTION: raise result[1] @@ -204,7 +207,7 @@ def __call__(self, func): return result[1] -def connectFontContextMenu(widget): +def connectFontContextMenu(widget: QtWidgets.QWidget) -> None: """ Connects a custom context menu with a "Select Font..." entry to the given widget. @@ -221,15 +224,19 @@ def _(pt): @action.triggered.connect def _(): - font, ok = QtWidgets.QFontDialog.getFont(widget.font(), widget, - "Select Font", QtWidgets.QFontDialog.DontUseNativeDialog) + font, ok = QtWidgets.QFontDialog.getFont( + widget.font(), + widget, + "Select Font", + QtWidgets.QFontDialog.DontUseNativeDialog, + ) if ok: widget.setFont(font) menu.exec_(widget.mapToGlobal(pt)) -def getSaveFileNameWithExt(*args, **kwargs): +def getSaveFileNameWithExt(*args, **kwargs) -> str: """ Return a file name, append extension from filter if no extension provided. """ @@ -237,10 +244,10 @@ def getSaveFileNameWithExt(*args, **kwargs): fname, filter = QtWidgets.QFileDialog.getSaveFileName(*args, **kwargs) if not fname: - return '' + return "" - if '.' not in os.path.split(fname)[-1]: - m = re.search(r'\*(\.[\w\.]+)', filter) + 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) @@ -248,18 +255,18 @@ def getSaveFileNameWithExt(*args, **kwargs): return fname -def getMonospaceFont(size=9): +def getMonospaceFont(size: int = 9) -> QtGui.QFont: """ Get the best looking monospace font for the current platform """ - if sys.platform == 'darwin': - family = 'Monaco' + if sys.platform == "darwin": + family = "Monaco" size += 3 - elif sys.platform == 'win32': - family = 'Consolas' + elif sys.platform == "win32": + family = "Consolas" else: - family = 'Monospace' + family = "Monospace" font = QtGui.QFont(family, size) font.setStyleHint(font.Monospace) @@ -267,7 +274,7 @@ def getMonospaceFont(size=9): return font -def loadUi(uifile, widget): +def loadUi(uifile: str, widget: QtWidgets.QWidget): """ Load .ui file into widget @@ -298,6 +305,7 @@ def decorator(cls, func): def wrapper(*args, **kwargs): with cls(): return func(*args, **kwargs) + return wrapper def __enter__(self): @@ -309,10 +317,9 @@ def __exit__(self, exc_type, e, tb): parent = QtWidgets.QApplication.focusWidget() - msg = str(e) or 'unknown error' - msgbox = QMB(QMB.Critical, 'Error', msg, QMB.Close, parent) - msgbox.setDetailedText(''.join(traceback.format_tb(tb))) + msg = str(e) or "unknown error" + msgbox = QMB(QMB.Critical, "Error", msg, QMB.Close, parent) + msgbox.setDetailedText("".join(traceback.format_tb(tb))) msgbox.exec_() return True -