Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

WIP: Using pyqt6 as main UI tool #382

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
97 changes: 0 additions & 97 deletions modules/pymol/Qt/__init__.py
Original file line number Diff line number Diff line change
@@ -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
118 changes: 44 additions & 74 deletions modules/pymol/Qt/utils.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
from pymol.Qt import *
import os
import re
import sys
import traceback

from PyQt5 import QtCore, QtGui, QtWidgets, uic


class UpdateLock:
Expand Down Expand Up @@ -29,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:
Expand All @@ -58,6 +63,7 @@ def wrapper(*args, **kwargs):
with self:
self.acquire()
return func(*args, **kwargs)

return wrapper


Expand All @@ -70,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):
Expand All @@ -81,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 _():
Expand Down Expand Up @@ -112,9 +118,10 @@ class AsyncFunc(QtCore.QThread):
25

"""

# 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__()
Expand Down Expand Up @@ -158,7 +165,8 @@ 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
Expand Down Expand Up @@ -191,22 +199,22 @@ 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]

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.

@type widget: QWidget
"""
widget.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
widget.setContextMenuPolicy(QtCore.Qt.ContextMenuPolicy.CustomContextMenu)

@widget.customContextMenuRequested.connect
def _(pt):
Expand All @@ -216,96 +224,65 @@ 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.
"""
import os, re

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)

return fname


def getMonospaceFont(size=9):
def getMonospaceFont(size: int = 9) -> QtGui.QFont:
"""
Get the best looking monospace font for the current platform
"""
import sys

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)

return font


def loadUi(uifile, widget):
def loadUi(uifile: str, widget: QtWidgets.QWidget):
"""
Load .ui file into widget

@param uifile: filename
@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:
Expand All @@ -328,28 +305,21 @@ 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.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


def conda_ask_install(packagespec, channel=None, msg="", parent=None, url=""):
"""
Install a conda package
"""
return True
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ authors = [
]
dependencies = [
"numpy>=1.26.4,<2",
"pyqt5==5.15.11",
]

[build-system]
Expand Down