commit ca258bc44519aabebc193ea8c5effe7dfe0bfdb6 Author: nobohan Date: Tue Jul 13 10:39:19 2021 +0200 initial commit diff --git a/Notes.md b/Notes.md new file mode 100644 index 0000000..a7a1d61 --- /dev/null +++ b/Notes.md @@ -0,0 +1,34 @@ +Test for a plugin that add an action in the layout +================================================= + +13/07/2021, Longlaville + +- created a new plugin using plugin builder +- make a symbolic link to the plugin folder + +``` +cd ~/.local/share/QGIS/QGIS3/profiles/default/python/plugins +ln -s /mnt/tera/ChampsLibres/Projets/FormationQGIS/pyQGIS/plugin_layout/plot_layout +``` + +- try to add a button in layout by copying the data plotly plugin + + - copier à peu près le dossier gui de DataPlotly: + + - gui/gui.py: PlotLayoutItemGuiMetadata, PlotLayoutItemWidget + - gui/gui_utils.py: GuiUtils + - gui/plot_settings_widget.py: DataPlotlyPanelWidget, + + - copier à peu près le dossier Layout: + + - layout/layout.py: PlotLayoutItemMetadata, PlotLayoutItem qui est un QgsLayoutItem: c'est le composant graphique dans un layout. + + + - ajouter dans PlotInLayout::__init__ : + + self.plot_item_metadata = PlotLayoutItemMetadata() + self.plot_item_gui_metadata = None + QgsApplication.layoutItemRegistry().addLayoutItemType(self.plot_item_metadata) + + + - ajouter dans PlotInLayout::initGui: diff --git a/plot_layout/Makefile b/plot_layout/Makefile new file mode 100644 index 0000000..fb807a3 --- /dev/null +++ b/plot_layout/Makefile @@ -0,0 +1,244 @@ +#/*************************************************************************** +# PlotInLayout +# +# test for adding plot in layout +# ------------------- +# begin : 2021-07-13 +# git sha : $Format:%H$ +# copyright : (C) 2021 by Champs-Libres +# email : julien.minet@champs-libres.coop +# ***************************************************************************/ +# +#/*************************************************************************** +# * * +# * This program is free software; you can redistribute it and/or modify * +# * it under the terms of the GNU General Public License as published by * +# * the Free Software Foundation; either version 2 of the License, or * +# * (at your option) any later version. * +# * * +# ***************************************************************************/ + +################################################# +# Edit the following to match your sources lists +################################################# + + +#Add iso code for any locales you want to support here (space separated) +# default is no locales +# LOCALES = af +LOCALES = + +# If locales are enabled, set the name of the lrelease binary on your system. If +# you have trouble compiling the translations, you may have to specify the full path to +# lrelease +#LRELEASE = lrelease +#LRELEASE = lrelease-qt4 + + +# translation +SOURCES = \ + __init__.py \ + plot_layout.py plot_layout_dialog.py + +PLUGINNAME = plot_layout + +PY_FILES = \ + __init__.py \ + plot_layout.py plot_layout_dialog.py + +UI_FILES = plot_layout_dialog_base.ui + +EXTRAS = metadata.txt icon.png + +EXTRA_DIRS = + +COMPILED_RESOURCE_FILES = resources.py + +PEP8EXCLUDE=pydev,resources.py,conf.py,third_party,ui + +# QGISDIR points to the location where your plugin should be installed. +# This varies by platform, relative to your HOME directory: +# * Linux: +# .local/share/QGIS/QGIS3/profiles/default/python/plugins/ +# * Mac OS X: +# Library/Application Support/QGIS/QGIS3/profiles/default/python/plugins +# * Windows: +# AppData\Roaming\QGIS\QGIS3\profiles\default\python\plugins' + +QGISDIR=/home/julien/.local/share/QGIS/QGIS3/profiles/default/python/plugins/ + +################################################# +# Normally you would not need to edit below here +################################################# + +HELP = help/build/html + +PLUGIN_UPLOAD = $(c)/plugin_upload.py + +RESOURCE_SRC=$(shell grep '^ *@@g;s/.*>//g' | tr '\n' ' ') + +.PHONY: default +default: + @echo While you can use make to build and deploy your plugin, pb_tool + @echo is a much better solution. + @echo A Python script, pb_tool provides platform independent management of + @echo your plugins and runs anywhere. + @echo You can install pb_tool using: pip install pb_tool + @echo See https://g-sherman.github.io/plugin_build_tool/ for info. + +compile: $(COMPILED_RESOURCE_FILES) + +%.py : %.qrc $(RESOURCES_SRC) + pyrcc5 -o $*.py $< + +%.qm : %.ts + $(LRELEASE) $< + +test: compile transcompile + @echo + @echo "----------------------" + @echo "Regression Test Suite" + @echo "----------------------" + + @# Preceding dash means that make will continue in case of errors + @-export PYTHONPATH=`pwd`:$(PYTHONPATH); \ + export QGIS_DEBUG=0; \ + export QGIS_LOG_FILE=/dev/null; \ + nosetests -v --with-id --with-coverage --cover-package=. \ + 3>&1 1>&2 2>&3 3>&- || true + @echo "----------------------" + @echo "If you get a 'no module named qgis.core error, try sourcing" + @echo "the helper script we have provided first then run make test." + @echo "e.g. source run-env-linux.sh ; make test" + @echo "----------------------" + +deploy: compile doc transcompile + @echo + @echo "------------------------------------------" + @echo "Deploying plugin to your .qgis2 directory." + @echo "------------------------------------------" + # The deploy target only works on unix like operating system where + # the Python plugin directory is located at: + # $HOME/$(QGISDIR)/python/plugins + mkdir -p $(HOME)/$(QGISDIR)/python/plugins/$(PLUGINNAME) + cp -vf $(PY_FILES) $(HOME)/$(QGISDIR)/python/plugins/$(PLUGINNAME) + cp -vf $(UI_FILES) $(HOME)/$(QGISDIR)/python/plugins/$(PLUGINNAME) + cp -vf $(COMPILED_RESOURCE_FILES) $(HOME)/$(QGISDIR)/python/plugins/$(PLUGINNAME) + cp -vf $(EXTRAS) $(HOME)/$(QGISDIR)/python/plugins/$(PLUGINNAME) + cp -vfr i18n $(HOME)/$(QGISDIR)/python/plugins/$(PLUGINNAME) + cp -vfr $(HELP) $(HOME)/$(QGISDIR)/python/plugins/$(PLUGINNAME)/help + # Copy extra directories if any + (foreach EXTRA_DIR,(EXTRA_DIRS), cp -R (EXTRA_DIR) (HOME)/(QGISDIR)/python/plugins/(PLUGINNAME)/;) + + +# The dclean target removes compiled python files from plugin directory +# also deletes any .git entry +dclean: + @echo + @echo "-----------------------------------" + @echo "Removing any compiled python files." + @echo "-----------------------------------" + find $(HOME)/$(QGISDIR)/python/plugins/$(PLUGINNAME) -iname "*.pyc" -delete + find $(HOME)/$(QGISDIR)/python/plugins/$(PLUGINNAME) -iname ".git" -prune -exec rm -Rf {} \; + + +derase: + @echo + @echo "-------------------------" + @echo "Removing deployed plugin." + @echo "-------------------------" + rm -Rf $(HOME)/$(QGISDIR)/python/plugins/$(PLUGINNAME) + +zip: deploy dclean + @echo + @echo "---------------------------" + @echo "Creating plugin zip bundle." + @echo "---------------------------" + # The zip target deploys the plugin and creates a zip file with the deployed + # content. You can then upload the zip file on http://plugins.qgis.org + rm -f $(PLUGINNAME).zip + cd $(HOME)/$(QGISDIR)/python/plugins; zip -9r $(CURDIR)/$(PLUGINNAME).zip $(PLUGINNAME) + +package: compile + # Create a zip package of the plugin named $(PLUGINNAME).zip. + # This requires use of git (your plugin development directory must be a + # git repository). + # To use, pass a valid commit or tag as follows: + # make package VERSION=Version_0.3.2 + @echo + @echo "------------------------------------" + @echo "Exporting plugin to zip package. " + @echo "------------------------------------" + rm -f $(PLUGINNAME).zip + git archive --prefix=$(PLUGINNAME)/ -o $(PLUGINNAME).zip $(VERSION) + echo "Created package: $(PLUGINNAME).zip" + +upload: zip + @echo + @echo "-------------------------------------" + @echo "Uploading plugin to QGIS Plugin repo." + @echo "-------------------------------------" + $(PLUGIN_UPLOAD) $(PLUGINNAME).zip + +transup: + @echo + @echo "------------------------------------------------" + @echo "Updating translation files with any new strings." + @echo "------------------------------------------------" + @chmod +x scripts/update-strings.sh + @scripts/update-strings.sh $(LOCALES) + +transcompile: + @echo + @echo "----------------------------------------" + @echo "Compiled translation files to .qm files." + @echo "----------------------------------------" + @chmod +x scripts/compile-strings.sh + @scripts/compile-strings.sh $(LRELEASE) $(LOCALES) + +transclean: + @echo + @echo "------------------------------------" + @echo "Removing compiled translation files." + @echo "------------------------------------" + rm -f i18n/*.qm + +clean: + @echo + @echo "------------------------------------" + @echo "Removing uic and rcc generated files" + @echo "------------------------------------" + rm $(COMPILED_UI_FILES) $(COMPILED_RESOURCE_FILES) + +doc: + @echo + @echo "------------------------------------" + @echo "Building documentation using sphinx." + @echo "------------------------------------" + cd help; make html + +pylint: + @echo + @echo "-----------------" + @echo "Pylint violations" + @echo "-----------------" + @pylint --reports=n --rcfile=pylintrc . || true + @echo + @echo "----------------------" + @echo "If you get a 'no module named qgis.core' error, try sourcing" + @echo "the helper script we have provided first then run make pylint." + @echo "e.g. source run-env-linux.sh ; make pylint" + @echo "----------------------" + + +# Run pep8 style checking +#http://pypi.python.org/pypi/pep8 +pep8: + @echo + @echo "-----------" + @echo "PEP8 issues" + @echo "-----------" + @pep8 --repeat --ignore=E203,E121,E122,E123,E124,E125,E126,E127,E128 --exclude $(PEP8EXCLUDE) . || true + @echo "-----------" + @echo "Ignored in PEP8 check:" + @echo $(PEP8EXCLUDE) diff --git a/plot_layout/README.html b/plot_layout/README.html new file mode 100644 index 0000000..a84e7e4 --- /dev/null +++ b/plot_layout/README.html @@ -0,0 +1,42 @@ + + +

Plugin Builder Results

+ +Congratulations! You just built a plugin for QGIS!

+ +
+Your plugin PlotInLayout was created in:
+  /mnt/tera/ChampsLibres/Projets/FormationQGIS/pyQGIS/plugin_layout/plot_layout +

+Your QGIS plugin directory is located at:
+  /home/julien/.local/share/QGIS/QGIS3/profiles/default/python/plugins +

+

What's Next

+
    +
  1. If resources.py is not present in your plugin directory, compile the resources file using pyrcc5 (simply use pb_tool or make if you have automake) +
  2. Optionally, test the generated sources using make test (or run tests from your IDE) +
  3. Copy the entire directory containing your new plugin to the QGIS plugin directory (see Notes below) +
  4. Test the plugin by enabling it in the QGIS plugin manager +
  5. Customize it by editing the implementation file plot_layout.py +
  6. Create your own custom icon, replacing the default icon.png +
  7. Modify your user interface by opening plot_layout_dialog_base.ui in Qt Designer +
+Notes: +
    +
  • You can use pb_tool to compile, deploy, and manage your plugin. Tweak the pb_tool.cfg file included with your plugin as you add files. Install pb_tool using + pip or easy_install. See http://loc8.cc/pb_tool for more information. +
  • You can also use the Makefile to compile and deploy when you + make changes. This requires GNU make (gmake). The Makefile is ready to use, however you + will have to edit it to add addional Python source files, dialogs, and translations. +
+
+
+

+For information on writing PyQGIS code, see http://loc8.cc/pyqgis_resources for a list of resources. +

+
+

+©2011-2019 GeoApt LLC - geoapt.com +

+ + diff --git a/plot_layout/README.txt b/plot_layout/README.txt new file mode 100644 index 0000000..7f93286 --- /dev/null +++ b/plot_layout/README.txt @@ -0,0 +1,32 @@ +Plugin Builder Results + +Your plugin PlotInLayout was created in: + /mnt/tera/ChampsLibres/Projets/FormationQGIS/pyQGIS/plugin_layout/plot_layout + +Your QGIS plugin directory is located at: + /home/julien/.local/share/QGIS/QGIS3/profiles/default/python/plugins + +What's Next: + + * Copy the entire directory containing your new plugin to the QGIS plugin + directory + + * Compile the resources file using pyrcc5 + + * Run the tests (``make test``) + + * Test the plugin by enabling it in the QGIS plugin manager + + * Customize it by editing the implementation file: ``plot_layout.py`` + + * Create your own custom icon, replacing the default icon.png + + * Modify your user interface by opening PlotInLayout_dialog_base.ui in Qt Designer + + * You can use the Makefile to compile your Ui and resource files when + you make changes. This requires GNU make (gmake) + +For more information, see the PyQGIS Developer Cookbook at: +http://www.qgis.org/pyqgis-cookbook/index.html + +(C) 2011-2018 GeoApt LLC - geoapt.com diff --git a/plot_layout/__init__.py b/plot_layout/__init__.py new file mode 100644 index 0000000..92f5eaa --- /dev/null +++ b/plot_layout/__init__.py @@ -0,0 +1,36 @@ +# -*- coding: utf-8 -*- +""" +/*************************************************************************** + PlotInLayout + A QGIS plugin + test for adding plot in layout + Generated by Plugin Builder: http://g-sherman.github.io/Qgis-Plugin-Builder/ + ------------------- + begin : 2021-07-13 + copyright : (C) 2021 by Champs-Libres + email : julien.minet@champs-libres.coop + git sha : $Format:%H$ + ***************************************************************************/ + +/*************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ + This script initializes the plugin, making it known to QGIS. +""" + + +# noinspection PyPep8Naming +def classFactory(iface): # pylint: disable=invalid-name + """Load PlotInLayout class from file PlotInLayout. + + :param iface: A QGIS interface instance. + :type iface: QgsInterface + """ + # + from .plot_layout import PlotInLayout + return PlotInLayout(iface) diff --git a/plot_layout/__pycache__/__init__.cpython-38.pyc b/plot_layout/__pycache__/__init__.cpython-38.pyc new file mode 100644 index 0000000..9b3edf8 Binary files /dev/null and b/plot_layout/__pycache__/__init__.cpython-38.pyc differ diff --git a/plot_layout/__pycache__/plot_layout.cpython-38.pyc b/plot_layout/__pycache__/plot_layout.cpython-38.pyc new file mode 100644 index 0000000..9dbcbbb Binary files /dev/null and b/plot_layout/__pycache__/plot_layout.cpython-38.pyc differ diff --git a/plot_layout/__pycache__/plot_layout_dialog.cpython-38.pyc b/plot_layout/__pycache__/plot_layout_dialog.cpython-38.pyc new file mode 100644 index 0000000..25f2acb Binary files /dev/null and b/plot_layout/__pycache__/plot_layout_dialog.cpython-38.pyc differ diff --git a/plot_layout/__pycache__/resources.cpython-38.pyc b/plot_layout/__pycache__/resources.cpython-38.pyc new file mode 100644 index 0000000..eeb1051 Binary files /dev/null and b/plot_layout/__pycache__/resources.cpython-38.pyc differ diff --git a/plot_layout/gui/gui.py b/plot_layout/gui/gui.py new file mode 100644 index 0000000..5bf8f0c --- /dev/null +++ b/plot_layout/gui/gui.py @@ -0,0 +1,277 @@ + +from qgis.PyQt.QtCore import ( + QCoreApplication, + QItemSelectionModel +) +from qgis.PyQt.QtWidgets import ( + QListWidget, + QHBoxLayout, + QPushButton, + QVBoxLayout +) +from qgis.gui import ( + QgsLayoutItemAbstractGuiMetadata, + QgsLayoutItemBaseWidget, + QgsLayoutItemPropertiesWidget +) + +from qgis.core import ( QgsLayoutItemRegistry ) + +from plot_settings_widget import DataPlotlyPanelWidget + +from gui_utils import GuiUtils + +ITEM_TYPE = QgsLayoutItemRegistry.PluginItem + 1338 + + +class PlotLayoutItemWidget(QgsLayoutItemBaseWidget): + """ + Configuration widget for layout plot items + """ + + def __init__(self, parent, layout_object): + super().__init__(parent, layout_object) + self.plot_item = layout_object + self.message_bar = None + + # vl = QVBoxLayout() + # vl.setContentsMargins(0, 0, 0, 0) + + # plot_tools_layout = QHBoxLayout() + + # plot_add_button = QPushButton() + # plot_add_button.setIcon(GuiUtils.get_icon('symbologyAdd.svg')) + # plot_add_button.setToolTip('Add a new plot') + # plot_tools_layout.addWidget(plot_add_button) + # plot_add_button.clicked.connect(self.add_plot) + + # plot_remove_button = QPushButton() + # plot_remove_button.setIcon(GuiUtils.get_icon('symbologyRemove.svg')) + # plot_remove_button.setToolTip('Remove selected plot') + # plot_tools_layout.addWidget(plot_remove_button) + # plot_remove_button.clicked.connect(self.remove_plot) + + # plot_move_up_button = QPushButton() + # plot_move_up_button.setIcon(GuiUtils.get_icon('mActionArrowUp.svg')) + # plot_move_up_button.setToolTip('Move selected plot up') + # plot_tools_layout.addWidget(plot_move_up_button) + # plot_move_up_button.clicked.connect(self.move_up_plot) + + # plot_move_down_button = QPushButton() + # plot_move_down_button.setIcon(GuiUtils.get_icon('mActionArrowDown.svg')) + # plot_move_down_button.setToolTip('Move selected plot down') + # plot_tools_layout.addWidget(plot_move_down_button) + # plot_move_down_button.clicked.connect(self.move_down_plot) + + # vl.addLayout(plot_tools_layout) + + # self.plot_list = QListWidget() + # self.plot_list.setSelectionMode(QListWidget.SingleSelection) + # vl.addWidget(self.plot_list) + # self.populate_plot_list() + + # plot_properties_button = QPushButton(self.tr('Setup Selected Plot')) + # vl.addWidget(plot_properties_button) + # plot_properties_button.clicked.connect(self.show_properties) + + # self.panel = None + # self.setPanelTitle(self.tr('Plot Properties')) + # self.item_properties_widget = QgsLayoutItemPropertiesWidget(self, layout_object) + # vl.addWidget(self.item_properties_widget) + # self.setLayout(vl) + + # def populate_plot_list(self): + # """ + # Clears and re-populates the plot list widget. The currently selection is retained + # """ + # selected_index = self.plot_list.currentRow() + # self.plot_list.clear() + # for setting in self.plot_item.plot_settings: + # plot_type = setting.plot_type if setting.plot_type is not None else '(not set)' + # legend_title = ('[' + setting.properties.get('name') + ']') \ + # if setting.properties.get('name', '') != '' else '' + # self.plot_list.addItem(plot_type + ' ' + legend_title) + + # # select index within range [0, len(plot_settings)-1] + # selected_index = max(0, min(len(self.plot_item.plot_settings) - 1, selected_index)) + # self.plot_list.setCurrentRow(selected_index, QItemSelectionModel.SelectCurrent) + + # def add_plot(self): + # """ + # Adds a new plot and updates the plot list and the plot item + # """ + # self.plot_item.add_plot() + # self.populate_plot_list() + # self.plot_item.refresh() + + # def remove_plot(self): + # """ + # Removes the selected plot and updates the plot list and the plot item + # """ + # selected_index = self.plot_list.currentRow() + # if selected_index < 0: + # return + + # self.plot_item.remove_plot(selected_index) + # self.populate_plot_list() + # self.plot_item.refresh() + + # def move_up_plot(self): + # """ + # Moves the selected plot up and updates the plot list and the plot item + # """ + # selected_index = self.plot_list.currentRow() + # if selected_index <= 0: + # return + + # item = self.plot_item.plot_settings.pop(selected_index) + # self.plot_item.plot_settings.insert(selected_index - 1, item) + # self.plot_list.setCurrentRow(selected_index - 1, QItemSelectionModel.SelectCurrent) + # self.populate_plot_list() + # self.plot_item.refresh() + + # def move_down_plot(self): + # """ + # Moves the selected plot down and updates the plot list and the plot item + # """ + # selected_index = self.plot_list.currentRow() + # if selected_index >= len(self.plot_item.plot_settings) - 1: + # return + + # item = self.plot_item.plot_settings.pop(selected_index) + # self.plot_item.plot_settings.insert(selected_index + 1, item) + # self.plot_list.setCurrentRow(selected_index + 1, QItemSelectionModel.SelectCurrent) + # self.populate_plot_list() + # self.plot_item.refresh() + + # def show_properties(self): + # """ + # Shows the plot properties panel + # """ + # selected_plot_index = self.plot_list.currentRow() + # if selected_plot_index < 0: + # return + + # self.panel = DataPlotlyPanelWidget(mode=DataPlotlyPanelWidget.MODE_LAYOUT, message_bar=self.message_bar) + + # # not quite right -- we ideally want to also add the source layer scope into the context given by plot item, + # # but that causes a hard lock in the Python GIL (because PyQt doesn't release the GIL when creating the menu + # # for the property override buttons). Nothing much we can do about that here (or in QGIS, + # # it's a Python/PyQt limitation) + # self.panel.registerExpressionContextGenerator(self.plot_item) + # self.panel.set_print_layout(self.plot_item.layout()) + + # self.panel.linked_map_combo.blockSignals(True) + # self.panel.linked_map_combo.setItem(self.plot_item.linked_map) + # self.panel.linked_map_combo.blockSignals(False) + + # self.panel.filter_by_map_check.toggled.connect(self.filter_by_map_toggled) + # self.panel.filter_by_atlas_check.toggled.connect(self.filter_by_atlas_toggled) + # self.panel.linked_map_combo.itemChanged.connect(self.linked_map_changed) + + # self.panel.filter_by_map_check.blockSignals(True) + # self.panel.filter_by_map_check.setChecked(self.plot_item.filter_by_map) + # self.panel.filter_by_map_check.blockSignals(False) + + # self.panel.filter_by_atlas_check.blockSignals(True) + # self.panel.filter_by_atlas_check.setChecked(self.plot_item.filter_by_atlas) + # self.panel.filter_by_atlas_check.blockSignals(False) + + # self.panel.set_settings(self.plot_item.plot_settings[selected_plot_index]) + # # self.panel.set_settings(self.layoutItem().plot_settings) + # self.openPanel(self.panel) + # self.panel.widgetChanged.connect(self.update_item_settings) + # self.panel.panelAccepted.connect(self.set_item_settings) + + # def update_item_settings(self): + # """ + # Updates the plot item without dismissing the properties panel + # """ + # if not self.panel: + # return + + # self.plot_item.set_plot_settings(self.plot_list.currentRow(), self.panel.get_settings()) + # self.populate_plot_list() + # self.plot_item.update() + + # def set_item_settings(self): + # """ + # Updates the plot item based on the settings from the properties panel + # """ + # if not self.panel: + # return + + # self.plot_item.set_plot_settings(self.plot_list.currentRow(), self.panel.get_settings()) + # self.populate_plot_list() + # self.panel = None + # self.plot_item.update() + + # def filter_by_map_toggled(self, value): + # """ + # Triggered when the filter by map option is toggled + # """ + # self.plot_item.filter_by_map = bool(value) + # self.plot_item.update() + + # def filter_by_atlas_toggled(self, value): + # """ + # Triggered when the filter by atlas option is toggled + # """ + # self.plot_item.filter_by_atlas = bool(value) + # self.plot_item.update() + + # def linked_map_changed(self, linked_map): + # """ + # Triggered when the linked map is changed + # """ + # self.plot_item.set_linked_map(linked_map) + # self.plot_item.update() + + # def setNewItem(self, item): # pylint: disable=missing-docstring + # if item.type() != ITEM_TYPE: + # return False + + # self.plot_item = item + # self.item_properties_widget.setItem(item) + # self.populate_plot_list() + + # if self.panel is not None: + # self.panel.set_settings(self.plot_item.plot_settings[0]) + + # self.panel.filter_by_map_check.blockSignals(True) + # self.panel.filter_by_map_check.setChecked(item.filter_by_map) + # self.panel.filter_by_map_check.blockSignals(False) + + # self.panel.filter_by_atlas_check.blockSignals(True) + # self.panel.filter_by_atlas_check.setChecked(item.filter_by_atlas) + # self.panel.filter_by_atlas_check.blockSignals(False) + + # self.panel.linked_map_combo.blockSignals(True) + # self.panel.linked_map_combo.setItem(self.plot_item.linked_map) + # self.panel.linked_map_combo.blockSignals(False) + + # return True + + # def setDesignerInterface(self, iface): # pylint: disable=missing-docstring + # super().setDesignerInterface(iface) + # self.message_bar = iface.messageBar() + # if self.panel: + # self.panel.message_bar = self.message_bar + + + + + +class PlotLayoutItemGuiMetadata(QgsLayoutItemAbstractGuiMetadata): + """ + Metadata for plot item GUI classes + """ + + def __init__(self): + super().__init__(ITEM_TYPE, 'test') + + def creationIcon(self): # pylint: disable=missing-docstring, no-self-use + return GuiUtils.get_icon('circle.svg') + + def createItemWidget(self, item): # pylint: disable=missing-docstring, no-self-use + return PlotLayoutItemWidget(None, item) diff --git a/plot_layout/gui/gui_utils.py b/plot_layout/gui/gui_utils.py new file mode 100644 index 0000000..bb86ddd --- /dev/null +++ b/plot_layout/gui/gui_utils.py @@ -0,0 +1,64 @@ +# -*- coding: utf-8 -*- +"""GUI Utilities + +.. note:: This program is free software; you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation; either version 2 of the License, or +(at your option) any later version. +""" + +import os +from qgis.PyQt.QtGui import QIcon + + +class GuiUtils: + """ + Utilities for GUI plugin components + """ + + @staticmethod + def get_icon(icon: str) -> QIcon: + """ + Returns a plugin icon + :param icon: icon name (svg file name) + :return: QIcon + """ + path = GuiUtils.get_icon_svg(icon) + if not path: + return QIcon() + + return QIcon(path) + + @staticmethod + def get_icon_svg(icon: str) -> str: + """ + Returns a plugin icon's SVG file path + :param icon: icon name (svg file name) + :return: icon svg path + """ + path = os.path.join( + os.path.dirname(__file__), + '..', + 'icons', + icon) + if not os.path.exists(path): + return '' + + return path + + @staticmethod + def get_ui_file_path(file: str) -> str: + """ + Returns a UI file's path + :param file: file name (uifile name) + :return: ui file path + """ + path = os.path.join( + os.path.dirname(__file__), + '..', + 'ui', + file) + if not os.path.exists(path): + return '' + + return path diff --git a/plot_layout/gui/plot_settings_widget.py b/plot_layout/gui/plot_settings_widget.py new file mode 100644 index 0000000..6ec52fb --- /dev/null +++ b/plot_layout/gui/plot_settings_widget.py @@ -0,0 +1,1465 @@ +# -*- coding: utf-8 -*- +""" +/*************************************************************************** + DataPlotlyDialog + A QGIS plugin + D3 Plots for QGIS + ------------------- + begin : 2017-03-05 + git sha : $Format:%H$ + copyright : (C) 2017 by matteo ghetta + email : matteo.ghetta@gmail.com + ***************************************************************************/ + +/*************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ +""" + +import json +from collections import OrderedDict +from shutil import copyfile +from functools import partial +import sys + +from qgis.PyQt import uic +from qgis.PyQt.QtWidgets import ( + QListWidgetItem, + QVBoxLayout, + QFileDialog, + QMenu +) +from qgis.PyQt.QtXml import QDomDocument + +from qgis.PyQt.QtGui import ( + QFont, + QImage, + QPainter, + QColor +) +from qgis.PyQt.QtCore import ( + QUrl, + pyqtSignal, + QDir +) +from qgis.PyQt.QtWebKit import QWebSettings +from qgis.PyQt.QtWebKitWidgets import ( + QWebView +) + +from qgis.core import ( + Qgis, + QgsNetworkAccessManager, + QgsFeatureRequest, + QgsMapLayerProxyModel, + QgsProject, + QgsFileUtils, + QgsReferencedRectangle, + QgsExpressionContextGenerator, + QgsPropertyCollection, + QgsLayoutItemRegistry, + QgsPropertyDefinition +) +from qgis.gui import ( + QgsPanelWidget, + QgsMessageBar, + QgsPropertyOverrideButton +) +from qgis.utils import iface + +from DataPlotly.core.plot_factory import PlotFactory +from DataPlotly.core.plot_settings import PlotSettings +from DataPlotly.gui.gui_utils import GuiUtils + +WIDGET, _ = uic.loadUiType(GuiUtils.get_ui_file_path('dataplotly_dockwidget_base.ui')) + + +class DataPlotlyPanelWidget(QgsPanelWidget, WIDGET): # pylint: disable=too-many-lines,too-many-instance-attributes,too-many-public-methods + """ + Main configuration panel widget for plot settings + """ + + MODE_CANVAS = 'CANVAS' + MODE_LAYOUT = 'LAYOUT' + + # emit signal when dialog is resized + resizeWindow = pyqtSignal() + + def __init__(self, mode=MODE_CANVAS, parent=None, override_iface=None, message_bar: QgsMessageBar = None): # pylint: disable=too-many-statements + """Constructor.""" + super().__init__(parent) + self.setupUi(self) + if override_iface is None: + self.iface = iface + else: + self.iface = override_iface + + self.mode = mode + self.message_bar = message_bar + + self.setPanelTitle(self.tr('Plot Properties')) + + self.save_to_project = True + self.read_from_project = True + + self.data_defined_properties = QgsPropertyCollection() + + # listen out for project save/restore, and update our state accordingly + QgsProject.instance().writeProject.connect(self.write_project) + QgsProject.instance().readProject.connect(self.read_project) + + if self.iface is not None: + self.listWidget.setIconSize(self.iface.iconSize(False)) + self.listWidget.setMaximumWidth(int(self.listWidget.iconSize().width() * 1.18)) + + # connect signal to function to reload the plot view + self.resizeWindow.connect(self.reloadPlotCanvas) + + # create the reload button with text and icon + self.reload_btn.setText("Reload") + self.reload_btn.setIcon(GuiUtils.get_icon('reload.svg')) + self.clear_btn.setIcon(GuiUtils.get_icon('clean.svg')) + self.update_btn.setIcon(GuiUtils.get_icon('refresh.svg')) + self.draw_btn.setIcon(GuiUtils.get_icon('create_plot.svg')) + # connect the button to the reload function + self.reload_btn.clicked.connect(self.reloadPlotCanvas2) + + self.configuration_menu = QMenu(self) + action_load_configuration = self.configuration_menu.addAction(self.tr("Load Configuration…")) + action_load_configuration.triggered.connect(self.load_configuration) + action_save_configuration = self.configuration_menu.addAction(self.tr("Save Configuration…")) + action_save_configuration.triggered.connect(self.save_configuration) + self.configuration_btn.setMenu(self.configuration_menu) + + # ListWidget icons and themes + self.listWidget_icons = [ + QListWidgetItem(GuiUtils.get_icon('list_properties.svg'), ""), + QListWidgetItem(GuiUtils.get_icon('list_custom.svg'), ""), + ] + + if self.mode == DataPlotlyPanelWidget.MODE_CANVAS: + self.listWidget_icons.extend([ + QListWidgetItem(GuiUtils.get_icon('list_plot.svg'), ""), + QListWidgetItem(GuiUtils.get_icon('list_help.svg'), ""), + QListWidgetItem(GuiUtils.get_icon('list_code.svg'), "") + ]) + + # fill the QListWidget with items and icons + for i in self.listWidget_icons: + self.listWidget.addItem(i) + + # highlight the first row when starting the first time + self.listWidget.setCurrentRow(0) + + self.marker_size.setValue(10) + self.marker_size.setSingleStep(0.2) + self.marker_size.setClearValue(10) + + self.marker_width.setValue(1) + self.marker_width.setSingleStep(0.2) + self.marker_width.setClearValue(0, self.tr('None')) + + # Populate PlotTypes combobox + # we sort available types by translated name + type_classes = [clazz for _, clazz in PlotFactory.PLOT_TYPES.items()] + type_classes.sort(key=lambda x: x.name().lower()) + for clazz in type_classes: + self.plot_combo.addItem(clazz.icon(), clazz.name(), clazz.type_name()) + + # default to scatter plots + self.set_plot_type('scatter') + + # SubPlots combobox + self.subcombo.addItem(self.tr('Single Plot'), 'single') + self.subcombo.addItem(self.tr('Subplots'), 'subplots') + + # connect to the functions to clean the UI and fill with the correct + # widgets + self.refreshWidgets() + self.refreshWidgets2() + self.plot_combo.currentIndexChanged.connect(self.refreshWidgets) + self.plot_combo.currentIndexChanged.connect(self.helpPage) + self.subcombo.currentIndexChanged.connect(self.refreshWidgets2) + self.marker_type_combo.currentIndexChanged.connect(self.refreshWidgets3) + + self.properties_group_box.collapsedStateChanged.connect(self.refreshWidgets) + + # fill the layer combobox with vector layers + self.layer_combo.setFilters(QgsMapLayerProxyModel.VectorLayer) + + # connect the combo boxes to the setLegend function + self.x_combo.fieldChanged.connect(self.setLegend) + self.y_combo.fieldChanged.connect(self.setLegend) + self.z_combo.fieldChanged.connect(self.setLegend) + + self.draw_btn.clicked.connect(self.create_plot) + self.update_btn.clicked.connect(self.UpdatePlot) + self.clear_btn.clicked.connect(self.clearPlotView) + self.save_plot_btn.clicked.connect(self.save_plot_as_image) + self.save_plot_html_btn.clicked.connect(self.save_plot_as_html) + self.save_plot_btn.setIcon(GuiUtils.get_icon('save_as_image.svg')) + self.save_plot_html_btn.setIcon(GuiUtils.get_icon('save_as_html.svg')) + + # initialize the empty dictionary of plots + self.plot_factories = {} + # start the index counter + self.idx = 1 + + # load the help html page into the help widget + self.layouth = QVBoxLayout() + self.layouth.setContentsMargins(0, 0, 0, 0) + self.help_widget.setLayout(self.layouth) + self.help_view = QWebView() + self.layouth.addWidget(self.help_view) + self.helpPage() + + # load the webview of the plot a the first running of the plugin + self.layoutw = QVBoxLayout() + self.layoutw.setContentsMargins(0, 0, 0, 0) + self.plot_qview.setLayout(self.layoutw) + self.plot_view = QWebView() + self.plot_view.page().setNetworkAccessManager(QgsNetworkAccessManager.instance()) + self.plot_view.statusBarMessage.connect(self.getJSmessage) + plot_view_settings = self.plot_view.settings() + plot_view_settings.setAttribute(QWebSettings.WebGLEnabled, True) + plot_view_settings.setAttribute(QWebSettings.DeveloperExtrasEnabled, True) + plot_view_settings.setAttribute(QWebSettings.Accelerated2dCanvasEnabled, True) + self.layoutw.addWidget(self.plot_view) + + # get the plot type from the combobox + self.ptype = self.plot_combo.currentData() + + self.layer_combo.layerChanged.connect(self.selected_layer_changed) + # fill combo boxes when launching the UI + self.selected_layer_changed(self.layer_combo.currentLayer()) + + self.register_data_defined_button(self.feature_subset_defined_button, PlotSettings.PROPERTY_FILTER) + self.register_data_defined_button(self.size_defined_button, PlotSettings.PROPERTY_MARKER_SIZE) + self.size_defined_button.registerEnabledWidget(self.marker_size, natural=False) + self.register_data_defined_button(self.stroke_defined_button, PlotSettings.PROPERTY_STROKE_WIDTH) + self.stroke_defined_button.registerEnabledWidget(self.marker_width, natural=False) + self.register_data_defined_button(self.in_color_defined_button, PlotSettings.PROPERTY_COLOR) + self.in_color_defined_button.changed.connect(self.data_defined_color_updated) + self.register_data_defined_button(self.out_color_defined_button, PlotSettings.PROPERTY_STROKE_COLOR) + self.out_color_defined_button.registerEnabledWidget(self.out_color_combo, natural=False) + self.register_data_defined_button(self.plot_title_defined_button, PlotSettings.PROPERTY_TITLE) + self.plot_title_defined_button.registerEnabledWidget(self.plot_title_line, natural=False) + self.register_data_defined_button(self.legend_title_defined_button, PlotSettings.PROPERTY_LEGEND_TITLE) + self.legend_title_defined_button.registerEnabledWidget(self.legend_title, natural=False) + self.register_data_defined_button(self.x_axis_title_defined_button, PlotSettings.PROPERTY_X_TITLE) + self.x_axis_title_defined_button.registerEnabledWidget(self.x_axis_title, natural=False) + self.register_data_defined_button(self.y_axis_title_defined_button, PlotSettings.PROPERTY_Y_TITLE) + self.y_axis_title_defined_button.registerEnabledWidget(self.y_axis_title, natural=False) + self.register_data_defined_button(self.z_axis_title_defined_button, PlotSettings.PROPERTY_Z_TITLE) + self.z_axis_title_defined_button.registerEnabledWidget(self.z_axis_title, natural=False) + self.register_data_defined_button(self.x_axis_min_defined_button, PlotSettings.PROPERTY_X_MIN) + self.x_axis_min_defined_button.registerEnabledWidget(self.x_axis_min, natural=False) + self.register_data_defined_button(self.x_axis_max_defined_button, PlotSettings.PROPERTY_X_MAX) + self.x_axis_max_defined_button.registerEnabledWidget(self.x_axis_max, natural=False) + self.register_data_defined_button(self.y_axis_min_defined_button, PlotSettings.PROPERTY_Y_MIN) + self.y_axis_min_defined_button.registerEnabledWidget(self.y_axis_min, natural=False) + self.register_data_defined_button(self.y_axis_max_defined_button, PlotSettings.PROPERTY_Y_MAX) + self.y_axis_max_defined_button.registerEnabledWidget(self.y_axis_max, natural=False) + + # connect to refreshing function of listWidget and stackedWidgets + self.listWidget.currentRowChanged.connect(self.updateStacked) + + # connect the plot changing to the color data defined buttons + self.plot_combo.currentIndexChanged.connect(self.data_defined_color_updated) + + # better default colors + self.in_color_combo.setColor(QColor('#8EBAD9')) + self.out_color_combo.setColor(QColor('#1F77B4')) + + # set range of axis min/max spin boxes + self.x_axis_min.setRange(sys.float_info.max * -1, sys.float_info.max) + self.x_axis_max.setRange(sys.float_info.max * -1, sys.float_info.max) + self.y_axis_min.setRange(sys.float_info.max * -1, sys.float_info.max) + self.y_axis_max.setRange(sys.float_info.max * -1, sys.float_info.max) + + self.pid = None + self.plot_path = None + self.plot_url = None + self.plot_file = None + + if self.mode == DataPlotlyPanelWidget.MODE_LAYOUT: + self.update_btn.setEnabled(True) + # hide for now + self.draw_btn.setVisible(False) + self.clear_btn.setVisible(False) + self.subcombo.setVisible(False) + self.subcombo_label.setVisible(False) + self.visible_feature_check.setVisible(False) + self.selected_feature_check.setVisible(False) + else: + self.iface.mapCanvas().extentsChanged.connect(self.update_plot_visible_rect) + self.label_linked_map.setVisible(False) + self.linked_map_combo.setVisible(False) + self.filter_by_map_check.setVisible(False) + self.filter_by_atlas_check.setVisible(False) + + QgsProject.instance().layerWillBeRemoved.connect(self.layer_will_be_removed) + + def updateStacked(self, row): + """ + according to the listWdiget row change the stackedWidget and + nestedStackedWidget + """ + + # stackedWidget index = 1 and change just the nestedStackedWidgets + if 0 <= row <= 1: + self.stackedPlotWidget.setCurrentIndex(0) + self.stackedNestedPlotWidget.setCurrentIndex(row) + + # change the stackedWidgets index + elif row > 1: + self.stackedPlotWidget.setCurrentIndex(row - 1) + + def registerExpressionContextGenerator(self, generator: QgsExpressionContextGenerator): + """ + Register the panel's expression context generator with all relevant children + """ + self.x_combo.registerExpressionContextGenerator(generator) + self.y_combo.registerExpressionContextGenerator(generator) + self.z_combo.registerExpressionContextGenerator(generator) + self.additional_info_combo.registerExpressionContextGenerator(generator) + + buttons = self.findChildren(QgsPropertyOverrideButton) + for button in buttons: + button.registerExpressionContextGenerator(generator) + + def register_data_defined_button(self, button, property_key: int): + """ + Registers a new data defined button, linked to the given property key (see values in PlotSettings) + """ + button.init(property_key, self.data_defined_properties, PlotSettings.DYNAMIC_PROPERTIES, None, False) + button.changed.connect(self._update_property) + + def _update_property(self): + """ + Triggered when a property override button value is changed + """ + button = self.sender() + self.data_defined_properties.setProperty(button.propertyKey(), button.toProperty()) + + def update_data_defined_button(self, button): + """ + Updates the current state of a property override button to reflect the current + property value + """ + if button.propertyKey() < 0: + return + + button.blockSignals(True) + button.setToProperty(self.data_defined_properties.property(button.propertyKey())) + button.blockSignals(False) + + def set_print_layout(self, print_layout): + """ + Sets the print layout linked with the widget, if in print layout mode + """ + self.linked_map_combo.setCurrentLayout(print_layout) + self.linked_map_combo.setItemType(QgsLayoutItemRegistry.LayoutMap) + + def set_plot_type(self, plot_type: str): + """ + Sets the current plot type shown in the dialog + """ + self.plot_combo.setCurrentIndex(self.plot_combo.findData(plot_type)) + + def data_defined_color_updated(self): + """ + refreshing function for color data defined button + + checks is the datadefined button is active and check also the plot type + in order to deactivate the color when not needed + """ + # if data defined button is active + if self.in_color_defined_button.isActive(): + # if plot is type for which using an expression for the color selection makes sense + if self.ptype in ['scatter', 'bar', 'pie', 'ternary', 'histogram']: + self.in_color_combo.setEnabled(False) + self.color_scale_data_defined_in.setVisible(True) + self.color_scale_data_defined_in.setEnabled(True) + self.color_scale_data_defined_in_label.setVisible(True) + self.color_scale_data_defined_in_label.setEnabled(True) + self.color_scale_data_defined_in_check.setVisible(True) + self.color_scale_data_defined_in_check.setEnabled(True) + self.color_scale_data_defined_in_invert_check.setVisible(True) + self.color_scale_data_defined_in_invert_check.setEnabled(True) + # if plot is type for which using an expression for the color selection does not make sense + else: + self.in_color_combo.setEnabled(True) + self.color_scale_data_defined_in.setVisible(False) + self.color_scale_data_defined_in.setEnabled(False) + self.color_scale_data_defined_in_label.setVisible(False) + self.color_scale_data_defined_in_label.setEnabled(False) + self.color_scale_data_defined_in_check.setVisible(False) + self.color_scale_data_defined_in_check.setEnabled(False) + self.color_scale_data_defined_in_invert_check.setVisible(False) + self.color_scale_data_defined_in_invert_check.setEnabled(False) + # if datadefined button is deactivated + else: + self.in_color_combo.setEnabled(True) + self.color_scale_data_defined_in.setVisible(False) + self.color_scale_data_defined_in.setEnabled(False) + self.color_scale_data_defined_in_label.setVisible(False) + self.color_scale_data_defined_in_check.setVisible(False) + self.color_scale_data_defined_in_invert_check.setVisible(False) + + def selected_layer_changed(self, layer): + """ + Trigger actions after selected layer changes + """ + self.x_combo.setLayer(layer) + self.y_combo.setLayer(layer) + self.z_combo.setLayer(layer) + self.additional_info_combo.setLayer(layer) + + buttons = self.findChildren(QgsPropertyOverrideButton) + for button in buttons: + button.setVectorLayer(layer) + + def layer_will_be_removed(self, layer_id): + """ + Triggered when a layer is about to be removed + """ + self.plot_factories = {k: v for k, v in self.plot_factories.items() if + not v.source_layer or v.source_layer.id() != layer_id} + + def getJSmessage(self, status): + """ + landing method for statusBarMessage signal coming from PLOT.js_callback + it decodes feature ids of clicked or selected plot elements, + selects on map canvas and triggers a pan/zoom to them + + the method handles several exceptions: + the first try/except is due to the connection to the init method + + second try/except looks into the decoded status, that is, it decodes + the js dictionary and loop where it is necessary + + the dic js dictionary contains several information useful to handle + correctly every operation + """ + + try: + dic = json.JSONDecoder().decode(status) + except: # pylint: disable=bare-except # noqa: F401 + dic = None + + # print('STATUS', status, dic) + + try: + # check the user behavior linked to the js script + + # if a selection event is performed + if dic['mode'] == 'selection': + if dic['type'] == 'scatter': + self.layer_combo.currentLayer().selectByIds(dic['id']) + else: + self.layer_combo.currentLayer().selectByIds(dic['tid']) + + # if a clicking event is performed depending on the plot type + elif dic["mode"] == 'clicking': + if dic['type'] == 'scatter': + self.layer_combo.currentLayer().selectByIds([dic['fidd']]) + elif dic["type"] == 'pie': + exp = """ "{}" = '{}' """.format(dic['field'], dic['label']) + # set the iterator with the expression as filter in feature request + request = QgsFeatureRequest().setFilterExpression(exp) + it = self.layer_combo.currentLayer().getFeatures(request) + self.layer_combo.currentLayer().selectByIds([f.id() for f in it]) + elif dic["type"] == 'histogram': + vmin = dic['id'] - dic['bin_step'] / 2 + vmax = dic['id'] + dic['bin_step'] / 2 + exp = """ "{}" <= {} AND "{}" > {} """.format(dic['field'], vmax, dic['field'], vmin) + request = QgsFeatureRequest().setFilterExpression(exp) + it = self.layer_combo.currentLayer().getFeatures(request) + self.layer_combo.currentLayer().selectByIds([f.id() for f in it]) + elif dic["type"] == 'scatterternary': + self.layer_combo.currentLayer().selectByIds([dic['fid']]) + else: + # build the expression from the js dic (customdata) + exp = """ "{}" = '{}' """.format(dic['field'], dic['id']) + # set the iterator with the expression as filter in feature request + request = QgsFeatureRequest().setFilterExpression(exp) + it = self.layer_combo.currentLayer().getFeatures(request) + self.layer_combo.currentLayer().selectByIds([f.id() for f in it]) + # print(exp) + except: # pylint: disable=bare-except # noqa: F401 + pass + + def helpPage(self): + """ + change the page of the manual according to the plot type selected and + the language (looks for translations) + """ + + # locale = QSettings().value('locale/userLocale', 'en_US')[0:2] + + self.help_view.load(QUrl('')) + self.layouth.addWidget(self.help_view) + help_url = QUrl('https://dataplotly-docs.readthedocs.io/en/latest/{}.html'.format(self.ptype)) + self.help_view.load(help_url) + + def resizeEvent(self, event): + """ + reimplemented event to detect the dialog resizing + """ + self.resizeWindow.emit() + return super(DataPlotlyPanelWidget, self).resizeEvent(event) + + def reloadPlotCanvas(self): + """ + just reload the plot view controlling the check state + """ + if self.live_update_check.isChecked(): + self.plot_view.reload() + + def reloadPlotCanvas2(self): + """ + just reload the plot view + """ + self.plot_view.reload() + + def refreshListWidget(self): + """ + highlight the item in the QListWidget when the QStackWidget changes + + needed to highligh the correct icon when the plot is rendered + """ + self.listWidget.setCurrentRow(self.stackedPlotWidget.currentIndex()) + + def refreshWidgets(self): # pylint: disable=too-many-statements,too-many-branches + """ + just for refreshing the UI + + widgets depending on the plot type in the combobox to have a cleaner + interface + + self.widgetType is a dict of widget depending on the plot type chosen + 'all': is for all the plot type, else the name of the plot is + explicitated + + BE AWARE: if loops are just for widgets that already exist! If a widget + is proper to a specific plot and is put within the if statement, the + method p.buildProperties will fail! + In the statement there have to be only widgets that, for example, need + to be re-rendered (label name...) + """ + + # get the plot type from the combobox + self.ptype = self.plot_combo.currentData() + + # BoxPlot BarPlot and Histogram orientation (same values) + self.orientation_combo.clear() + self.orientation_combo.addItem(self.tr('Vertical'), 'v') + self.orientation_combo.addItem(self.tr('Horizontal'), 'h') + + # BoxPlot and Violin outliers + self.outliers_combo.clear() + self.outliers_combo.addItem(self.tr('No Outliers'), False) + self.outliers_combo.addItem(self.tr('Standard Outliers'), 'outliers') + self.outliers_combo.addItem(self.tr('Suspected Outliers'), 'suspectedoutliers') + self.outliers_combo.addItem(self.tr('All Points'), 'all') + + # BoxPlot statistic types + self.box_statistic_combo.clear() + self.box_statistic_combo.addItem(self.tr('None'), False) + self.box_statistic_combo.addItem(self.tr('Mean'), True) + self.box_statistic_combo.addItem(self.tr('Standard Deviation'), 'sd') + + # BoxPlot and ScatterPlot X axis type + self.x_axis_mode_combo.clear() + self.x_axis_mode_combo.addItem(self.tr('Linear'), 'linear') + self.x_axis_mode_combo.addItem(self.tr('Logarithmic'), 'log') + self.x_axis_mode_combo.addItem(self.tr('Categorized'), 'category') + self.y_axis_mode_combo.clear() + self.y_axis_mode_combo.addItem(self.tr('Linear'), 'linear') + self.y_axis_mode_combo.addItem(self.tr('Logarithmic'), 'log') + self.y_axis_mode_combo.addItem(self.tr('Categorized'), 'category') + + # ScatterPlot marker types + self.marker_types = OrderedDict([ + (self.tr('Points'), 'markers'), + (self.tr('Lines'), 'lines'), + (self.tr('Points and Lines'), 'lines+markers') + ]) + self.marker_type_combo.clear() + for k, v in self.marker_types.items(): + self.marker_type_combo.addItem(k, v) + + # Point types + self.point_types = OrderedDict([ + (GuiUtils.get_icon('circle.svg'), 'circle'), + (GuiUtils.get_icon('square.svg'), 'square'), + (GuiUtils.get_icon('diamond.svg'), 'diamond'), + (GuiUtils.get_icon('cross.svg'), 'cross'), + (GuiUtils.get_icon('x.svg'), 'x'), + (GuiUtils.get_icon('triangle.svg'), 'triangle'), + (GuiUtils.get_icon('penta.svg'), 'penta'), + (GuiUtils.get_icon('star.svg'), 'star'), + ]) + + self.point_types2 = OrderedDict([ + ('circle', 0), + ('square', 1), + ('diamond', 2), + ('cross', 3), + ('x', 4), + ('triangle', 5), + ('penta', 13), + ('star', 17), + ]) + + self.point_combo.clear() + for k, v in self.point_types.items(): + self.point_combo.addItem(k, '', v) + + self.line_types = OrderedDict([ + (GuiUtils.get_icon('solid.png'), self.tr('Solid Line')), + (GuiUtils.get_icon('dot.png'), self.tr('Dot Line')), + (GuiUtils.get_icon('dash.png'), self.tr('Dash Line')), + (GuiUtils.get_icon('longdash.png'), self.tr('Long Dash Line')), + (GuiUtils.get_icon('dotdash.png'), self.tr('Dot Dash Line')), + (GuiUtils.get_icon('longdashdot.png'), self.tr('Long Dash Dot Line')), + ]) + + self.line_types2 = OrderedDict([ + (self.tr('Solid Line'), 'solid'), + (self.tr('Dot Line'), 'dot'), + (self.tr('Dash Line'), 'dash'), + (self.tr('Long Dash Line'), 'longdash'), + (self.tr('Dot Dash Line'), 'dashdot'), + (self.tr('Long Dash Dot Line'), 'longdashdot'), + ]) + + self.line_combo.clear() + for k, v in self.line_types.items(): + self.line_combo.addItem(k, v) + + # BarPlot bar mode + self.bar_mode_combo.clear() + self.bar_mode_combo.addItem(self.tr('Grouped'), 'group') + self.bar_mode_combo.addItem(self.tr('Stacked'), 'stack') + self.bar_mode_combo.addItem(self.tr('Overlay'), 'overlay') + + # Histogram normalization mode + self.hist_norm_combo.clear() + self.hist_norm_combo.addItem(self.tr('Enumerated'), '') + self.hist_norm_combo.addItem(self.tr('Percents'), 'percent') + self.hist_norm_combo.addItem(self.tr('Probability'), 'probability') + self.hist_norm_combo.addItem(self.tr('Density'), 'density') + self.hist_norm_combo.addItem(self.tr('Prob Density'), 'probability density') + + # Contour Plot rendering type + self.contour_type = OrderedDict([ + (self.tr('Fill'), 'fill'), + (self.tr('Heatmap'), 'heatmap'), + (self.tr('Only Lines'), 'lines'), + ]) + self.contour_type_combo.clear() + for k, v in self.contour_type.items(): + self.contour_type_combo.addItem(k, v) + + # Contour Plot color scale and Data Defined Color scale + + scale_color_dict = {'Grey Scale': 'Greys', + 'Green Scale': 'Greens', + 'Fire Scale': 'Hot', + 'BlueYellowRed': 'Portland', + 'BlueGreenRed': 'Jet', + 'BlueToRed': 'RdBu', + 'BlueToRed Soft': 'Bluered', + 'BlackRedYellowBlue': 'Blackbody', + 'Terrain': 'Earth', + 'Electric Scale': 'Electric', + 'RedOrangeYellow': 'YIOrRd', + 'DeepblueBlueWhite': 'YIGnBu', + 'BlueWhitePurple': 'Picnic'} + + self.color_scale_combo.clear() + self.color_scale_data_defined_in.clear() + + for k, v in scale_color_dict.items(): + self.color_scale_combo.addItem(k, v) + self.color_scale_data_defined_in.addItem(k, v) + + # according to the plot type, change the label names + + # BoxPlot + if self.ptype == 'box' or self.ptype == 'violin': + self.x_label.setText(self.tr('Grouping field \n(optional)')) + # set the horizontal and vertical size of the label and reduce the label font size + ff = QFont() + ff.setPointSizeF(8) + self.x_label.setFont(ff) + self.x_label.setFixedWidth(100) + self.orientation_label.setText(self.tr('Box orientation')) + self.in_color_lab.setText(self.tr('Box color')) + self.register_data_defined_button(self.in_color_defined_button, PlotSettings.PROPERTY_COLOR) + + elif self.ptype in ('scatter', 'ternary', 'bar', '2dhistogram', 'contour', 'polar'): + self.x_label.setText(self.tr('X field')) + self.x_label.setFont(self.font()) + + if self.ptype in ('scatter', 'ternary'): + self.in_color_lab.setText(self.tr('Marker color')) + elif self.ptype == 'bar': + self.orientation_label.setText(self.tr('Bar orientation')) + self.in_color_lab.setText(self.tr('Bar color')) + self.register_data_defined_button(self.in_color_defined_button, PlotSettings.PROPERTY_COLOR) + + elif self.ptype == 'pie': + self.x_label.setText(self.tr('Grouping field')) + ff = QFont() + ff.setPointSizeF(8.5) + self.x_label.setFont(ff) + self.x_label.setFixedWidth(80) + # Register button again with more specific help text + self.in_color_defined_button.init( + PlotSettings.PROPERTY_COLOR, self.data_defined_properties.property(PlotSettings.PROPERTY_COLOR), + QgsPropertyDefinition( + 'color', QgsPropertyDefinition.DataType.DataTypeString, 'Color Array', + "string [r,g,b,a] as int 0-255 or #AARRGGBB as hex or color as color's name, " + "or an array of such strings" + ), None, False + ) + self.in_color_defined_button.changed.connect(self._update_property) + + elif self.ptype == 'histogram': + # Register button again with more specific help text + self.in_color_defined_button.init( + PlotSettings.PROPERTY_COLOR, self.data_defined_properties.property(PlotSettings.PROPERTY_COLOR), + QgsPropertyDefinition( + 'color', QgsPropertyDefinition.DataType.DataTypeString, 'Color Array', + "string [r,g,b,a] as int 0-255 or #AARRGGBB as hex or color as color's name, " + "or an array of such strings" + ), None, False + ) + + # info combo for data hovering + self.info_combo.clear() + self.info_combo.addItem(self.tr('All Values'), 'all') + self.info_combo.addItem(self.tr('X Values'), 'x') + self.info_combo.addItem(self.tr('Y Values'), 'y') + self.info_combo.addItem(self.tr('No Data'), 'none') + + # label text position choice + self.combo_text_position.clear() + self.combo_text_position.addItem(self.tr('Automatic'), 'auto') + self.combo_text_position.addItem(self.tr('Inside bar'), 'inside') + self.combo_text_position.addItem(self.tr('Outside bar'), 'outside') + + # Violin side + self.violinSideCombo.clear() + self.violinSideCombo.addItem(self.tr('Both Sides'), 'both') + self.violinSideCombo.addItem(self.tr('Only Left'), 'negative') + self.violinSideCombo.addItem(self.tr('Only right'), 'positive') + + # dictionary with all the widgets and the plot they belong to + self.widgetType = { + # plot properties + self.layer_combo: ['all'], + self.feature_subset_defined_button: ['all'], + self.x_label: ['all'], + self.x_combo: ['all'], + self.y_label: ['scatter', 'bar', 'box', 'pie', '2dhistogram', 'polar', 'ternary', 'contour', 'violin'], + self.y_combo: ['scatter', 'bar', 'box', 'pie', '2dhistogram', 'polar', 'ternary', 'contour', 'violin'], + self.z_label: ['ternary'], + self.z_combo: ['ternary'], + self.info_label: ['scatter'], + self.info_combo: ['scatter'], + self.in_color_lab: ['scatter', 'bar', 'box', 'pie', 'histogram', 'polar', 'ternary', 'violin'], + self.in_color_combo: ['scatter', 'bar', 'box', 'pie', 'histogram', 'polar', 'ternary', 'violin'], + self.in_color_defined_button: ['scatter', 'bar', 'box', 'pie', 'histogram', 'ternary'], + self.color_scale_data_defined_in: ['scatter', 'bar', 'pie', 'histogram', 'ternary'], + self.color_scale_data_defined_in_label: ['scatter', 'bar', 'ternary'], + self.color_scale_data_defined_in_check: ['scatter', 'bar', 'ternary'], + self.color_scale_data_defined_in_invert_check: ['bar', 'ternary'], + self.out_color_lab: ['scatter', 'bar', 'box', 'pie', 'histogram', 'polar', 'ternary', 'violin'], + self.out_color_combo: ['scatter', 'bar', 'box', 'pie', 'histogram', 'polar', 'ternary', 'violin'], + self.marker_width_lab: ['scatter', 'bar', 'box', 'histogram', 'polar', 'ternary', 'violin'], + self.marker_width: ['scatter', 'bar', 'box', 'histogram', 'polar', 'ternary', 'violin'], + self.stroke_defined_button: ['scatter', 'bar', 'box', 'histogram', 'polar', 'ternary', 'violin'], + self.marker_size_lab: ['scatter', 'polar', 'ternary'], + self.marker_size: ['scatter', 'polar', 'ternary'], + self.size_defined_button: ['scatter', 'ternary'], + self.marker_type_lab: ['scatter', 'polar'], + self.marker_type_combo: ['scatter', 'polar'], + self.alpha_lab: ['scatter', 'bar', 'box', 'histogram', 'polar', 'ternary', 'violin', 'contour'], + self.opacity_widget: ['scatter', 'bar', 'box', 'pie', 'histogram', 'polar', 'ternary', 'violin', 'contour'], + self.properties_group_box: ['scatter', 'bar', 'box', 'pie', 'histogram', 'polar', 'ternary', 'contour', '2dhistogram', + 'violin'], + self.bar_mode_lab: ['bar', 'histogram'], + self.bar_mode_combo: ['bar', 'histogram'], + self.legend_label: ['all'], + self.legend_title: ['all'], + self.legend_title_defined_button: ['all'], + self.point_lab: ['scatter', 'ternary', 'polar'], + self.point_combo: ['scatter', 'ternary', 'polar'], + self.line_lab: ['scatter', 'polar'], + self.line_combo: ['scatter', 'polar'], + self.color_scale_label: ['contour', '2dhistogram'], + self.color_scale_combo: ['contour', '2dhistogram'], + self.contour_type_label: ['contour'], + self.contour_type_combo: ['contour'], + self.show_lines_check: ['contour'], + + # layout customization + self.show_legend_check: ['all'], + self.orientation_legend_check: ['scatter', 'bar', 'box', 'histogram', 'ternary', 'pie', 'violin'], + self.plot_title_lab: ['all'], + self.plot_title_line: ['all'], + self.plot_title_defined_button: ['all'], + self.x_axis_label: ['scatter', 'bar', 'box', 'histogram', '2dhistogram', 'ternary', 'violin'], + self.x_axis_title: ['scatter', 'bar', 'box', 'histogram', '2dhistogram', 'ternary', 'violin'], + self.x_axis_title_defined_button: ['scatter', 'bar', 'box', 'histogram', '2dhistogram', 'ternary', 'violin'], + self.y_axis_label: ['scatter', 'bar', 'box', 'histogram', '2dhistogram', 'ternary', 'violin'], + self.y_axis_title: ['scatter', 'bar', 'box', 'histogram', '2dhistogram', 'ternary', 'violin'], + self.y_axis_title_defined_button: ['scatter', 'bar', 'box', 'histogram', '2dhistogram', 'ternary', 'violin'], + self.z_axis_label: ['ternary'], + self.z_axis_title: ['ternary'], + self.z_axis_title_defined_button: ['ternary'], + self.x_axis_mode_label: ['scatter', 'box'], + self.y_axis_mode_label: ['scatter', 'box'], + self.x_axis_mode_combo: ['scatter', 'box'], + self.y_axis_mode_combo: ['scatter', 'box'], + self.invert_x_check: ['scatter', 'bar', 'box', 'histogram', '2dhistogram'], + self.invert_y_check: ['scatter', 'bar', 'box', 'histogram', '2dhistogram'], + self.x_axis_min: ['scatter', 'bar', 'box', 'histogram', '2dhistogram', 'violin'], + self.x_axis_max: ['scatter', 'bar', 'box', 'histogram', '2dhistogram', 'violin'], + self.x_axis_min_defined_button: ['scatter', 'bar', 'box', 'histogram', '2dhistogram', 'violin'], + self.x_axis_max_defined_button: ['scatter', 'bar', 'box', 'histogram', '2dhistogram', 'violin'], + self.y_axis_min: ['scatter', 'bar', 'box', 'histogram', '2dhistogram', 'violin'], + self.y_axis_max: ['scatter', 'bar', 'box', 'histogram', '2dhistogram', 'violin'], + self.y_axis_min_defined_button: ['scatter', 'bar', 'box', 'histogram', '2dhistogram', 'violin'], + self.y_axis_max_defined_button: ['scatter', 'bar', 'box', 'histogram', '2dhistogram', 'violin'], + self.orientation_label: ['bar', 'box', 'histogram', 'violin'], + self.orientation_combo: ['bar', 'box', 'histogram', 'violin'], + self.box_statistic_label: ['box'], + self.box_statistic_combo: ['box'], + self.outliers_label: ['box', 'violin'], + self.outliers_combo: ['box', 'violin'], + self.showMeanCheck: ['violin'], + self.range_slider_combo: ['scatter'], + self.hist_norm_label: ['histogram'], + self.hist_norm_combo: ['histogram'], + self.additional_info_label: ['scatter', 'ternary', 'bar'], + self.additional_info_combo: ['scatter', 'ternary', 'bar'], + self.hover_as_text_check: ['scatter'], + self.label_text_position: ['bar'], + self.combo_text_position: ['bar'], + self.cumulative_hist_check: ['histogram'], + self.invert_hist_check: ['histogram'], + self.bins_check: ['histogram'], + self.bins_value: ['histogram'], + self.bar_gap_label: ['histogram'], + self.bar_gap: ['histogram'], + self.violinSideLabel: ['violin'], + self.violinSideCombo: ['violin'], + self.violinBox: ['violin'], + } + + # enable the widget according to the plot type + for k, v in self.widgetType.items(): + if 'all' in v or self.ptype in v: + k.setEnabled(True) + k.setVisible(True) + else: + k.setEnabled(False) + k.setVisible(False) + + # disable by default the bins value box + # if not explicit, the upper loop will enable it + self.bins_value.setEnabled(False) + + # disable at first run the color data defined buttons + self.color_scale_data_defined_in.setVisible(False) + self.color_scale_data_defined_in_label.setVisible(False) + self.color_scale_data_defined_in_check.setVisible(False) + self.color_scale_data_defined_in_invert_check.setVisible(False) + + def refreshWidgets2(self): + """ + just refresh the UI to make the radiobuttons visible when SubPlots + """ + + # enable radio buttons for subplots + if self.subcombo.currentData() == 'subplots': + self.radio_rows.setEnabled(True) + self.radio_rows.setVisible(True) + self.radio_columns.setEnabled(True) + self.radio_columns.setVisible(True) + else: + self.radio_rows.setEnabled(False) + self.radio_rows.setVisible(False) + self.radio_columns.setEnabled(False) + self.radio_columns.setVisible(False) + + def refreshWidgets3(self): + """ + refresh the UI according to Point, Lines or both choosen for the symbols + of scatterplot + """ + + if self.marker_type_combo.currentText() == self.tr('Points'): + self.point_lab.setEnabled(True) + self.point_lab.setVisible(True) + self.point_combo.setEnabled(True) + self.point_combo.setVisible(True) + self.line_lab.setEnabled(False) + self.line_lab.setVisible(False) + self.line_combo.setEnabled(False) + self.line_combo.setVisible(False) + elif self.marker_type_combo.currentText() == self.tr('Lines'): + self.point_lab.setEnabled(False) + self.point_lab.setVisible(False) + self.point_combo.setEnabled(False) + self.point_combo.setVisible(False) + self.line_lab.setEnabled(True) + self.line_lab.setVisible(True) + self.line_combo.setEnabled(True) + self.line_combo.setVisible(True) + else: + self.point_lab.setEnabled(True) + self.point_lab.setVisible(True) + self.point_combo.setEnabled(True) + self.point_combo.setVisible(True) + self.line_lab.setEnabled(True) + self.line_lab.setVisible(True) + self.line_combo.setEnabled(True) + self.line_combo.setVisible(True) + + def setLegend(self): + """ + set the legend from the fields combo boxes + """ + if self.ptype in ('box', 'bar'): + self.legend_title.setText(self.y_combo.currentText()) + elif self.ptype == 'histogram': + self.legend_title.setText(self.x_combo.currentText()) + else: + legend_title_string = ('{} - {}'.format(self.x_combo.currentText(), self.y_combo.currentText())) + self.legend_title.setText(legend_title_string) + + def get_settings(self) -> PlotSettings: # pylint: disable=R0915 + """ + Returns the plot settings as currently defined in the dialog + """ + # get the plot type from the combo box + self.ptype = self.plot_combo.currentData() + + # if colorscale should be visible or not + color_scale_visible = self.color_scale_data_defined_in_check.isVisible() and self.color_scale_data_defined_in_check.isChecked() + + # dictionary of all the plot properties + plot_properties = {'custom': [self.x_combo.currentText()], + 'hover_text': self.info_combo.currentData(), + 'hover_label_text': '+text' if self.hover_as_text_check.isChecked() else None, + 'hover_label_position': self.combo_text_position.currentData(), + 'x_name': self.x_combo.currentText(), + 'y_name': self.y_combo.currentText(), + 'z_name': self.z_combo.currentText(), + 'in_color': self.in_color_combo.color().name(), + 'show_colorscale_legend': color_scale_visible, + 'invert_color_scale': self.color_scale_data_defined_in_invert_check.isChecked(), + 'out_color': self.out_color_combo.color().name(), + 'marker_width': self.marker_width.value(), + 'marker_size': self.marker_size.value(), + 'marker_symbol': self.point_types2[self.point_combo.currentData()], + 'line_dash': self.line_types2[self.line_combo.currentText()], + 'box_orientation': self.orientation_combo.currentData(), + 'marker': self.marker_types[self.marker_type_combo.currentText()], + 'opacity': self.opacity_widget.opacity(), + 'box_stat': self.box_statistic_combo.currentData(), + 'box_outliers': self.outliers_combo.currentData(), + 'name': self.legend_title.text(), + 'normalization': self.hist_norm_combo.currentData(), + 'cont_type': self.contour_type[self.contour_type_combo.currentText()], + 'color_scale': None, + 'show_lines': self.show_lines_check.isChecked(), + 'cumulative': self.cumulative_hist_check.isChecked(), + 'invert_hist': 'decreasing' if self.invert_hist_check.isChecked() else 'increasing', + 'bins': self.bins_value.value(), + 'show_mean_line': self.showMeanCheck.isChecked(), + 'violin_side': self.violinSideCombo.currentData(), + 'violin_box': self.violinBox.isChecked(), + 'selected_features_only': self.selected_feature_check.isChecked(), + 'visible_features_only': self.visible_feature_check.isChecked(), + 'color_scale_data_defined_in_check': False, + 'color_scale_data_defined_in_invert_check': False, + 'marker_type_combo': self.marker_type_combo.currentText(), + 'point_combo': self.point_combo.currentText(), + 'line_combo': self.line_combo.currentText(), + 'contour_type_combo': self.contour_type_combo.currentText(), + 'show_lines_check': self.show_lines_check.isChecked() + } + + if self.in_color_defined_button.isActive(): + plot_properties['color_scale_data_defined_in_check'] = self.color_scale_data_defined_in_check.isChecked() + plot_properties[ + 'color_scale_data_defined_in_invert_check'] = self.color_scale_data_defined_in_invert_check.isChecked() + if self.ptype in self.widgetType[self.color_scale_data_defined_in]: + plot_properties['color_scale'] = self.color_scale_data_defined_in.currentData() + else: + plot_properties['color_scale'] = self.color_scale_combo.currentData() + + # add widgets properties to the dictionary + + # build the layout customizations + layout_properties = {'legend': self.show_legend_check.isChecked(), + 'legend_orientation': 'h' if self.orientation_legend_check.isChecked() else 'v', + 'title': self.plot_title_line.text(), + 'x_title': self.x_axis_title.text(), + 'y_title': self.y_axis_title.text(), + 'z_title': self.z_axis_title.text(), + 'range_slider': {'visible': self.range_slider_combo.isChecked(), + 'borderwidth': 1}, + 'bar_mode': self.bar_mode_combo.currentData(), + 'x_type': self.x_axis_mode_combo.currentData(), + 'y_type': self.y_axis_mode_combo.currentData(), + 'x_inv': None if not self.invert_x_check.isChecked() else 'reversed', + 'y_inv': None if not self.invert_y_check.isChecked() else 'reversed', + 'x_min': self.x_axis_min.value() if self.x_axis_bounds_check.isChecked() else None, + 'x_max': self.x_axis_max.value() if self.x_axis_bounds_check.isChecked() else None, + 'y_min': self.y_axis_min.value() if self.y_axis_bounds_check.isChecked() else None, + 'y_max': self.y_axis_max.value() if self.y_axis_bounds_check.isChecked() else None, + 'bargaps': self.bar_gap.value(), + 'additional_info_expression': self.additional_info_combo.expression(), + 'bins_check': self.bins_check.isChecked()} + + settings = PlotSettings(plot_type=self.ptype, properties=plot_properties, layout=layout_properties, + source_layer_id=self.layer_combo.currentLayer().id() if self.layer_combo.currentLayer() else None) + settings.data_defined_properties = self.data_defined_properties + return settings + + def set_layer_id(self, layer_id: str): + """ + Sets the layer to use for plotting, by layer ID + """ + layer = QgsProject.instance().mapLayer(layer_id) + if not layer: + return + + self.layer_combo.setLayer(layer) + + def set_settings(self, settings: PlotSettings): # pylint: disable=too-many-statements + """ + Takes a PlotSettings object and fill the widgets with the settings + """ + + self.set_plot_type(settings.plot_type) + + # Set the plot properties + self.set_layer_id(settings.source_layer_id) + self.selected_feature_check.setChecked(settings.properties.get('selected_features_only', False)) + self.visible_feature_check.setChecked(settings.properties.get('visible_features_only', False)) + + self.data_defined_properties = settings.data_defined_properties + buttons = self.findChildren(QgsPropertyOverrideButton) + for button in buttons: + self.update_data_defined_button(button) + + self.x_combo.setExpression(settings.properties.get('x_name', '')) + self.y_combo.setExpression(settings.properties.get('y_name', '')) + self.z_combo.setExpression(settings.properties.get('z_name', '')) + self.in_color_combo.setColor( + QColor(settings.properties.get('in_color', '#8ebad9'))) + self.marker_size.setValue(settings.properties.get('marker_size', 10)) + + self.color_scale_data_defined_in.setCurrentIndex( + self.color_scale_data_defined_in.findData(settings.properties.get('color_scale', None))) + self.color_scale_data_defined_in_check.setChecked(settings.properties.get('color_scale_data_defined_in_check', False)) + self.color_scale_data_defined_in_invert_check.setChecked( + settings.properties.get('color_scale_data_defined_in_invert_check', False)) + self.out_color_combo.setColor( + QColor(settings.properties.get('out_color', '#1f77b4'))) + self.marker_width.setValue(settings.properties.get('marker_width', 1)) + self.marker_type_combo.setCurrentText( + settings.properties.get('marker_type_combo', 'Points')) + self.point_combo.setCurrentText(settings.properties.get('point_combo', '')) + self.line_combo.setCurrentText( + settings.properties.get('line_combo', 'Solid Line')) + self.contour_type_combo.setCurrentText( + settings.properties.get('contour_type_combo', 'Fill')) + self.show_lines_check.setChecked(settings.properties.get('show_lines_check', False)) + self.color_scale_combo.setCurrentIndex(self.color_scale_combo.findData(settings.properties.get('color_scale', None))) + self.opacity_widget.setOpacity(settings.properties.get('opacity', 1)) + self.orientation_legend_check.setChecked(settings.layout.get('legend_orientation') == 'h') + self.range_slider_combo.setChecked(settings.layout['range_slider']['visible']) + self.plot_title_line.setText( + settings.layout.get('title', 'Plot Title')) + self.legend_title.setText(settings.properties.get('name', '')) + self.x_axis_title.setText(settings.layout.get('x_title', '')) + self.y_axis_title.setText(settings.layout.get('y_title', '')) + self.z_axis_title.setText(settings.layout.get('z_title', '')) + self.info_combo.setCurrentIndex(self.info_combo.findData(settings.properties.get('hover_text', None))) + self.additional_info_combo.setExpression(settings.layout.get('additional_info_expression', '')) + self.hover_as_text_check.setChecked(settings.properties.get('hover_label_text') == '+text') + self.combo_text_position.setCurrentIndex( + self.combo_text_position.findData(settings.layout.get('hover_label_position', 'auto'))) + self.invert_x_check.setChecked(settings.layout.get('x_inv') == 'reversed') + self.x_axis_mode_combo.setCurrentIndex(self.x_axis_mode_combo.findData(settings.layout.get('x_type', None))) + self.invert_y_check.setChecked(settings.layout.get('y_inv') == 'reversed') + self.y_axis_mode_combo.setCurrentIndex(self.y_axis_mode_combo.findData(settings.layout.get('y_type', None))) + self.x_axis_bounds_check.setChecked(settings.layout.get('x_min', None) is not None) + self.x_axis_bounds_check.setCollapsed(settings.layout.get('x_min', None) is None) + self.x_axis_min.setValue(settings.layout.get('x_min') if settings.layout.get('x_min', None) is not None else 0.0) + self.x_axis_max.setValue(settings.layout.get('x_max') if settings.layout.get('x_max', None) is not None else 0.0) + self.y_axis_bounds_check.setChecked(settings.layout.get('y_min', None) is not None) + self.y_axis_bounds_check.setCollapsed(settings.layout.get('y_min', None) is None) + self.y_axis_min.setValue(settings.layout.get('y_min') if settings.layout.get('y_min', None) is not None else 0.0) + self.y_axis_max.setValue(settings.layout.get('y_max') if settings.layout.get('y_max', None) is not None else 0.0) + self.orientation_combo.setCurrentIndex(self.orientation_combo.findData(settings.properties.get('box_orientation', 'v'))) + self.bar_mode_combo.setCurrentIndex(self.bar_mode_combo.findData(settings.layout.get('bar_mode', None))) + self.hist_norm_combo.setCurrentIndex(self.hist_norm_combo.findData(settings.properties.get('normalization', None))) + self.box_statistic_combo.setCurrentIndex(self.box_statistic_combo.findData(settings.properties.get('box_stat', None))) + self.outliers_combo.setCurrentIndex(self.outliers_combo.findData(settings.properties.get('box_outliers', None))) + self.violinSideCombo.setCurrentIndex(self.violinSideCombo.findData(settings.properties.get('violin_side', None))) + self.violinBox.setChecked(settings.properties.get('violin_box', False)) + self.showMeanCheck.setChecked(settings.properties.get('show_mean_line', False)) + self.cumulative_hist_check.setChecked(settings.properties.get('cumulative', False)) + self.invert_hist_check.setChecked(settings.properties.get('invert_hist') == 'decreasing') + self.bins_check.setChecked(settings.layout.get('bins_check', False)) + self.bins_value.setValue(settings.properties.get('bins', 0)) + self.bar_gap.setValue(settings.layout.get('bargaps', 0)) + self.show_legend_check.setChecked(settings.layout.get('legend', True)) + + def create_plot_factory(self) -> PlotFactory: + """ + Creates a PlotFactory based on the settings defined in the dialog + """ + settings = self.get_settings() + + visible_region = None + if settings.properties['visible_features_only']: + visible_region = QgsReferencedRectangle(self.iface.mapCanvas().extent(), + self.iface.mapCanvas().mapSettings().destinationCrs()) + + # plot instance + plot_factory = PlotFactory(settings, visible_region=visible_region) + + # unique name for each plot trace (name is idx_plot, e.g. 1_scatter) + self.pid = ('{}_{}'.format(str(self.idx), settings.plot_type)) + + # create default dictionary that contains all the plot and properties + self.plot_factories[self.pid] = plot_factory + + plot_factory.plot_built.connect(partial(self.refresh_plot, plot_factory)) + + # just add 1 to the index + self.idx += 1 + + # enable the Update Plot button + self.update_btn.setEnabled(True) + + return plot_factory + + def update_plot_visible_rect(self): + """ + Called when the canvas rect changes, and we may need to update filtered plots + """ + region = QgsReferencedRectangle(self.iface.mapCanvas().extent(), + self.iface.mapCanvas().mapSettings().destinationCrs()) + for _, factory in self.plot_factories.items(): + factory.set_visible_region(region) + + def refresh_plot(self, factory): + """ + Refreshes the plot built by the specified factory + """ + self.plot_path = factory.build_figure() + self.refreshPlotView() + + def create_plot(self): + """ + call the method to effectively draw the final plot + + before creating the plot, it calls the method plotProperties in order + to create the plot instance with all the properties taken from the UI + """ + + # call the method to build all the Plot plotProperties + plot_factory = self.create_plot_factory() + + # set the correct index page of the widget + self.stackedPlotWidget.setCurrentIndex(1) + # highlight the correct plot row in the listwidget + self.listWidget.setCurrentRow(2) + + if self.subcombo.currentData() == 'single': + + # plot single plot, check the object dictionary length + if len(self.plot_factories) <= 1: + self.plot_path = plot_factory.build_figure() + + # to plot many plots in the same figure + else: + # plot list ready to be called within go.Figure + pl = [] + + for _, v in self.plot_factories.items(): + pl.append(v.trace[0]) + + self.plot_path = plot_factory.build_figures(self.ptype, pl) + + # choice to draw subplots instead depending on the combobox + elif self.subcombo.currentData() == 'subplots': + try: + gr = len(self.plot_factories) + pl = [] + + for _, v in self.plot_factories.items(): + pl.append(v.trace[0]) + + # plot in single row and many columns + if self.radio_rows.isChecked(): + + self.plot_path = plot_factory.build_sub_plots('row', 1, gr, pl) + + # plot in single column and many rows + elif self.radio_columns.isChecked(): + + self.plot_path = plot_factory.build_sub_plots('col', gr, 1, pl) + except: # pylint: disable=bare-except # noqa: F401 + if self.message_bar: + self.message_bar.pushMessage( + self.tr("{} plot is not compatible for subplotting\n see ".format(self.ptype)), + Qgis.MessageLevel(2), duration=5) + return + + # connect to simple function that reloads the view + self.refreshPlotView() + + def UpdatePlot(self): + """ + updates only the LAST plot created + get the key of the last plot created and delete it from the plot container + and call the method to create the plot with the updated settings + """ + if self.mode == DataPlotlyPanelWidget.MODE_CANVAS: + plot_to_update = (sorted(self.plot_factories.keys())[-1]) + del self.plot_factories[plot_to_update] + + self.create_plot() + else: + self.widgetChanged.emit() + + def refreshPlotView(self): + """ + just refresh the view, if the reload method is called immediately after + the view creation it won't reload the page + """ + + self.plot_url = QUrl.fromLocalFile(self.plot_path) + self.plot_view.load(self.plot_url) + self.layoutw.addWidget(self.plot_view) + + self.raw_plot_text.clear() + with open(self.plot_path, 'r') as myfile: + plot_text = myfile.read() + + self.raw_plot_text.setPlainText(plot_text) + + def clearPlotView(self): + """ + clear the content of the QWebView by loading an empty url and clear the + raw text of the QPlainTextEdit + """ + + self.plot_factories = {} + + try: + self.plot_view.load(QUrl('')) + self.layoutw.addWidget(self.plot_view) + self.raw_plot_text.clear() + if self.mode == DataPlotlyPanelWidget.MODE_CANVAS: + # disable the Update Plot Button + self.update_btn.setEnabled(False) + except: # pylint: disable=bare-except # noqa: F401 + pass + + def save_plot_as_image(self): + """ + Save the current plot view as a png image. + The user can choose the path and the file name + """ + plot_file, _ = QFileDialog.getSaveFileName(self, self.tr("Save Plot"), "", "*.png") + if not plot_file: + return + + plot_file = QgsFileUtils.ensureFileNameHasExtension(plot_file, ['png']) + + frame = self.plot_view.page().mainFrame() + self.plot_view.page().setViewportSize(frame.contentsSize()) + # render image + image = QImage(self.plot_view.page().viewportSize(), QImage.Format_ARGB32) + painter = QPainter(image) + frame.render(painter) + painter.end() + image.save(plot_file) + if self.message_bar: + self.message_bar.pushSuccess(self.tr('DataPlotly'), + self.tr('Plot saved to {}').format( + QUrl.fromLocalFile(plot_file).toString(), + QDir.toNativeSeparators(plot_file))) + + def save_plot_as_html(self): + """ + Saves the plot as a local html file. Basically just let the user choose + where to save the already existing html file created by plotly + """ + + plot_file, _ = QFileDialog.getSaveFileName(self, self.tr("Save Plot"), "", "*.html") + if not plot_file: + return + + plot_file = QgsFileUtils.ensureFileNameHasExtension(plot_file, ['html']) + + copyfile(self.plot_path, plot_file) + if self.message_bar: + self.message_bar.pushSuccess(self.tr('DataPlotly'), + self.tr('Saved plot to {}').format( + QUrl.fromLocalFile(plot_file).toString(), + QDir.toNativeSeparators(plot_file))) + + def showPlotFromDic(self, plot_input_dic): + """ + Allows to call the plugin from the python console + + param: + plot_input_dic (dictionary): dictionary with specific structure, + see below + + + Code usage example: + + #import the plugins of QGIS + from qgis.utils import plugins + # create the instace of the plugin + myplugin = plugins['DataPlotly'] + + # create a dictionary of values that will be elaborated by the method + + vl = iface.activeLayer() + # empty dictionary that should filled up with GUI values + dq = {} + dq['plot_prop'] = {} + dq['layout_prop'] = {} + # plot type + dq['plot_type'] = 'scatter' + # vector layer + dq["layer"] = vl + # minimum X and Y axis values + dq['plot_prop']['x'] = [i["some_field"] for i in vl.getFeatures()] + dq['plot_prop']['y'] = [i["some_field"] for i in vl.getFeatures()] + + # call the final method + myplugin.loadPlotFromDic(dq) + """ + # keys of the nested plot_prop and layout_prop have to be the SAME of + # those created in buildProperties and buildLayout method + + # set some dialog widget from the input dictionary + # plot type in the plot_combo combobox + self.plot_combo.setCurrentIndex(self.plot_combo.findData(plot_input_dic["plot_type"])) + + try: + self.layer_combo.setLayer(plot_input_dic["layer"]) + if 'x_name' in plot_input_dic["plot_prop"] and plot_input_dic["plot_prop"]["x_name"]: + self.x_combo.setField(plot_input_dic["plot_prop"]["x_name"]) + if 'y_name' in plot_input_dic["plot_prop"] and plot_input_dic["plot_prop"]["y_name"]: + self.y_combo.setField(plot_input_dic["plot_prop"]["y_name"]) + if 'z_name' in plot_input_dic["plot_prop"] and plot_input_dic["plot_prop"]["z_name"]: + self.z_combo.setField(plot_input_dic["plot_prop"]["z_name"]) + except: # pylint: disable=bare-except # noqa: F401 + pass + + settings = PlotSettings(plot_input_dic['plot_type'], + properties=plot_input_dic["plot_prop"], + layout=plot_input_dic["layout_prop"]) + + # create Plot instance + factory = PlotFactory(settings) + + standalone_plot_path = factory.build_figure() + standalone_plot_url = QUrl.fromLocalFile(standalone_plot_path) + + self.plot_view.load(standalone_plot_url) + self.layoutw.addWidget(self.plot_view) + + # enable the Update Button to allow the updating of the plot + self.update_btn.setEnabled(True) + + # the following code is necessary to let the user add other plots in + # different plot canvas after the creation of the standolone plot + + # unique name for each plot trace (name is idx_plot, e.g. 1_scatter) + self.pid = ('{}_{}'.format(str(self.idx), plot_input_dic["plot_type"])) + + # create default dictionary that contains all the plot and properties + self.plot_factories[self.pid] = factory + + # just add 1 to the index + self.idx += 1 + + def write_project(self, document: QDomDocument): + """ + Called when the current project is being saved. + + If the dialog opts to store its current settings in the project, it will + use this hook to store them in the project XML + """ + if not self.save_to_project: + return + + settings = self.get_settings() + settings.write_to_project(document) + + def read_project(self, document: QDomDocument): + """ + Called when the current project is being read. + + If the dialog opts to read its current settings from a project, it will + use this hook to read them from the project XML + """ + if not self.read_from_project: + return + + settings = PlotSettings() + if settings.read_from_project(document): + # update the dock state to match the read settings + self.set_settings(settings) + + def load_configuration(self): + """ + Loads configuration settings from a file + """ + file, _ = QFileDialog.getOpenFileName(self, self.tr("Load Configuration"), "", "XML files (*.xml)") + if file: + settings = PlotSettings() + if settings.read_from_file(file): + self.set_settings(settings) + else: + if self.message_bar: + self.message_bar.pushWarning(self.tr('DataPlotly'), self.tr('Could not read settings from file')) + + def save_configuration(self): + """ + Saves configuration settings to a file + """ + file, _ = QFileDialog.getSaveFileName(self, self.tr("Save Configuration"), "", "XML files (*.xml)") + if file: + file = QgsFileUtils.ensureFileNameHasExtension(file, ['xml']) + self.get_settings().write_to_file(file) + if self.message_bar: + self.message_bar.pushSuccess(self.tr('DataPlotly'), + self.tr('Saved configuration to {}').format( + QUrl.fromLocalFile(file).toString(), QDir.toNativeSeparators(file))) diff --git a/plot_layout/help/Makefile b/plot_layout/help/Makefile new file mode 100644 index 0000000..9def777 --- /dev/null +++ b/plot_layout/help/Makefile @@ -0,0 +1,130 @@ +# Makefile for Sphinx documentation +# + +# You can set these variables from the command line. +SPHINXOPTS = +SPHINXBUILD = sphinx-build +PAPER = +BUILDDIR = build + +# Internal variables. +PAPEROPT_a4 = -D latex_paper_size=a4 +PAPEROPT_letter = -D latex_paper_size=letter +ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source + +.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest + +help: + @echo "Please use \`make ' where is one of" + @echo " html to make standalone HTML files" + @echo " dirhtml to make HTML files named index.html in directories" + @echo " singlehtml to make a single large HTML file" + @echo " pickle to make pickle files" + @echo " json to make JSON files" + @echo " htmlhelp to make HTML files and a HTML help project" + @echo " qthelp to make HTML files and a qthelp project" + @echo " devhelp to make HTML files and a Devhelp project" + @echo " epub to make an epub" + @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" + @echo " latexpdf to make LaTeX files and run them through pdflatex" + @echo " text to make text files" + @echo " man to make manual pages" + @echo " changes to make an overview of all changed/added/deprecated items" + @echo " linkcheck to check all external links for integrity" + @echo " doctest to run all doctests embedded in the documentation (if enabled)" + +clean: + -rm -rf $(BUILDDIR)/* + +html: + $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html + @echo + @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." + +dirhtml: + $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml + @echo + @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." + +singlehtml: + $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml + @echo + @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." + +pickle: + $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle + @echo + @echo "Build finished; now you can process the pickle files." + +json: + $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json + @echo + @echo "Build finished; now you can process the JSON files." + +htmlhelp: + $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp + @echo + @echo "Build finished; now you can run HTML Help Workshop with the" \ + ".hhp project file in $(BUILDDIR)/htmlhelp." + +qthelp: + $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp + @echo + @echo "Build finished; now you can run "qcollectiongenerator" with the" \ + ".qhcp project file in $(BUILDDIR)/qthelp, like this:" + @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/template_class.qhcp" + @echo "To view the help file:" + @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/template_class.qhc" + +devhelp: + $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp + @echo + @echo "Build finished." + @echo "To view the help file:" + @echo "# mkdir -p $$HOME/.local/share/devhelp/template_class" + @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/template_class" + @echo "# devhelp" + +epub: + $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub + @echo + @echo "Build finished. The epub file is in $(BUILDDIR)/epub." + +latex: + $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex + @echo + @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." + @echo "Run \`make' in that directory to run these through (pdf)latex" \ + "(use \`make latexpdf' here to do that automatically)." + +latexpdf: + $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex + @echo "Running LaTeX files through pdflatex..." + make -C $(BUILDDIR)/latex all-pdf + @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." + +text: + $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text + @echo + @echo "Build finished. The text files are in $(BUILDDIR)/text." + +man: + $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man + @echo + @echo "Build finished. The manual pages are in $(BUILDDIR)/man." + +changes: + $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes + @echo + @echo "The overview file is in $(BUILDDIR)/changes." + +linkcheck: + $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck + @echo + @echo "Link check complete; look for any errors in the above output " \ + "or in $(BUILDDIR)/linkcheck/output.txt." + +doctest: + $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest + @echo "Testing of doctests in the sources finished, look at the " \ + "results in $(BUILDDIR)/doctest/output.txt." diff --git a/plot_layout/help/make.bat b/plot_layout/help/make.bat new file mode 100644 index 0000000..3377610 --- /dev/null +++ b/plot_layout/help/make.bat @@ -0,0 +1,155 @@ +@ECHO OFF + +REM Command file for Sphinx documentation + +if "%SPHINXBUILD%" == "" ( + set SPHINXBUILD=sphinx-build +) +set BUILDDIR=build +set ALLSPHINXOPTS=-d %BUILDDIR%/doctrees %SPHINXOPTS% source +if NOT "%PAPER%" == "" ( + set ALLSPHINXOPTS=-D latex_paper_size=%PAPER% %ALLSPHINXOPTS% +) + +if "%1" == "" goto help + +if "%1" == "help" ( + :help + echo.Please use `make ^` where ^ is one of + echo. html to make standalone HTML files + echo. dirhtml to make HTML files named index.html in directories + echo. singlehtml to make a single large HTML file + echo. pickle to make pickle files + echo. json to make JSON files + echo. htmlhelp to make HTML files and a HTML help project + echo. qthelp to make HTML files and a qthelp project + echo. devhelp to make HTML files and a Devhelp project + echo. epub to make an epub + echo. latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter + echo. text to make text files + echo. man to make manual pages + echo. changes to make an overview over all changed/added/deprecated items + echo. linkcheck to check all external links for integrity + echo. doctest to run all doctests embedded in the documentation if enabled + goto end +) + +if "%1" == "clean" ( + for /d %%i in (%BUILDDIR%\*) do rmdir /q /s %%i + del /q /s %BUILDDIR%\* + goto end +) + +if "%1" == "html" ( + %SPHINXBUILD% -b html %ALLSPHINXOPTS% %BUILDDIR%/html + echo. + echo.Build finished. The HTML pages are in %BUILDDIR%/html. + goto end +) + +if "%1" == "dirhtml" ( + %SPHINXBUILD% -b dirhtml %ALLSPHINXOPTS% %BUILDDIR%/dirhtml + echo. + echo.Build finished. The HTML pages are in %BUILDDIR%/dirhtml. + goto end +) + +if "%1" == "singlehtml" ( + %SPHINXBUILD% -b singlehtml %ALLSPHINXOPTS% %BUILDDIR%/singlehtml + echo. + echo.Build finished. The HTML pages are in %BUILDDIR%/singlehtml. + goto end +) + +if "%1" == "pickle" ( + %SPHINXBUILD% -b pickle %ALLSPHINXOPTS% %BUILDDIR%/pickle + echo. + echo.Build finished; now you can process the pickle files. + goto end +) + +if "%1" == "json" ( + %SPHINXBUILD% -b json %ALLSPHINXOPTS% %BUILDDIR%/json + echo. + echo.Build finished; now you can process the JSON files. + goto end +) + +if "%1" == "htmlhelp" ( + %SPHINXBUILD% -b htmlhelp %ALLSPHINXOPTS% %BUILDDIR%/htmlhelp + echo. + echo.Build finished; now you can run HTML Help Workshop with the ^ +.hhp project file in %BUILDDIR%/htmlhelp. + goto end +) + +if "%1" == "qthelp" ( + %SPHINXBUILD% -b qthelp %ALLSPHINXOPTS% %BUILDDIR%/qthelp + echo. + echo.Build finished; now you can run "qcollectiongenerator" with the ^ +.qhcp project file in %BUILDDIR%/qthelp, like this: + echo.^> qcollectiongenerator %BUILDDIR%\qthelp\template_class.qhcp + echo.To view the help file: + echo.^> assistant -collectionFile %BUILDDIR%\qthelp\template_class.ghc + goto end +) + +if "%1" == "devhelp" ( + %SPHINXBUILD% -b devhelp %ALLSPHINXOPTS% %BUILDDIR%/devhelp + echo. + echo.Build finished. + goto end +) + +if "%1" == "epub" ( + %SPHINXBUILD% -b epub %ALLSPHINXOPTS% %BUILDDIR%/epub + echo. + echo.Build finished. The epub file is in %BUILDDIR%/epub. + goto end +) + +if "%1" == "latex" ( + %SPHINXBUILD% -b latex %ALLSPHINXOPTS% %BUILDDIR%/latex + echo. + echo.Build finished; the LaTeX files are in %BUILDDIR%/latex. + goto end +) + +if "%1" == "text" ( + %SPHINXBUILD% -b text %ALLSPHINXOPTS% %BUILDDIR%/text + echo. + echo.Build finished. The text files are in %BUILDDIR%/text. + goto end +) + +if "%1" == "man" ( + %SPHINXBUILD% -b man %ALLSPHINXOPTS% %BUILDDIR%/man + echo. + echo.Build finished. The manual pages are in %BUILDDIR%/man. + goto end +) + +if "%1" == "changes" ( + %SPHINXBUILD% -b changes %ALLSPHINXOPTS% %BUILDDIR%/changes + echo. + echo.The overview file is in %BUILDDIR%/changes. + goto end +) + +if "%1" == "linkcheck" ( + %SPHINXBUILD% -b linkcheck %ALLSPHINXOPTS% %BUILDDIR%/linkcheck + echo. + echo.Link check complete; look for any errors in the above output ^ +or in %BUILDDIR%/linkcheck/output.txt. + goto end +) + +if "%1" == "doctest" ( + %SPHINXBUILD% -b doctest %ALLSPHINXOPTS% %BUILDDIR%/doctest + echo. + echo.Testing of doctests in the sources finished, look at the ^ +results in %BUILDDIR%/doctest/output.txt. + goto end +) + +:end diff --git a/plot_layout/help/source/conf.py b/plot_layout/help/source/conf.py new file mode 100644 index 0000000..46fa40e --- /dev/null +++ b/plot_layout/help/source/conf.py @@ -0,0 +1,216 @@ +# -*- coding: utf-8 -*- +# +# PlotInLayout documentation build configuration file, created by +# sphinx-quickstart on Sun Feb 12 17:11:03 2012. +# +# This file is execfile()d with the current directory set to its containing dir. +# +# Note that not all possible configuration values are present in this +# autogenerated file. +# +# All configuration values have a default; values that are commented out +# serve to show the default. + +import sys, os + +# If extensions (or modules to document with autodoc) are in another directory, +# add these directories to sys.path here. If the directory is relative to the +# documentation root, use os.path.abspath to make it absolute, like shown here. +#sys.path.insert(0, os.path.abspath('.')) + +# -- General configuration ----------------------------------------------------- + +# If your documentation needs a minimal Sphinx version, state it here. +#needs_sphinx = '1.0' + +# Add any Sphinx extension module names here, as strings. They can be extensions +# coming with Sphinx (named 'sphinx.ext.*') or your custom ones. +extensions = ['sphinx.ext.todo', 'sphinx.ext.imgmath', 'sphinx.ext.viewcode'] + +# Add any paths that contain templates here, relative to this directory. +templates_path = ['_templates'] + +# The suffix of source filenames. +source_suffix = '.rst' + +# The encoding of source files. +#source_encoding = 'utf-8-sig' + +# The master toctree document. +master_doc = 'index' + +# General information about the project. +project = u'PlotInLayout' +copyright = u'2013, Champs-Libres' + +# The version info for the project you're documenting, acts as replacement for +# |version| and |release|, also used in various other places throughout the +# built documents. +# +# The short X.Y version. +version = '0.1' +# The full version, including alpha/beta/rc tags. +release = '0.1' + +# The language for content autogenerated by Sphinx. Refer to documentation +# for a list of supported languages. +#language = None + +# There are two options for replacing |today|: either, you set today to some +# non-false value, then it is used: +#today = '' +# Else, today_fmt is used as the format for a strftime call. +#today_fmt = '%B %d, %Y' + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +exclude_patterns = [] + +# The reST default role (used for this markup: `text`) to use for all documents. +#default_role = None + +# If true, '()' will be appended to :func: etc. cross-reference text. +#add_function_parentheses = True + +# If true, the current module name will be prepended to all description +# unit titles (such as .. function::). +#add_TemplateModuleNames = True + +# If true, sectionauthor and moduleauthor directives will be shown in the +# output. They are ignored by default. +#show_authors = False + +# The name of the Pygments (syntax highlighting) style to use. +pygments_style = 'sphinx' + +# A list of ignored prefixes for module index sorting. +#modindex_common_prefix = [] + + +# -- Options for HTML output --------------------------------------------------- + +# The theme to use for HTML and HTML Help pages. See the documentation for +# a list of builtin themes. +html_theme = 'default' + +# Theme options are theme-specific and customize the look and feel of a theme +# further. For a list of options available for each theme, see the +# documentation. +#html_theme_options = {} + +# Add any paths that contain custom themes here, relative to this directory. +#html_theme_path = [] + +# The name for this set of Sphinx documents. If None, it defaults to +# " v documentation". +#html_title = None + +# A shorter title for the navigation bar. Default is the same as html_title. +#html_short_title = None + +# The name of an image file (relative to this directory) to place at the top +# of the sidebar. +#html_logo = None + +# The name of an image file (within the static path) to use as favicon of the +# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 +# pixels large. +#html_favicon = None + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +html_static_path = ['_static'] + +# If not '', a 'Last updated on:' timestamp is inserted at every page bottom, +# using the given strftime format. +#html_last_updated_fmt = '%b %d, %Y' + +# If true, SmartyPants will be used to convert quotes and dashes to +# typographically correct entities. +#html_use_smartypants = True + +# Custom sidebar templates, maps document names to template names. +#html_sidebars = {} + +# Additional templates that should be rendered to pages, maps page names to +# template names. +#html_additional_pages = {} + +# If false, no module index is generated. +#html_domain_indices = True + +# If false, no index is generated. +#html_use_index = True + +# If true, the index is split into individual pages for each letter. +#html_split_index = False + +# If true, links to the reST sources are added to the pages. +#html_show_sourcelink = True + +# If true, "Created using Sphinx" is shown in the HTML footer. Default is True. +#html_show_sphinx = True + +# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. +#html_show_copyright = True + +# If true, an OpenSearch description file will be output, and all pages will +# contain a tag referring to it. The value of this option must be the +# base URL from which the finished HTML is served. +#html_use_opensearch = '' + +# This is the file name suffix for HTML files (e.g. ".xhtml"). +#html_file_suffix = None + +# Output file base name for HTML help builder. +htmlhelp_basename = 'TemplateClassdoc' + + +# -- Options for LaTeX output -------------------------------------------------- + +# The paper size ('letter' or 'a4'). +#latex_paper_size = 'letter' + +# The font size ('10pt', '11pt' or '12pt'). +#latex_font_size = '10pt' + +# Grouping the document tree into LaTeX files. List of tuples +# (source start file, target name, title, author, documentclass [howto/manual]). +latex_documents = [ + ('index', 'PlotInLayout.tex', u'PlotInLayout Documentation', + u'Champs-Libres', 'manual'), +] + +# The name of an image file (relative to this directory) to place at the top of +# the title page. +#latex_logo = None + +# For "manual" documents, if this is true, then toplevel headings are parts, +# not chapters. +#latex_use_parts = False + +# If true, show page references after internal links. +#latex_show_pagerefs = False + +# If true, show URL addresses after external links. +#latex_show_urls = False + +# Additional stuff for the LaTeX preamble. +#latex_preamble = '' + +# Documents to append as an appendix to all manuals. +#latex_appendices = [] + +# If false, no module index is generated. +#latex_domain_indices = True + + +# -- Options for manual page output -------------------------------------------- + +# One entry per manual page. List of tuples +# (source start file, name, description, authors, manual section). +man_pages = [ + ('index', 'TemplateClass', u'PlotInLayout Documentation', + [u'Champs-Libres'], 1) +] diff --git a/plot_layout/help/source/index.rst b/plot_layout/help/source/index.rst new file mode 100644 index 0000000..5e98497 --- /dev/null +++ b/plot_layout/help/source/index.rst @@ -0,0 +1,20 @@ +.. PlotInLayout documentation master file, created by + sphinx-quickstart on Sun Feb 12 17:11:03 2012. + You can adapt this file completely to your liking, but it should at least + contain the root `toctree` directive. + +Welcome to PlotInLayout's documentation! +============================================ + +Contents: + +.. toctree:: + :maxdepth: 2 + +Indices and tables +================== + +* :ref:`genindex` +* :ref:`modindex` +* :ref:`search` + diff --git a/plot_layout/i18n/af.ts b/plot_layout/i18n/af.ts new file mode 100644 index 0000000..615a88c --- /dev/null +++ b/plot_layout/i18n/af.ts @@ -0,0 +1,11 @@ + + + + @default + + + Good morning + Goeie more + + + diff --git a/plot_layout/icon.png b/plot_layout/icon.png new file mode 100644 index 0000000..f696c00 Binary files /dev/null and b/plot_layout/icon.png differ diff --git a/plot_layout/layouts/__pycache__/layout.cpython-38.pyc b/plot_layout/layouts/__pycache__/layout.cpython-38.pyc new file mode 100644 index 0000000..b61897f Binary files /dev/null and b/plot_layout/layouts/__pycache__/layout.cpython-38.pyc differ diff --git a/plot_layout/layouts/layout.py b/plot_layout/layouts/layout.py new file mode 100644 index 0000000..b48682a --- /dev/null +++ b/plot_layout/layouts/layout.py @@ -0,0 +1,53 @@ +from qgis.core import ( + QgsLayoutItem, + QgsLayoutItemRegistry, + QgsLayoutItemAbstractMetadata, + QgsNetworkAccessManager, + QgsMessageLog, + QgsGeometry +) + + +ITEM_TYPE = QgsLayoutItemRegistry.PluginItem + 1338 + + +class PlotLayoutItem(QgsLayoutItem): + + def __init__(self, layout): + super().__init__(layout) + # self.setCacheMode(QGraphicsItem.NoCache) + # self.plot_settings = [] + # self.plot_settings.append(PlotSettings()) + # self.linked_map_uuid = '' + # self.linked_map = None + + # self.filter_by_map = False + # self.filter_by_atlas = False + + # 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.html_units_to_layout_units = self.calculate_html_units_to_layout_units() + + # self.sizePositionChanged.connect(self.refresh) + + def type(self): + return ITEM_TYPE + +class PlotLayoutItemMetadata(QgsLayoutItemAbstractMetadata): + + def __init__(self): + super().__init__(ITEM_TYPE, 'test') + + def createItem(self, layout): + return PlotLayoutItem(layout) \ No newline at end of file diff --git a/plot_layout/metadata.txt b/plot_layout/metadata.txt new file mode 100644 index 0000000..c9e7cad --- /dev/null +++ b/plot_layout/metadata.txt @@ -0,0 +1,47 @@ +# This file contains metadata for your plugin. + +# This file should be included when you package your plugin.# Mandatory items: + +[general] +name=PlotInLayout +qgisMinimumVersion=3.0 +description=test for adding plot in layout +version=0.1 +author=Champs-Libres +email=julien.minet@champs-libres.coop + +about=test for setting a button in layout + +tracker=http://bugs +repository=http://repo +# End of mandatory metadata + +# Recommended items: + +hasProcessingProvider=no +# Uncomment the following line and add your changelog: +# changelog= + +# Tags are comma separated with spaces allowed +tags=python + +homepage=http://homepage +category=Plugins +icon=icon.png +# experimental flag +experimental=True + +# deprecated flag (applies to the whole plugin, not just a single version) +deprecated=False + +# Since QGIS 3.8, a comma separated list of plugins to be installed +# (or upgraded) can be specified. +# Check the documentation for more information. +# plugin_dependencies= + +Category of the plugin: Raster, Vector, Database or Web +# category= + +# If the plugin can run on QGIS Server. +server=False + diff --git a/plot_layout/pb_tool.cfg b/plot_layout/pb_tool.cfg new file mode 100644 index 0000000..27291c5 --- /dev/null +++ b/plot_layout/pb_tool.cfg @@ -0,0 +1,80 @@ +#/*************************************************************************** +# PlotInLayout +# +# Configuration file for plugin builder tool (pb_tool) +# Generated by Plugin Builder: http://g-sherman.github.io/Qgis-Plugin-Builder/ +# ------------------- +# begin : 2021-07-13 +# copyright : (C) 2021 by Champs-Libres +# email : julien.minet@champs-libres.coop +# ***************************************************************************/ +# +#/*************************************************************************** +# * * +# * This program is free software; you can redistribute it and/or modify * +# * it under the terms of the GNU General Public License as published by * +# * the Free Software Foundation; either version 2 of the License, or * +# * (at your option) any later version. * +# * * +# ***************************************************************************/ +# +# +# You can install pb_tool using: +# pip install http://geoapt.net/files/pb_tool.zip +# +# Consider doing your development (and install of pb_tool) in a virtualenv. +# +# For details on setting up and using pb_tool, see: +# http://g-sherman.github.io/plugin_build_tool/ +# +# Issues and pull requests here: +# https://github.com/g-sherman/plugin_build_tool: +# +# Sane defaults for your plugin generated by the Plugin Builder are +# already set below. +# +# As you add Python source files and UI files to your plugin, add +# them to the appropriate [files] section below. + +[plugin] +# Name of the plugin. This is the name of the directory that will +# be created in .qgis2/python/plugins +name: plot_layout + +# Full path to where you want your plugin directory copied. If empty, +# the QGIS default path will be used. Don't include the plugin name in +# the path. +plugin_path: + +[files] +# Python files that should be deployed with the plugin +python_files: __init__.py plot_layout.py plot_layout_dialog.py + +# The main dialog file that is loaded (not compiled) +main_dialog: plot_layout_dialog_base.ui + +# Other ui files for dialogs you create (these will be compiled) +compiled_ui_files: + +# Resource file(s) that will be compiled +resource_files: resources.qrc + +# Other files required for the plugin +extras: metadata.txt icon.png + +# Other directories to be deployed with the plugin. +# These must be subdirectories under the plugin directory +extra_dirs: + +# ISO code(s) for any locales (translations), separated by spaces. +# Corresponding .ts files must exist in the i18n directory +locales: + +[help] +# the built help directory that should be deployed with the plugin +dir: help/build/html +# the name of the directory to target in the deployed plugin +target: help + + + diff --git a/plot_layout/plot_layout.py b/plot_layout/plot_layout.py new file mode 100644 index 0000000..3692657 --- /dev/null +++ b/plot_layout/plot_layout.py @@ -0,0 +1,215 @@ +# -*- coding: utf-8 -*- +""" +/*************************************************************************** + PlotInLayout + A QGIS plugin + test for adding plot in layout + Generated by Plugin Builder: http://g-sherman.github.io/Qgis-Plugin-Builder/ + ------------------- + begin : 2021-07-13 + git sha : $Format:%H$ + copyright : (C) 2021 by Champs-Libres + email : julien.minet@champs-libres.coop + ***************************************************************************/ + +/*************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ +""" +from qgis.PyQt.QtCore import QSettings, QTranslator, QCoreApplication +from qgis.PyQt.QtGui import QIcon +from qgis.PyQt.QtWidgets import QAction +from qgis.core import QgsApplication +from qgis.gui import QgsGui + + +# Initialize Qt resources from file resources.py +from .resources import * +# Import the code for the dialog +from .plot_layout_dialog import PlotInLayoutDialog +import os.path + +# import layout classes +from .layouts.layout import PlotLayoutItemMetadata +from .gui.gui import PlotLayoutItemGuiMetadata + +class PlotInLayout: + """QGIS Plugin Implementation.""" + + def __init__(self, iface): + """Constructor. + + :param iface: An interface instance that will be passed to this class + which provides the hook by which you can manipulate the QGIS + application at run time. + :type iface: QgsInterface + """ + # Save reference to the QGIS interface + self.iface = iface + # initialize plugin directory + self.plugin_dir = os.path.dirname(__file__) + # initialize locale + locale = QSettings().value('locale/userLocale')[0:2] + locale_path = os.path.join( + self.plugin_dir, + 'i18n', + 'PlotInLayout_{}.qm'.format(locale)) + + if os.path.exists(locale_path): + self.translator = QTranslator() + self.translator.load(locale_path) + QCoreApplication.installTranslator(self.translator) + + # Declare instance attributes + self.actions = [] + self.menu = self.tr(u'&PlotInLayout') + + # Check if plugin was started the first time in current QGIS session + # Must be set in initGui() to survive plugin reloads + self.first_start = None + + self.plot_item_metadata = PlotLayoutItemMetadata() + self.plot_item_gui_metadata = None + QgsApplication.layoutItemRegistry().addLayoutItemType(self.plot_item_metadata) + + print('INIT') + + # noinspection PyMethodMayBeStatic + def tr(self, message): + """Get the translation for a string using Qt translation API. + + We implement this ourselves since we do not inherit QObject. + + :param message: String for translation. + :type message: str, QString + + :returns: Translated version of message. + :rtype: QString + """ + # noinspection PyTypeChecker,PyArgumentList,PyCallByClass + return QCoreApplication.translate('PlotInLayout', message) + + + def add_action( + self, + icon_path, + text, + callback, + enabled_flag=True, + add_to_menu=True, + add_to_toolbar=True, + status_tip=None, + whats_this=None, + parent=None): + """Add a toolbar icon to the toolbar. + + :param icon_path: Path to the icon for this action. Can be a resource + path (e.g. ':/plugins/foo/bar.png') or a normal file system path. + :type icon_path: str + + :param text: Text that should be shown in menu items for this action. + :type text: str + + :param callback: Function to be called when the action is triggered. + :type callback: function + + :param enabled_flag: A flag indicating if the action should be enabled + by default. Defaults to True. + :type enabled_flag: bool + + :param add_to_menu: Flag indicating whether the action should also + be added to the menu. Defaults to True. + :type add_to_menu: bool + + :param add_to_toolbar: Flag indicating whether the action should also + be added to the toolbar. Defaults to True. + :type add_to_toolbar: bool + + :param status_tip: Optional text to show in a popup when mouse pointer + hovers over the action. + :type status_tip: str + + :param parent: Parent widget for the new action. Defaults None. + :type parent: QWidget + + :param whats_this: Optional text to show in the status bar when the + mouse pointer hovers over the action. + + :returns: The action that was created. Note that the action is also + added to self.actions list. + :rtype: QAction + """ + + icon = QIcon(icon_path) + action = QAction(icon, text, parent) + action.triggered.connect(callback) + action.setEnabled(enabled_flag) + + if status_tip is not None: + action.setStatusTip(status_tip) + + if whats_this is not None: + action.setWhatsThis(whats_this) + + if add_to_toolbar: + # Adds plugin icon to Plugins toolbar + self.iface.addToolBarIcon(action) + + if add_to_menu: + self.iface.addPluginToMenu( + self.menu, + action) + + self.actions.append(action) + + return action + + def initGui(self): + """Create the menu entries and toolbar icons inside the QGIS GUI.""" + + icon_path = ':/plugins/plot_layout/icon.png' + self.add_action( + icon_path, + text=self.tr(u''), + callback=self.run, + parent=self.iface.mainWindow()) + + # will be set False in run() + self.first_start = True + + # Add layout gui utils + self.plot_item_gui_metadata = PlotLayoutItemGuiMetadata() + QgsGui.layoutItemGuiRegistry().addLayoutItemGuiMetadata(self.plot_item_gui_metadata) + + def unload(self): + """Removes the plugin menu item and icon from QGIS GUI.""" + for action in self.actions: + self.iface.removePluginMenu( + self.tr(u'&PlotInLayout'), + action) + self.iface.removeToolBarIcon(action) + + + def run(self): + """Run method that performs all the real work""" + + # Create the dialog with elements (after translation) and keep reference + # Only create GUI ONCE in callback, so that it will only load when the plugin is started + if self.first_start == True: + self.first_start = False + self.dlg = PlotInLayoutDialog() + + # show the dialog + self.dlg.show() + # Run the dialog event loop + result = self.dlg.exec_() + # See if OK was pressed + if result: + # Do something useful here - delete the line containing pass and + # substitute with your code. + pass diff --git a/plot_layout/plot_layout_dialog.py b/plot_layout/plot_layout_dialog.py new file mode 100644 index 0000000..a85cc1a --- /dev/null +++ b/plot_layout/plot_layout_dialog.py @@ -0,0 +1,44 @@ +# -*- coding: utf-8 -*- +""" +/*************************************************************************** + PlotInLayoutDialog + A QGIS plugin + test for adding plot in layout + Generated by Plugin Builder: http://g-sherman.github.io/Qgis-Plugin-Builder/ + ------------------- + begin : 2021-07-13 + git sha : $Format:%H$ + copyright : (C) 2021 by Champs-Libres + email : julien.minet@champs-libres.coop + ***************************************************************************/ + +/*************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ +""" + +import os + +from qgis.PyQt import uic +from qgis.PyQt import QtWidgets + +# This loads your .ui file so that PyQt can populate your plugin with the elements from Qt Designer +FORM_CLASS, _ = uic.loadUiType(os.path.join( + os.path.dirname(__file__), 'plot_layout_dialog_base.ui')) + + +class PlotInLayoutDialog(QtWidgets.QDialog, FORM_CLASS): + def __init__(self, parent=None): + """Constructor.""" + super(PlotInLayoutDialog, self).__init__(parent) + # Set up the user interface from Designer through FORM_CLASS. + # After self.setupUi() you can access any designer object by doing + # self., and you can use autoconnect slots - see + # http://qt-project.org/doc/qt-4.8/designer-using-a-ui-file.html + # #widgets-and-dialogs-with-auto-connect + self.setupUi(self) diff --git a/plot_layout/plot_layout_dialog_base.ui b/plot_layout/plot_layout_dialog_base.ui new file mode 100644 index 0000000..d95bca6 --- /dev/null +++ b/plot_layout/plot_layout_dialog_base.ui @@ -0,0 +1,67 @@ + + PlotInLayoutDialogBase + + + + 0 + 0 + 400 + 300 + + + + PlotInLayout + + + + + 30 + 240 + 341 + 32 + + + + Qt::Horizontal + + + QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + button_box + accepted() + PlotInLayoutDialogBase + accept() + + + 248 + 254 + + + 157 + 274 + + + + + button_box + rejected() + PlotInLayoutDialogBase + reject() + + + 316 + 260 + + + 286 + 274 + + + + + diff --git a/plot_layout/plugin_upload.py b/plot_layout/plugin_upload.py new file mode 100755 index 0000000..a88ea2b --- /dev/null +++ b/plot_layout/plugin_upload.py @@ -0,0 +1,111 @@ +#!/usr/bin/env python +# coding=utf-8 +"""This script uploads a plugin package to the plugin repository. + Authors: A. Pasotti, V. Picavet + git sha : $TemplateVCSFormat +""" + +import sys +import getpass +import xmlrpc.client +from optparse import OptionParser + +standard_library.install_aliases() + +# Configuration +PROTOCOL = 'https' +SERVER = 'plugins.qgis.org' +PORT = '443' +ENDPOINT = '/plugins/RPC2/' +VERBOSE = False + + +def main(parameters, arguments): + """Main entry point. + + :param parameters: Command line parameters. + :param arguments: Command line arguments. + """ + address = "{protocol}://{username}:{password}@{server}:{port}{endpoint}".format( + protocol=PROTOCOL, + username=parameters.username, + password=parameters.password, + server=parameters.server, + port=parameters.port, + endpoint=ENDPOINT) + print("Connecting to: %s" % hide_password(address)) + + server = xmlrpc.client.ServerProxy(address, verbose=VERBOSE) + + try: + with open(arguments[0], 'rb') as handle: + plugin_id, version_id = server.plugin.upload( + xmlrpc.client.Binary(handle.read())) + print("Plugin ID: %s" % plugin_id) + print("Version ID: %s" % version_id) + except xmlrpc.client.ProtocolError as err: + print("A protocol error occurred") + print("URL: %s" % hide_password(err.url, 0)) + print("HTTP/HTTPS headers: %s" % err.headers) + print("Error code: %d" % err.errcode) + print("Error message: %s" % err.errmsg) + except xmlrpc.client.Fault as err: + print("A fault occurred") + print("Fault code: %d" % err.faultCode) + print("Fault string: %s" % err.faultString) + + +def hide_password(url, start=6): + """Returns the http url with password part replaced with '*'. + + :param url: URL to upload the plugin to. + :type url: str + + :param start: Position of start of password. + :type start: int + """ + start_position = url.find(':', start) + 1 + end_position = url.find('@') + return "%s%s%s" % ( + url[:start_position], + '*' * (end_position - start_position), + url[end_position:]) + + +if __name__ == "__main__": + parser = OptionParser(usage="%prog [options] plugin.zip") + parser.add_option( + "-w", "--password", dest="password", + help="Password for plugin site", metavar="******") + parser.add_option( + "-u", "--username", dest="username", + help="Username of plugin site", metavar="user") + parser.add_option( + "-p", "--port", dest="port", + help="Server port to connect to", metavar="80") + parser.add_option( + "-s", "--server", dest="server", + help="Specify server name", metavar="plugins.qgis.org") + options, args = parser.parse_args() + if len(args) != 1: + print("Please specify zip file.\n") + parser.print_help() + sys.exit(1) + if not options.server: + options.server = SERVER + if not options.port: + options.port = PORT + if not options.username: + # interactive mode + username = getpass.getuser() + print("Please enter user name [%s] :" % username, end=' ') + + res = input() + if res != "": + options.username = res + else: + options.username = username + if not options.password: + # interactive mode + options.password = getpass.getpass() + main(options, args) diff --git a/plot_layout/pylintrc b/plot_layout/pylintrc new file mode 100644 index 0000000..7e168f6 --- /dev/null +++ b/plot_layout/pylintrc @@ -0,0 +1,281 @@ +[MASTER] + +# Specify a configuration file. +#rcfile= + +# Python code to execute, usually for sys.path manipulation such as +# pygtk.require(). +#init-hook= + +# Profiled execution. +profile=no + +# Add files or directories to the blacklist. They should be base names, not +# paths. +ignore=CVS + +# Pickle collected data for later comparisons. +persistent=yes + +# List of plugins (as comma separated values of python modules names) to load, +# usually to register additional checkers. +load-plugins= + + +[MESSAGES CONTROL] + +# Enable the message, report, category or checker with the given id(s). You can +# either give multiple identifier separated by comma (,) or put this option +# multiple time. See also the "--disable" option for examples. +#enable= + +# Disable the message, report, category or checker with the given id(s). You +# can either give multiple identifiers separated by comma (,) or put this +# option multiple times (only on the command line, not in the configuration +# file where it should appear only once).You can also use "--disable=all" to +# disable everything first and then reenable specific checks. For example, if +# you want to run only the similarities checker, you can use "--disable=all +# --enable=similarities". If you want to run only the classes checker, but have +# no Warning level messages displayed, use"--disable=all --enable=classes +# --disable=W" +# see http://stackoverflow.com/questions/21487025/pylint-locally-defined-disables-still-give-warnings-how-to-suppress-them +disable=locally-disabled,C0103 + + +[REPORTS] + +# Set the output format. Available formats are text, parseable, colorized, msvs +# (visual studio) and html. You can also give a reporter class, eg +# mypackage.mymodule.MyReporterClass. +output-format=text + +# Put messages in a separate file for each module / package specified on the +# command line instead of printing them on stdout. Reports (if any) will be +# written in a file name "pylint_global.[txt|html]". +files-output=no + +# Tells whether to display a full report or only the messages +reports=yes + +# Python expression which should return a note less than 10 (10 is the highest +# note). You have access to the variables errors warning, statement which +# respectively contain the number of errors / warnings messages and the total +# number of statements analyzed. This is used by the global evaluation report +# (RP0004). +evaluation=10.0 - ((float(5 * error + warning + refactor + convention) / statement) * 10) + +# Add a comment according to your evaluation note. This is used by the global +# evaluation report (RP0004). +comment=no + +# Template used to display messages. This is a python new-style format string +# used to format the message information. See doc for all details +#msg-template= + + +[BASIC] + +# Required attributes for module, separated by a comma +required-attributes= + +# List of builtins function names that should not be used, separated by a comma +bad-functions=map,filter,apply,input + +# Regular expression which should only match correct module names +module-rgx=(([a-z_][a-z0-9_]*)|([A-Z][a-zA-Z0-9]+))$ + +# Regular expression which should only match correct module level names +const-rgx=(([A-Z_][A-Z0-9_]*)|(__.*__))$ + +# Regular expression which should only match correct class names +class-rgx=[A-Z_][a-zA-Z0-9]+$ + +# Regular expression which should only match correct function names +function-rgx=[a-z_][a-z0-9_]{2,30}$ + +# Regular expression which should only match correct method names +method-rgx=[a-z_][a-z0-9_]{2,30}$ + +# Regular expression which should only match correct instance attribute names +attr-rgx=[a-z_][a-z0-9_]{2,30}$ + +# Regular expression which should only match correct argument names +argument-rgx=[a-z_][a-z0-9_]{2,30}$ + +# Regular expression which should only match correct variable names +variable-rgx=[a-z_][a-z0-9_]{2,30}$ + +# Regular expression which should only match correct attribute names in class +# bodies +class-attribute-rgx=([A-Za-z_][A-Za-z0-9_]{2,30}|(__.*__))$ + +# Regular expression which should only match correct list comprehension / +# generator expression variable names +inlinevar-rgx=[A-Za-z_][A-Za-z0-9_]*$ + +# Good variable names which should always be accepted, separated by a comma +good-names=i,j,k,ex,Run,_ + +# Bad variable names which should always be refused, separated by a comma +bad-names=foo,bar,baz,toto,tutu,tata + +# Regular expression which should only match function or class names that do +# not require a docstring. +no-docstring-rgx=__.*__ + +# Minimum line length for functions/classes that require docstrings, shorter +# ones are exempt. +docstring-min-length=-1 + + +[MISCELLANEOUS] + +# List of note tags to take in consideration, separated by a comma. +notes=FIXME,XXX,TODO + + +[TYPECHECK] + +# Tells whether missing members accessed in mixin class should be ignored. A +# mixin class is detected if its name ends with "mixin" (case insensitive). +ignore-mixin-members=yes + +# List of classes names for which member attributes should not be checked +# (useful for classes with attributes dynamically set). +ignored-classes=SQLObject + +# When zope mode is activated, add a predefined set of Zope acquired attributes +# to generated-members. +zope=no + +# List of members which are set dynamically and missed by pylint inference +# system, and so shouldn't trigger E0201 when accessed. Python regular +# expressions are accepted. +generated-members=REQUEST,acl_users,aq_parent + + +[VARIABLES] + +# Tells whether we should check for unused import in __init__ files. +init-import=no + +# A regular expression matching the beginning of the name of dummy variables +# (i.e. not used). +dummy-variables-rgx=_$|dummy + +# List of additional names supposed to be defined in builtins. Remember that +# you should avoid to define new builtins when possible. +additional-builtins= + + +[FORMAT] + +# Maximum number of characters on a single line. +max-line-length=80 + +# Regexp for a line that is allowed to be longer than the limit. +ignore-long-lines=^\s*(# )??$ + +# Allow the body of an if to be on the same line as the test if there is no +# else. +single-line-if-stmt=no + +# List of optional constructs for which whitespace checking is disabled +no-space-check=trailing-comma,dict-separator + +# Maximum number of lines in a module +max-module-lines=1000 + +# String used as indentation unit. This is usually " " (4 spaces) or "\t" (1 +# tab). +indent-string=' ' + + +[SIMILARITIES] + +# Minimum lines number of a similarity. +min-similarity-lines=4 + +# Ignore comments when computing similarities. +ignore-comments=yes + +# Ignore docstrings when computing similarities. +ignore-docstrings=yes + +# Ignore imports when computing similarities. +ignore-imports=no + + +[IMPORTS] + +# Deprecated modules which should not be used, separated by a comma +deprecated-modules=regsub,TERMIOS,Bastion,rexec + +# Create a graph of every (i.e. internal and external) dependencies in the +# given file (report RP0402 must not be disabled) +import-graph= + +# Create a graph of external dependencies in the given file (report RP0402 must +# not be disabled) +ext-import-graph= + +# Create a graph of internal dependencies in the given file (report RP0402 must +# not be disabled) +int-import-graph= + + +[DESIGN] + +# Maximum number of arguments for function / method +max-args=5 + +# Argument names that match this expression will be ignored. Default to name +# with leading underscore +ignored-argument-names=_.* + +# Maximum number of locals for function / method body +max-locals=15 + +# Maximum number of return / yield for function / method body +max-returns=6 + +# Maximum number of branch for function / method body +max-branches=12 + +# Maximum number of statements in function / method body +max-statements=50 + +# Maximum number of parents for a class (see R0901). +max-parents=7 + +# Maximum number of attributes for a class (see R0902). +max-attributes=7 + +# Minimum number of public methods for a class (see R0903). +min-public-methods=2 + +# Maximum number of public methods for a class (see R0904). +max-public-methods=20 + + +[CLASSES] + +# List of interface methods to ignore, separated by a comma. This is used for +# instance to not check methods defines in Zope's Interface base class. +ignore-iface-methods=isImplementedBy,deferred,extends,names,namesAndDescriptions,queryDescriptionFor,getBases,getDescriptionFor,getDoc,getName,getTaggedValue,getTaggedValueTags,isEqualOrExtendedBy,setTaggedValue,isImplementedByInstancesOf,adaptWith,is_implemented_by + +# List of method names used to declare (i.e. assign) instance attributes. +defining-attr-methods=__init__,__new__,setUp + +# List of valid names for the first argument in a class method. +valid-classmethod-first-arg=cls + +# List of valid names for the first argument in a metaclass class method. +valid-metaclass-classmethod-first-arg=mcs + + +[EXCEPTIONS] + +# Exceptions that will emit a warning when being caught. Defaults to +# "Exception" +overgeneral-exceptions=Exception diff --git a/plot_layout/resources.py b/plot_layout/resources.py new file mode 100644 index 0000000..c2c99a6 --- /dev/null +++ b/plot_layout/resources.py @@ -0,0 +1,128 @@ +# -*- coding: utf-8 -*- + +# Resource object code +# +# Created by: The Resource Compiler for PyQt5 (Qt v5.12.8) +# +# WARNING! All changes made in this file will be lost! + +from PyQt5 import QtCore + +qt_resource_data = b"\ +\x00\x00\x04\x0a\ +\x89\ +\x50\x4e\x47\x0d\x0a\x1a\x0a\x00\x00\x00\x0d\x49\x48\x44\x52\x00\ +\x00\x00\x17\x00\x00\x00\x18\x08\x06\x00\x00\x00\x11\x7c\x66\x75\ +\x00\x00\x00\x01\x73\x52\x47\x42\x00\xae\xce\x1c\xe9\x00\x00\x00\ +\x06\x62\x4b\x47\x44\x00\xff\x00\xff\x00\xff\xa0\xbd\xa7\x93\x00\ +\x00\x00\x09\x70\x48\x59\x73\x00\x00\x0b\x13\x00\x00\x0b\x13\x01\ +\x00\x9a\x9c\x18\x00\x00\x00\x07\x74\x49\x4d\x45\x07\xd9\x02\x15\ +\x16\x11\x2c\x9d\x48\x83\xbb\x00\x00\x03\x8a\x49\x44\x41\x54\x48\ +\xc7\xad\x95\x4b\x68\x5c\x55\x18\xc7\x7f\xe7\xdc\x7b\x67\xe6\xce\ +\x4c\x66\x26\x49\xd3\x24\x26\xa6\xc6\xf8\x40\x21\xa5\x04\xb3\x28\ +\xda\x98\x20\xa5\x0b\xad\x55\xa8\x2b\xc5\x50\x1f\xa0\x6e\x34\x2b\ +\x45\x30\x14\x02\xba\x52\x69\x15\x17\x66\x63\x45\x97\x95\xa0\xad\ +\x0b\xfb\xc0\x06\x25\xb6\x71\x61\x12\x41\x50\xdb\x2a\x21\xd1\xe2\ +\x24\xf3\x9e\xc9\xcc\xbd\xe7\x1c\x17\x35\x43\x1e\x33\x21\xb6\xfd\ +\x56\x87\xf3\x9d\xfb\xfb\x1e\xf7\xff\x9d\x23\x8c\x31\x43\x95\xf4\ +\x85\x1e\x3f\x3b\x35\xac\xfd\xcc\x43\xdc\xa4\x49\x3b\xfe\x9d\x1d\ +\xdb\x7b\x22\x90\x78\xf8\xb2\x28\xa7\xbe\x7d\xc1\x4b\x9d\x79\xdf\ +\x18\x15\xe5\x16\x99\x10\x56\xde\x69\xdc\x3f\x22\xfd\xec\xd4\xf0\ +\xad\x04\x03\x18\xa3\xa2\x7e\x76\x6a\x58\xde\x68\x2b\xb4\x36\xf8\ +\xbe\xc6\x18\x53\xdb\xef\xe7\xfa\xec\xed\x67\x63\x10\x42\x00\xf0\ +\xfb\xd5\x65\x2a\x15\x45\xc7\x6d\x0d\x00\xc4\xa2\xc1\xaa\x6f\x0d\ +\x3e\x6c\xab\xc2\x1c\x56\xa4\x77\x4b\xb0\xf2\x35\x15\x5f\x21\x85\ +\xe0\xc8\x6b\x5f\x92\x2d\x37\x33\x39\xf9\x03\x27\x8e\x1f\xa2\xf7\ +\xbe\x9d\x04\x1c\x0b\x37\xe4\xac\xff\xa6\x30\x87\xbd\xba\x00\x6a\ +\x06\x79\xe5\xf5\xaf\x89\xd9\x92\xc5\xcc\x0a\xd9\x7c\x19\xcf\xe9\ +\xe2\xe4\xa9\x2f\x78\x7c\xff\x01\x72\x85\x0a\x2b\x65\x1f\xa5\x4c\ +\xb5\xb2\x55\x16\x80\xbd\x31\xda\xda\x20\x1f\x7d\x3e\xcd\xc2\xfd\ +\x59\xa6\x93\x39\x92\xd1\x22\xea\x9b\x16\xce\x9d\x3f\xce\xe0\x83\ +\x03\x24\x82\x59\x3a\xdb\x7b\x88\xc7\x82\x68\x63\x58\xc9\xcc\x62\ +\x8c\x21\x18\xb0\x6a\xc3\x37\x06\x49\x16\xff\x24\x6b\xa5\x49\xbb\ +\x25\xbc\xa2\xa6\x21\xbb\x40\x7f\xdf\x00\x83\xbd\x01\x8e\x3c\xd5\ +\x45\xd7\x8e\x6b\x9c\x9c\x98\x25\x1a\xb6\xe8\xbe\x3d\xc2\xdd\x77\ +\x44\x48\xc4\x1c\x22\xe1\xeb\x58\x59\xaf\xcf\xd3\x33\x29\x2e\x34\ +\x2d\x91\x93\x3e\xbe\x34\x78\x01\xc5\xe2\x61\xc5\xae\x72\x8e\x70\ +\xc8\xc2\x0d\x5a\xbc\xf5\xee\x2f\x9c\xfa\x3e\x86\x69\x7a\x8e\xcf\ +\x26\xe6\xf9\x63\xa1\x44\xa1\xa4\xd0\xda\x6c\x0d\x2f\x15\x7c\xb4\ +\x67\x28\x59\x0a\xcf\xd6\x54\xe2\x06\x13\x87\x2b\x6f\x68\xa6\x27\ +\xaf\x31\x32\x36\xc7\xb2\x7f\x17\xef\x7d\x7c\x8c\x33\x67\xcf\x12\ +\x70\x24\x4a\x69\xd6\x6a\x46\xd6\xd3\x70\x72\xa9\x82\x67\x34\x45\ +\xad\x28\xdb\x1a\x15\x34\x98\xff\x46\xed\xef\x37\x0d\x99\xbf\x4a\ +\x3c\x30\x38\xc0\xc8\x4b\xaf\x92\x5a\x9c\xe2\xe0\x23\x6d\x74\xb4\ +\xba\x84\x5d\x0b\x29\x45\x7d\xb8\x94\x82\x96\xb6\x10\xf3\xc5\x12\ +\x2a\xef\x53\x11\x1a\x63\xad\x3f\x93\x19\x85\xf1\xb1\x77\x58\x5a\ +\xf8\x99\x97\x9f\xe9\xa6\x75\x47\x90\xc6\xb8\x43\xd8\xb5\xb6\xce\ +\xfc\xfa\xfd\x00\xfb\x3e\xf4\xc8\x05\x35\xba\x5e\xeb\x46\x21\xf9\ +\xcf\x0a\xa9\x8c\x87\xe3\x48\xdc\x90\xb5\x6e\x98\x6a\xaa\x65\xf2\ +\x52\x92\x43\x2f\x5e\xc2\x8c\x02\x1a\x10\xf5\x07\xac\xc3\x75\x70\ +\x83\x92\x80\xb3\xf9\xd0\x26\xf8\x8f\xb3\x29\xc6\x3e\xb8\x8c\x19\ +\x35\x75\x6b\x7b\x7e\x3c\xca\x45\x0c\x7e\x49\x31\xf4\x58\x3b\xf7\ +\xf6\x34\x90\x88\x39\x04\x1c\x59\x1f\xfe\xdb\xd5\x3c\x5f\x9d\x4b\ +\x32\xfd\x44\xb2\xba\xd7\xfa\xb6\x60\xcf\xde\x16\xdc\x90\x45\x4c\ +\x4a\x2a\x9e\x62\xfe\x4e\xc5\xc8\xc1\x4e\xda\x76\x86\xe8\xe9\x0a\ +\xe3\xd8\x92\x58\xd4\xc6\xb2\x44\x6d\x78\x2a\x53\xe1\xca\x7c\x99\ +\x63\x5d\xbf\x56\x9d\xbd\x9f\x44\x18\x7a\xba\x95\x27\x0f\xb4\xd3\ +\xdc\x18\xc0\xf3\x0d\x52\x40\xd8\xb5\xb0\xa4\x20\x14\xb2\x70\x6c\ +\x81\x63\xcb\xaa\x42\xd6\xfd\xb7\xf4\xec\xa3\x06\xa0\x50\x52\xd8\ +\x4e\x1b\x7e\x4a\xd3\x31\xf9\x29\xcf\xfe\xd4\x49\x7f\x5f\x13\xfb\ +\xfa\x9b\x71\x43\x92\x58\xd4\x21\x18\x90\xac\xde\xb0\x42\x50\x13\ +\x58\x33\xf3\x88\x6b\xa1\xfd\x65\x96\xf2\x79\xc6\x43\x7b\xd8\x75\ +\x38\xcc\x3d\xdd\xd1\xaa\xcf\x71\xe4\xff\x7f\x91\x56\x33\xaf\xea\ +\x37\xe7\xa1\x94\x21\x16\xb5\xd1\x06\x2c\x29\x36\xf5\x72\x9b\x96\ +\x95\xc0\xc4\xda\x9d\x78\x83\x43\x53\x22\x80\x65\x09\x1c\xfb\x86\ +\xc1\x00\xe7\x25\x70\x14\x48\x6f\x1e\x22\x51\xe3\x75\xd9\xb6\xa5\ +\x81\xa3\x32\xb1\xfb\xf4\x0c\x30\xb8\xb1\x82\x9b\xb0\x09\x60\x30\ +\xb1\xfb\xf4\xcc\xbf\xa0\xe9\x6e\xae\x5a\xdf\x4b\x81\x00\x00\x00\ +\x00\x49\x45\x4e\x44\xae\x42\x60\x82\ +" + +qt_resource_name = b"\ +\x00\x07\ +\x07\x3b\xe0\xb3\ +\x00\x70\ +\x00\x6c\x00\x75\x00\x67\x00\x69\x00\x6e\x00\x73\ +\x00\x0b\ +\x06\xc6\xab\x84\ +\x00\x70\ +\x00\x6c\x00\x6f\x00\x74\x00\x5f\x00\x6c\x00\x61\x00\x79\x00\x6f\x00\x75\x00\x74\ +\x00\x08\ +\x0a\x61\x5a\xa7\ +\x00\x69\ +\x00\x63\x00\x6f\x00\x6e\x00\x2e\x00\x70\x00\x6e\x00\x67\ +" + +qt_resource_struct_v1 = b"\ +\x00\x00\x00\x00\x00\x02\x00\x00\x00\x01\x00\x00\x00\x01\ +\x00\x00\x00\x00\x00\x02\x00\x00\x00\x01\x00\x00\x00\x02\ +\x00\x00\x00\x14\x00\x02\x00\x00\x00\x01\x00\x00\x00\x03\ +\x00\x00\x00\x30\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\ +" + +qt_resource_struct_v2 = b"\ +\x00\x00\x00\x00\x00\x02\x00\x00\x00\x01\x00\x00\x00\x01\ +\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x00\x00\x02\x00\x00\x00\x01\x00\x00\x00\x02\ +\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x14\x00\x02\x00\x00\x00\x01\x00\x00\x00\x03\ +\x00\x00\x00\x00\x00\x00\x00\x00\ +\x00\x00\x00\x30\x00\x00\x00\x00\x00\x01\x00\x00\x00\x00\ +\x00\x00\x01\x7a\x9e\xc1\x47\x93\ +" + +qt_version = [int(v) for v in QtCore.qVersion().split('.')] +if qt_version < [5, 8, 0]: + rcc_version = 1 + qt_resource_struct = qt_resource_struct_v1 +else: + rcc_version = 2 + qt_resource_struct = qt_resource_struct_v2 + +def qInitResources(): + QtCore.qRegisterResourceData(rcc_version, qt_resource_struct, qt_resource_name, qt_resource_data) + +def qCleanupResources(): + QtCore.qUnregisterResourceData(rcc_version, qt_resource_struct, qt_resource_name, qt_resource_data) + +qInitResources() diff --git a/plot_layout/resources.qrc b/plot_layout/resources.qrc new file mode 100644 index 0000000..015bbaf --- /dev/null +++ b/plot_layout/resources.qrc @@ -0,0 +1,5 @@ + + + icon.png + + diff --git a/plot_layout/scripts/compile-strings.sh b/plot_layout/scripts/compile-strings.sh new file mode 100644 index 0000000..9d76083 --- /dev/null +++ b/plot_layout/scripts/compile-strings.sh @@ -0,0 +1,12 @@ +#!/bin/bash +LRELEASE=$1 +LOCALES=$2 + + +for LOCALE in ${LOCALES} +do + echo "Processing: ${LOCALE}.ts" + # Note we don't use pylupdate with qt .pro file approach as it is flakey + # about what is made available. + $LRELEASE i18n/${LOCALE}.ts +done diff --git a/plot_layout/scripts/run-env-linux.sh b/plot_layout/scripts/run-env-linux.sh new file mode 100644 index 0000000..668247c --- /dev/null +++ b/plot_layout/scripts/run-env-linux.sh @@ -0,0 +1,28 @@ +#!/bin/bash + +QGIS_PREFIX_PATH=/usr/local/qgis-2.0 +if [ -n "$1" ]; then + QGIS_PREFIX_PATH=$1 +fi + +echo ${QGIS_PREFIX_PATH} + + +export QGIS_PREFIX_PATH=${QGIS_PREFIX_PATH} +export QGIS_PATH=${QGIS_PREFIX_PATH} +export LD_LIBRARY_PATH=${QGIS_PREFIX_PATH}/lib +export PYTHONPATH=${QGIS_PREFIX_PATH}/share/qgis/python:${QGIS_PREFIX_PATH}/share/qgis/python/plugins:${PYTHONPATH} + +echo "QGIS PATH: $QGIS_PREFIX_PATH" +export QGIS_DEBUG=0 +export QGIS_LOG_FILE=/tmp/inasafe/realtime/logs/qgis.log + +export PATH=${QGIS_PREFIX_PATH}/bin:$PATH + +echo "This script is intended to be sourced to set up your shell to" +echo "use a QGIS 2.0 built in $QGIS_PREFIX_PATH" +echo +echo "To use it do:" +echo "source $BASH_SOURCE /your/optional/install/path" +echo +echo "Then use the make file supplied here e.g. make guitest" diff --git a/plot_layout/scripts/update-strings.sh b/plot_layout/scripts/update-strings.sh new file mode 100644 index 0000000..a31f712 --- /dev/null +++ b/plot_layout/scripts/update-strings.sh @@ -0,0 +1,56 @@ +#!/bin/bash +LOCALES=$* + +# Get newest .py files so we don't update strings unnecessarily + +CHANGED_FILES=0 +PYTHON_FILES=`find . -regex ".*\(ui\|py\)$" -type f` +for PYTHON_FILE in $PYTHON_FILES +do + CHANGED=$(stat -c %Y $PYTHON_FILE) + if [ ${CHANGED} -gt ${CHANGED_FILES} ] + then + CHANGED_FILES=${CHANGED} + fi +done + +# Qt translation stuff +# for .ts file +UPDATE=false +for LOCALE in ${LOCALES} +do + TRANSLATION_FILE="i18n/$LOCALE.ts" + if [ ! -f ${TRANSLATION_FILE} ] + then + # Force translation string collection as we have a new language file + touch ${TRANSLATION_FILE} + UPDATE=true + break + fi + + MODIFICATION_TIME=$(stat -c %Y ${TRANSLATION_FILE}) + if [ ${CHANGED_FILES} -gt ${MODIFICATION_TIME} ] + then + # Force translation string collection as a .py file has been updated + UPDATE=true + break + fi +done + +if [ ${UPDATE} == true ] +# retrieve all python files +then + echo ${PYTHON_FILES} + # update .ts + echo "Please provide translations by editing the translation files below:" + for LOCALE in ${LOCALES} + do + echo "i18n/"${LOCALE}".ts" + # Note we don't use pylupdate with qt .pro file approach as it is flakey + # about what is made available. + pylupdate4 -noobsolete ${PYTHON_FILES} -ts i18n/${LOCALE}.ts + done +else + echo "No need to edit any translation files (.ts) because no python files" + echo "has been updated since the last update translation. " +fi