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 = '
'
+
+ for template in data:
+ layout = template.get('layout')
+ theme = template.get('theme')
+ zoom_method = template.get('zoom_method')
+ scale = template.get('scale')
+ margin = template.get('margin')
+ html += f'- {layout} - {theme}'
+ for z in ZoomMethodType:
+ if z.value.data == zoom_method:
+ html += f' - {z.value.label}'
+ if zoom_method == ZoomMethodType.FixScale.value.data:
+ html += f' - {scale}'
+ elif zoom_method == ZoomMethodType.Margin.value.data:
+ html += f' - {margin}'
+ break
+ html += '
\n'
+
+ 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
-
+
1
QgsFieldComboBox
QComboBox
-
+
QgsFileWidget
QWidget
-
+
QgsMapLayerComboBox
QComboBox
-
+
QgsScaleWidget
QWidget
-
+
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
+
+
+
+
+
+