diff --git a/lizmap/config/models.py b/lizmap/config/models.py index e329fb6a..d71803c6 100644 --- a/lizmap/config/models.py +++ b/lizmap/config/models.py @@ -1,11 +1,17 @@ from types import MappingProxyType from typing import ( Any, - NotRequired, Sequence, TypedDict, ) +# Works on Python 3.11+ +try: + from typing import NotRequired +except ImportError: + # Fallback for Python < 3.11 (or if you want to ensure typing_extensions is used) + from typing_extensions import NotRequired + from qgis.core import Qgis MappingQgisGeometryType = MappingProxyType( diff --git a/lizmap/definitions/base.py b/lizmap/definitions/base.py index 9f6c7bc1..9d79d2df 100644 --- a/lizmap/definitions/base.py +++ b/lizmap/definitions/base.py @@ -26,6 +26,7 @@ class InputType(Enum): SpinBox = 'SpinBox' # QSpinbox Text = 'Text' # QLineEdit MultiLine = 'MultiLine' # QPlainTextEdit or QgsCodeEditorHTML + Scale = 'Scale' # QgsScaleWidget class BaseDefinitions: diff --git a/lizmap/definitions/online_help.py b/lizmap/definitions/online_help.py index 9bebd569..8dfa2d8f 100644 --- a/lizmap/definitions/online_help.py +++ b/lizmap/definitions/online_help.py @@ -50,20 +50,21 @@ class Panels: AttributeTable = 4 Editing = 5 Layouts = 6 - DxfExport = 7 - FormFiltering = 8 - Dataviz = 9 - FilteredLayers = 10 - Actions = 11 - TimeManager = 12 - Atlas = 13 - LocateByLayer = 14 - ToolTip = 15 - Checks = 16 - AutoFix = 17 - Settings = 18 - Upload = 19 - Training = 20 + Portfolio = 7 + DxfExport = 8 + FormFiltering = 9 + Dataviz = 10 + FilteredLayers = 11 + Actions = 12 + TimeManager = 13 + Atlas = 14 + LocateByLayer = 15 + ToolTip = 16 + Checks = 17 + AutoFix = 18 + Settings = 19 + Upload = 20 + Training = 21 MAPPING_INDEX_DOC = { @@ -88,6 +89,7 @@ class Panels: Panels.Upload: None, Panels.Training: None, Panels.DxfExport: 'publish/lizmap_plugin/dxf_export.html', + Panels.Portfolio: 'publish/lizmap_plugin/portfolio.html', } diff --git a/lizmap/definitions/portfolio.py b/lizmap/definitions/portfolio.py new file mode 100644 index 00000000..70e82553 --- /dev/null +++ b/lizmap/definitions/portfolio.py @@ -0,0 +1,148 @@ +"""Definitions for portfolio.""" + +from enum import Enum, unique +from typing import ( + Dict, +) + +from lizmap.definitions.base import BaseDefinitions, InputType +from lizmap.toolbelt.i18n import tr + + +def represent_layouts(data: Dict) -> str: + """Generate HTMl string for the tooltip instead of JSON representation.""" + html = '\n' + return html + + +@unique +class GeometryType(Enum): + Point = { + 'data': 'point', + 'label': tr('Point'), + } + Line = { + 'data': 'line', + 'label': tr('Line'), + } + Polygon = { + 'data': 'polygon', + 'label': tr('Polygon'), + } + + +@unique +class ZoomMethodType(Enum): + FixScale = { + 'data': 'fix_scale', + 'label': tr('Fix scale'), + } + Margin = { + 'data': 'margin', + 'label': tr('Margin'), + } + BestScale = { + 'data': 'best_scale', + 'label': tr('Best scale'), + } + + +class PortfolioDefinitions(BaseDefinitions): + + def __init__(self): + super().__init__() + self._layer_config['title'] = { + 'type': InputType.Text, + 'header': tr('Title'), + 'default': '', + 'tooltip': tr('The title of the portfolio, when displayed in the portfolio dock'), + } + self._layer_config['description'] = { + 'type': InputType.HtmlWysiwyg, + 'header': tr('Description'), + 'default': '', + 'tooltip': tr('The description of the portfolio. HTML is supported.'), + } + self._layer_config['drawing_geometry'] = { + 'type': InputType.List, + 'header': tr('Geometry'), + 'items': GeometryType, + 'default': GeometryType.Point, + 'tooltip': tr('The geometry type of the portfolio.') + } + self._layer_config['layouts'] = { + 'type': InputType.Collection, + 'header': tr('Layouts'), + 'tooltip': tr('Textual representations of layout tuples'), + 'items': [ + 'layout', + 'theme', + 'zoom_method', + 'fix_scale', + 'margin', + ], + 'represent_value': represent_layouts, + } + self._layer_config['layout'] = { + 'plural': 'layout_{}', + 'type': InputType.List, + 'header': tr('Layout'), + 'tooltip': tr('The zoom to geometry method, depends on the geometry type.') + } + self._layer_config['theme'] = { + 'plural': 'theme_{}', + 'type': InputType.List, + 'header': tr('Theme'), + 'tooltip': tr('The zoom to geometry method, depends on the geometry type.') + } + self._layer_config['zoom_method'] = { + 'plural': 'zoom_method_{}', + 'type': InputType.List, + 'header': tr('Zoom method'), + 'items': ZoomMethodType, + 'default': ZoomMethodType.FixScale, + 'tooltip': tr('The zoom to geometry method, depends on the geometry type.') + } + self._layer_config['fix_scale'] = { + 'plural': 'fix_scale_{}', + 'type': InputType.Scale, + 'header': tr('Fix scale'), + 'default': 5000, + 'tooltip': tr('The scale of the portfolio for point geometry.') + } + self._layer_config['margin'] = { + 'plural': 'margin_{}', + 'type': InputType.SpinBox, + 'header': tr('Margin'), + 'default': 10, + 'tooltip': tr('The margin around line or polygon geometry.') + } + + @staticmethod + def primary_keys() -> tuple: + return tuple() + + def key(self) -> str: + return 'portfolioLayers' + + def help_path(self) -> str: + return 'publish/lizmap_plugin/portfolio.html' diff --git a/lizmap/forms/base_edition_dialog.py b/lizmap/forms/base_edition_dialog.py index 884c10ee..1b44c0cc 100644 --- a/lizmap/forms/base_edition_dialog.py +++ b/lizmap/forms/base_edition_dialog.py @@ -15,6 +15,7 @@ QMessageBox, QPlainTextEdit, ) +from qgis.utils import iface from lizmap.definitions.base import InputType from lizmap.definitions.definitions import LwcVersions, ServerComboData @@ -123,9 +124,18 @@ def setup_ui(self): widget.setSuffix(unit) default = layer_config.get('default') - if unit: + if unit: # TO FIX replaced by default widget.setValue(default) + if layer_config['type'] == InputType.Scale: + if widget is not None: + widget.setShowCurrentScaleButton(True) + widget.setMapCanvas(iface.mapCanvas()) + widget.setAllowNull(False) + default = layer_config.get('default') + if default: + widget.setScale(default) + if layer_config['type'] == InputType.Color: if widget is not None: if layer_config['default'] == '': @@ -386,6 +396,8 @@ def load_form(self, data: OrderedDict) -> None: definition['widget'].setCurrentIndex(index) elif definition['type'] == InputType.SpinBox: definition['widget'].setValue(value) + elif definition['type'] == InputType.Scale: + widget.setScale(value) elif definition['type'] == InputType.Text: definition['widget'].setText(value) elif definition['type'] == InputType.Json: @@ -443,6 +455,8 @@ def save_form(self) -> OrderedDict: value = definition['widget'].currentData() elif definition['type'] == InputType.SpinBox: value = definition['widget'].value() + elif definition['type'] == InputType.Scale: + value = definition['widget'].scale() elif definition['type'] == InputType.Text: value = definition['widget'].text().strip(' \t') elif definition['type'] == InputType.MultiLine: diff --git a/lizmap/forms/layout_portfolio_edition.py b/lizmap/forms/layout_portfolio_edition.py new file mode 100644 index 00000000..744ca2ee --- /dev/null +++ b/lizmap/forms/layout_portfolio_edition.py @@ -0,0 +1,111 @@ +"""Dialog for layout portfolio edition.""" +from collections import OrderedDict + +from qgis.core import QgsProject +from qgis.PyQt.QtWidgets import QDialog, QDialogButtonBox + +from lizmap.definitions.base import InputType +from lizmap.definitions.portfolio import ( + GeometryType, + PortfolioDefinitions, + ZoomMethodType, +) +from lizmap.toolbelt.resources import load_ui + +__copyright__ = 'Copyright 2020, 3Liz' +__license__ = 'GPL version 3' +__email__ = 'info@3liz.org' + + +CLASS = load_ui('ui_portfolio_layout.ui') + + +class LayouPortfolioEditionDialog(QDialog, CLASS): + + def __init__(self, parent, geometry, uniques): + super().__init__(parent) + self.config = PortfolioDefinitions() + self._geometry = geometry + self.uniques = uniques + self.setupUi(self) + + self.config.add_layer_widget('layout', self.layout) + self.config.add_layer_widget('theme', self.theme) + self.config.add_layer_widget('zoom_method', self.zoom_method) + self.config.add_layer_widget('fix_scale', self.fix_scale) + self.config.add_layer_widget('margin', self.margin) + + self.config.add_layer_label('layout', self.label_layout) + self.config.add_layer_label('theme', self.label_theme) + self.config.add_layer_label('zoom_method', self.label_zoom_method) + self.config.add_layer_label('fix_scale', self.label_fix_scale) + self.config.add_layer_label('margin', self.label_margin) + + layout_manager = QgsProject.instance().layoutManager() + for layout in layout_manager.printLayouts(): + if layout.atlas().enabled(): + continue + self.layout.addItem(layout.name(), layout.name()) + + theme_collection = QgsProject.instance().mapThemeCollection() + for theme_name in theme_collection.mapThemes(): + self.theme.addItem(theme_name, theme_name) + + if self._geometry == GeometryType.Point.value['data']: + fix_scale = ZoomMethodType.FixScale + self.zoom_method.addItem(fix_scale.value['label'], fix_scale.value['data']) + self.zoom_method.setCurrentText(fix_scale.value['data']) + self.zoom_method.setEnabled(False) + else: + for item_type in ZoomMethodType: + if item_type == ZoomMethodType.FixScale: + continue + self.zoom_method.addItem(item_type.value['label'], item_type.value['data']) + + # connect + self.zoom_method.currentTextChanged.connect(self.zoom_method_changed) + + self.button_box.button(QDialogButtonBox.StandardButton.Cancel).clicked.connect(self.close) + self.button_box.button(QDialogButtonBox.StandardButton.Ok).clicked.connect(self.accept) + self.error.setVisible(False) + + self.zoom_method_changed() + + def zoom_method_changed(self): + """Enable or disable scale and margin widgets.""" + zoom_method = self.zoom_method.currentData() + enable_margin = False + enable_fix_scale = False + if zoom_method == ZoomMethodType.Margin.value['data']: + enable_margin = True + elif zoom_method == ZoomMethodType.FixScale.value['data']: + enable_fix_scale = True + + self.margin.setEnabled(enable_margin) + self.fix_scale.setEnabled(enable_fix_scale) + + def save_form(self) -> OrderedDict: + """Save the form into a dictionary.""" + data = OrderedDict() + + for key in self.config.layer_config['layouts']['items']: + definition = self.config.layer_config[key] + if definition['type'] == InputType.List: + value = definition['widget'].currentData() + elif definition['type'] == InputType.Scale: + value = definition['widget'].scale() + elif definition['type'] == InputType.SpinBox: + value = definition['widget'].value() + else: + raise Exception('InputType "{}" not implemented'.format(definition['type'])) + + data[key] = value + + if data['zoom_method'] == ZoomMethodType.BestScale.value['data']: + del data['fix_scale'] + del data['margin'] + elif data['zoom_method'] == ZoomMethodType.Margin.value['data']: + del data['fix_scale'] + elif data['zoom_method'] == ZoomMethodType.FixScale.value['data']: + del data['margin'] + return data diff --git a/lizmap/forms/portfolio_edition.py b/lizmap/forms/portfolio_edition.py new file mode 100644 index 00000000..c5b5a72b --- /dev/null +++ b/lizmap/forms/portfolio_edition.py @@ -0,0 +1,189 @@ +"""Dialog for portfolio edition.""" + +from typing import TYPE_CHECKING, Dict, Optional + +from qgis.core import QgsApplication +from qgis.PyQt.QtCore import Qt +from qgis.PyQt.QtGui import QIcon +from qgis.PyQt.QtWidgets import ( + QAbstractItemView, + QDialog, + QHeaderView, + QMessageBox, + QTableWidgetItem, +) + +from lizmap.definitions.base import InputType +from lizmap.definitions.definitions import LwcVersions +from lizmap.definitions.portfolio import GeometryType, PortfolioDefinitions +from lizmap.forms.base_edition_dialog import BaseEditionDialog +from lizmap.forms.layout_portfolio_edition import LayouPortfolioEditionDialog +from lizmap.toolbelt.i18n import tr +from lizmap.toolbelt.resources import load_ui, resources_path + +if TYPE_CHECKING: + from lizmap.dialogs.main import LizmapDialog + + +CLASS = load_ui('ui_form_portfolio.ui') + + +class PortfolioEditionDialog(BaseEditionDialog, CLASS): + + def __init__( + self, + parent: Optional["LizmapDialog"] = None, + unicity: Optional[Dict[str, str]] = None, + lwc_version: Optional[LwcVersions] = None, + ): + super().__init__(parent, unicity, lwc_version) + self.setupUi(self) + self.parent = parent + self._drawing_geometry = None + self.config = PortfolioDefinitions() + self.config.add_layer_widget('title', self.title) + self.config.add_layer_widget('description', self.text_description) + self.config.add_layer_widget('drawing_geometry', self.drawing_geometry) + self.config.add_layer_widget('layouts', self.layouts) + + self.config.add_layer_label('title', self.label_title) + self.config.add_layer_label('description', self.label_description) + self.config.add_layer_label('drawing_geometry', self.label_drawing_geometry) + self.config.add_layer_label('layouts', self.label_layouts) + + # noinspection PyCallByClass,PyArgumentList + self.add_layout.setText('') + self.add_layout.setIcon(QIcon(QgsApplication.iconPath('symbologyAdd.svg'))) + self.add_layout.setToolTip(tr('Add a new layout to the portfolio.')) + self.remove_layout.setText('') + self.remove_layout.setIcon(QIcon(QgsApplication.iconPath('symbologyRemove.svg'))) + self.remove_layout.setToolTip(tr('Remove the selected layout from the portfolio.')) + + # Set layouts table + items = self.config.layer_config['layouts']['items'] + self.layouts.setColumnCount(len(items)) + for i, item in enumerate(items): + sub_definition = self.config.layer_config[item] + column = QTableWidgetItem(sub_definition['header']) + column.setToolTip(sub_definition['tooltip']) + self.layouts.setHorizontalHeaderItem(i, column) + header = self.layouts.horizontalHeader() + header.setSectionResizeMode(QHeaderView.ResizeMode.ResizeToContents) + header.setSectionResizeMode(0, QHeaderView.ResizeMode.Stretch) + + self.layouts.setSelectionMode(QAbstractItemView.SelectionMode.SingleSelection) + self.layouts.setEditTriggers(QAbstractItemView.EditTrigger.NoEditTriggers) + self.layouts.setSelectionBehavior(QAbstractItemView.SelectionBehavior.SelectRows) + self.layouts.setAlternatingRowColors(True) + + # connect + self.add_layout.clicked.connect(self.add_new_layout) + self.remove_layout.clicked.connect(self.remove_selected_layout) + self.drawing_geometry.currentTextChanged.connect(self.geometry_changed) + + self.setup_ui() + # self._drawing_geometry = self.drawing_geometry.getCurrentData() + + def add_new_layout(self): + """Add a new layout in the table after clicking the 'add' button.""" + + dialog = LayouPortfolioEditionDialog( + self.parent, self.drawing_geometry.currentData(), list() + ) + result = dialog.exec() + if result != QDialog.DialogCode.Accepted: + return + + data = dialog.save_form() + row = self.layouts.rowCount() + self.layouts.setRowCount(row + 1) + self._edit_layout_row(row, data) + + def _edit_layout_row(self, row, data): + """Internal function to edit a row.""" + for i, key in enumerate(self.config.layer_config['layouts']['items']): + cell = QTableWidgetItem() + + value = data.get(key) + if not value: + cell.setText('') + cell.setData(Qt.ItemDataRole.UserRole, '') + self.layouts.setItem(row, i, cell) + continue + + input_type = self.config.layer_config[key]['type'] + if input_type == InputType.List: + cell.setData(Qt.ItemDataRole.UserRole, value) + cell.setData(Qt.ItemDataRole.ToolTipRole, value) + + items = self.config.layer_config[key].get('items') + if items: + # Display label from Python enum + for item_enum in items: + if item_enum.value['data'] != value: + continue + cell.setText(item_enum.value['label']) + break + else: + # Some settings are a list, but not using a Python enum yet + # Like layouts and themes + cell.setText(value) + + elif input_type == InputType.Scale: + cell.setData(Qt.ItemDataRole.UserRole, value) + cell.setData(Qt.ItemDataRole.ToolTipRole, value) + # Format scale value + cell.setText(f'1 : {value}') + + elif input_type == InputType.SpinBox: + cell.setText(f'{value}') + cell.setData(Qt.ItemDataRole.UserRole, value) + cell.setData(Qt.ItemDataRole.ToolTipRole, value) + + else: + raise Exception('InputType "{}" not implemented'.format(input_type)) + + self.layouts.setItem(row, i, cell) + self.layouts.clearSelection() + + def remove_selected_layout(self): + """Remove the selected layout in the table after clicking the 'remove' button.""" + selection = self.layouts.selectedIndexes() + if len(selection) <= 0: + return + + row = selection[0].row() + self.layouts.clearSelection() + self.layouts.removeRow(row) + + def geometry_changed(self): + current_geometry = self.drawing_geometry.currentData() + if current_geometry == self._drawing_geometry: + return + + if current_geometry == GeometryType.Point.value['data'] \ + or self._drawing_geometry == GeometryType.Point.value['data']: + + if self.layouts.rowCount() > 0: + box = QMessageBox(self.parent) + box.setIcon(QMessageBox.Icon.Question) + box.setWindowIcon(QIcon(resources_path('icons', 'icon.png')) ) + box.setStandardButtons(QMessageBox.StandardButton.Yes | QMessageBox.StandardButton.No) + box.setDefaultButton(QMessageBox.StandardButton.Yes) + box.setWindowTitle(tr('Change the geometry')) + box.setText(tr('Are you sure to change the geometry?\nIt will clear the layouts table.')) + + result = box.exec() + if result == QMessageBox.StandardButton.No: + for item_type in GeometryType: + if item_type.value['data'] != self._drawing_geometry: + continue + self.drawing_geometry.setCurrentText(item_type.value['label']) + break + return + + # clear the table + self.layouts.clearSelection() + self.layouts.setRowCount(0) + + self._drawing_geometry = current_geometry diff --git a/lizmap/plugin.py b/lizmap/plugin.py index 1d8fcf5e..2e3539c4 100644 --- a/lizmap/plugin.py +++ b/lizmap/plugin.py @@ -95,6 +95,7 @@ from lizmap.definitions.filter_by_login import FilterByLoginDefinitions from lizmap.definitions.filter_by_polygon import FilterByPolygonDefinitions from lizmap.definitions.layouts import LayoutsDefinitions +from lizmap.definitions.portfolio import PortfolioDefinitions from lizmap.definitions.lizmap_cloud import ( CLOUD_MAX_PARENT_FOLDER, CLOUD_NAME, @@ -134,6 +135,7 @@ from lizmap.forms.filter_by_login import FilterByLoginEditionDialog from lizmap.forms.filter_by_polygon import FilterByPolygonEditionDialog from lizmap.forms.layout_edition import LayoutEditionDialog +from lizmap.forms.portfolio_edition import PortfolioEditionDialog from lizmap.forms.locate_layer_edition import LocateLayerEditionDialog from lizmap.forms.time_manager_edition import TimeManagerEditionDialog from lizmap.forms.tooltip_edition import ToolTipEditionDialog @@ -853,6 +855,16 @@ def write_log_message(message, tag, level): 'downButton': self.dlg.down_layout_form_button, 'manager': None, }, + 'portfolio': { + 'panel': Panels.Portfolio, + 'tableWidget': self.dlg.table_portfolio, + 'addButton': self.dlg.add_portfolio_button, + 'removeButton': self.dlg.remove_portfolio_button, + 'editButton': self.dlg.edit_portfolio_button, + 'upButton': self.dlg.up_portfolio_button, + 'downButton': self.dlg.down_portfolio_button, + 'manager': None, + }, 'dxfExport': { 'panel': Panels.DxfExport, 'tableWidget': self.dlg.table_dxf_export, @@ -1452,6 +1464,9 @@ def on_dxf_export_toggled(checked): elif key == 'layouts': definition = LayoutsDefinitions() dialog = LayoutEditionDialog + elif key == 'portfolio': + definition = PortfolioDefinitions() + dialog = PortfolioEditionDialog elif key == 'locateByLayer': definition = LocateByLayerDefinitions() dialog = LocateLayerEditionDialog diff --git a/lizmap/resources/ui/ui_form_portfolio.ui b/lizmap/resources/ui/ui_form_portfolio.ui new file mode 100644 index 00000000..7a7ddd9e --- /dev/null +++ b/lizmap/resources/ui/ui_form_portfolio.ui @@ -0,0 +1,159 @@ + + + Dialog + + + + 0 + 0 + 858 + 733 + + + + Portfolio + + + + + + true + + + + + 0 + 0 + 838 + 659 + + + + + + + Map Portfolio printing: the user, after activating the portfolio module, can choose a portfolio, draws the geometry, customizes it (color, text), and launches the portfolio printing that will generate 1 PDF file by tuple (template, theme, zoom method and scale, margin or nothing depending on the selected zoom method). + + + true + + + + + + + + + + + + + + + + + + + + + + - + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + + + Geometry + + + + + + + Layouts + + + + + + + Title + + + true + + + + + + + Description + + + + + + + + + + + + + + + + + + + QLabel { color : red; } + + + ERROR + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Help|QDialogButtonBox::Ok + + + + + + + + HtmlEditorWidget + QWidget +
lizmap.widgets.html_editor
+ 1 +
+
+ + +
diff --git a/lizmap/resources/ui/ui_lizmap.ui b/lizmap/resources/ui/ui_lizmap.ui index 07412e56..1e7fd48e 100644 --- a/lizmap/resources/ui/ui_lizmap.ui +++ b/lizmap/resources/ui/ui_lizmap.ui @@ -133,6 +133,11 @@ QListWidget::item::selected { Layouts + + + Portfolio + + DXF Export @@ -326,7 +331,7 @@ QListWidget::item::selected { - 1 + 7 @@ -1503,8 +1508,8 @@ This is different to the map maximum extent (defined in QGIS project properties, 0 0 - 811 - 817 + 585 + 209 @@ -2793,8 +2798,8 @@ This is different to the map maximum extent (defined in QGIS project properties, 0 0 - 811 - 817 + 497 + 396 @@ -3077,6 +3082,75 @@ This is different to the map maximum extent (defined in QGIS project properties, + + + + + + Configure portfolios for your project. + + + true + + + + + + + + + + + + + + + + + + + + - + + + + + + + edit + + + + + + + down + + + + + + + up + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + @@ -5772,28 +5846,28 @@ This is different to the map maximum extent (defined in QGIS project properties, QgsCollapsibleGroupBox QGroupBox -
qgis.gui
+
qgscollapsiblegroupbox.h
1
QgsFieldComboBox QComboBox -
qgis.gui
+
qgsfieldcombobox.h
QgsFileWidget QWidget -
qgis.gui
+
qgsfilewidget.h
QgsMapLayerComboBox QComboBox -
qgis.gui
+
qgsmaplayercombobox.h
QgsScaleWidget QWidget -
qgis.gui
+
qgsscalewidget.h
HtmlEditorWidget diff --git a/lizmap/resources/ui/ui_portfolio_layout.ui b/lizmap/resources/ui/ui_portfolio_layout.ui new file mode 100644 index 00000000..c1e936c2 --- /dev/null +++ b/lizmap/resources/ui/ui_portfolio_layout.ui @@ -0,0 +1,109 @@ + + + Dialog + + + + 0 + 0 + 387 + 215 + + + + Dataviz + + + + + + + + + + + Zoom method + + + + + + + Theme + + + + + + + Scale + + + + + + + + + + Layout + + + + + + + + + + Margin + + + + + + + true + + + + + + + + + + + + QLabel { color : red; } + + + ERROR + + + Qt::AlignRight|Qt::AlignTrailing|Qt::AlignVCenter + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + + QgsScaleWidget + QWidget +
qgis.gui
+
+
+ + +