From c0e159a043c8ece48a27555209ae4a43d744a599 Mon Sep 17 00:00:00 2001 From: Etienne Trimaille Date: Mon, 13 Nov 2023 10:25:46 +0100 Subject: [PATCH] UX - Add a new dock for HTML maptip preview --- lizmap/dialogs/dock_html_preview.py | 182 ++++++++++++++++++++++ lizmap/plugin.py | 27 ++++ lizmap/resources/html/maptip_preview.html | 6 + lizmap/resources/ui/ui_lizmap.ui | 7 + 4 files changed, 222 insertions(+) create mode 100644 lizmap/dialogs/dock_html_preview.py create mode 100644 lizmap/resources/html/maptip_preview.html diff --git a/lizmap/dialogs/dock_html_preview.py b/lizmap/dialogs/dock_html_preview.py new file mode 100644 index 00000000..56dc1035 --- /dev/null +++ b/lizmap/dialogs/dock_html_preview.py @@ -0,0 +1,182 @@ +__copyright__ = 'Copyright 2023, 3Liz' +__license__ = 'GPL version 3' +__email__ = 'info@3liz.org' + +import logging + +from qgis.core import ( + Qgis, + QgsApplication, + QgsExpression, + QgsExpressionContext, + QgsExpressionContextUtils, + QgsMapLayerProxyModel, + QgsProject, +) + +if Qgis.QGIS_VERSION_INT >= 31400: + from qgis.gui import QgsFeaturePickerWidget + +from qgis.gui import QgsMapLayerComboBox +from qgis.PyQt.QtCore import QDateTime, QLocale, QUrl +from qgis.PyQt.QtGui import QIcon +from qgis.PyQt.QtWidgets import ( + QDockWidget, + QHBoxLayout, + QLabel, + QPushButton, + QSizePolicy, + QVBoxLayout, + QWidget, +) +from qgis.utils import iface + +from lizmap.qgis_plugin_tools.tools.i18n import tr +from lizmap.qgis_plugin_tools.tools.resources import resources_path + +try: + from qgis.PyQt.QtWebKitWidgets import QWebView + WEBKIT_AVAILABLE = True +except ModuleNotFoundError: + WEBKIT_AVAILABLE = False + +LOGGER = logging.getLogger('Lizmap') + + +class HtmlPreview(QDockWidget): + + # noinspection PyArgumentList + def __init__(self, parent, *__args): + """ Constructor. """ + super().__init__(parent, *__args) + self.setWindowTitle("Lizmap HTML Maptip Preview") + + self._server_url = None + + self.dock = QWidget(parent) + self.layout = QVBoxLayout(self.dock) + + if Qgis.QGIS_VERSION_INT < 31400: + self.label = QLabel('You must install at least QGIS 3.14') + self.label.setWordWrap(True) + self.layout.addWidget(self.label) + return + + if not WEBKIT_AVAILABLE: + self.label = QLabel(tr('You must install Qt Webkit to enable this feature.')) + else: + self.label = QLabel(tr("This only a preview of the HTML maptip. Lizmap will add more CSS classes.")) + + self.label.setWordWrap(True) + self.layout.addWidget(self.label) + + if not WEBKIT_AVAILABLE: + return + + horizontal = QHBoxLayout(self.dock) + + self.layer = QgsMapLayerComboBox(self.dock) + horizontal.addWidget(self.layer) + + self.feature = QgsFeaturePickerWidget(self.dock) + horizontal.addWidget(self.feature) + + self.layout.addLayout(horizontal) + + horizontal = QHBoxLayout(self.dock) + + self.refresh = QPushButton(self.dock) + self.refresh.setIcon(QIcon(QgsApplication.iconPath('mActionRefresh.svg'))) + # noinspection PyUnresolvedReferences + self.refresh.clicked.connect(self.update_html) + horizontal.addWidget(self.refresh) + + self.label = QLabel() + horizontal.addWidget(self.label) + + self.layout.addLayout(horizontal) + + self.web_view = QWebView(self.dock) + size_policy = QSizePolicy(QSizePolicy.Preferred, QSizePolicy.MinimumExpanding) + size_policy.setHorizontalStretch(0) + size_policy.setVerticalStretch(0) + size_policy.setHeightForWidth(self.web_view.sizePolicy().hasHeightForWidth()) + self.web_view.setSizePolicy(size_policy) + self.layout.addWidget(self.web_view) + + self.setWidget(self.dock) + + self.layer.setFilters(QgsMapLayerProxyModel.VectorLayer) + # noinspection PyUnresolvedReferences + self.layer.layerChanged.connect(self.current_layer_changed) + self.current_layer_changed() + # noinspection PyUnresolvedReferences + self.feature.featureChanged.connect(self.update_html) + self.feature.setShowBrowserButtons(True) + + # We don't have a better signal to listen to + QgsProject.instance().dirtySet.connect(self.update_html) + + self.update_html() + + def set_server_url(self, url: str): + """ Set the server URL according to the main dialog. """ + if not url.endswith('/'): + url += '/' + self._server_url = url + + def css(self) -> str: + """ Link to CSS style sheet according to server. """ + asset = 'assets/css/bootstrap.min.css' + return self._server_url + asset + + def current_layer_changed(self): + """ When the layer has changed. """ + self.feature.setLayer(self.layer.currentLayer()) + # Need to disconnect all layers before ? + # self.layer.currentLayer().repaintRequested.connect(self.update_html()) + + # noinspection PyArgumentList + def update_html(self): + """ Update the HTML preview. """ + # This function is called when the project is "setDirty", + # because it means maybe the vector layer properties has been "applied" + + if not self.isVisible(): + # If the dock is not visible, we don't care + return + + layer = self.layer.currentLayer() + if not layer: + return + + if iface.activeLayer() != layer: + # This function is called when the project is "setDirty", + # because it means maybe the vector layer properties has been "applied" + return + + feature = self.feature.feature() + if not feature: + return + + now = QDateTime.currentDateTime() + now_str = now.toString(QLocale.c().timeFormat(QLocale.ShortFormat)) + self.label.setText(tr("Last update") + " " + now_str) + + exp_context = QgsExpressionContext() + exp_context.appendScope(QgsExpressionContextUtils.globalScope()) + exp_context.appendScope(QgsExpressionContextUtils.projectScope(QgsProject.instance())) + exp_context.appendScope(QgsExpressionContextUtils.layerScope(layer)) + exp_context.setFeature(feature) + html_string = QgsExpression.replaceExpressionText(layer.mapTipTemplate(), exp_context) + base_url = QUrl.fromLocalFile(resources_path('images', 'non_existing_file.png')) + + with open(resources_path('html', 'maptip_preview.html'), encoding='utf8') as f: + html_template = f.read() + + html_content = html_template.format( + css=self.css(), + maptip=html_string, + ) + + self.web_view.setHtml(html_content, base_url) diff --git a/lizmap/plugin.py b/lizmap/plugin.py index ef24ecc3..0a7295a6 100755 --- a/lizmap/plugin.py +++ b/lizmap/plugin.py @@ -97,6 +97,7 @@ from lizmap.definitions.time_manager import TimeManagerDefinitions from lizmap.definitions.tooltip import ToolTipDefinitions from lizmap.definitions.warnings import Warnings +from lizmap.dialogs.dock_html_preview import HtmlPreview from lizmap.dialogs.html_editor import HtmlEditorDialog from lizmap.dialogs.html_maptip import HtmlMapTipDialog from lizmap.dialogs.lizmap_popup import LizmapPopupDialog @@ -223,6 +224,7 @@ def __init__(self, iface): self.version = version() self.is_dev_version = any(item in self.version for item in UNSTABLE_VERSION_PREFIX) self.dlg = LizmapDialog(is_dev_version=self.is_dev_version) + self.dock_html_preview = None self.version_checker = None if self.is_dev_version: @@ -833,6 +835,11 @@ def target_server_changed(self): self.dlg.check_qgis_version(widget=True) self.check_webdav() + if self.dock_html_preview: + # Change the URL for the CSS + self.dock_html_preview: HtmlPreview + self.dock_html_preview.set_server_url(self.dlg.current_server_info(ServerComboData.ServerUrl.value)) + lizmap_cloud = is_lizmap_cloud(current_metadata) for item in self.lizmap_cloud: item.setVisible(lizmap_cloud) @@ -962,6 +969,14 @@ def initGui(self): # noinspection PyUnresolvedReferences self.action.triggered.connect(self.run) + self.dock_html_preview = HtmlPreview(None) + self.dock_html_preview.set_server_url(self.dlg.current_server_info(ServerComboData.ServerUrl.value)) + self.iface.addDockWidget(Qt.RightDockWidgetArea, self.dock_html_preview) + self.dock_html_preview.setVisible(False) + self.dlg.button_maptip_preview.setText('') + self.dlg.button_maptip_preview.setIcon(QIcon(":images/themes/default/mActionShowAllLayers.svg")) + self.dlg.button_maptip_preview.clicked.connect(self.open_dock_preview_maptip) + if self.is_dev_version: label = 'Lizmap dev' self.action_debug_pg_qgis = QAction(icon, "Add PostgreSQL connection from datasource") @@ -1360,6 +1375,10 @@ def unload(self): self.iface.webMenu().removeAction(self.action) self.iface.removeWebToolBarIcon(self.action) + if self.dock_html_preview: + self.iface.removeDockWidget(self.dock_html_preview) + del self.dock_html_preview + if self.help_action: self.iface.pluginHelpMenu().removeAction(self.help_action) del self.help_action @@ -1378,6 +1397,7 @@ def enable_popup_source_button(self): data = self.layer_options_list['popupSource']['widget'].currentData() self.dlg.btConfigurePopup.setVisible(data in ('lizmap', 'qgis')) self.dlg.widget_qgis_maptip.setVisible(data == 'qgis') + self.dlg.button_maptip_preview.setVisible(data == 'qgis') if data == 'lizmap': layer = self._current_selected_layer() @@ -4018,6 +4038,13 @@ def on_project_read(self): self.reinit_default_properties() self.dlg.close() + def open_dock_preview_maptip(self): + if self.dock_html_preview.isVisible(): + return + + self.iface.addDockWidget(Qt.RightDockWidgetArea, self.dock_html_preview) + self.dock_html_preview.setVisible(True) + def run(self) -> bool: """Plugin run method : launch the GUI.""" self.dlg.check_action_file_exists() diff --git a/lizmap/resources/html/maptip_preview.html b/lizmap/resources/html/maptip_preview.html new file mode 100644 index 00000000..b3684e20 --- /dev/null +++ b/lizmap/resources/html/maptip_preview.html @@ -0,0 +1,6 @@ + + + + + {maptip} + diff --git a/lizmap/resources/ui/ui_lizmap.ui b/lizmap/resources/ui/ui_lizmap.ui index 47736065..ff95796b 100755 --- a/lizmap/resources/ui/ui_lizmap.ui +++ b/lizmap/resources/ui/ui_lizmap.ui @@ -1735,6 +1735,13 @@ This is different to the map maximum extent (defined in QGIS project properties, + + + + PREVIEW + + +