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

support Qt6 #324

Merged
merged 19 commits into from
Dec 22, 2021
Merged
13 changes: 10 additions & 3 deletions .github/workflows/test_and_deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,17 +20,24 @@ jobs:
matrix:
python-version: ["3.7", "3.8", "3.9"]
platform: [ubuntu-latest, macos-latest, windows-latest]
backend: [pyqt5, pyside2]
include:
# until pyside2 is available for 3.10
- python-version: '3.10'
backend: pyqt
platform: ubuntu-latest
backend: pyqt5
- python-version: '3.10'
backend: pyqt
platform: macos-latest
backend: pyqt5
- python-version: '3.10'
backend: pyqt
platform: windows-latest
backend: pyqt5
- python-version: '3.10'
platform: ubuntu-latest
backend: pyqt6
- python-version: '3.10'
platform: ubuntu-latest
backend: pyside6
steps:
- uses: actions/checkout@v2
- name: Set up Python ${{ matrix.python-version }}
Expand Down
4 changes: 2 additions & 2 deletions magicgui/backends/_qtpy/application.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,6 @@ def _mgui_get_backend_name(self):

def _mgui_process_events(self):
app = self._mgui_get_native_app()
app.flush()
app.processEvents()

def _mgui_run(self):
Expand All @@ -31,7 +30,8 @@ def _mgui_get_native_app(self):
# Get native app
self._app = QApplication.instance()
if not self._app:
QApplication.setAttribute(Qt.AA_EnableHighDpiScaling)
if hasattr(Qt, "AA_EnableHighDpiScaling"):
QApplication.setAttribute(Qt.AA_EnableHighDpiScaling)
self._app = QApplication(sys.argv)
self._app.setApplicationName(APPLICATION_NAME)
return self._app
Expand Down
41 changes: 25 additions & 16 deletions magicgui/backends/_qtpy/widgets.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
from __future__ import annotations

import math
import re
from typing import TYPE_CHECKING, Any, Iterable, Sequence

import qtpy
Expand Down Expand Up @@ -144,7 +145,7 @@ def _mgui_render(self):
img = self._qwidget.grab().toImage()
bits = img.constBits()
h, w, c = img.height(), img.width(), 4
if qtpy.API_NAME == "PySide2":
if qtpy.API_NAME.startswith("PySide"):
arr = np.array(bits).reshape(h, w, c)
else:
bits.setsize(h * w * c)
Expand Down Expand Up @@ -662,7 +663,11 @@ def _mgui_get_value(self) -> Any:
return self._qwidget.itemData(self._qwidget.currentIndex())

def _mgui_set_value(self, value) -> None:
self._qwidget.setCurrentIndex(self._qwidget.findData(value))
# Note: there's a bug in PyQt6, where CombBox.findData(value) will not
# find the data if value is an Enum. So we do it manually
wdg = self._qwidget
idx = next((i for i in range(wdg.count()) if wdg.itemData(i) == value), -1)
self._qwidget.setCurrentIndex(idx)

def _mgui_set_choice(self, choice_name: str, data: Any) -> None:
"""Set data for ``choice_name``."""
Expand Down Expand Up @@ -722,7 +727,9 @@ def __init__(self):

def _emit_data(self):
data = self._qwidget.selectedItems()
self._event_filter.valueChanged.emit([d.data(Qt.UserRole) for d in data])
self._event_filter.valueChanged.emit(
[d.data(Qt.ItemDataRole.UserRole) for d in data]
)

def _mgui_bind_change_callback(self, callback):
self._event_filter.valueChanged.connect(callback)
Expand All @@ -733,33 +740,33 @@ def _mgui_get_count(self) -> int:

def _mgui_get_choice(self, choice_name: str) -> list[Any]:
items = self._qwidget.findItems(choice_name, Qt.MatchExactly)
return [i.data(Qt.UserRole) for i in items]
return [i.data(Qt.ItemDataRole.UserRole) for i in items]

def _mgui_get_current_choice(self) -> list[str]: # type: ignore[override]
return [i.text() for i in self._qwidget.selectedItems()]

def _mgui_get_value(self) -> Any:
return [i.data(Qt.UserRole) for i in self._qwidget.selectedItems()]
return [i.data(Qt.ItemDataRole.UserRole) for i in self._qwidget.selectedItems()]

def _mgui_set_value(self, value) -> None:
if not isinstance(value, (list, tuple)):
value = [value]
for i in range(self._qwidget.count()):
item = self._qwidget.item(i)
item.setSelected(item.data(Qt.UserRole) in value)
item.setSelected(item.data(Qt.ItemDataRole.UserRole) in value)

def _mgui_set_choice(self, choice_name: str, data: Any) -> None:
"""Set data for ``choice_name``."""
items = self._qwidget.findItems(choice_name, Qt.MatchExactly)
# if it's not in the list, add a new item
if not items:
item = QtW.QListWidgetItem(choice_name)
item.setData(Qt.UserRole, data)
item.setData(Qt.ItemDataRole.UserRole, data)
self._qwidget.addItem(item)
# otherwise update its data
else:
for item in items:
item.setData(Qt.UserRole, data)
item.setData(Qt.ItemDataRole.UserRole, data)

def _mgui_set_choices(self, choices: Iterable[tuple[str, Any]]) -> None:
"""Set current items in categorical type ``widget`` to ``choices``."""
Expand All @@ -786,7 +793,10 @@ def _mgui_del_choice(self, choice_name: str) -> None:
def _mgui_get_choices(self) -> tuple[tuple[str, Any], ...]:
"""Get available choices."""
return tuple(
(self._qwidget.item(i).text(), self._qwidget.item(i).data(Qt.UserRole))
(
self._qwidget.item(i).text(),
self._qwidget.item(i).data(Qt.ItemDataRole.UserRole),
)
for i in range(self._qwidget.count())
)

Expand Down Expand Up @@ -952,14 +962,13 @@ def show_file_dialog(
return result or None


def get_text_width(text) -> int:
"""Return the width required to render ``text`` (including rich text elements)."""
if qtpy.PYSIDE2:
from qtpy.QtGui import Qt as _Qt
else:
from qtpy.QtCore import Qt as _Qt
def _might_be_rich_text(text):
return bool(re.search("<[^\n]+>", text))


if _Qt.mightBeRichText(text):
def get_text_width(text: str) -> int:
"""Return the width required to render ``text`` (including rich text elements)."""
if _might_be_rich_text(text):
doc = QTextDocument()
doc.setHtml(text)
return doc.size().width()
Expand Down
11 changes: 7 additions & 4 deletions tests/test_backends.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,16 @@
from magicgui import event_loop, widgets


@pytest.mark.skipif(API_NAME == "PyQt5", reason="Couldn't delete app on PyQt")
@pytest.mark.skipif("PyQt" in API_NAME, reason="Couldn't delete app on PyQt")
def test_event():
"""Test that the event loop makes a Qt app."""
if QtW.QApplication.instance():
import shiboken2

shiboken2.delete(QtW.QApplication.instance())
if API_NAME == "PySide2":
__import__("shiboken2").delete(QtW.QApplication.instance())
elif API_NAME == "PySide6":
__import__("shiboken6").delete(QtW.QApplication.instance())
else:
raise AssertionError(f"known API name: {API_NAME}")
assert not QtW.QApplication.instance()
with event_loop():
app = QtW.QApplication.instance()
Expand Down
15 changes: 10 additions & 5 deletions tox.ini
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
[tox]
envlist = py{37,38,39,310}-{linux,macos,windows}-{pyqt,pyside}
envlist = py{37,38,39,310}-{linux,macos,windows}-{pyqt5,pyside2,pyqt6,pyside6}
toxworkdir=/tmp/.tox

[gh-actions]
Expand All @@ -15,8 +15,10 @@ PLATFORM =
macos-latest: macos
windows-latest: windows
BACKEND =
pyqt: pyqt
pyside: pyside
pyqt5: pyqt5
pyqt6: pyqt6
pyside2: pyside2
pyside6: pyside6

[testenv]
platform =
Expand All @@ -26,10 +28,13 @@ platform =
passenv = CI GITHUB_ACTIONS DISPLAY XAUTHORITY
setenv =
PYTHONPATH = {toxinidir}
deps =
pyqt6: PyQt6
pyside6: PySide6
extras =
testing
pyqt: PyQt5
pyside: PySide2
pyqt5: PyQt5
pyside2: PySide2
commands =
pytest -v --color=yes --cov-report=xml --cov=magicgui --basetemp={envtmpdir} {posargs}

Expand Down