From fa535039ad3fa900f51e5cf9681b310aaa27ecf2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Bruno=20Escalh=C3=A3o?= Date: Tue, 31 Mar 2026 10:43:36 -0300 Subject: [PATCH] feat: dual WebKit/WebEngine support for plot layout item ref #377 --- DataPlotly/layouts/plot_layout_item.py | 152 +++++++++++++++++-------- 1 file changed, 105 insertions(+), 47 deletions(-) diff --git a/DataPlotly/layouts/plot_layout_item.py b/DataPlotly/layouts/plot_layout_item.py index e859dc65..6f5000a7 100644 --- a/DataPlotly/layouts/plot_layout_item.py +++ b/DataPlotly/layouts/plot_layout_item.py @@ -16,6 +16,7 @@ from qgis.PyQt.QtWidgets import QGraphicsItem from qgis.core import ( + Qgis, QgsLayoutItem, QgsLayoutItemRegistry, QgsLayoutItemAbstractMetadata, @@ -23,48 +24,84 @@ QgsGeometry, QgsPropertyCollection ) -from qgis.PyQt.QtWebEngineWidgets import QWebEngineView -from qgis.PyQt.QtWebEngineCore import QWebEnginePage + +if Qgis.versionInt() >= 40000: + from qgis.PyQt.QtWebEngineWidgets import QWebEngineView + from qgis.PyQt.QtWebEngineCore import QWebEnginePage +else: + from qgis.PyQt.QtGui import QPalette + from qgis.PyQt.QtWebKitWidgets import QWebPage + from qgis.core import QgsNetworkAccessManager from DataPlotly.core.plot_settings import PlotSettings from DataPlotly.core.plot_factory import PlotFactory, FilterRegion from DataPlotly.gui.gui_utils import GuiUtils -ITEM_TYPE = QgsLayoutItemRegistry.ItemType.PluginItem + 1337 +if Qgis.versionInt() >= 40000: + ITEM_TYPE = QgsLayoutItemRegistry.ItemType.PluginItem + 1337 +else: + ITEM_TYPE = QgsLayoutItemRegistry.PluginItem + 1337 + + +if Qgis.versionInt() >= 40000: + + class LoggingWebPage(QWebEnginePage): + + def __init__(self, parent=None): + super().__init__(parent) + def javaScriptConsoleMessage(self, level, message, lineNumber, source): + QgsMessageLog.logMessage(f'{source}:{lineNumber} {message}', 'DataPlotly') -class LoggingWebPage(QWebEnginePage): +else: - def __init__(self, parent=None): - super().__init__(parent) + class LoggingWebPage(QWebPage): - def javaScriptConsoleMessage(self, level, message, lineNumber, source): - QgsMessageLog.logMessage(f'{source}:{lineNumber} {message}', 'DataPlotly') + def __init__(self, parent=None): + super().__init__(parent) + + def javaScriptConsoleMessage(self, message, lineNumber, source): + QgsMessageLog.logMessage(f'{source}:{lineNumber} {message}', 'DataPlotly') class PlotLayoutItem(QgsLayoutItem): def __init__(self, layout): super().__init__(layout) - self.setCacheMode(QGraphicsItem.CacheMode.NoCache) + if Qgis.versionInt() >= 40000: + self.setCacheMode(QGraphicsItem.CacheMode.NoCache) + else: + self.setCacheMode(QGraphicsItem.NoCache) self.plot_settings = [] self.plot_settings.append(PlotSettings()) self.linked_map_uuid = '' self.linked_map = None + self._loading = False + self._captured_pixmap = None - self.web_page = LoggingWebPage(self) - self.web_page.setBackgroundColor(Qt.GlobalColor.transparent) + if Qgis.versionInt() >= 40000: + self.web_page = LoggingWebPage(self) + self.web_page.setBackgroundColor(Qt.GlobalColor.transparent) - self.web_view = QWebEngineView() - self.web_view.setPage(self.web_page) - self.web_view.setAttribute(Qt.WidgetAttribute.WA_DontShowOnScreen) - self.web_view.setZoomFactor(10.0) - self.web_view.show() + self.web_view = QWebEngineView() + self.web_view.setPage(self.web_page) + self.web_view.setAttribute(Qt.WidgetAttribute.WA_DontShowOnScreen) + self.web_view.setZoomFactor(10.0) + self.web_view.show() + else: + self.web_page = LoggingWebPage(self) + self.web_page.setNetworkAccessManager(QgsNetworkAccessManager.instance()) + + # This makes the background transparent. (copied from QgsLayoutItemLabel) + palette = self.web_page.palette() + palette.setBrush(QPalette.Base, Qt.transparent) + self.web_page.setPalette(palette) + self.web_page.mainFrame().setZoomFactor(10.0) + self.web_page.mainFrame().setScrollBarPolicy(Qt.Horizontal, Qt.ScrollBarAlwaysOff) + self.web_page.mainFrame().setScrollBarPolicy(Qt.Vertical, Qt.ScrollBarAlwaysOff) self.web_page.loadFinished.connect(self.loading_html_finished) self.html_loaded = False - self._loading = False - self._captured_pixmap = None self.html_units_to_layout_units = self.calculate_html_units_to_layout_units() self.sizePositionChanged.connect(self.refresh) @@ -82,7 +119,9 @@ def calculate_html_units_to_layout_units(self): # Hm - why is this? Something internal in Plotly which is auto-scaling the html content? # we may need to expose this as a "scaling" setting - return 8 + if Qgis.versionInt() >= 40000: + return 8 + return 72 def set_linked_map(self, map): """ @@ -162,11 +201,18 @@ def draw(self, context): painter = context.renderContext().painter() painter.save() - if self._captured_pixmap and not self._captured_pixmap.isNull(): - sx = self.rect().width() * context.renderContext().scaleFactor() / self._captured_pixmap.width() - sy = self.rect().height() * context.renderContext().scaleFactor() / self._captured_pixmap.height() - painter.scale(sx, sy) - painter.drawPixmap(0, 0, self._captured_pixmap) + if Qgis.versionInt() >= 40000: + if self._captured_pixmap and not self._captured_pixmap.isNull(): + sx = self.rect().width() * context.renderContext().scaleFactor() / self._captured_pixmap.width() + sy = self.rect().height() * context.renderContext().scaleFactor() / self._captured_pixmap.height() + painter.scale(sx, sy) + painter.drawPixmap(0, 0, self._captured_pixmap) + else: + # almost a direct copy from QgsLayoutItemLabel! + painter.scale(context.renderContext().scaleFactor() / self.html_units_to_layout_units, + context.renderContext().scaleFactor() / self.html_units_to_layout_units) + self.web_page.mainFrame().render(painter) + painter.restore() def create_plot(self): @@ -213,15 +259,22 @@ def get_polygon_filter(self, index=0): return polygon_filter, visible_features_only def load_content(self): - import tempfile - self._loading = True self.html_loaded = False - self.web_view.resize(QSize(int(self.rect().width()) * self.html_units_to_layout_units, - int(self.rect().height()) * self.html_units_to_layout_units)) - self._tmp_file = tempfile.NamedTemporaryFile(suffix='.html', delete=False) - self._tmp_file.write(self.create_plot().encode('utf-8')) - self._tmp_file.close() - self.web_page.load(QUrl.fromLocalFile(self._tmp_file.name)) + + if Qgis.versionInt() >= 40000: + import tempfile + self._loading = True + self.web_view.resize(QSize(int(self.rect().width()) * self.html_units_to_layout_units, + int(self.rect().height()) * self.html_units_to_layout_units)) + self._tmp_file = tempfile.NamedTemporaryFile(suffix='.html', delete=False) + self._tmp_file.write(self.create_plot().encode('utf-8')) + self._tmp_file.close() + self.web_page.load(QUrl.fromLocalFile(self._tmp_file.name)) + else: + base_url = QUrl.fromLocalFile(self.layout().project().absoluteFilePath()) + self.web_page.setViewportSize(QSize(int(self.rect().width()) * self.html_units_to_layout_units, + int(self.rect().height()) * self.html_units_to_layout_units)) + self.web_page.mainFrame().setHtml(self.create_plot(), base_url) def writePropertiesToElement(self, element, document, _) -> bool: for plot_setting in self.plot_settings: @@ -261,22 +314,27 @@ def finalizeRestoreFromXml(self): self.set_linked_map(map) def loading_html_finished(self): - self.web_page.runJavaScript("document.documentElement.style.overflow='hidden'") - self._render_retries = 0 - js = """(function() { - var plot = document.querySelector('.js-plotly-plot'); - if (plot && typeof Plotly !== 'undefined') { - Plotly.toImage(plot, {format: 'png', scale: 2}).then(function(dataUrl) { - window._capturedImage = dataUrl; - }).catch(function() { + if Qgis.versionInt() >= 40000: + self.web_page.runJavaScript("document.documentElement.style.overflow='hidden'") + self._render_retries = 0 + js = """(function() { + var plot = document.querySelector('.js-plotly-plot'); + if (plot && typeof Plotly !== 'undefined') { + Plotly.toImage(plot, {format: 'png', scale: 2}).then(function(dataUrl) { + window._capturedImage = dataUrl; + }).catch(function() { + window._capturedImage = ''; + }); + } else { window._capturedImage = ''; - }); - } else { - window._capturedImage = ''; - } - })()""" - self.web_page.runJavaScript(js) - self._wait_for_image_capture() + } + })()""" + self.web_page.runJavaScript(js) + self._wait_for_image_capture() + else: + self.html_loaded = True + self.invalidateCache() + self.update() def _wait_for_image_capture(self): """Poll until Plotly.toImage() has produced the image."""