From bbd3cf695c6f4244feb44b44f3232931bf7b6539 Mon Sep 17 00:00:00 2001 From: Jody Klymak Date: Mon, 15 Jun 2020 15:02:02 -0700 Subject: [PATCH 1/4] Backport PR #15656: Support fractional HiDpi scaling with Qt backends Merge pull request #15656 from timhoffm/qt-fractional-hidpi Support fractional HiDpi scaling with Qt backends Conflicts: lib/matplotlib/backends/backend_qt5.py lib/matplotlib/backends/qt_compat.py - only backport relevant changes --- lib/matplotlib/backends/backend_qt5.py | 16 ++++----- lib/matplotlib/backends/backend_qt5agg.py | 6 ++-- lib/matplotlib/backends/backend_qt5cairo.py | 10 +++--- lib/matplotlib/backends/qt_compat.py | 36 ++++++++++++++++++++- lib/matplotlib/tests/test_backend_qt.py | 23 +++++++++++++ 5 files changed, 70 insertions(+), 21 deletions(-) diff --git a/lib/matplotlib/backends/backend_qt5.py b/lib/matplotlib/backends/backend_qt5.py index 718c005f1f0d..4b1420f238a2 100644 --- a/lib/matplotlib/backends/backend_qt5.py +++ b/lib/matplotlib/backends/backend_qt5.py @@ -15,10 +15,12 @@ import matplotlib.backends.qt_editor.figureoptions as figureoptions from matplotlib.backends.qt_editor.formsubplottool import UiSubplotTool from matplotlib.backend_managers import ToolManager - +from . import qt_compat from .qt_compat import ( QtCore, QtGui, QtWidgets, _isdeleted, _getSaveFileName, - is_pyqt5, __version__, QT_API) + is_pyqt5, __version__, QT_API, + _devicePixelRatioF) + backend_version = __version__ @@ -267,12 +269,7 @@ def _update_figure_dpi(self): @property def _dpi_ratio(self): - # Not available on Qt4 or some older Qt5. - try: - # self.devicePixelRatio() returns 0 in rare cases - return self.devicePixelRatio() or 1 - except AttributeError: - return 1 + return _devicePixelRatioF(self) def _update_dpi(self): # As described in __init__ above, we need to be careful in cases with @@ -683,8 +680,7 @@ def _icon(self, name, color=None): if is_pyqt5(): name = name.replace('.png', '_large.png') pm = QtGui.QPixmap(os.path.join(self.basedir, name)) - if hasattr(pm, 'setDevicePixelRatio'): - pm.setDevicePixelRatio(self.canvas._dpi_ratio) + qt_compat._setDevicePixelRatioF(pm, _devicePixelRatioF(self)) if color is not None: mask = pm.createMaskFromColor(QtGui.QColor('black'), QtCore.Qt.MaskOutColor) diff --git a/lib/matplotlib/backends/backend_qt5agg.py b/lib/matplotlib/backends/backend_qt5agg.py index 09a2a261844d..56db30b335f1 100644 --- a/lib/matplotlib/backends/backend_qt5agg.py +++ b/lib/matplotlib/backends/backend_qt5agg.py @@ -11,7 +11,7 @@ from .backend_qt5 import ( QtCore, QtGui, QtWidgets, _BackendQT5, FigureCanvasQT, FigureManagerQT, NavigationToolbar2QT, backend_version) -from .qt_compat import QT_API +from .qt_compat import QT_API, _setDevicePixelRatioF class FigureCanvasQTAgg(FigureCanvasAgg, FigureCanvasQT): @@ -64,9 +64,7 @@ def paintEvent(self, event): qimage = QtGui.QImage(buf, buf.shape[1], buf.shape[0], QtGui.QImage.Format_ARGB32_Premultiplied) - if hasattr(qimage, 'setDevicePixelRatio'): - # Not available on Qt4 or some older Qt5. - qimage.setDevicePixelRatio(self._dpi_ratio) + _setDevicePixelRatioF(qimage, self._dpi_ratio) # set origin using original QT coordinates origin = QtCore.QPoint(rect.left(), rect.top()) painter.drawImage(origin, qimage) diff --git a/lib/matplotlib/backends/backend_qt5cairo.py b/lib/matplotlib/backends/backend_qt5cairo.py index 5a38a80864be..d29997410323 100644 --- a/lib/matplotlib/backends/backend_qt5cairo.py +++ b/lib/matplotlib/backends/backend_qt5cairo.py @@ -2,7 +2,7 @@ from .backend_cairo import cairo, FigureCanvasCairo, RendererCairo from .backend_qt5 import QtCore, QtGui, _BackendQT5, FigureCanvasQT -from .qt_compat import QT_API +from .qt_compat import QT_API, _setDevicePixelRatioF class FigureCanvasQTCairo(FigureCanvasQT, FigureCanvasCairo): @@ -19,8 +19,8 @@ def draw(self): def paintEvent(self, event): self._update_dpi() dpi_ratio = self._dpi_ratio - width = dpi_ratio * self.width() - height = dpi_ratio * self.height() + width = int(dpi_ratio * self.width()) + height = int(dpi_ratio * self.height()) if (width, height) != self._renderer.get_canvas_width_height(): surface = cairo.ImageSurface(cairo.FORMAT_ARGB32, width, height) self._renderer.set_ctx_from_surface(surface) @@ -33,9 +33,7 @@ def paintEvent(self, event): # QImage under PySide on Python 3. if QT_API == 'PySide': ctypes.c_long.from_address(id(buf)).value = 1 - if hasattr(qimage, 'setDevicePixelRatio'): - # Not available on Qt4 or some older Qt5. - qimage.setDevicePixelRatio(dpi_ratio) + _setDevicePixelRatioF(qimage, dpi_ratio) painter = QtGui.QPainter(self) painter.eraseRect(event.rect()) painter.drawImage(0, 0, qimage) diff --git a/lib/matplotlib/backends/qt_compat.py b/lib/matplotlib/backends/qt_compat.py index 2a4a3aa53629..90dddc29faa3 100644 --- a/lib/matplotlib/backends/qt_compat.py +++ b/lib/matplotlib/backends/qt_compat.py @@ -173,4 +173,38 @@ def is_pyqt5(): # These globals are only defined for backcompatibility purposes. ETS = dict(pyqt=(QT_API_PYQTv2, 4), pyside=(QT_API_PYSIDE, 4), pyqt5=(QT_API_PYQT5, 5), pyside2=(QT_API_PYSIDE2, 5)) -QT_RC_MAJOR_VERSION = 5 if is_pyqt5() else 4 + +QT_RC_MAJOR_VERSION = int(QtCore.qVersion().split(".")[0]) + + +def _devicePixelRatioF(obj): + """ + Return obj.devicePixelRatioF() with graceful fallback for older Qt. + + This can be replaced by the direct call when we require Qt>=5.6. + """ + try: + # Not available on Qt<5.6 + return obj.devicePixelRatioF() or 1 + except AttributeError: + pass + try: + # Not available on Qt4 or some older Qt5. + # self.devicePixelRatio() returns 0 in rare cases + return obj.devicePixelRatio() or 1 + except AttributeError: + return 1 + + +def _setDevicePixelRatioF(obj, val): + """ + Call obj.setDevicePixelRatioF(val) with graceful fallback for older Qt. + + This can be replaced by the direct call when we require Qt>=5.6. + """ + if hasattr(obj, 'setDevicePixelRatioF'): + # Not available on Qt<5.6 + obj.setDevicePixelRatioF(val) + if hasattr(obj, 'setDevicePixelRatio'): + # Not available on Qt4 or some older Qt5. + obj.setDevicePixelRatio(val) diff --git a/lib/matplotlib/tests/test_backend_qt.py b/lib/matplotlib/tests/test_backend_qt.py index e0c0a3b2010f..c49af2ddfbbc 100644 --- a/lib/matplotlib/tests/test_backend_qt.py +++ b/lib/matplotlib/tests/test_backend_qt.py @@ -253,6 +253,29 @@ def test_dpi_ratio_change(): assert qt_canvas.get_width_height() == (600, 240) assert (fig.get_size_inches() == (5, 2)).all() + p.return_value = 1.5 + + assert qt_canvas._dpi_ratio == 1.5 + + qt_canvas.draw() + qApp.processEvents() + # this second processEvents is required to fully run the draw. + # On `update` we notice the DPI has changed and trigger a + # resize event to refresh, the second processEvents is + # required to process that and fully update the window sizes. + qApp.processEvents() + + # The DPI and the renderer width/height change + assert fig.dpi == 180 + assert qt_canvas.renderer.width == 900 + assert qt_canvas.renderer.height == 360 + + # The actual widget size and figure physical size don't change + assert size.width() == 600 + assert size.height() == 240 + assert qt_canvas.get_width_height() == (600, 240) + assert (fig.get_size_inches() == (5, 2)).all() + @pytest.mark.backend('Qt5Agg') def test_subplottool(): From d91fa3a0315dce228b75a5845e04c214ee808dea Mon Sep 17 00:00:00 2001 From: Elliott Sales de Andrade Date: Thu, 11 Jun 2020 17:53:05 -0400 Subject: [PATCH 2/4] Merge pull request #17618 from tacaswell/doc_event_loop Doc event loop --- lib/matplotlib/backend_bases.py | 2 +- lib/matplotlib/backends/backend_qt5.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/lib/matplotlib/backend_bases.py b/lib/matplotlib/backend_bases.py index d9a4447744a0..ac19d29884c4 100644 --- a/lib/matplotlib/backend_bases.py +++ b/lib/matplotlib/backend_bases.py @@ -2293,7 +2293,7 @@ def start_event_loop(self, timeout=0): The event loop blocks until a callback function triggers `stop_event_loop`, or *timeout* is reached. - If *timeout* is negative, never timeout. + If *timeout* is 0 or negative, never timeout. Only interactive backends need to reimplement this method and it relies on `flush_events` being properly implemented. diff --git a/lib/matplotlib/backends/backend_qt5.py b/lib/matplotlib/backends/backend_qt5.py index 4b1420f238a2..6bfe20e87cac 100644 --- a/lib/matplotlib/backends/backend_qt5.py +++ b/lib/matplotlib/backends/backend_qt5.py @@ -451,7 +451,7 @@ def start_event_loop(self, timeout=0): if hasattr(self, "_event_loop") and self._event_loop.isRunning(): raise RuntimeError("Event loop already running") self._event_loop = event_loop = QtCore.QEventLoop() - if timeout: + if timeout > 0: timer = QtCore.QTimer.singleShot(timeout * 1000, event_loop.quit) event_loop.exec_() From c12c4ffb37043af4d24a574cd2b8494dec692e2a Mon Sep 17 00:00:00 2001 From: Jody Klymak Date: Mon, 15 Jun 2020 14:48:58 -0700 Subject: [PATCH 3/4] Backport PR #17600: FIX: work with PyQt 5.15 Merge pull request #17600 from tacaswell/mnt_more_qt515_fixes FIX: work with PyQt 5.15 Conflicts: lib/matplotlib/backends/backend_qt5.py - on this branch the blitting code is still in backend_qt5agg.py --- lib/matplotlib/backends/backend_qt5.py | 3 ++- lib/matplotlib/backends/backend_qt5agg.py | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/lib/matplotlib/backends/backend_qt5.py b/lib/matplotlib/backends/backend_qt5.py index 6bfe20e87cac..4aa804ad7e34 100644 --- a/lib/matplotlib/backends/backend_qt5.py +++ b/lib/matplotlib/backends/backend_qt5.py @@ -452,7 +452,8 @@ def start_event_loop(self, timeout=0): raise RuntimeError("Event loop already running") self._event_loop = event_loop = QtCore.QEventLoop() if timeout > 0: - timer = QtCore.QTimer.singleShot(timeout * 1000, event_loop.quit) + timer = QtCore.QTimer.singleShot(int(timeout * 1000), + event_loop.quit) event_loop.exec_() def stop_event_loop(self, event=None): diff --git a/lib/matplotlib/backends/backend_qt5agg.py b/lib/matplotlib/backends/backend_qt5agg.py index 56db30b335f1..f4d7842cbd1f 100644 --- a/lib/matplotlib/backends/backend_qt5agg.py +++ b/lib/matplotlib/backends/backend_qt5agg.py @@ -85,7 +85,7 @@ def blit(self, bbox=None): bbox = self.figure.bbox # repaint uses logical pixels, not physical pixels like the renderer. - l, b, w, h = [pt / self._dpi_ratio for pt in bbox.bounds] + l, b, w, h = [int(pt / self._dpi_ratio) for pt in bbox.bounds] t = b + h self.repaint(l, self.renderer.height / self._dpi_ratio - t, w, h) From ff32cc7428b8b5b8116a6890ebf3373bda264852 Mon Sep 17 00:00:00 2001 From: hannah Date: Tue, 16 Jun 2020 00:26:57 -0400 Subject: [PATCH 4/4] Backport PR #17640: More qt fractional DPI fixes Merge pull request #17640 from QuLogic/more-pyqt5-fixes More qt fractional DPI fixes --- lib/matplotlib/backends/backend_qt5.py | 4 ++-- lib/matplotlib/backends/qt_compat.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/lib/matplotlib/backends/backend_qt5.py b/lib/matplotlib/backends/backend_qt5.py index 4aa804ad7e34..dc4f6e26b066 100644 --- a/lib/matplotlib/backends/backend_qt5.py +++ b/lib/matplotlib/backends/backend_qt5.py @@ -18,7 +18,7 @@ from . import qt_compat from .qt_compat import ( QtCore, QtGui, QtWidgets, _isdeleted, _getSaveFileName, - is_pyqt5, __version__, QT_API, + is_pyqt5, __version__, QT_API, _setDevicePixelRatioF, _devicePixelRatioF) @@ -681,7 +681,7 @@ def _icon(self, name, color=None): if is_pyqt5(): name = name.replace('.png', '_large.png') pm = QtGui.QPixmap(os.path.join(self.basedir, name)) - qt_compat._setDevicePixelRatioF(pm, _devicePixelRatioF(self)) + _setDevicePixelRatioF(pm, _devicePixelRatioF(self)) if color is not None: mask = pm.createMaskFromColor(QtGui.QColor('black'), QtCore.Qt.MaskOutColor) diff --git a/lib/matplotlib/backends/qt_compat.py b/lib/matplotlib/backends/qt_compat.py index 90dddc29faa3..906845a68096 100644 --- a/lib/matplotlib/backends/qt_compat.py +++ b/lib/matplotlib/backends/qt_compat.py @@ -205,6 +205,6 @@ def _setDevicePixelRatioF(obj, val): if hasattr(obj, 'setDevicePixelRatioF'): # Not available on Qt<5.6 obj.setDevicePixelRatioF(val) - if hasattr(obj, 'setDevicePixelRatio'): + elif hasattr(obj, 'setDevicePixelRatio'): # Not available on Qt4 or some older Qt5. obj.setDevicePixelRatio(val)