├── .flake8
├── .gitmodules
├── .pre-commit-config.yaml
├── .qgis-plugin-ci
├── LICENSE
├── README.md
├── qfieldsync
├── __init__.py
├── core
│ ├── __init__.py
│ ├── cloud_api.py
│ ├── cloud_converter.py
│ ├── cloud_project.py
│ ├── cloud_transferrer.py
│ ├── message_bus.py
│ └── preferences.py
├── gui
│ ├── __init__.py
│ ├── attachment_naming_widget.py
│ ├── checker_feedback_table.py
│ ├── cloud_browser_tree.py
│ ├── cloud_create_project_widget.py
│ ├── cloud_login_dialog.py
│ ├── cloud_projects_dialog.py
│ ├── cloud_transfer_dialog.py
│ ├── dirs_to_copy_widget.py
│ ├── layers_config_widget.py
│ ├── map_layer_config_widget.py
│ ├── mapthemes_config_widget.py
│ ├── package_dialog.py
│ ├── preferences_widget.py
│ ├── project_configuration_dialog.py
│ ├── project_configuration_widget.py
│ ├── relationship_configuration_widget.py
│ ├── synchronize_dialog.py
│ └── utils.py
├── metadata.txt
├── qfield_sync.py
├── resources.qrc
├── resources
│ ├── add.svg
│ ├── arrow_back-green.svg
│ ├── arrow_back-orange.svg
│ ├── arrow_back-red.svg
│ ├── arrow_back.svg
│ ├── arrow_forward-green.svg
│ ├── arrow_forward-orange.svg
│ ├── arrow_forward-red.svg
│ ├── arrow_forward.svg
│ ├── cloud.svg
│ ├── cloud_create.svg
│ ├── cloud_download.svg
│ ├── cloud_off.svg
│ ├── cloud_project.svg
│ ├── cloud_project_remote.svg
│ ├── cloud_synchronize.svg
│ ├── cloud_upload.svg
│ ├── computer.svg
│ ├── delete-red.svg
│ ├── delete.svg
│ ├── edit.svg
│ ├── file.svg
│ ├── file_add-green.svg
│ ├── file_refresh-orange.svg
│ ├── icon.png
│ ├── idea.svg
│ ├── launch.svg
│ ├── missing.svg
│ ├── package.svg
│ ├── project_properties.svg
│ ├── qfield_logo.svg
│ ├── qfieldcloud_logo.png
│ ├── refresh-reverse.png
│ ├── refresh.png
│ ├── refresh.svg
│ ├── sync.svg
│ ├── sync_disabled.svg
│ ├── synchronize.svg
│ ├── visibility.svg
│ └── warning.png
├── ui
│ ├── cloud_create_project_widget.ui
│ ├── cloud_login_dialog.ui
│ ├── cloud_projects_dialog.ui
│ ├── cloud_transfer_dialog.ui
│ ├── dirs_to_copy_widget.ui
│ ├── layers_config_widget.ui
│ ├── map_layer_config_widget.ui
│ ├── package_dialog.ui
│ ├── preferences_widget.ui
│ ├── project_configuration_widget.ui
│ └── synchronize_dialog.ui
└── utils
│ ├── __init__.py
│ ├── cloud_utils.py
│ ├── file_utils.py
│ ├── permissions.py
│ ├── qgis_utils.py
│ └── qt_utils.py
├── requirements.txt
├── scripts
└── prepare-commit.sh
└── tox.ini
/.flake8:
--------------------------------------------------------------------------------
1 | [flake8]
2 | ignore = E501, F401, W503
3 | max-line-length = 120
4 | max-complexity = 30
5 | exclude = **/.git/, **/qfieldsync/libqfieldsync/
6 |
--------------------------------------------------------------------------------
/.gitmodules:
--------------------------------------------------------------------------------
1 | [submodule "qfieldsync/setting_manager"]
2 | path = qfieldsync/setting_manager
3 | url = https://github.com/opengisch/qgissettingmanager.git
4 |
--------------------------------------------------------------------------------
/.pre-commit-config.yaml:
--------------------------------------------------------------------------------
1 | repos:
2 | # Fix end of files
3 | - repo: https://github.com/pre-commit/pre-commit-hooks
4 | rev: v4.4.0
5 | hooks:
6 | - id: trailing-whitespace
7 | - id: end-of-file-fixer
8 | - id: mixed-line-ending
9 | args:
10 | - '--fix=lf'
11 |
12 | # Lint and format
13 | - repo: https://github.com/astral-sh/ruff-pre-commit
14 | # Ruff version.
15 | rev: v0.1.4
16 | hooks:
17 | # Run the linter.
18 | - id: ruff
19 | args: [ --fix ]
20 |
21 | # Run the formatter.
22 | - id: ruff-format
23 |
24 | # Sort imports alphabetically
25 | - repo: https://github.com/PyCQA/isort
26 | rev: 6.0.0
27 | hooks:
28 | - id: isort
29 | args: ["--profile", "black"]
30 |
--------------------------------------------------------------------------------
/.qgis-plugin-ci:
--------------------------------------------------------------------------------
1 | plugin_path: qfieldsync
2 | github_organization_slug: opengisch
3 | project_slug: qfieldsync
4 | transifex_resource: QFieldSync
5 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | GNU LESSER GENERAL PUBLIC LICENSE
2 | Version 3, 29 June 2007
3 |
4 | Copyright (C) 2007 Free Software Foundation, Inc.
5 | Everyone is permitted to copy and distribute verbatim copies
6 | of this license document, but changing it is not allowed.
7 |
8 |
9 | This version of the GNU Lesser General Public License incorporates
10 | the terms and conditions of version 3 of the GNU General Public
11 | License, supplemented by the additional permissions listed below.
12 |
13 | 0. Additional Definitions.
14 |
15 | As used herein, "this License" refers to version 3 of the GNU Lesser
16 | General Public License, and the "GNU GPL" refers to version 3 of the GNU
17 | General Public License.
18 |
19 | "The Library" refers to a covered work governed by this License,
20 | other than an Application or a Combined Work as defined below.
21 |
22 | An "Application" is any work that makes use of an interface provided
23 | by the Library, but which is not otherwise based on the Library.
24 | Defining a subclass of a class defined by the Library is deemed a mode
25 | of using an interface provided by the Library.
26 |
27 | A "Combined Work" is a work produced by combining or linking an
28 | Application with the Library. The particular version of the Library
29 | with which the Combined Work was made is also called the "Linked
30 | Version".
31 |
32 | The "Minimal Corresponding Source" for a Combined Work means the
33 | Corresponding Source for the Combined Work, excluding any source code
34 | for portions of the Combined Work that, considered in isolation, are
35 | based on the Application, and not on the Linked Version.
36 |
37 | The "Corresponding Application Code" for a Combined Work means the
38 | object code and/or source code for the Application, including any data
39 | and utility programs needed for reproducing the Combined Work from the
40 | Application, but excluding the System Libraries of the Combined Work.
41 |
42 | 1. Exception to Section 3 of the GNU GPL.
43 |
44 | You may convey a covered work under sections 3 and 4 of this License
45 | without being bound by section 3 of the GNU GPL.
46 |
47 | 2. Conveying Modified Versions.
48 |
49 | If you modify a copy of the Library, and, in your modifications, a
50 | facility refers to a function or data to be supplied by an Application
51 | that uses the facility (other than as an argument passed when the
52 | facility is invoked), then you may convey a copy of the modified
53 | version:
54 |
55 | a) under this License, provided that you make a good faith effort to
56 | ensure that, in the event an Application does not supply the
57 | function or data, the facility still operates, and performs
58 | whatever part of its purpose remains meaningful, or
59 |
60 | b) under the GNU GPL, with none of the additional permissions of
61 | this License applicable to that copy.
62 |
63 | 3. Object Code Incorporating Material from Library Header Files.
64 |
65 | The object code form of an Application may incorporate material from
66 | a header file that is part of the Library. You may convey such object
67 | code under terms of your choice, provided that, if the incorporated
68 | material is not limited to numerical parameters, data structure
69 | layouts and accessors, or small macros, inline functions and templates
70 | (ten or fewer lines in length), you do both of the following:
71 |
72 | a) Give prominent notice with each copy of the object code that the
73 | Library is used in it and that the Library and its use are
74 | covered by this License.
75 |
76 | b) Accompany the object code with a copy of the GNU GPL and this license
77 | document.
78 |
79 | 4. Combined Works.
80 |
81 | You may convey a Combined Work under terms of your choice that,
82 | taken together, effectively do not restrict modification of the
83 | portions of the Library contained in the Combined Work and reverse
84 | engineering for debugging such modifications, if you also do each of
85 | the following:
86 |
87 | a) Give prominent notice with each copy of the Combined Work that
88 | the Library is used in it and that the Library and its use are
89 | covered by this License.
90 |
91 | b) Accompany the Combined Work with a copy of the GNU GPL and this license
92 | document.
93 |
94 | c) For a Combined Work that displays copyright notices during
95 | execution, include the copyright notice for the Library among
96 | these notices, as well as a reference directing the user to the
97 | copies of the GNU GPL and this license document.
98 |
99 | d) Do one of the following:
100 |
101 | 0) Convey the Minimal Corresponding Source under the terms of this
102 | License, and the Corresponding Application Code in a form
103 | suitable for, and under terms that permit, the user to
104 | recombine or relink the Application with a modified version of
105 | the Linked Version to produce a modified Combined Work, in the
106 | manner specified by section 6 of the GNU GPL for conveying
107 | Corresponding Source.
108 |
109 | 1) Use a suitable shared library mechanism for linking with the
110 | Library. A suitable mechanism is one that (a) uses at run time
111 | a copy of the Library already present on the user's computer
112 | system, and (b) will operate properly with a modified version
113 | of the Library that is interface-compatible with the Linked
114 | Version.
115 |
116 | e) Provide Installation Information, but only if you would otherwise
117 | be required to provide such information under section 6 of the
118 | GNU GPL, and only to the extent that such information is
119 | necessary to install and execute a modified version of the
120 | Combined Work produced by recombining or relinking the
121 | Application with a modified version of the Linked Version. (If
122 | you use option 4d0, the Installation Information must accompany
123 | the Minimal Corresponding Source and Corresponding Application
124 | Code. If you use option 4d1, you must provide the Installation
125 | Information in the manner specified by section 6 of the GNU GPL
126 | for conveying Corresponding Source.)
127 |
128 | 5. Combined Libraries.
129 |
130 | You may place library facilities that are a work based on the
131 | Library side by side in a single library together with other library
132 | facilities that are not Applications and are not covered by this
133 | License, and convey such a combined library under terms of your
134 | choice, if you do both of the following:
135 |
136 | a) Accompany the combined library with a copy of the same work based
137 | on the Library, uncombined with any other library facilities,
138 | conveyed under the terms of this License.
139 |
140 | b) Give prominent notice with the combined library that part of it
141 | is a work based on the Library, and explaining where to find the
142 | accompanying uncombined form of the same work.
143 |
144 | 6. Revised Versions of the GNU Lesser General Public License.
145 |
146 | The Free Software Foundation may publish revised and/or new versions
147 | of the GNU Lesser General Public License from time to time. Such new
148 | versions will be similar in spirit to the present version, but may
149 | differ in detail to address new problems or concerns.
150 |
151 | Each version is given a distinguishing version number. If the
152 | Library as you received it specifies that a certain numbered version
153 | of the GNU Lesser General Public License "or any later version"
154 | applies to it, you have the option of following the terms and
155 | conditions either of that published version or of any later version
156 | published by the Free Software Foundation. If the Library as you
157 | received it does not specify a version number of the GNU Lesser
158 | General Public License, you may choose any version of the GNU Lesser
159 | General Public License ever published by the Free Software Foundation.
160 |
161 | If the Library as you received it specifies that a proxy can decide
162 | whether future versions of the GNU Lesser General Public License shall
163 | apply, that proxy's public statement of acceptance of any version is
164 | permanent authorization for you to choose that version for the
165 | Library.
166 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | [](https://docs.qfield.org/get-started/)
2 | [](https://github.com/opengisch/QFieldSync/releases)
3 | [](https://travis-ci.org/opengisch/qfieldsync)
4 |
5 | # QFieldSync
6 |
7 | This plugin facilitates packaging and synchronizing QGIS projects for use with [QField](http://www.qfield.org).
8 |
9 | It analyses the QGIS project and suggests and performs actions needed to make the project working on QField.
10 |
11 | More information can be found in the [QField documentation](https://docs.qfield.org/get-started/).
12 |
13 | The plugin can be download on the [QGIS plugin repository](https://plugins.qgis.org/plugins/qfieldsync/).
14 |
15 |
16 | ## Contribute
17 |
18 | QFieldSync is an open source project, licensed under the terms of the GPLv3 or later.
19 | This means that it is free to use and modify and will stay like that.
20 |
21 | We are very happy if this app helps you to get your job done or in whatever creative way you may use it.
22 |
23 | If you found it useful, we will be even happier if you could give something back.
24 | A couple of things you can do are:
25 |
26 | - Rate the plugin at [plugins.qgis.org](https://plugins.qgis.org/plugins/qfieldsync/) ★★★★★
27 | - Write about your experience (please let us know!).
28 | - [Help with the documentation](https://github.com/opengisch/QField-docs/).
29 | - Translate [the QFieldSync QGIS plugin](https://app.transifex.com/opengisch/qfieldsync/dashboard/), [the QField app](https://app.transifex.com/opengisch/qfield-for-qgis/dashboard/) or [the documentation](https://app.transifex.com/opengisch/qfield-documentation/dashboard/).
30 | - [Sponsor a feature](https://docs.qfield.org/get-started/sponsor/)
31 | - And just drop by to say thank you or have a beer with us next time you meet [OPENGIS.ch](https://opengis.ch) at a conference.
32 | - [Develop a new feature or fix a bug](#development).
33 |
34 |
35 | ## Development
36 |
37 | ### Getting the source code
38 |
39 | 1) Checkout [`qfieldsync`](https://github.com/opengisch/qfieldsync/) locally:
40 |
41 | ```shell
42 | git clone --recurse-submodules git@github.com:opengisch/qfieldsync.git
43 | ```
44 |
45 | 2) Make a link of the QFieldSync checkout to `qfieldsync` directory in your current QGIS profile:
46 |
47 | ```shell
48 | ln -s ${PWD}/qfieldsync/qfieldsync ${HOME}/.local/share/QGIS/QGIS3/profiles/default/python/plugins
49 | ```
50 |
51 | 3) Checkout [`libqfieldsync`](https://github.com/opengisch/libqfieldsync/) locally:
52 |
53 | ```shell
54 | git clone git@github.com:opengisch/libqfieldsync.git
55 | ```
56 |
57 | 4) Install your local `libqfieldsync` as editable dependency (assuming you are in the same directory as step 3):
58 |
59 | ```shell
60 | pip install -e libqfieldsync
61 | ```
62 |
63 | > [!NOTE]
64 | > On more recent Linux distributions you might get an error `error: externally-managed-environment` and you have to pass additional `--break-system-packages`.
65 | > Despite the name, we promise this is not going to break system packages.
66 |
67 | ### Opening a PR
68 |
69 | Make sure each new feature or bug fix are in a separate PR.
70 |
71 | QFieldSync stores the respective `libqfieldsync` commit SHA in the bottom of [`requirements.txt`](https://github.com/opengisch/qfieldsync/blob/master/requirements.txt#L9-L10).
72 | Sometimes changes in QFieldSync require modifications in [`libqfieldsync`](https://github.com/opengisch/libqfieldsync/).
73 | In these cases please update the commit sha of `libqfieldsync` to point to the respective commit on `libqfieldsync`'s master branch.
74 |
--------------------------------------------------------------------------------
/qfieldsync/__init__.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | """
3 | /***************************************************************************
4 | QFieldSync
5 | A QGIS plugin
6 | Sync your projects to QField on android
7 | -------------------
8 | begin : 2015-05-20
9 | copyright : (C) 2015 by OPENGIS.ch
10 | email : info@opengis.ch
11 | git sha : $Format:%H$
12 | ***************************************************************************/
13 |
14 | /***************************************************************************
15 | * *
16 | * This program is free software; you can redistribute it and/or modify *
17 | * it under the terms of the GNU General Public License as published by *
18 | * the Free Software Foundation; either version 2 of the License, or *
19 | * (at your option) any later version. *
20 | * *
21 | ***************************************************************************/
22 | This script initializes the plugin, making it known to QGIS.
23 | """
24 |
25 | from __future__ import absolute_import
26 |
27 | import importlib
28 | import pathlib
29 | import re
30 | import sys
31 |
32 | src_dir = pathlib.Path(__file__).parent.resolve()
33 |
34 | # remove previously loaded `libqfieldsync.whl` from the python import path
35 | for python_path in sys.path:
36 | if re.search(r"libqfieldsync.*\.whl$", python_path):
37 | sys.path.remove(python_path)
38 |
39 | # add the new `libqfieldsync.whl` file to the python import path
40 | for libqfieldsync_whl in src_dir.glob("libqfieldsync*.whl"):
41 | sys.path.append(str(libqfieldsync_whl))
42 |
43 | # force reload all the `libqfieldsync` modules from the new path
44 | module_names = list(sys.modules.keys())
45 | for module_name in module_names:
46 | if module_name.startswith("libqfieldsync"):
47 | importlib.reload(sys.modules[module_name])
48 |
49 |
50 | # noinspection PyPep8Naming
51 | def classFactory(iface): # pylint: disable=invalid-name
52 | """Load QFieldSync class from file QFieldSync.
53 |
54 | :param iface: A QGIS interface instance.
55 | :type iface: QgsInterface
56 | """
57 |
58 | from qfieldsync.qfield_sync import QFieldSync
59 |
60 | return QFieldSync(iface)
61 |
--------------------------------------------------------------------------------
/qfieldsync/core/__init__.py:
--------------------------------------------------------------------------------
1 | from .cloud_converter import CloudConverter # NOQA
2 | from .cloud_project import CloudProject # NOQA
3 | from .preferences import Preferences # NOQA
4 |
--------------------------------------------------------------------------------
/qfieldsync/core/cloud_converter.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | """
3 | /***************************************************************************
4 | QFieldSync
5 | -------------------
6 | begin : 2021-07-20
7 | git sha : $Format:%H$
8 | copyright : (C) 2021 by OPENGIS.ch
9 | email : info@opengis.ch
10 | ***************************************************************************/
11 |
12 | /***************************************************************************
13 | * *
14 | * This program is free software; you can redistribute it and/or modify *
15 | * it under the terms of the GNU General Public License as published by *
16 | * the Free Software Foundation; either version 2 of the License, or *
17 | * (at your option) any later version. *
18 | * *
19 | ***************************************************************************/
20 | """
21 |
22 | from pathlib import Path
23 |
24 | from libqfieldsync.layer import LayerSource
25 | from libqfieldsync.utils.file_utils import copy_attachments
26 | from libqfieldsync.utils.qgis import get_qgis_files_within_dir, make_temp_qgis_file
27 | from qgis.core import QgsMapLayer, QgsProject, QgsVirtualLayerDefinition
28 | from qgis.PyQt.QtCore import QCoreApplication, QObject, QUrl, pyqtSignal
29 | from qgis.utils import iface
30 |
31 | from qfieldsync.core.preferences import Preferences
32 | from qfieldsync.utils.qgis_utils import open_project
33 |
34 |
35 | class CloudConverter(QObject):
36 | progressStopped = pyqtSignal()
37 | warning = pyqtSignal(str, str)
38 | total_progress_updated = pyqtSignal(int, int, str)
39 |
40 | def __init__(
41 | self,
42 | project: QgsProject,
43 | export_dirname: str,
44 | ):
45 | super(CloudConverter, self).__init__(parent=None)
46 | self.project = project
47 | self.__layers = list()
48 |
49 | # elipsis workaround
50 | self.trUtf8 = self.tr
51 |
52 | self.export_dirname = Path(export_dirname)
53 |
54 | def convert(self) -> None: # noqa: C901
55 | """
56 | Convert the project to a cloud project.
57 | """
58 |
59 | original_project_path = self.project.fileName()
60 | project_path = self.export_dirname.joinpath(
61 | f"{self.project.baseName()}_cloud.qgs"
62 | )
63 | backup_project_path = make_temp_qgis_file(self.project)
64 | is_converted = False
65 |
66 | try:
67 | if not self.export_dirname.exists():
68 | self.export_dirname.mkdir(parents=True, exist_ok=True)
69 |
70 | if get_qgis_files_within_dir(self.export_dirname):
71 | raise Exception(
72 | self.tr("The destination folder already contains a project file")
73 | )
74 |
75 | self.total_progress_updated.emit(0, 100, self.trUtf8("Converting project…"))
76 | self.__layers = list(self.project.mapLayers().values())
77 |
78 | # Loop through all layers and copy them to the destination folder
79 | for current_layer_index, layer in enumerate(self.__layers):
80 | self.total_progress_updated.emit(
81 | current_layer_index,
82 | len(self.__layers),
83 | self.trUtf8("Copying layers…"),
84 | )
85 |
86 | layer_source = LayerSource(layer)
87 | if not layer_source.is_supported:
88 | self.project.removeMapLayer(layer)
89 | continue
90 |
91 | if layer.dataProvider() is not None:
92 | # layer stored in localized data path, skip
93 | if layer_source.is_localized_path:
94 | continue
95 |
96 | if layer.type() == QgsMapLayer.VectorLayer:
97 | if (
98 | layer.dataProvider()
99 | and layer.dataProvider().name() == "virtual"
100 | ):
101 | url = QUrl.fromEncoded(layer.source().encode("ascii"))
102 | valid = url.isValid()
103 | if valid:
104 | definition = QgsVirtualLayerDefinition.fromUrl(url)
105 | for source in definition.sourceLayers():
106 | if not source.isReferenced():
107 | valid = False
108 | break
109 | if not valid:
110 | # virtual layers with non-referenced sources are not supported
111 | self.warning.emit(
112 | self.tr("Cloud Converter"),
113 | self.tr(
114 | "The virtual layer '{}' is not valid or contains non-referenced source(s) and could not be converted and was therefore removed from the cloud project."
115 | ).format(layer.name()),
116 | )
117 | self.project.removeMapLayer(layer)
118 | continue
119 | else:
120 | if not layer_source.convert_to_gpkg(self.export_dirname):
121 | # something went wrong, remove layer and inform the user that layer will be missing
122 | self.warning.emit(
123 | self.tr("Cloud Converter"),
124 | self.tr(
125 | "The layer '{}' could not be converted and was therefore removed from the cloud project."
126 | ).format(layer.name()),
127 | )
128 | self.project.removeMapLayer(layer)
129 | continue
130 | else:
131 | layer_source.copy(self.export_dirname, list())
132 | layer.setCustomProperty(
133 | "QFieldSync/cloud_action", layer_source.default_cloud_action
134 | )
135 |
136 | # save the offline project twice so that the offline plugin can "know" that it's a relative path
137 | if not self.project.write(str(project_path)):
138 | raise Exception(
139 | self.tr('Failed to save project to "{}".').format(project_path)
140 | )
141 |
142 | # export the DCIM folder
143 | for attachment_dir in Preferences().value("attachmentDirs"):
144 | copy_attachments(
145 | Path(original_project_path).parent,
146 | project_path.parent,
147 | attachment_dir,
148 | )
149 |
150 | title = self.project.title()
151 | title_suffix = self.tr("(QFieldCloud)")
152 | if not title.endswith(title_suffix):
153 | self.project.setTitle("{} {}".format(title, title_suffix))
154 | # Now we have a project state which can be saved as cloud project
155 | self.project.write(str(project_path))
156 | is_converted = True
157 | finally:
158 | # We need to let the app handle events before loading the next project or QGIS will crash with rasters
159 | QCoreApplication.processEvents()
160 | self.project.clear()
161 | QCoreApplication.processEvents()
162 |
163 | # TODO whatcha gonna do if QgsProject::read()/write() fails
164 | if is_converted:
165 | iface.addProject(str(project_path))
166 | else:
167 | open_project(original_project_path, backup_project_path)
168 |
169 | self.total_progress_updated.emit(100, 100, self.tr("Finished"))
170 |
--------------------------------------------------------------------------------
/qfieldsync/core/message_bus.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | """
3 | /***************************************************************************
4 | QFieldSync
5 | -------------------
6 | begin : 2022-08-09
7 | git sha : $Format:%H$
8 | copyright : (C) 2022 by OPENGIS.ch
9 | email : info@opengis.ch
10 | ***************************************************************************/
11 |
12 | /***************************************************************************
13 | * *
14 | * This program is free software; you can redistribute it and/or modify *
15 | * it under the terms of the GNU General Public License as published by *
16 | * the Free Software Foundation; either version 2 of the License, or *
17 | * (at your option) any later version. *
18 | * *
19 | ***************************************************************************/
20 | """
21 |
22 |
23 | from qgis.PyQt.QtCore import QObject, pyqtSignal
24 |
25 |
26 | class MessageBus(QObject):
27 | """Super minimal implementation of a message bus.
28 |
29 | Allows communication between unrelated parts of the plugin.
30 | """
31 |
32 | """The signal that passes the message."""
33 | messaged = pyqtSignal(str)
34 |
35 |
36 | # Modules are evaluated only once, therefore it works as a poor man version of singleton.
37 | message_bus = MessageBus()
38 |
--------------------------------------------------------------------------------
/qfieldsync/core/preferences.py:
--------------------------------------------------------------------------------
1 | from pathlib import Path
2 |
3 | from qfieldsync.setting_manager import (
4 | Bool,
5 | Dictionary,
6 | Scope,
7 | SettingManager,
8 | String,
9 | Stringlist,
10 | )
11 |
12 | pluginName = "QFieldSync"
13 |
14 |
15 | class Preferences(SettingManager):
16 | def __init__(self):
17 | SettingManager.__init__(self, pluginName, False)
18 | home = Path.home()
19 | self.add_setting(
20 | String("exportDirectory", Scope.Global, str(home.joinpath("QField/export")))
21 | )
22 | self.add_setting(String("exportDirectoryProject", Scope.Project, None))
23 | self.add_setting(
24 | String("importDirectory", Scope.Global, str(home.joinpath("QField/import")))
25 | )
26 | self.add_setting(Bool("showPackagingActions", Scope.Global, True))
27 | self.add_setting(String("importDirectoryProject", Scope.Project, None))
28 | self.add_setting(Dictionary("dirsToCopy", Scope.Project, {}))
29 | self.add_setting(Stringlist("attachmentDirs", Scope.Project, ["DCIM"]))
30 | self.add_setting(Dictionary("qfieldCloudProjectLocalDirs", Scope.Global, {}))
31 | self.add_setting(Dictionary("qfieldCloudLastProjectFiles", Scope.Global, {}))
32 | self.add_setting(String("qfieldCloudServerUrl", Scope.Global, ""))
33 | self.add_setting(String("qfieldCloudAuthcfg", Scope.Global, ""))
34 | self.add_setting(Bool("qfieldCloudRememberMe", Scope.Global, False))
35 | self.add_setting(
36 | String("cloudDirectory", Scope.Global, str(home.joinpath("QField/cloud")))
37 | )
38 | self.add_setting(Bool("firstRun", Scope.Global, True))
39 |
--------------------------------------------------------------------------------
/qfieldsync/gui/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/opengisch/qfieldsync/561b20451d9d4b152fb6e845bc8e5951340f51f9/qfieldsync/gui/__init__.py
--------------------------------------------------------------------------------
/qfieldsync/gui/attachment_naming_widget.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | """
3 | /***************************************************************************
4 | AttachmentNamingTableWidget
5 | A QGIS plugin
6 | Sync your projects to QField
7 | -------------------
8 | begin : 2020-06-15
9 | git sha : $Format:%H$
10 | copyright : (C) 2020 by OPENGIS.ch
11 | email : info@opengis.ch
12 | ***************************************************************************/
13 |
14 | /***************************************************************************
15 | * *
16 | * This program is free software; you can redistribute it and/or modify *
17 | * it under the terms of the GNU General Public License as published by *
18 | * the Free Software Foundation; either version 2 of the License, or *
19 | * (at your option) any later version. *
20 | * *
21 | ***************************************************************************/
22 | """
23 |
24 | from qgis.core import QgsMapLayer
25 | from qgis.gui import QgsFieldExpressionWidget
26 | from qgis.PyQt.QtCore import Qt
27 | from qgis.PyQt.QtWidgets import QTableWidget, QTableWidgetItem
28 |
29 |
30 | class AttachmentNamingTableWidget(QTableWidget):
31 | def __init__(self):
32 | super(AttachmentNamingTableWidget, self).__init__()
33 |
34 | self.setColumnCount(3)
35 | self.setHorizontalHeaderLabels(
36 | [self.tr("Layer"), self.tr("Field"), self.tr("Naming Expression")]
37 | )
38 | self.horizontalHeaderItem(2).setToolTip(
39 | self.tr("Enter expression for a file path with the extension .jpg")
40 | )
41 | self.horizontalHeader().setStretchLastSection(True)
42 | self.setRowCount(0)
43 | self.resizeColumnsToContents()
44 | self.setMinimumHeight(100)
45 |
46 | def addLayerFields(self, layer_source):
47 | layer = layer_source.layer
48 |
49 | if layer.type() != QgsMapLayer.VectorLayer:
50 | return
51 |
52 | for field_name in layer_source.get_attachment_fields().keys():
53 | row = self.rowCount()
54 |
55 | self.insertRow(row)
56 | item = QTableWidgetItem(layer.name())
57 | item.setData(Qt.UserRole, layer_source)
58 | item.setFlags(Qt.ItemIsEnabled)
59 | self.setItem(row, 0, item)
60 | item = QTableWidgetItem(field_name)
61 | item.setFlags(Qt.ItemIsEnabled)
62 | self.setItem(row, 1, item)
63 | ew = QgsFieldExpressionWidget()
64 | ew.setLayer(layer)
65 | ew.setExpression(layer_source.attachment_naming(field_name))
66 | self.setCellWidget(row, 2, ew)
67 |
68 | self.resizeColumnsToContents()
69 |
70 | def setLayerColumnHidden(self, is_hidden):
71 | self.setColumnHidden(0, is_hidden)
72 |
73 | def syncLayerSourceValues(self, should_apply=False):
74 | for i in range(self.rowCount()):
75 | layer_source = self.item(i, 0).data(Qt.UserRole)
76 | field_name = self.item(i, 1).text()
77 | new_expression = self.cellWidget(i, 2).currentText()
78 | layer_source.set_attachment_naming(field_name, new_expression)
79 |
80 | if should_apply:
81 | layer_source.apply()
82 |
--------------------------------------------------------------------------------
/qfieldsync/gui/checker_feedback_table.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | """
3 | /***************************************************************************
4 | QFieldSync
5 | A QGIS plugin
6 | Sync your projects to QField
7 | -------------------
8 | begin : 2023-04-11
9 | git sha : $Format:%H$
10 | copyright : (C) 2015 by OPENGIS.ch
11 | email : info@opengis.ch
12 | ***************************************************************************/
13 |
14 | /***************************************************************************
15 | * *
16 | * This program is free software; you can redistribute it and/or modify *
17 | * it under the terms of the GNU General Public License as published by *
18 | * the Free Software Foundation; either version 2 of the License, or *
19 | * (at your option) any later version. *
20 | * *
21 | ***************************************************************************/
22 | """
23 |
24 | from libqfieldsync.project_checker import Feedback, ProjectCheckerFeedback
25 | from qgis.core import QgsApplication
26 | from qgis.PyQt.QtCore import Qt
27 | from qgis.PyQt.QtWidgets import QLabel, QSizePolicy, QTableWidget, QTableWidgetItem
28 |
29 | from ..utils.qt_utils import make_icon
30 |
31 |
32 | class CheckerFeedbackTable(QTableWidget):
33 | def __init__(self, checker_feedback: ProjectCheckerFeedback, *args, **kwargs):
34 | super().__init__(*args, **kwargs)
35 |
36 | self.setColumnCount(2)
37 | self.setHorizontalHeaderLabels(["", self.tr("Message")])
38 | self.horizontalHeader().setStretchLastSection(True)
39 | self.setRowCount(0)
40 | self.setMinimumHeight(100)
41 | self.setSizePolicy(QSizePolicy.MinimumExpanding, QSizePolicy.MinimumExpanding)
42 |
43 | for layer_id in checker_feedback.feedbacks.keys():
44 | for feedback in checker_feedback.feedbacks[layer_id]:
45 | row = self.rowCount()
46 |
47 | self.insertRow(row)
48 |
49 | # first column
50 | if feedback.level == Feedback.Level.WARNING:
51 | level_icon = make_icon("idea.svg")
52 | level_text = self.tr("Warning")
53 | else:
54 | level_icon = QgsApplication.getThemeIcon("/mIconWarning.svg")
55 | level_text = self.tr("Error")
56 |
57 | item = QTableWidgetItem(level_icon, "")
58 | item.setFlags(Qt.ItemIsEnabled)
59 | item.setToolTip(level_text)
60 | self.setItem(row, 0, item)
61 |
62 | # second column
63 | if feedback.layer_id:
64 | source = self.tr('Layer "{}"').format(feedback.layer_name)
65 | else:
66 | source = self.tr("Project")
67 |
68 | item = QTableWidgetItem()
69 | item.setFlags(Qt.ItemIsEnabled)
70 | item.setToolTip(level_text)
71 | self.setItem(row, 1, item)
72 |
73 | # we do not escape the values on purpose to support Markdown/HTML
74 | label = QLabel("**{}**\n\n{}".format(source, feedback.message))
75 | label.setWordWrap(True)
76 | label.setMargin(5)
77 | label.setTextFormat(Qt.MarkdownText)
78 | label.setTextInteractionFlags(
79 | Qt.TextSelectableByMouse
80 | | Qt.TextSelectableByKeyboard
81 | | Qt.LinksAccessibleByMouse
82 | | Qt.LinksAccessibleByKeyboard
83 | )
84 | label.setOpenExternalLinks(True)
85 | self.setCellWidget(row, 1, label)
86 |
87 | self.verticalHeader().hide()
88 | self.resizeColumnsToContents()
89 | self.resizeRowsToContents()
90 | self.horizontalHeader().sectionResized.connect(self.resizeRowsToContents)
91 | self.setWordWrap(True)
92 |
--------------------------------------------------------------------------------
/qfieldsync/gui/cloud_browser_tree.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | """
3 | /***************************************************************************
4 | QFieldCloud
5 | -------------------
6 | begin : 2020-07-13
7 | git sha : $Format:%H$
8 | copyright : (C) 2020 by OPENGIS.ch
9 | email : info@opengis.ch
10 | ***************************************************************************/
11 |
12 | /***************************************************************************
13 | * *
14 | * This program is free software; you can redistribute it and/or modify *
15 | * it under the terms of the GNU General Public License as published by *
16 | * the Free Software Foundation; either version 2 of the License, or *
17 | * (at your option) any later version. *
18 | * *
19 | ***************************************************************************/
20 | """
21 |
22 | import os
23 | from pathlib import Path
24 | from typing import List
25 |
26 | from libqfieldsync.utils.qgis import get_qgis_files_within_dir
27 | from qgis.core import (
28 | QgsDataCollectionItem,
29 | QgsDataItem,
30 | QgsDataItemProvider,
31 | QgsDataProvider,
32 | QgsErrorItem,
33 | )
34 | from qgis.gui import QgsDataItemGuiProvider
35 | from qgis.PyQt.QtCore import QObject
36 | from qgis.PyQt.QtGui import QIcon
37 | from qgis.utils import iface
38 |
39 | from qfieldsync.core.cloud_api import CloudNetworkAccessManager
40 | from qfieldsync.core.cloud_project import CloudProject
41 | from qfieldsync.gui.cloud_login_dialog import CloudLoginDialog
42 | from qfieldsync.gui.cloud_projects_dialog import CloudProjectsDialog
43 | from qfieldsync.gui.cloud_transfer_dialog import CloudTransferDialog
44 |
45 |
46 | class QFieldCloudItemProvider(QgsDataItemProvider):
47 | def __init__(self, network_manager: CloudNetworkAccessManager):
48 | QgsDataItemProvider.__init__(self)
49 | self.network_manager = network_manager
50 |
51 | def name(self):
52 | return "QFieldCloudItemProvider"
53 |
54 | def capabilities(self):
55 | return QgsDataProvider.Net
56 |
57 | def createDataItem(self, path, parentItem):
58 | if not parentItem:
59 | root_item = QFieldCloudRootItem(self.network_manager)
60 | return root_item
61 | else:
62 | return
63 |
64 |
65 | class QFieldCloudRootItem(QgsDataCollectionItem):
66 | """QFieldCloud root"""
67 |
68 | def __init__(self, network_manager: CloudNetworkAccessManager):
69 | QgsDataCollectionItem.__init__(
70 | self, None, "QFieldCloud", "/QFieldCloud", "QFieldCloudProvider"
71 | )
72 | self.setIcon(
73 | QIcon(os.path.join(os.path.dirname(__file__), "../resources/cloud_off.svg"))
74 | )
75 | self.network_manager = network_manager
76 | self.error = None
77 |
78 | self.network_manager.login_finished.connect(lambda: self.update_icon())
79 | self.network_manager.token_changed.connect(lambda: self.update_icon())
80 | self.network_manager.projects_cache.projects_updated.connect(
81 | lambda: self.refreshing_cloud_projects()
82 | )
83 |
84 | def capabilities2(self):
85 | return QgsDataItem.Fast
86 |
87 | def createChildren(self):
88 | items = []
89 |
90 | if not self.network_manager.has_token():
91 | CloudLoginDialog.show_auth_dialog(self.network_manager)
92 | self.setState(QgsDataItem.Populating)
93 | return []
94 |
95 | self.setState(QgsDataItem.Populated)
96 |
97 | if self.error:
98 | error_item = QgsErrorItem(self, self.error, "/QFieldCloud/error")
99 | error_item.setIcon(
100 | QIcon(os.path.join(os.path.dirname(__file__), "../resources/cloud.svg"))
101 | )
102 | return [error_item]
103 |
104 | my_projects = QFieldCloudGroupItem(
105 | self, "My projects", "private", "../resources/cloud.svg", 1
106 | )
107 | items.append(my_projects)
108 |
109 | # TODO uncomment when public projects are ready
110 | # public_projects = QFieldCloudGroupItem(
111 | # self, "Community", "public", "../resources/cloud.svg", 2
112 | # )
113 | # items.append(public_projects)
114 |
115 | return items
116 |
117 | def refreshing_cloud_projects(self):
118 | self.depopulate()
119 | self.refresh()
120 |
121 | def update_icon(self):
122 | if self.network_manager.has_token():
123 | self.setIcon(
124 | QIcon(os.path.join(os.path.dirname(__file__), "../resources/cloud.svg"))
125 | )
126 | else:
127 | self.setIcon(
128 | QIcon(
129 | os.path.join(
130 | os.path.dirname(__file__), "../resources/cloud_off.svg"
131 | )
132 | )
133 | )
134 |
135 |
136 | class QFieldCloudGroupItem(QgsDataCollectionItem):
137 | """QFieldCloud group data item."""
138 |
139 | def __init__(self, parent, name, project_type, icon, order):
140 | super(QFieldCloudGroupItem, self).__init__(parent, name, "/QFieldCloud/" + name)
141 |
142 | self.network_manager = parent.network_manager
143 | self.project_type = project_type
144 | self.setIcon(QIcon(os.path.join(os.path.dirname(__file__), icon)))
145 | self.setSortKey(order)
146 |
147 | def createChildren(self):
148 | items = []
149 |
150 | projects: List[CloudProject] = self.network_manager.projects_cache.projects
151 |
152 | if projects is None:
153 | try:
154 | # TODO try to be make it Fast Fertile
155 | self.network_manager.projects_cache.refresh_not_async()
156 | except Exception:
157 | return []
158 |
159 | projects = self.network_manager.projects_cache.projects
160 |
161 | for project in self.network_manager.projects_cache.projects:
162 | if (self.project_type == "public" and not project.is_private) or (
163 | self.project_type == "private" and project.is_private
164 | ):
165 | item = QFieldCloudProjectItem(self, project)
166 | item.setState(QgsDataItem.Populated)
167 | items.append(item)
168 |
169 | return items
170 |
171 |
172 | class QFieldCloudProjectItem(QgsDataItem):
173 | """QFieldCloud project item."""
174 |
175 | def __init__(self, parent, project):
176 | super(QFieldCloudProjectItem, self).__init__(
177 | QgsDataItem.Collection,
178 | parent,
179 | project.name,
180 | "/QFieldCloud/project/" + project.id,
181 | )
182 | self.project_id = project.id
183 | project = parent.network_manager.projects_cache.find_project(self.project_id)
184 | self.setIcon(
185 | QIcon(
186 | str(
187 | Path(__file__).parent.joinpath(
188 | "../resources/cloud_project.svg"
189 | if project.local_dir
190 | else "../resources/cloud_project_remote.svg"
191 | )
192 | )
193 | )
194 | )
195 |
196 |
197 | class QFieldCloudItemGuiProvider(QgsDataItemGuiProvider):
198 | def __init__(self, network_manager: CloudNetworkAccessManager):
199 | QgsDataItemGuiProvider.__init__(self)
200 | self.network_manager = network_manager
201 |
202 | def name(self):
203 | return "QFieldCloudItemGuiProvider"
204 |
205 | def populateContextMenu(self, item, menu, selectedItems, context):
206 | if type(item) is QFieldCloudProjectItem:
207 | project = self.network_manager.projects_cache.find_project(item.project_id)
208 | if project and project.local_dir:
209 | open_action = menu.addAction(QObject().tr("Open Project"))
210 | open_action.triggered.connect(lambda: self.open_project(item))
211 |
212 | sync_action = menu.addAction(
213 | QIcon(os.path.join(os.path.dirname(__file__), "../resources/sync.svg")),
214 | QObject().tr("Synchronize Project"),
215 | )
216 | sync_action.triggered.connect(
217 | lambda: self.show_cloud_synchronize_dialog(item)
218 | )
219 |
220 | properties_action = menu.addAction(QObject().tr("Project Properties"))
221 | properties_action.triggered.connect(
222 | lambda: self._create_projects_dialog(item).show_project_form()
223 | )
224 | elif type(item) is QFieldCloudGroupItem:
225 | create_action = menu.addAction(
226 | QIcon(os.path.join(os.path.dirname(__file__), "../resources/edit.svg")),
227 | QObject().tr("Create New Project"),
228 | )
229 | create_action.triggered.connect(
230 | lambda: CloudProjectsDialog(
231 | self.network_manager, iface.mainWindow()
232 | ).show_create_project()
233 | )
234 | elif type(item) is QFieldCloudRootItem:
235 | projects_overview_action = menu.addAction(
236 | QIcon(
237 | os.path.join(os.path.dirname(__file__), "../resources/cloud.svg")
238 | ),
239 | QObject().tr("Projects Overview"),
240 | )
241 | projects_overview_action.triggered.connect(
242 | lambda: CloudProjectsDialog(
243 | self.network_manager, iface.mainWindow()
244 | ).show()
245 | )
246 |
247 | refresh_action = menu.addAction(
248 | QIcon(
249 | os.path.join(os.path.dirname(__file__), "../resources/refresh.png")
250 | ),
251 | QObject().tr("Refresh Projects"),
252 | )
253 | refresh_action.triggered.connect(lambda: self.refresh_cloud_projects())
254 |
255 | def handleDoubleClick(self, item, context):
256 | if type(item) is QFieldCloudProjectItem:
257 | if not self.open_project(item):
258 | self.show_cloud_synchronize_dialog(item)
259 | return True
260 | return False
261 |
262 | def _create_projects_dialog(self, item) -> CloudProjectsDialog:
263 | # it is important to get the project like this, because if the project list is refreshed, here we will store an old reference
264 | project = self.network_manager.projects_cache.find_project(item.project_id)
265 | return CloudProjectsDialog(
266 | self.network_manager, iface.mainWindow(), project=project
267 | )
268 |
269 | def show_cloud_synchronize_dialog(self, item):
270 | project = self.network_manager.projects_cache.find_project(item.project_id)
271 | CloudTransferDialog.show_transfer_dialog(
272 | self.network_manager, project, None, None, iface.mainWindow()
273 | )
274 |
275 | def open_project(self, item) -> bool:
276 | project = self.network_manager.projects_cache.find_project(item.project_id)
277 | if project and project.local_dir:
278 | project_file_name = get_qgis_files_within_dir(Path(project.local_dir))
279 | if project_file_name:
280 | iface.addProject(os.path.join(project.local_dir, project_file_name[0]))
281 | return True
282 |
283 | return False
284 |
285 | def refresh_cloud_projects(self):
286 | if not self.network_manager.has_token():
287 | CloudLoginDialog.show_auth_dialog(
288 | self.network_manager, lambda: self.refresh_cloud_projects()
289 | )
290 | return
291 |
292 | if self.network_manager.is_login_active:
293 | return
294 |
295 | self.network_manager.projects_cache.refresh()
296 |
--------------------------------------------------------------------------------
/qfieldsync/gui/cloud_login_dialog.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | """
3 | /***************************************************************************
4 | QFieldCloudDialog
5 | A QGIS plugin
6 | Sync your projects to QField
7 | -------------------
8 | begin : 2020-08-01
9 | git sha : $Format:%H$
10 | copyright : (C) 2020 by OPENGIS.ch
11 | email : info@opengis.ch
12 | ***************************************************************************/
13 |
14 | /***************************************************************************
15 | * *
16 | * This program is free software; you can redistribute it and/or modify *
17 | * it under the terms of the GNU General Public License as published by *
18 | * the Free Software Foundation; either version 2 of the License, or *
19 | * (at your option) any later version. *
20 | * *
21 | ***************************************************************************/
22 | """
23 | import os
24 | from typing import Callable
25 |
26 | from qgis.PyQt.QtCore import Qt
27 | from qgis.PyQt.QtGui import QPixmap
28 | from qgis.PyQt.QtWidgets import (
29 | QApplication,
30 | QDialog,
31 | QDialogButtonBox,
32 | QMainWindow,
33 | QWidget,
34 | )
35 | from qgis.PyQt.uic import loadUiType
36 |
37 | from qfieldsync.core import Preferences
38 | from qfieldsync.core.cloud_api import CloudNetworkAccessManager
39 |
40 | CloudLoginDialogUi, _ = loadUiType(
41 | os.path.join(os.path.dirname(__file__), "../ui/cloud_login_dialog.ui")
42 | )
43 |
44 |
45 | class CloudLoginDialog(QDialog, CloudLoginDialogUi):
46 | instance = None
47 |
48 | @staticmethod
49 | def show_auth_dialog(
50 | network_manager: CloudNetworkAccessManager,
51 | accepted_cb: Callable = None,
52 | rejected_cb: Callable = None,
53 | parent: QWidget = None,
54 | ):
55 | if CloudLoginDialog.instance:
56 | CloudLoginDialog.instance.show()
57 | CloudLoginDialog.instance.raise_()
58 | CloudLoginDialog.instance.activateWindow()
59 | return CloudLoginDialog.instance
60 |
61 | CloudLoginDialog.instance = CloudLoginDialog(network_manager, parent)
62 | CloudLoginDialog.instance.authenticate()
63 |
64 | if accepted_cb:
65 | CloudLoginDialog.instance.accepted.connect(accepted_cb)
66 | if rejected_cb:
67 | CloudLoginDialog.instance.rejected.connect(rejected_cb)
68 |
69 | def on_finished(result):
70 | CloudLoginDialog.instance = None
71 |
72 | CloudLoginDialog.instance.finished.connect(on_finished)
73 |
74 | return CloudLoginDialog.instance
75 |
76 | def __init__(
77 | self, network_manager: CloudNetworkAccessManager, parent: QWidget = None
78 | ) -> None:
79 | """Constructor."""
80 | super(CloudLoginDialog, self).__init__(parent=parent)
81 | self.setupUi(self)
82 | self.preferences = Preferences()
83 | self.network_manager = network_manager
84 |
85 | self.buttonBox.button(QDialogButtonBox.Ok).setText(self.tr("Sign In"))
86 | self.buttonBox.button(QDialogButtonBox.Ok).clicked.connect(
87 | self.on_login_button_clicked
88 | )
89 | self.buttonBox.button(QDialogButtonBox.Cancel).clicked.connect(
90 | self.on_cancel_button_clicked
91 | )
92 |
93 | self.serverUrlLabel.setVisible(False)
94 | self.serverUrlCmb.setVisible(False)
95 | for server_url in self.network_manager.server_urls():
96 | self.serverUrlCmb.addItem(server_url)
97 |
98 | cfg = self.network_manager.auth()
99 | remember_me = self.preferences.value("qfieldCloudRememberMe")
100 |
101 | self.serverUrlCmb.setCurrentText(cfg.uri() or self.network_manager.url)
102 | self.usernameLineEdit.setText(cfg.config("username"))
103 | self.passwordLineEdit.setText(cfg.config("password"))
104 | self.rememberMeCheckBox.setChecked(remember_me)
105 |
106 | self.network_manager.login_finished.connect(self.on_login_finished)
107 |
108 | self.qfieldCloudIcon.setAlignment(Qt.AlignHCenter)
109 | self.qfieldCloudIcon.setPixmap(
110 | QPixmap(
111 | os.path.join(
112 | os.path.dirname(__file__), "../resources/qfieldcloud_logo.png"
113 | )
114 | )
115 | )
116 | self.qfieldCloudIcon.setMinimumSize(175, 180)
117 | self.qfieldCloudIcon.mouseDoubleClickEvent = (
118 | lambda event: self.toggle_server_url_visibility()
119 | )
120 | self.rejected.connect(self.on_rejected)
121 | self.hide()
122 |
123 | def on_rejected(self) -> None:
124 | QApplication.restoreOverrideCursor()
125 | if self.parent():
126 | self.parent().setEnabled(True)
127 | self.setEnabled(True)
128 |
129 | def toggle_server_url_visibility(self) -> None:
130 | self.serverUrlLabel.setVisible(not self.serverUrlLabel.isVisible())
131 | self.serverUrlCmb.setVisible(not self.serverUrlCmb.isVisible())
132 |
133 | def authenticate(self) -> None:
134 | self.usernameLineEdit.setEnabled(True)
135 | self.passwordLineEdit.setEnabled(True)
136 | self.rememberMeCheckBox.setEnabled(True)
137 | self.buttonBox.button(QDialogButtonBox.Ok).setEnabled(True)
138 |
139 | if self.parent() and not isinstance(self.parent(), QMainWindow):
140 | self.parent().setEnabled(False)
141 | self.setEnabled(True)
142 |
143 | cfg = self.network_manager.auth()
144 |
145 | if cfg.config("token"):
146 | self.usernameLineEdit.setEnabled(False)
147 | self.passwordLineEdit.setEnabled(False)
148 | self.rememberMeCheckBox.setEnabled(False)
149 | self.buttonBox.button(QDialogButtonBox.Ok).setEnabled(False)
150 |
151 | self.network_manager.set_url(cfg.uri())
152 | self.network_manager.set_auth(self.network_manager.url, token="")
153 | # don't trust the password, just login once again
154 | self.network_manager.login(cfg.config("username"), cfg.config("password"))
155 |
156 | if not cfg.config("token") or not self.parent():
157 | self.show()
158 | self.raise_()
159 | self.activateWindow()
160 |
161 | def on_login_button_clicked(self) -> None:
162 | QApplication.setOverrideCursor(Qt.WaitCursor)
163 |
164 | self.buttonBox.button(QDialogButtonBox.Ok).setEnabled(False)
165 | self.rememberMeCheckBox.setEnabled(False)
166 |
167 | server_url = self.serverUrlCmb.currentText()
168 | username = self.usernameLineEdit.text()
169 | password = self.passwordLineEdit.text()
170 | remember_me = self.rememberMeCheckBox.isChecked()
171 |
172 | self.network_manager.set_auth(server_url, username=username, password=password)
173 | self.network_manager.set_url(server_url)
174 | self.network_manager.login(username, password)
175 |
176 | self.preferences.set_value("qfieldCloudRememberMe", remember_me)
177 |
178 | def on_login_finished(self) -> None:
179 | QApplication.restoreOverrideCursor()
180 | if self.parent():
181 | self.parent().setEnabled(True)
182 | self.setEnabled(True)
183 |
184 | if not self.network_manager.has_token():
185 | self.loginFeedbackLabel.setText(self.network_manager.get_last_login_error())
186 | self.loginFeedbackLabel.setVisible(True)
187 | self.usernameLineEdit.setEnabled(True)
188 | self.passwordLineEdit.setEnabled(True)
189 | self.rememberMeCheckBox.setEnabled(True)
190 | self.buttonBox.button(QDialogButtonBox.Ok).setEnabled(True)
191 | return
192 |
193 | self.usernameLineEdit.setEnabled(False)
194 | self.passwordLineEdit.setEnabled(False)
195 | self.rememberMeCheckBox.setEnabled(False)
196 | self.done(QDialog.Accepted)
197 |
198 | def on_cancel_button_clicked(self):
199 | self.reject()
200 |
--------------------------------------------------------------------------------
/qfieldsync/gui/dirs_to_copy_widget.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | """
3 | /***************************************************************************
4 | AttachmentNamingTableWidget
5 | A QGIS plugin
6 | Sync your projects to QField
7 | -------------------
8 | begin : 2023-02-26
9 | git sha : $Format:%H$
10 | copyright : (C) 2023 by OPENGIS.ch
11 | email : info@opengis.ch
12 | ***************************************************************************/
13 |
14 | /***************************************************************************
15 | * *
16 | * This program is free software; you can redistribute it and/or modify *
17 | * it under the terms of the GNU General Public License as published by *
18 | * the Free Software Foundation; either version 2 of the License, or *
19 | * (at your option) any later version. *
20 | * *
21 | ***************************************************************************/
22 | """
23 |
24 | import os
25 | from pathlib import Path
26 | from typing import Dict, Optional
27 |
28 | from qgis.PyQt.QtCore import Qt
29 | from qgis.PyQt.QtWidgets import QTreeWidgetItem, QWidget
30 | from qgis.PyQt.uic import loadUiType
31 |
32 | from qfieldsync.core.preferences import Preferences
33 | from qfieldsync.utils.file_utils import (
34 | DirectoryTreeDict,
35 | DirectoryTreeType,
36 | path_to_dict,
37 | )
38 | from qfieldsync.utils.qt_utils import build_file_tree_widget_from_dict
39 |
40 | LayersConfigWidgetUi, _ = loadUiType(
41 | os.path.join(os.path.dirname(__file__), "../ui/dirs_to_copy_widget.ui")
42 | )
43 |
44 | DirsToCopySettings = Dict[str, bool]
45 |
46 |
47 | class DirsToCopyWidget(QWidget, LayersConfigWidgetUi):
48 | def __init__(self, parent=None):
49 | super().__init__(parent=parent)
50 | self.setupUi(self)
51 |
52 | self.path: Optional[Path] = None
53 | self.preferences: Preferences = Preferences()
54 | self.refreshButton.clicked.connect(lambda: self.refresh_tree())
55 | self.selectAllButton.clicked.connect(
56 | lambda: self._set_checked_state_recursively(True)
57 | )
58 | self.deselectAllButton.clicked.connect(
59 | lambda: self._set_checked_state_recursively(False)
60 | )
61 |
62 | def refresh_tree(self):
63 | if self.path is None:
64 | return
65 |
66 | if not self.path.is_dir():
67 | return
68 |
69 | dirs_to_copy = self.load_settings()
70 |
71 | self.dirsTreeWidget.clear()
72 |
73 | def build_item_cb(item: QTreeWidgetItem, node: DirectoryTreeDict):
74 | relative_path = node["path"].relative_to(self.path)
75 | str_path = str(relative_path)
76 |
77 | # TODO decide whether or not to take into account the attachment_dirs into account
78 | # attachment_dirs = self.preferences.value("attachmentDirs") # TODO move this in the outer scope
79 | # matches = False
80 |
81 | # for attachment_dir in attachment_dirs:
82 | # if str_path.startswith(attachment_dir):
83 | # matches = True
84 |
85 | # if not matches:
86 | # return False
87 |
88 | check_state = (
89 | Qt.Checked if dirs_to_copy.get(str_path, True) else Qt.Unchecked
90 | )
91 |
92 | item.setCheckState(0, check_state)
93 | item.setExpanded(True)
94 | item.setData(0, Qt.UserRole, str_path)
95 | item.setToolTip(0, str(node["path"].absolute()))
96 | item.setFlags(
97 | item.flags()
98 | | Qt.ItemIsUserCheckable
99 | | Qt.ItemIsSelectable
100 | | Qt.ItemIsAutoTristate
101 | )
102 |
103 | root_item = self.dirsTreeWidget.invisibleRootItem()
104 | dict_paths = path_to_dict(self.path, dirs_only=True)
105 |
106 | if dict_paths["type"] == DirectoryTreeType.DIRECTORY:
107 | for subnode in dict_paths["content"]:
108 | build_file_tree_widget_from_dict(
109 | root_item,
110 | subnode,
111 | build_item_cb,
112 | )
113 |
114 | def dirs_to_copy(self) -> DirsToCopySettings:
115 | def extract_dirs_data(root_item: QTreeWidgetItem) -> Dict[str, bool]:
116 | data = {}
117 | for i in range(root_item.childCount()):
118 | item = root_item.child(i)
119 | relative_path = item.data(0, Qt.UserRole)
120 | is_checked = item.checkState(0) == Qt.Checked
121 | data[relative_path] = is_checked
122 |
123 | if item.childCount() > 0:
124 | data.update(extract_dirs_data(item))
125 |
126 | return data
127 |
128 | dirs_to_copy = extract_dirs_data(self.dirsTreeWidget.invisibleRootItem())
129 |
130 | return dirs_to_copy
131 |
132 | def set_path(self, path: str) -> None:
133 | if path.strip():
134 | self.path = Path(path)
135 | else:
136 | self.path = None
137 |
138 | is_enabled = bool(self.path and self.path.is_dir())
139 | self.refreshButton.setEnabled(is_enabled)
140 |
141 | def load_settings(self) -> DirsToCopySettings:
142 | return self.preferences.value("dirsToCopy")
143 |
144 | def save_settings(self) -> None:
145 | self.preferences.set_value("dirsToCopy", self.dirs_to_copy())
146 |
147 | def _set_checked_state_recursively(self, checked: bool) -> None:
148 | def set_checked_state(item: QTreeWidgetItem) -> None:
149 | checked_state = Qt.Checked if checked else Qt.Unchecked
150 | item.setCheckState(0, checked_state)
151 |
152 | for i in range(item.childCount()):
153 | set_checked_state(item.child(i))
154 |
155 | set_checked_state(self.dirsTreeWidget.invisibleRootItem())
156 |
--------------------------------------------------------------------------------
/qfieldsync/gui/layers_config_widget.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | """
3 | /***************************************************************************
4 | QFieldSyncDialog
5 | A QGIS plugin
6 | Sync your projects to QField
7 | -------------------
8 | begin : 2020-10-10
9 | git sha : $Format:%H$
10 | copyright : (C) 2020 by OPENGIS.ch
11 | email : info@opengis.ch
12 | ***************************************************************************/
13 |
14 | /***************************************************************************
15 | * *
16 | * This program is free software; you can redistribute it and/or modify *
17 | * it under the terms of the GNU General Public License as published by *
18 | * the Free Software Foundation; either version 2 of the License, or *
19 | * (at your option) any later version. *
20 | * *
21 | ***************************************************************************/
22 | """
23 | import os
24 | from typing import Callable
25 |
26 | from libqfieldsync.layer import LayerSource, SyncAction
27 | from PyQt5.QtWidgets import QPushButton
28 | from qgis.core import Qgis, QgsMapLayerModel, QgsProject
29 | from qgis.PyQt.QtCore import Qt
30 | from qgis.PyQt.QtGui import QIcon
31 | from qgis.PyQt.QtWidgets import (
32 | QAction,
33 | QComboBox,
34 | QMenu,
35 | QTableWidgetItem,
36 | QToolButton,
37 | QWidget,
38 | )
39 | from qgis.PyQt.uic import loadUiType
40 | from qgis.utils import iface
41 |
42 | from qfieldsync.core.message_bus import message_bus
43 | from qfieldsync.gui.utils import set_available_actions
44 |
45 | LayersConfigWidgetUi, _ = loadUiType(
46 | os.path.join(os.path.dirname(__file__), "../ui/layers_config_widget.ui")
47 | )
48 |
49 |
50 | class LayersConfigWidget(QWidget, LayersConfigWidgetUi):
51 | def __init__(self, project, use_cloud_actions, layer_sources, parent=None):
52 | """Constructor."""
53 | super(LayersConfigWidget, self).__init__(parent=parent)
54 | self.setupUi(self)
55 |
56 | self.project = project
57 | self.use_cloud_actions = use_cloud_actions
58 | self.layer_sources = layer_sources
59 |
60 | self.multipleToggleButton.setIcon(
61 | QIcon(
62 | os.path.join(os.path.dirname(__file__), "../resources/visibility.svg")
63 | )
64 | )
65 | self.toggleMenu = QMenu(self)
66 | self.removeAllAction = QAction(self.tr("Remove All Layers"), self.toggleMenu)
67 | self.toggleMenu.addAction(self.removeAllAction)
68 | self.removeHiddenAction = QAction(
69 | self.tr("Remove Hidden Layers"), self.toggleMenu
70 | )
71 | self.toggleMenu.addAction(self.removeHiddenAction)
72 | self.addAllCopyAction = QAction(self.tr("Add All Layers"), self.toggleMenu)
73 | self.toggleMenu.addAction(self.addAllCopyAction)
74 | self.addVisibleCopyAction = QAction(
75 | self.tr("Add Visible Layers"), self.toggleMenu
76 | )
77 | self.toggleMenu.addAction(self.addVisibleCopyAction)
78 | self.addAllOfflineAction = QAction(
79 | self.tr("Add All Vector Layers as Offline"), self.toggleMenu
80 | )
81 | self.toggleMenu.addAction(self.addAllOfflineAction)
82 | self.addVisibleOfflineAction = QAction(
83 | self.tr("Add Visible Vector Layers as Offline"), self.toggleMenu
84 | )
85 | self.toggleMenu.addAction(self.addVisibleOfflineAction)
86 | self.multipleToggleButton.setMenu(self.toggleMenu)
87 | self.multipleToggleButton.setAutoRaise(True)
88 | self.multipleToggleButton.setPopupMode(QToolButton.InstantPopup)
89 | self.toggleMenu.triggered.connect(self.toggleMenu_triggered)
90 | self.unsupportedLayersList = list()
91 |
92 | self._on_message_bus_messaged_wrapper = (
93 | lambda msg: self._on_message_bus_messaged(msg)
94 | )
95 | message_bus.messaged.connect(self._on_message_bus_messaged_wrapper)
96 |
97 | self.showVisibleLayersOnlyCheckbox.stateChanged.connect(self.reloadProject)
98 | self.textFilterBox.textChanged.connect(self.reloadProject)
99 |
100 | self.reloadProject()
101 |
102 | def get_available_actions(self, layer_source):
103 | if self.use_cloud_actions:
104 | return layer_source.available_cloud_actions
105 | else:
106 | return layer_source.available_actions
107 |
108 | def get_layer_action(self, layer_source):
109 | if self.use_cloud_actions:
110 | return layer_source.cloud_action
111 | else:
112 | return layer_source.action
113 |
114 | def set_layer_action(self, layer_source, action):
115 | if self.use_cloud_actions:
116 | layer_source.cloud_action = action
117 | else:
118 | layer_source.action = action
119 |
120 | def get_default_action(self, layer_source):
121 | if self.use_cloud_actions:
122 | return layer_source.default_cloud_action
123 | else:
124 | return layer_source.default_action
125 |
126 | def reloadProject(self):
127 | """
128 | Load all layers from the map layer registry into the table.
129 | """
130 | self.unsupportedLayersList = list()
131 |
132 | self.layersTable.setRowCount(0)
133 | self.layersTable.setSortingEnabled(False)
134 |
135 | # Get filtered layers
136 | show_visible_only = self.showVisibleLayersOnlyCheckbox.isChecked()
137 | filter_text = self.textFilterBox.text().lower()
138 |
139 | layers = []
140 | if show_visible_only:
141 | for layer_source in self.layer_sources:
142 | if (
143 | QgsProject.instance()
144 | .layerTreeRoot()
145 | .findLayer(layer_source.layer.id())
146 | .isVisible()
147 | ):
148 | layers.append(layer_source)
149 | else:
150 | layers = self.layer_sources
151 |
152 | for layer_source in layers:
153 | layer_name = layer_source.layer.name().lower()
154 |
155 | if filter_text and filter_text not in layer_name:
156 | continue
157 |
158 | count = self.layersTable.rowCount()
159 | self.layersTable.insertRow(count)
160 | item = QTableWidgetItem(layer_source.layer.name())
161 | item.setData(Qt.UserRole, layer_source)
162 | item.setData(Qt.EditRole, layer_source.layer.name())
163 | item.setIcon(QgsMapLayerModel.iconForLayer(layer_source.layer))
164 | self.layersTable.setItem(count, 0, item)
165 |
166 | cmb = QComboBox()
167 | available_actions = self.get_available_actions(layer_source)
168 | set_available_actions(
169 | cmb, available_actions, self.get_layer_action(layer_source)
170 | )
171 |
172 | properties_btn = QPushButton()
173 | properties_btn.setText(self.tr("Properties"))
174 | properties_btn.clicked.connect(self.propertiesBtn_clicked(layer_source))
175 |
176 | self.layersTable.setCellWidget(count, 1, cmb)
177 | self.layersTable.setCellWidget(count, 2, properties_btn)
178 |
179 | if not layer_source.is_supported:
180 | self.unsupportedLayersList.append(layer_source)
181 | self.layersTable.item(count, 0).setFlags(Qt.NoItemFlags)
182 | self.layersTable.cellWidget(count, 1).setEnabled(False)
183 | cmb.setCurrentIndex(cmb.findData(SyncAction.REMOVE))
184 |
185 | self.layersTable.resizeColumnsToContents()
186 | self.layersTable.sortByColumn(0, Qt.AscendingOrder)
187 | self.layersTable.setSortingEnabled(True)
188 |
189 | if self.unsupportedLayersList:
190 | self.unsupportedLayersLabel.setVisible(True)
191 |
192 | unsupported_layers_text = "{}: ".format(self.tr("Warning"))
193 | unsupported_layers_text += self.tr(
194 | "There are unsupported layers in your project which will not be available in QField."
195 | )
196 | unsupported_layers_text += self.tr(
197 | " If needed, you can create a Base Map to include those layers in your packaged project."
198 | )
199 | self.unsupportedLayersLabel.setText(unsupported_layers_text)
200 |
201 | def propertiesBtn_clicked(self, layer_source: LayerSource) -> Callable:
202 | def clicked_callback() -> None:
203 | if Qgis.QGIS_VERSION_INT >= 31900:
204 | iface.showLayerProperties(layer_source.layer, "QFieldLayerSettingsPage")
205 | else:
206 | iface.showLayerProperties(layer_source.layer)
207 |
208 | return clicked_callback
209 |
210 | def toggleMenu_triggered(self, action):
211 | """
212 | Toggles usage of layers
213 | :param action: the menu action that triggered this
214 | """
215 | sync_action = None
216 | if action in (self.removeHiddenAction, self.removeAllAction):
217 | sync_action = SyncAction.REMOVE
218 | elif action in (self.addAllOfflineAction, self.addVisibleOfflineAction):
219 | sync_action = SyncAction.OFFLINE
220 |
221 | is_project_dirty = False
222 | # all layers
223 | if action in (
224 | self.removeAllAction,
225 | self.addAllCopyAction,
226 | self.addAllOfflineAction,
227 | ):
228 | for i in range(self.layersTable.rowCount()):
229 | item = self.layersTable.item(i, 0)
230 | layer_source = item.data(Qt.UserRole)
231 | old_action = self.get_layer_action(layer_source)
232 | available_actions, _ = zip(*self.get_available_actions(layer_source))
233 | layer_sync_action = (
234 | self.get_default_action(layer_source)
235 | if sync_action is None
236 | else sync_action
237 | )
238 | if layer_sync_action in available_actions:
239 | self.set_layer_action(layer_source, layer_sync_action)
240 | if self.get_layer_action(layer_source) != old_action:
241 | self.project.setDirty(True)
242 | layer_source.apply()
243 | is_project_dirty |= layer_source.apply()
244 | # based on visibility
245 | elif action in (
246 | self.removeHiddenAction,
247 | self.addVisibleCopyAction,
248 | self.addVisibleOfflineAction,
249 | ):
250 | visible = action != self.removeHiddenAction
251 | root = QgsProject.instance().layerTreeRoot()
252 | for layer in QgsProject.instance().mapLayers().values():
253 | node = root.findLayer(layer.id())
254 | if node and node.isVisible() == visible:
255 | layer_source = LayerSource(layer)
256 | old_action = self.get_layer_action(layer_source)
257 | available_actions, _ = zip(
258 | *self.get_available_actions(layer_source)
259 | )
260 | layer_sync_action = (
261 | self.get_default_action(layer_source)
262 | if sync_action is None
263 | else sync_action
264 | )
265 | if layer_sync_action in available_actions:
266 | self.set_layer_action(layer_source, layer_sync_action)
267 | if self.get_layer_action(layer_source) != old_action:
268 | self.project.setDirty(True)
269 | layer_source.apply()
270 | is_project_dirty |= layer_source.apply()
271 |
272 | if is_project_dirty:
273 | self.project.setDirty(True)
274 |
275 | self.reloadProject()
276 |
277 | def apply(self):
278 | is_project_dirty = False
279 |
280 | for i in range(self.layersTable.rowCount()):
281 | item = self.layersTable.item(i, 0)
282 | layer_source = item.data(Qt.UserRole)
283 | cmb = self.layersTable.cellWidget(i, 1)
284 |
285 | self.set_layer_action(layer_source, cmb.itemData(cmb.currentIndex()))
286 |
287 | is_project_dirty |= layer_source.apply()
288 |
289 | if is_project_dirty:
290 | self.project.setDirty(True)
291 |
292 | def _on_message_bus_messaged(self, msg: str) -> None:
293 | # when MapLayerConfigWidget.apply() detects changes in layer settings,
294 | # the event is emitted with `layer_config_saved` as a message.
295 | # check ./gui/map_layer_config_widget.py
296 | if msg != "layer_config_saved":
297 | return
298 |
299 | for layer_source in self.layer_sources.copy():
300 | try:
301 | layer_source.read_layer()
302 | except RuntimeError:
303 | self.layer_sources.remove(layer_source)
304 |
305 | # quite ugly workaround, but this method sometimes operates on deleted objects,
306 | # so we need to make sure we don't get exceptions
307 | try:
308 | self.reloadProject()
309 | except Exception:
310 | message_bus.messaged.disconnect(self._on_message_bus_messaged_wrapper)
311 |
--------------------------------------------------------------------------------
/qfieldsync/gui/map_layer_config_widget.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | """
3 | /***************************************************************************
4 | QFieldSyncDialog
5 | A QGIS plugin
6 | Sync your projects to QField
7 | -------------------
8 | begin : 2020-06-15
9 | git sha : $Format:%H$
10 | copyright : (C) 2020 by OPENGIS.ch
11 | email : info@opengis.ch
12 | ***************************************************************************/
13 |
14 | /***************************************************************************
15 | * *
16 | * This program is free software; you can redistribute it and/or modify *
17 | * it under the terms of the GNU General Public License as published by *
18 | * the Free Software Foundation; either version 2 of the License, or *
19 | * (at your option) any later version. *
20 | * *
21 | ***************************************************************************/
22 | """
23 | import os
24 |
25 | from libqfieldsync.layer import LayerSource
26 | from qgis.core import QgsMapLayer, QgsProject, QgsProperty, QgsPropertyDefinition
27 | from qgis.gui import QgsMapLayerConfigWidget, QgsMapLayerConfigWidgetFactory, QgsSpinBox
28 | from qgis.PyQt.QtWidgets import QLabel
29 | from qgis.PyQt.uic import loadUiType
30 |
31 | from qfieldsync.core.message_bus import message_bus
32 | from qfieldsync.gui.attachment_naming_widget import AttachmentNamingTableWidget
33 | from qfieldsync.gui.relationship_configuration_widget import (
34 | RelationshipConfigurationTableWidget,
35 | )
36 | from qfieldsync.gui.utils import set_available_actions
37 |
38 | WidgetUi, _ = loadUiType(
39 | os.path.join(os.path.dirname(__file__), "../ui/map_layer_config_widget.ui")
40 | )
41 |
42 |
43 | class MapLayerConfigWidgetFactory(QgsMapLayerConfigWidgetFactory):
44 | def __init__(self, title, icon):
45 | super(MapLayerConfigWidgetFactory, self).__init__(title, icon)
46 |
47 | def createWidget(self, layer, canvas, dock_widget, parent):
48 | return MapLayerConfigWidget(layer, canvas, parent)
49 |
50 | def supportsLayer(self, layer):
51 | return LayerSource(layer).is_supported
52 |
53 | def supportLayerPropertiesDialog(self):
54 | return True
55 |
56 |
57 | class MapLayerConfigWidget(QgsMapLayerConfigWidget, WidgetUi):
58 | PROPERTY_GEOMETRY_LOCKED = 1
59 |
60 | def __init__(self, layer, canvas, parent):
61 | super(MapLayerConfigWidget, self).__init__(layer, canvas, parent)
62 | self.setupUi(self)
63 | self.layer_source = LayerSource(layer)
64 | self.project = QgsProject.instance()
65 |
66 | set_available_actions(
67 | self.cloudLayerActionComboBox,
68 | self.layer_source.available_cloud_actions,
69 | self.layer_source.cloud_action,
70 | )
71 | set_available_actions(
72 | self.cableLayerActionComboBox,
73 | self.layer_source.available_actions,
74 | self.layer_source.action,
75 | )
76 |
77 | self.attachmentNamingTable = AttachmentNamingTableWidget()
78 | self.attachmentNamingTable.addLayerFields(self.layer_source)
79 | self.attachmentNamingTable.setLayerColumnHidden(True)
80 |
81 | self.relationshipConfigurationTable = RelationshipConfigurationTableWidget()
82 | self.relationshipConfigurationTable.addLayerFields(self.layer_source)
83 | self.relationshipConfigurationTable.setLayerColumnHidden(True)
84 |
85 | self.valueMapButtonInterfaceSpinBox.setClearValueMode(
86 | QgsSpinBox.CustomValue, self.tr("Disabled")
87 | )
88 |
89 | self.measurementTypeComboBox.addItem(
90 | "Elapsed time (seconds since start of tracking)"
91 | )
92 | self.measurementTypeComboBox.addItem(
93 | self.tr("Timestamp (milliseconds since epoch)")
94 | )
95 | self.measurementTypeComboBox.addItem(self.tr("Ground speed"))
96 | self.measurementTypeComboBox.addItem(self.tr("Bearing"))
97 | self.measurementTypeComboBox.addItem(self.tr("Horizontal accuracy"))
98 | self.measurementTypeComboBox.addItem(self.tr("Vertical accuracy"))
99 | self.measurementTypeComboBox.addItem(self.tr("PDOP"))
100 | self.measurementTypeComboBox.addItem(self.tr("HDOP"))
101 | self.measurementTypeComboBox.addItem(self.tr("VDOP"))
102 |
103 | if layer.type() == QgsMapLayer.VectorLayer:
104 | prop = QgsProperty.fromExpression(
105 | self.layer_source.geometry_locked_expression
106 | )
107 | prop.setActive(self.layer_source.is_geometry_locked_expression_active)
108 | prop_definition = QgsPropertyDefinition(
109 | "is_geometry_locked",
110 | QgsPropertyDefinition.DataType.DataTypeBoolean,
111 | "Geometry Locked",
112 | "",
113 | )
114 | self.isGeometryLockedDDButton.init(
115 | MapLayerConfigWidget.PROPERTY_GEOMETRY_LOCKED,
116 | prop,
117 | prop_definition,
118 | None,
119 | False,
120 | )
121 | self.isGeometryLockedDDButton.setVectorLayer(layer)
122 |
123 | self.isGeometryLockedCheckBox.setEnabled(
124 | self.layer_source.can_lock_geometry
125 | )
126 | self.isGeometryLockedCheckBox.setChecked(
127 | self.layer_source.is_geometry_locked
128 | )
129 |
130 | self.valueMapButtonInterfaceSpinBox.setValue(
131 | self.layer_source.value_map_button_interface_threshold
132 | )
133 | self.valueMapButtonInterfaceSpinBox.setVisible(True)
134 |
135 | # append the attachment naming table to the layout
136 | self.attachmentsRelationsLayout.insertRow(
137 | -1, self.tr("Attachment\nnaming"), self.attachmentNamingTable
138 | )
139 | tip = QLabel(
140 | self.tr(
141 | "In your expressions, use {filename} and {extension} tags to refer to attachment filenames and extensions."
142 | )
143 | )
144 | tip.setWordWrap(True)
145 | self.attachmentsRelationsLayout.insertRow(-1, "", tip)
146 | self.attachmentNamingTable.setEnabled(
147 | self.attachmentNamingTable.rowCount() > 0
148 | )
149 |
150 | # append the relationship configuration table to the layout
151 | self.attachmentsRelationsLayout.insertRow(
152 | -1,
153 | self.tr("Relationship\nconfiguration"),
154 | self.relationshipConfigurationTable,
155 | )
156 | self.relationshipConfigurationTable.setEnabled(
157 | self.relationshipConfigurationTable.rowCount() > 0
158 | )
159 |
160 | self.trackingSessionGroupBox.setChecked(
161 | self.layer_source.tracking_session_active
162 | )
163 | self.timeRequirementCheckBox.setChecked(
164 | self.layer_source.tracking_time_requirement_active
165 | )
166 | self.timeRequirementIntervalSecondsSpinBox.setValue(
167 | self.layer_source.tracking_time_requirement_interval_seconds
168 | )
169 | self.distanceRequirementCheckBox.setChecked(
170 | self.layer_source.tracking_distance_requirement_active
171 | )
172 | self.distanceRequirementMinimumMetersSpinBox.setValue(
173 | self.layer_source.tracking_distance_requirement_minimum_meters
174 | )
175 | self.sensorDataRequirementCheckBox.setChecked(
176 | self.layer_source.tracking_sensor_data_requirement_active
177 | )
178 | self.allRequirementsCheckBox.setChecked(
179 | self.layer_source.tracking_all_requirements_active
180 | )
181 | self.erroneousDistanceSafeguardCheckBox.setChecked(
182 | self.layer_source.tracking_erroneous_distance_safeguard_active
183 | )
184 | self.erroneousDistanceSafeguardMaximumMetersSpinBox.setValue(
185 | self.layer_source.tracking_erroneous_distance_safeguard_maximum_meters
186 | )
187 | self.measurementTypeComboBox.setCurrentIndex(
188 | self.layer_source.tracking_measurement_type
189 | )
190 | else:
191 | self.isGeometryLockedDDButton.setVisible(False)
192 | self.isGeometryLockedCheckBox.setVisible(False)
193 |
194 | self.valueMapButtonInterfaceSpinBox.setVisible(False)
195 | self.attachmentsRelationsGroupBox.setVisible(False)
196 | self.trackingSessionGroupBox.setVisible(False)
197 |
198 | def apply(self):
199 | self.layer_source.action
200 | self.layer_source.cloud_action
201 | self.layer_source.is_geometry_locked
202 |
203 | self.layer_source.cloud_action = self.cloudLayerActionComboBox.itemData(
204 | self.cloudLayerActionComboBox.currentIndex()
205 | )
206 | self.layer_source.action = self.cableLayerActionComboBox.itemData(
207 | self.cableLayerActionComboBox.currentIndex()
208 | )
209 | self.layer_source.is_geometry_locked = self.isGeometryLockedCheckBox.isChecked()
210 | prop = self.isGeometryLockedDDButton.toProperty()
211 | self.layer_source.is_geometry_locked_expression_active = prop.isActive()
212 | self.layer_source.geometry_locked_expression = prop.asExpression()
213 | print(self.valueMapButtonInterfaceSpinBox.value())
214 | print(self.valueMapButtonInterfaceSpinBox.value())
215 | self.layer_source.value_map_button_interface_threshold = (
216 | self.valueMapButtonInterfaceSpinBox.value()
217 | )
218 | self.attachmentNamingTable.syncLayerSourceValues()
219 | self.relationshipConfigurationTable.syncLayerSourceValues()
220 |
221 | self.layer_source.tracking_session_active = (
222 | self.trackingSessionGroupBox.isChecked()
223 | )
224 | self.layer_source.tracking_time_requirement_active = (
225 | self.timeRequirementCheckBox.isChecked()
226 | )
227 | self.layer_source.tracking_time_requirement_interval_seconds = (
228 | self.timeRequirementIntervalSecondsSpinBox.value()
229 | )
230 | self.layer_source.tracking_distance_requirement_active = (
231 | self.distanceRequirementCheckBox.isChecked()
232 | )
233 | self.layer_source.tracking_distance_requirement_minimum_meters = (
234 | self.distanceRequirementMinimumMetersSpinBox.value()
235 | )
236 | self.layer_source.tracking_sensor_data_requirement_active = (
237 | self.sensorDataRequirementCheckBox.isChecked()
238 | )
239 | self.layer_source.tracking_all_requirements_active = (
240 | self.allRequirementsCheckBox.isChecked()
241 | )
242 | self.layer_source.tracking_erroneous_distance_safeguard_active = (
243 | self.erroneousDistanceSafeguardCheckBox.isChecked()
244 | )
245 | self.layer_source.tracking_erroneous_distance_safeguard_maximum_meters = (
246 | self.erroneousDistanceSafeguardMaximumMetersSpinBox.value()
247 | )
248 | self.layer_source.tracking_measurement_type = (
249 | self.measurementTypeComboBox.currentIndex()
250 | )
251 |
252 | if self.layer_source.apply():
253 | self.project.setDirty(True)
254 | message_bus.messaged.emit("layer_config_saved")
255 |
--------------------------------------------------------------------------------
/qfieldsync/gui/mapthemes_config_widget.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | """
3 | /***************************************************************************
4 | MapThemesConfigWidget
5 | A QGIS plugin
6 | Sync your projects to QField
7 | -------------------
8 | begin : 2024-07-22
9 | git sha : $Format:%H$
10 | copyright : (C) 2024 by OPENGIS.ch
11 | email : info@opengis.ch
12 | ***************************************************************************/
13 |
14 | /***************************************************************************
15 | * *
16 | * This program is free software; you can redistribute it and/or modify *
17 | * it under the terms of the GNU General Public License as published by *
18 | * the Free Software Foundation; either version 2 of the License, or *
19 | * (at your option) any later version. *
20 | * *
21 | ***************************************************************************/
22 | """
23 |
24 | from qgis.core import Qgis, QgsMapLayerProxyModel
25 | from qgis.gui import QgsMapLayerComboBox
26 | from qgis.PyQt.QtCore import Qt
27 | from qgis.PyQt.QtWidgets import QTableWidget, QTableWidgetItem
28 |
29 |
30 | class MapThemesConfigWidget(QTableWidget):
31 | def __init__(self, project, configuration, parent=None):
32 | """Constructor."""
33 | super(QTableWidget, self).__init__(parent=parent)
34 |
35 | self.project = project
36 |
37 | self.setMinimumHeight(200)
38 | self.setColumnCount(2)
39 | self.setHorizontalHeaderLabels(
40 | [self.tr("Map Theme"), self.tr("Default Active Layer")]
41 | )
42 |
43 | self.reload(configuration)
44 |
45 | def reload(self, configuration):
46 | """
47 | Load map themes into table.
48 | """
49 |
50 | self.setRowCount(0)
51 | self.setSortingEnabled(False)
52 | map_themes = self.project.mapThemeCollection().mapThemes()
53 | for map_theme in map_themes:
54 | count = self.rowCount()
55 | self.insertRow(count)
56 | item = QTableWidgetItem(map_theme)
57 | item.setData(Qt.EditRole, map_theme)
58 | self.setItem(count, 0, item)
59 |
60 | cmb = QgsMapLayerComboBox()
61 | cmb.setAllowEmptyLayer(True)
62 | if Qgis.QGIS_VERSION_INT >= 32400:
63 | cmb.setProject(self.project)
64 | cmb.setFilters(QgsMapLayerProxyModel.VectorLayer)
65 | if map_theme in configuration:
66 | cmb.setLayer(self.project.mapLayer(configuration[map_theme]))
67 | self.setCellWidget(count, 1, cmb)
68 |
69 | self.setColumnWidth(0, int(self.width() * 0.2))
70 | self.setColumnWidth(1, int(self.width() * 0.75))
71 | self.sortByColumn(0, Qt.AscendingOrder)
72 | self.setSortingEnabled(True)
73 |
74 | def createConfiguration(self):
75 | configuration = {}
76 | for i in range(self.rowCount()):
77 | item = self.item(i, 0)
78 | map_theme = item.data(Qt.EditRole)
79 | cmb = self.cellWidget(i, 1)
80 | layer_id = cmb.currentLayer().id() if cmb.currentLayer() else ""
81 | configuration[map_theme] = layer_id
82 |
83 | return configuration
84 |
--------------------------------------------------------------------------------
/qfieldsync/gui/preferences_widget.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | """
3 | /***************************************************************************
4 | QFieldSyncDialog
5 | A QGIS plugin
6 | Sync your projects to QField
7 | -------------------
8 | begin : 2015-05-20
9 | git sha : $Format:%H$
10 | copyright : (C) 2015 by OPENGIS.ch
11 | email : info@opengis.ch
12 | ***************************************************************************/
13 |
14 | /***************************************************************************
15 | * *
16 | * This program is free software; you can redistribute it and/or modify *
17 | * it under the terms of the GNU General Public License as published by *
18 | * the Free Software Foundation; either version 2 of the License, or *
19 | * (at your option) any later version. *
20 | * *
21 | ***************************************************************************/
22 | """
23 |
24 | import os
25 |
26 | from qgis.gui import QgsFileWidget, QgsOptionsPageWidget
27 | from qgis.PyQt.uic import loadUiType
28 |
29 | from qfieldsync.core.preferences import Preferences
30 | from qfieldsync.setting_manager import SettingDialog
31 |
32 | WidgetUi, _ = loadUiType(
33 | os.path.join(os.path.dirname(__file__), "../ui/preferences_widget.ui")
34 | )
35 |
36 |
37 | class PreferencesWidget(WidgetUi, QgsOptionsPageWidget, SettingDialog):
38 | def __init__(self, qfieldSync, parent=None):
39 | self.qfieldSync = qfieldSync
40 | preferences = Preferences()
41 | SettingDialog.__init__(self, setting_manager=preferences)
42 | super().__init__(parent, setting_manager=preferences)
43 | self.setupUi(self)
44 | self.init_widgets()
45 |
46 | self.setting_widget("importDirectory").widget.setStorageMode(
47 | QgsFileWidget.GetDirectory
48 | )
49 | self.setting_widget("exportDirectory").widget.setStorageMode(
50 | QgsFileWidget.GetDirectory
51 | )
52 | self.setting_widget("cloudDirectory").widget.setStorageMode(
53 | QgsFileWidget.GetDirectory
54 | )
55 |
56 | def apply(self):
57 | self.set_values_from_widgets()
58 | self.qfieldSync.update_button_visibility()
59 |
--------------------------------------------------------------------------------
/qfieldsync/gui/project_configuration_dialog.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | """
3 | /***************************************************************************
4 | -------------------
5 | begin : 21.11.2016
6 | git sha : :%H$
7 | copyright : (C) 2016 by OPENGIS.ch
8 | email : info@opengis.ch
9 | ***************************************************************************/
10 |
11 | /***************************************************************************
12 | * *
13 | * This program is free software; you can redistribute it and/or modify *
14 | * it under the terms of the GNU General Public License as published by *
15 | * the Free Software Foundation; either version 2 of the License, or *
16 | * (at your option) any later version. *
17 | * *
18 | ***************************************************************************/
19 | """
20 |
21 | from qgis.gui import QgsGui
22 | from qgis.PyQt.QtWidgets import QDialog, QDialogButtonBox, QVBoxLayout
23 |
24 | from qfieldsync.gui.project_configuration_widget import ProjectConfigurationWidget
25 |
26 |
27 | class ProjectConfigurationDialog(QDialog):
28 | """
29 | Configuration dialog for QFieldSync on a particular project.
30 | """
31 |
32 | def __init__(self, parent=None):
33 | """Constructor."""
34 | super(ProjectConfigurationDialog, self).__init__(parent=parent)
35 |
36 | self.setMinimumWidth(500)
37 | QgsGui.instance().enableAutoGeometryRestore(self)
38 |
39 | self.setWindowTitle("QFieldSync Project Properties")
40 |
41 | self.projectConfigurationWidget = ProjectConfigurationWidget(self)
42 |
43 | self.buttonBox = QDialogButtonBox(QDialogButtonBox.Ok | QDialogButtonBox.Cancel)
44 | self.buttonBox.accepted.connect(lambda: self.onAccepted())
45 | self.buttonBox.rejected.connect(self.reject)
46 |
47 | self.layout = QVBoxLayout()
48 | self.layout.addWidget(self.projectConfigurationWidget)
49 | self.layout.addWidget(self.buttonBox)
50 | self.setLayout(self.layout)
51 |
52 | def onAccepted(self):
53 | self.projectConfigurationWidget.apply()
54 | self.close()
55 |
--------------------------------------------------------------------------------
/qfieldsync/gui/relationship_configuration_widget.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | """
3 | /***************************************************************************
4 | RelationshipConfigurationWidget
5 | A QGIS plugin
6 | Sync your projects to QField
7 | -------------------
8 | begin : 2020-06-15
9 | git sha : $Format:%H$
10 | copyright : (C) 2020 by OPENGIS.ch
11 | email : info@opengis.ch
12 | ***************************************************************************/
13 |
14 | /***************************************************************************
15 | * *
16 | * This program is free software; you can redistribute it and/or modify *
17 | * it under the terms of the GNU General Public License as published by *
18 | * the Free Software Foundation; either version 2 of the License, or *
19 | * (at your option) any later version. *
20 | * *
21 | ***************************************************************************/
22 | """
23 |
24 | from qgis.core import QgsMapLayer, QgsProject
25 | from qgis.gui import QgsSpinBox
26 | from qgis.PyQt.QtCore import Qt
27 | from qgis.PyQt.QtWidgets import QTableWidget, QTableWidgetItem
28 |
29 |
30 | class RelationshipConfigurationTableWidget(QTableWidget):
31 | def __init__(self):
32 | super(RelationshipConfigurationTableWidget, self).__init__()
33 |
34 | self.setColumnCount(4)
35 | self.setHorizontalHeaderLabels(
36 | [
37 | self.tr("Layer"),
38 | "",
39 | self.tr("Relationship"),
40 | self.tr("Maximum number of items visible"),
41 | ]
42 | )
43 | self.horizontalHeader().setStretchLastSection(True)
44 | self.setRowCount(0)
45 | self.resizeColumnsToContents()
46 | self.setMinimumHeight(100)
47 |
48 | def addLayerFields(self, layer_source):
49 | layer = layer_source.layer
50 |
51 | if layer.type() != QgsMapLayer.VectorLayer:
52 | return
53 |
54 | for relation in (
55 | QgsProject.instance().relationManager().referencedRelations(layer)
56 | ):
57 | row = self.rowCount()
58 | self.insertRow(row)
59 | layer_name_item = QTableWidgetItem(layer.name())
60 | layer_name_item.setData(Qt.UserRole, layer_source)
61 | layer_name_item.setFlags(Qt.ItemIsEnabled)
62 | self.setItem(row, 0, layer_name_item)
63 | relation_id_item = QTableWidgetItem(relation.id())
64 | relation_id_item.setFlags(Qt.ItemIsEnabled)
65 | self.setItem(row, 1, relation_id_item)
66 | relation_name_item = QTableWidgetItem(relation.name())
67 | relation_name_item.setFlags(Qt.ItemIsEnabled)
68 | self.setItem(row, 2, relation_name_item)
69 | spin_item = QgsSpinBox()
70 | spin_item.setMinimum(0)
71 | spin_item.setMaximum(100)
72 | spin_item.setSingleStep(1)
73 | spin_item.setClearValue(0, self.tr("unlimited"))
74 | spin_item.setShowClearButton(True)
75 | spin_item.setValue(layer_source.relationship_maximum_visible(relation.id()))
76 | self.setCellWidget(row, 3, spin_item)
77 | self.setColumnHidden(1, True)
78 |
79 | self.resizeColumnsToContents()
80 |
81 | def setLayerColumnHidden(self, is_hidden):
82 | self.setColumnHidden(0, is_hidden)
83 |
84 | def syncLayerSourceValues(self, should_apply=False):
85 | for i in range(self.rowCount()):
86 | layer_source = self.item(i, 0).data(Qt.UserRole)
87 | relation_id = self.item(i, 1).text()
88 | relationship_maximum_visible = self.cellWidget(i, 3).value()
89 | layer_source.set_relationship_maximum_visible(
90 | relation_id, relationship_maximum_visible
91 | )
92 |
93 | if should_apply:
94 | layer_source.apply()
95 |
--------------------------------------------------------------------------------
/qfieldsync/gui/synchronize_dialog.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | """
3 | /***************************************************************************
4 | QFieldSyncDialog
5 | A QGIS plugin
6 | Sync your projects to QField
7 | -------------------
8 | begin : 2015-05-20
9 | git sha : $Format:%H$
10 | copyright : (C) 2015 by OPENGIS.ch
11 | email : info@opengis.ch
12 | ***************************************************************************/
13 |
14 | /***************************************************************************
15 | * *
16 | * This program is free software; you can redistribute it and/or modify *
17 | * it under the terms of the GNU General Public License as published by *
18 | * the Free Software Foundation; either version 2 of the License, or *
19 | * (at your option) any later version. *
20 | * *
21 | ***************************************************************************/
22 | """
23 | import os
24 | from pathlib import Path
25 |
26 | from libqfieldsync.project import ProjectConfiguration
27 | from libqfieldsync.utils.exceptions import NoProjectFoundError
28 | from libqfieldsync.utils.file_utils import (
29 | copy_attachments,
30 | get_project_in_folder,
31 | import_file_checksum,
32 | )
33 | from libqfieldsync.utils.qgis import make_temp_qgis_file, open_project
34 | from qgis.core import QgsProject
35 | from qgis.PyQt.QtCore import QDir
36 | from qgis.PyQt.QtWidgets import QDialog, QDialogButtonBox, QMessageBox
37 | from qgis.PyQt.uic import loadUiType
38 |
39 | from qfieldsync.core.preferences import Preferences
40 | from qfieldsync.gui.dirs_to_copy_widget import DirsToCopyWidget
41 | from qfieldsync.utils.qgis_utils import import_checksums_of_project
42 | from qfieldsync.utils.qt_utils import make_folder_selector
43 |
44 | DialogUi, _ = loadUiType(
45 | os.path.join(os.path.dirname(__file__), "../ui/synchronize_dialog.ui")
46 | )
47 |
48 |
49 | class SynchronizeDialog(QDialog, DialogUi):
50 | def __init__(self, iface, offline_editing, parent=None):
51 | """Constructor."""
52 | super(SynchronizeDialog, self).__init__(parent=parent)
53 | self.setupUi(self)
54 | self.iface = iface
55 | self.preferences = Preferences()
56 | self.offline_editing = offline_editing
57 | self.dirsToCopyWidget = DirsToCopyWidget()
58 |
59 | self.advancedOptionsGroupBox.layout().addWidget(self.dirsToCopyWidget)
60 |
61 | self.button_box.button(QDialogButtonBox.Save).setText(self.tr("Synchronize"))
62 | self.button_box.button(QDialogButtonBox.Save).clicked.connect(
63 | lambda: self.start_synchronization()
64 | )
65 | import_dir = self.preferences.value("importDirectoryProject")
66 | if not import_dir:
67 | import_dir = self.preferences.value("importDirectory")
68 |
69 | self.qfieldDir.setText(QDir.toNativeSeparators(import_dir))
70 | self.qfieldDir.textChanged.connect(lambda: self._on_qfield_dir_text_changed())
71 |
72 | self.offline_editing_done = False
73 |
74 | self.qfieldDir_button.clicked.connect(lambda: self._on_qfield_dir_chosen())
75 |
76 | self.dirsToCopyWidget.set_path(self.qfieldDir.text())
77 | self.dirsToCopyWidget.refresh_tree()
78 |
79 | def start_synchronization(self):
80 | self.button_box.button(QDialogButtonBox.Save).setEnabled(False)
81 | project = QgsProject.instance()
82 | current_path = Path(project.fileName())
83 | qfield_project_str_path = self.qfieldDir.text()
84 | qfield_path = Path(qfield_project_str_path)
85 | self.preferences.set_value("importDirectoryProject", str(qfield_path))
86 | self.dirsToCopyWidget.save_settings()
87 | backup_project_path = make_temp_qgis_file(project)
88 |
89 | try:
90 | current_import_file_checksum = import_file_checksum(str(qfield_path))
91 | imported_files_checksums = import_checksums_of_project(qfield_path)
92 |
93 | if (
94 | imported_files_checksums
95 | and current_import_file_checksum
96 | and current_import_file_checksum in imported_files_checksums
97 | ):
98 | message = self.tr(
99 | "Data from this file are already synchronized with the original project."
100 | )
101 | raise NoProjectFoundError(message)
102 |
103 | open_project(get_project_in_folder(str(qfield_path)))
104 |
105 | self.offline_editing.progressStopped.connect(self.update_done)
106 | self.offline_editing.layerProgressUpdated.connect(self.update_total)
107 | self.offline_editing.progressModeSet.connect(self.update_mode)
108 | self.offline_editing.progressUpdated.connect(self.update_value)
109 |
110 | try:
111 | self.offline_editing.synchronize(True)
112 | except Exception:
113 | self.offline_editing.synchronize()
114 |
115 | project_config = ProjectConfiguration(QgsProject.instance())
116 | original_path = Path(project_config.original_project_path or "")
117 |
118 | if not original_path.exists():
119 | answer = QMessageBox.warning(
120 | self,
121 | self.tr("Original project not found"),
122 | self.tr(
123 | 'The original project path at "{}" is not found. Would you like to use the currently opened project path at "{}" instead?'
124 | ).format(
125 | original_path,
126 | current_path,
127 | ),
128 | QMessageBox.Yes | QMessageBox.No,
129 | )
130 |
131 | if answer == QMessageBox.Ok:
132 | project_config.original_project_path = str(current_path)
133 | original_path = current_path
134 | else:
135 | self.iface.messageBar().pushInfo(
136 | "QFieldSync",
137 | self.tr('No original project path found at "{}"').format(
138 | original_path
139 | ),
140 | )
141 |
142 | if not self.offline_editing_done:
143 | raise NoProjectFoundError(
144 | self.tr(
145 | "The project you imported does not seem to be an offline project"
146 | )
147 | )
148 |
149 | if original_path.exists() and open_project(
150 | str(original_path), backup_project_path
151 | ):
152 | import_dirs_to_copy = self.dirsToCopyWidget.load_settings()
153 |
154 | # use the import dirs to copy selection if available, otherwise keep the old behavior
155 | if import_dirs_to_copy:
156 | for path, should_copy in import_dirs_to_copy.items():
157 | if not should_copy:
158 | continue
159 |
160 | copy_attachments(
161 | qfield_path,
162 | original_path.parent,
163 | path,
164 | )
165 | else:
166 | for attachment_dir in self.preferences.value("attachmentDirs"):
167 | copy_attachments(
168 | qfield_path,
169 | original_path.parent,
170 | attachment_dir,
171 | )
172 |
173 | # save the data_file_checksum to the project and save it
174 | imported_files_checksums.append(import_file_checksum(str(qfield_path)))
175 | ProjectConfiguration(
176 | QgsProject.instance()
177 | ).imported_files_checksums = imported_files_checksums
178 | QgsProject.instance().write()
179 | self.iface.messageBar().pushInfo(
180 | "QFieldSync",
181 | self.tr("Opened original project {}".format(original_path)),
182 | )
183 | else:
184 | self.iface.messageBar().pushInfo(
185 | "QFieldSync",
186 | self.tr(
187 | "The data has been synchronized successfully but the original project ({}) could not be opened".format(
188 | original_path
189 | )
190 | ),
191 | )
192 | self.close()
193 | except NoProjectFoundError as e:
194 | self.iface.messageBar().pushWarning("QFieldSync", str(e))
195 |
196 | def update_total(self, current, layer_count):
197 | self.totalProgressBar.setMaximum(layer_count)
198 | self.totalProgressBar.setValue(current)
199 |
200 | def update_value(self, progress):
201 | self.layerProgressBar.setValue(progress)
202 |
203 | def update_mode(self, _, mode_count):
204 | self.layerProgressBar.setMaximum(mode_count)
205 | self.layerProgressBar.setValue(0)
206 |
207 | def update_done(self):
208 | self.offline_editing.progressStopped.disconnect(self.update_done)
209 | self.offline_editing.layerProgressUpdated.disconnect(self.update_total)
210 | self.offline_editing.progressModeSet.disconnect(self.update_mode)
211 | self.offline_editing.progressUpdated.disconnect(self.update_value)
212 | self.offline_editing_done = True
213 |
214 | def _on_qfield_dir_text_changed(self):
215 | self.dirsToCopyWidget.set_path(self.qfieldDir.text())
216 |
217 | def _on_qfield_dir_chosen(self):
218 | make_folder_selector(self.qfieldDir)()
219 | self.dirsToCopyWidget.set_path(self.qfieldDir.text())
220 |
--------------------------------------------------------------------------------
/qfieldsync/gui/utils.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | """
3 | /***************************************************************************
4 | QFieldSyncDialog
5 | A QGIS plugin
6 | Sync your projects to QField
7 | -------------------
8 | begin : 2020-06-15
9 | git sha : $Format:%H$
10 | copyright : (C) 2020 by OPENGIS.ch
11 | email : info@opengis.ch
12 | ***************************************************************************/
13 |
14 | /***************************************************************************
15 | * *
16 | * This program is free software; you can redistribute it and/or modify *
17 | * it under the terms of the GNU General Public License as published by *
18 | * the Free Software Foundation; either version 2 of the License, or *
19 | * (at your option) any later version. *
20 | * *
21 | ***************************************************************************/
22 | """
23 |
24 |
25 | def set_available_actions(combobox, actions, default_action):
26 | """Sets available actions on a checkbox and selects the current one.
27 |
28 | Args:
29 | combobox (QComboBox): target combobox
30 | layer_source (LayerSource): target layer
31 | """
32 | for action, description in actions:
33 | combobox.addItem(description)
34 | combobox.setItemData(combobox.count() - 1, action)
35 |
36 | if action == default_action:
37 | combobox.setCurrentIndex(combobox.count() - 1)
38 |
--------------------------------------------------------------------------------
/qfieldsync/metadata.txt:
--------------------------------------------------------------------------------
1 | # This file contains metadata for your plugin. Since
2 | # version 2.0 of QGIS this is the proper way to supply
3 | # information about a plugin. The old method of
4 | # embedding metadata in __init__.py
5 | # is no longer supported since version 2.0.
6 |
7 | # Mandatory items:
8 |
9 | [general]
10 | name=QField Sync
11 | qgisMinimumVersion=3.22
12 | description=Sync your projects to QField
13 | version=dev
14 | author=OPENGIS.ch
15 | email=info@opengis.ch
16 |
17 | # End of mandatory metadata
18 |
19 | # Recommended items:
20 |
21 | changelog=
22 | We've been busy improving QFieldSync, enjoy this new release.
23 |
24 | Check out the complete changelog on: https://github.com/opengisch/qfieldsync/releases
25 |
26 | # Tags are comma separated with spaces allowed
27 | tags=QField, Android, iOS, Windows, Linux, mobile, smartphone, tablet, QFieldCloud, cloud, field, fieldwork, GNSS, GPS, sensors, synchronization, offline, qfieldsync, simple, collaborative
28 |
29 | homepage=https://docs.qfield.org/get-started/
30 | tracker=https://github.com/opengisch/QFieldSync/issues
31 | repository=https://github.com/opengisch/QFieldSync
32 | category=Plugins
33 | icon=resources/icon.png
34 | # experimental flag
35 | experimental=False
36 |
37 | # deprecated flag (applies to the whole plugin, not just a single version)
38 | deprecated=False
39 |
40 | about=
41 | This plugin facilitates packaging QGIS projects for QField.
42 | It analyses the current project and suggests (and performs) actions needed to make the project working on QField.
43 |
--------------------------------------------------------------------------------
/qfieldsync/resources.qrc:
--------------------------------------------------------------------------------
1 |
2 |
3 | resources/icon-refresh-128.png
4 | resources/icon-refresh-128-reverse.png
5 | resources/icon.png
6 | resources/visibility.svg
7 |
8 |
9 |
--------------------------------------------------------------------------------
/qfieldsync/resources/add.svg:
--------------------------------------------------------------------------------
1 |
2 |
16 |
--------------------------------------------------------------------------------
/qfieldsync/resources/arrow_back-green.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/qfieldsync/resources/arrow_back-orange.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/qfieldsync/resources/arrow_back-red.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/qfieldsync/resources/arrow_back.svg:
--------------------------------------------------------------------------------
1 |
2 |
16 |
--------------------------------------------------------------------------------
/qfieldsync/resources/arrow_forward-green.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/qfieldsync/resources/arrow_forward-orange.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/qfieldsync/resources/arrow_forward-red.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/qfieldsync/resources/arrow_forward.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/qfieldsync/resources/cloud.svg:
--------------------------------------------------------------------------------
1 |
2 |
15 |
--------------------------------------------------------------------------------
/qfieldsync/resources/cloud_create.svg:
--------------------------------------------------------------------------------
1 |
2 |
59 |
--------------------------------------------------------------------------------
/qfieldsync/resources/cloud_download.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/qfieldsync/resources/cloud_off.svg:
--------------------------------------------------------------------------------
1 |
2 |
15 |
--------------------------------------------------------------------------------
/qfieldsync/resources/cloud_project.svg:
--------------------------------------------------------------------------------
1 |
2 |
15 |
--------------------------------------------------------------------------------
/qfieldsync/resources/cloud_project_remote.svg:
--------------------------------------------------------------------------------
1 |
2 |
17 |
--------------------------------------------------------------------------------
/qfieldsync/resources/cloud_synchronize.svg:
--------------------------------------------------------------------------------
1 |
2 |
16 |
--------------------------------------------------------------------------------
/qfieldsync/resources/cloud_upload.svg:
--------------------------------------------------------------------------------
1 |
2 |
15 |
--------------------------------------------------------------------------------
/qfieldsync/resources/computer.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/qfieldsync/resources/delete-red.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/qfieldsync/resources/delete.svg:
--------------------------------------------------------------------------------
1 |
2 |
16 |
--------------------------------------------------------------------------------
/qfieldsync/resources/edit.svg:
--------------------------------------------------------------------------------
1 |
2 |
16 |
--------------------------------------------------------------------------------
/qfieldsync/resources/file.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/qfieldsync/resources/file_add-green.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/qfieldsync/resources/file_refresh-orange.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/qfieldsync/resources/icon.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/opengisch/qfieldsync/561b20451d9d4b152fb6e845bc8e5951340f51f9/qfieldsync/resources/icon.png
--------------------------------------------------------------------------------
/qfieldsync/resources/idea.svg:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
--------------------------------------------------------------------------------
/qfieldsync/resources/launch.svg:
--------------------------------------------------------------------------------
1 |
2 |
16 |
--------------------------------------------------------------------------------
/qfieldsync/resources/missing.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/qfieldsync/resources/package.svg:
--------------------------------------------------------------------------------
1 |
2 |
140 |
--------------------------------------------------------------------------------
/qfieldsync/resources/project_properties.svg:
--------------------------------------------------------------------------------
1 |
2 |
8 |
--------------------------------------------------------------------------------
/qfieldsync/resources/qfield_logo.svg:
--------------------------------------------------------------------------------
1 |
2 |
74 |
--------------------------------------------------------------------------------
/qfieldsync/resources/qfieldcloud_logo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/opengisch/qfieldsync/561b20451d9d4b152fb6e845bc8e5951340f51f9/qfieldsync/resources/qfieldcloud_logo.png
--------------------------------------------------------------------------------
/qfieldsync/resources/refresh-reverse.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/opengisch/qfieldsync/561b20451d9d4b152fb6e845bc8e5951340f51f9/qfieldsync/resources/refresh-reverse.png
--------------------------------------------------------------------------------
/qfieldsync/resources/refresh.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/opengisch/qfieldsync/561b20451d9d4b152fb6e845bc8e5951340f51f9/qfieldsync/resources/refresh.png
--------------------------------------------------------------------------------
/qfieldsync/resources/refresh.svg:
--------------------------------------------------------------------------------
1 |
2 |
16 |
--------------------------------------------------------------------------------
/qfieldsync/resources/sync.svg:
--------------------------------------------------------------------------------
1 |
2 |
20 |
--------------------------------------------------------------------------------
/qfieldsync/resources/sync_disabled.svg:
--------------------------------------------------------------------------------
1 |
2 |
--------------------------------------------------------------------------------
/qfieldsync/resources/synchronize.svg:
--------------------------------------------------------------------------------
1 |
2 |
107 |
--------------------------------------------------------------------------------
/qfieldsync/resources/visibility.svg:
--------------------------------------------------------------------------------
1 |
15 |
--------------------------------------------------------------------------------
/qfieldsync/resources/warning.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/opengisch/qfieldsync/561b20451d9d4b152fb6e845bc8e5951340f51f9/qfieldsync/resources/warning.png
--------------------------------------------------------------------------------
/qfieldsync/ui/cloud_login_dialog.ui:
--------------------------------------------------------------------------------
1 |
2 |
3 | QFieldCloudLoginDialog
4 |
5 |
6 |
7 | 0
8 | 0
9 | 380
10 | 390
11 |
12 |
13 |
14 | QFieldCloud Sign In
15 |
16 |
17 | -
18 |
19 |
20 |
21 | 1
22 | 0
23 |
24 |
25 |
26 | Double-click me to show or hide advanced options
27 |
28 |
29 | false
30 |
31 |
32 |
33 | -
34 |
35 |
36 |
37 | 1
38 | 0
39 |
40 |
41 |
42 | The easiest way to transfer your project from QGIS to your devices! <a href="https://qfield.cloud/">Learn more about QFieldCloud</a>.
43 |
44 |
45 | true
46 |
47 |
48 | true
49 |
50 |
51 |
52 | -
53 |
54 |
55 |
56 | 0
57 |
58 |
59 | 0
60 |
61 |
-
62 |
63 |
64 | Server URL
65 |
66 |
67 |
68 | -
69 |
70 |
71 |
72 | 0
73 | 0
74 |
75 |
76 |
77 | true
78 |
79 |
80 |
81 | -
82 |
83 |
84 | Username
85 |
86 |
87 |
88 | -
89 |
90 |
91 |
92 |
93 |
94 |
95 | -
96 |
97 |
98 | Password
99 |
100 |
101 |
102 | -
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 |
111 |
112 | -
113 |
114 |
115 | true
116 |
117 |
118 | color: red
119 |
120 |
121 |
122 |
123 |
124 | true
125 |
126 |
127 | true
128 |
129 |
130 | Qt::LinksAccessibleByKeyboard|Qt::LinksAccessibleByMouse|Qt::TextBrowserInteraction|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse
131 |
132 |
133 |
134 | -
135 |
136 |
137 | Store QFieldCloud credentials and automatically sign in on QGIS startup.
138 |
139 |
140 | Stay signed in
141 |
142 |
143 |
144 | -
145 |
146 |
147 | <html><head/><body><p>New user? <a href="https://app.qfield.cloud/accounts/signup/"><span>Register an account</span></a>.</p></body></html>
148 |
149 |
150 | Qt::RichText
151 |
152 |
153 | true
154 |
155 |
156 | Qt::LinksAccessibleByKeyboard|Qt::LinksAccessibleByMouse|Qt::TextBrowserInteraction|Qt::TextSelectableByKeyboard|Qt::TextSelectableByMouse
157 |
158 |
159 |
160 | -
161 |
162 |
163 | Qt::Vertical
164 |
165 |
166 |
167 | 40
168 | 20
169 |
170 |
171 |
172 |
173 | -
174 |
175 |
176 | QDialogButtonBox::Cancel|QDialogButtonBox::Ok
177 |
178 |
179 |
180 |
181 |
182 |
183 |
184 | QgsPasswordLineEdit
185 | QLineEdit
186 |
187 |
188 |
189 |
190 |
191 |
192 |
--------------------------------------------------------------------------------
/qfieldsync/ui/dirs_to_copy_widget.ui:
--------------------------------------------------------------------------------
1 |
2 |
3 | Form
4 |
5 |
6 |
7 | 0
8 | 0
9 | 234
10 | 146
11 |
12 |
13 |
14 | Form
15 |
16 |
17 |
18 | 0
19 |
20 |
21 | 0
22 |
23 |
24 | 0
25 |
26 |
27 | 0
28 |
29 | -
30 |
31 |
32 | Directories to be copied
33 |
34 |
35 |
36 | -
37 |
38 |
39 |
40 | Directory
41 |
42 |
43 |
44 |
45 | -
46 |
47 |
-
48 |
49 |
50 | false
51 |
52 |
53 | Refresh directories
54 |
55 |
56 | ...
57 |
58 |
59 |
60 | ../resources/refresh.svg../resources/refresh.svg
61 |
62 |
63 |
64 | -
65 |
66 |
67 | Qt::Horizontal
68 |
69 |
70 |
71 | 40
72 | 20
73 |
74 |
75 |
76 |
77 | -
78 |
79 |
80 | Select All
81 |
82 |
83 |
84 | -
85 |
86 |
87 | Deselect All
88 |
89 |
90 |
91 |
92 |
93 |
94 |
95 |
96 |
97 |
98 |
--------------------------------------------------------------------------------
/qfieldsync/ui/layers_config_widget.ui:
--------------------------------------------------------------------------------
1 |
2 |
3 | Form
4 |
5 |
6 |
7 | 0
8 | 0
9 | 343
10 | 204
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 | 0
19 |
20 |
21 | 0
22 |
23 |
24 | 0
25 |
26 |
27 | 0
28 |
29 | -
30 |
31 |
32 | false
33 |
34 |
35 |
36 |
37 |
38 | Qt::RichText
39 |
40 |
41 | true
42 |
43 |
44 |
45 | -
46 |
47 |
48 | true
49 |
50 |
51 |
52 | Layer
53 |
54 |
55 |
56 |
57 | Action
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 | -
68 |
69 |
-
70 |
71 |
72 | Filter layers...
73 |
74 |
75 | true
76 |
77 |
78 |
79 |
80 |
81 |
82 | -
83 |
84 |
85 | Show Visible Layers Only
86 |
87 |
88 |
89 | -
90 |
91 |
92 | Toggle layers
93 |
94 |
95 |
96 |
97 |
98 |
99 | ../resources/visibility.svg../resources/visibility.svg
100 |
101 |
102 | QToolButton::InstantPopup
103 |
104 |
105 | true
106 |
107 |
108 |
109 |
110 |
111 |
112 |
113 |
114 |
115 | QgsFilterLineEdit
116 | QLineEdit
117 |
118 |
119 |
120 |
121 |
122 |
123 |
--------------------------------------------------------------------------------
/qfieldsync/ui/map_layer_config_widget.ui:
--------------------------------------------------------------------------------
1 |
2 |
3 | QFieldLayerSettingsPage
4 |
5 |
6 |
7 | 0
8 | 0
9 | 500
10 | 250
11 |
12 |
13 |
14 | Form
15 |
16 |
17 | -
18 |
19 |
20 | General Settings
21 |
22 |
23 |
-
24 |
25 |
26 |
27 | 0
28 | 0
29 |
30 |
31 |
32 |
33 | -
34 |
35 |
36 | Cloud layer action
37 |
38 |
39 |
40 | -
41 |
42 |
43 | Cable layer action
44 |
45 |
46 |
47 | -
48 |
49 |
-
50 |
51 |
52 | When enabled, this option disables adding and deleting features, as well as modifying the geometries of existing features.
53 |
54 |
55 | Lock geometries
56 |
57 |
58 |
59 | -
60 |
61 |
62 | …
63 |
64 |
65 |
66 | -
67 |
68 |
69 | Qt::Horizontal
70 |
71 |
72 |
73 | 20
74 | 20
75 |
76 |
77 |
78 |
79 |
80 |
81 | -
82 |
83 |
84 |
85 |
86 |
87 | -
88 |
89 |
90 | Feature Form, Attachments and Relationships Settings
91 |
92 |
93 |
-
94 |
95 |
-
96 |
97 |
98 |
99 | 1
100 | 0
101 |
102 |
103 |
104 | Item threshold under which value map editor widgets will use a toggle buttons interface
105 |
106 |
107 | true
108 |
109 |
110 |
111 | -
112 |
113 |
114 |
115 | 0
116 | 0
117 |
118 |
119 |
120 | item(s)
121 |
122 |
123 | 0
124 |
125 |
126 | 9999999
127 |
128 |
129 | 0
130 |
131 |
132 | true
133 |
134 |
135 | false
136 |
137 |
138 |
139 |
140 |
141 |
142 |
143 |
144 | -
145 |
146 |
147 | Tracking Session
148 |
149 |
150 | true
151 |
152 |
153 | false
154 |
155 |
156 | true
157 |
158 |
159 |
-
160 |
161 |
162 | When enabled, QField will automatically start a tracking session upon successfully loading the project.
163 |
164 |
165 | true
166 |
167 |
168 |
169 | -
170 |
171 |
172 | Requirement Settings
173 |
174 |
175 |
-
176 |
177 |
178 | Enable time requirement
179 |
180 |
181 |
182 | -
183 |
184 |
185 |
186 | 1
187 | 0
188 |
189 |
190 |
191 | seconds
192 |
193 |
194 | 1
195 |
196 |
197 | 600
198 |
199 |
200 | 30
201 |
202 |
203 | true
204 |
205 |
206 | 30
207 |
208 |
209 |
210 | -
211 |
212 |
213 | Enable distance requirement
214 |
215 |
216 |
217 | -
218 |
219 |
220 |
221 | 1
222 | 0
223 |
224 |
225 |
226 | meters
227 |
228 |
229 | 1
230 |
231 |
232 | 1000
233 |
234 |
235 | 30
236 |
237 |
238 | true
239 |
240 |
241 | 30
242 |
243 |
244 |
245 | -
246 |
247 |
248 | Enable sensor data requirement
249 |
250 |
251 |
252 | -
253 |
254 |
255 | Wait for all active requirements
256 |
257 |
258 |
259 |
260 |
261 |
262 | -
263 |
264 |
265 | General Settings
266 |
267 |
268 |
-
269 |
270 |
271 | Enable erroneous distance safeguard
272 |
273 |
274 |
275 | -
276 |
277 |
278 |
279 | 1
280 | 0
281 |
282 |
283 |
284 | meters
285 |
286 |
287 | 1
288 |
289 |
290 | 1000
291 |
292 |
293 | 100
294 |
295 |
296 | true
297 |
298 |
299 | 100
300 |
301 |
302 |
303 | -
304 |
305 |
306 | Measure (M) value attached to vertices
307 |
308 |
309 | true
310 |
311 |
312 |
313 | -
314 |
315 |
316 |
317 | 1
318 | 0
319 |
320 |
321 |
322 | QComboBox::AdjustToMinimumContentsLengthWithIcon
323 |
324 |
325 |
326 |
327 |
328 |
329 |
330 |
331 |
332 |
333 |
334 |
335 |
336 | QgsCollapsibleGroupBox
337 | QGroupBox
338 |
339 | 1
340 |
341 |
342 | QgsPropertyOverrideButton
343 | QToolButton
344 | qgspropertyoverridebutton.h
345 |
346 |
347 | QgsSpinBox
348 | QSpinBox
349 |
350 |
351 |
352 |
353 |
354 |
355 |
--------------------------------------------------------------------------------
/qfieldsync/ui/package_dialog.ui:
--------------------------------------------------------------------------------
1 |
2 |
3 | QFieldPackageDialogBase
4 |
5 |
6 |
7 | 0
8 | 0
9 | 599
10 | 700
11 |
12 |
13 |
14 | Package Project for QField
15 |
16 |
17 | -
18 |
19 |
20 | 0
21 |
22 |
23 |
24 |
25 | 0
26 |
27 |
28 | 0
29 |
30 |
31 | 0
32 |
33 |
34 | 0
35 |
36 |
-
37 |
38 |
39 |
40 | 0
41 | 0
42 |
43 |
44 |
45 | The project checks provides automatic feedback for some common issues when preparing projects for QField and QFieldCloud. Find below a list of improvement suggestions and errors that must be fixed before proceeding.
46 |
47 |
48 | true
49 |
50 |
51 |
52 | -
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 | 0
61 |
62 |
63 | 0
64 |
65 |
66 | 0
67 |
68 |
69 | 0
70 |
71 | -
72 |
73 |
74 | Packaged Project Title
75 |
76 |
77 |
-
78 |
79 |
80 |
81 |
82 |
83 | -
84 |
85 |
86 | Packaged Project Filename
87 |
88 |
89 |
-
90 |
91 |
92 | QgsFileWidget::SaveFile
93 |
94 |
95 |
96 | -
97 |
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 |
106 | -
107 |
108 |
109 | Advanced
110 |
111 |
112 |
113 |
114 | -
115 |
116 |
117 | Progress
118 |
119 |
120 |
-
121 |
122 |
123 | Total
124 |
125 |
126 |
127 | -
128 |
129 |
130 | 0
131 |
132 |
133 |
134 | -
135 |
136 |
137 | Layer
138 |
139 |
140 |
141 | -
142 |
143 |
144 | 0
145 |
146 |
147 |
148 | -
149 |
150 |
151 | Qt::Vertical
152 |
153 |
154 | QSizePolicy::Expanding
155 |
156 |
157 |
158 | 16
159 | 16
160 |
161 |
162 |
163 |
164 | -
165 |
166 |
167 | Information
168 |
169 |
170 |
-
171 |
172 |
173 |
174 | 0
175 | 0
176 |
177 |
178 |
179 | The current project relies on datasets stored in localized data paths, make sure to copy the relevant datasets into the localized data path of devices running QField. On most devices, the path is <u>/QField/basemaps.</u>
180 |
181 |
182 | true
183 |
184 |
185 |
186 | -
187 |
188 |
189 |
190 | 0
191 | 0
192 |
193 |
194 |
195 |
196 |
197 |
198 | true
199 |
200 |
201 |
202 |
203 |
204 |
205 |
206 |
207 |
208 |
209 |
210 |
211 |
212 | -
213 |
214 |
215 | 0
216 |
217 |
-
218 |
219 |
220 | Qt::Horizontal
221 |
222 |
223 |
224 | 40
225 | 20
226 |
227 |
228 |
229 |
230 | -
231 |
232 |
233 | Next
234 |
235 |
236 |
237 |
238 |
239 | -
240 |
241 |
242 | Qt::Horizontal
243 |
244 |
245 | QDialogButtonBox::Close|QDialogButtonBox::Reset|QDialogButtonBox::Save
246 |
247 |
248 |
249 |
250 |
251 |
252 |
253 | QgsCollapsibleGroupBox
254 | QGroupBox
255 |
256 | 1
257 |
258 |
259 | QgsFileWidget
260 | QWidget
261 |
262 |
263 |
264 |
265 |
266 |
267 | button_box
268 | rejected()
269 | QFieldPackageDialogBase
270 | reject()
271 |
272 |
273 | 20
274 | 20
275 |
276 |
277 | 20
278 | 20
279 |
280 |
281 |
282 |
283 |
284 |
--------------------------------------------------------------------------------
/qfieldsync/ui/preferences_widget.ui:
--------------------------------------------------------------------------------
1 |
2 |
3 | QFieldPreferences
4 |
5 |
6 |
7 | 0
8 | 0
9 | 378
10 | 342
11 |
12 |
13 |
14 | QFieldSync Preferences
15 |
16 |
17 | -
18 |
19 |
20 | QFieldCloud Settings
21 |
22 |
23 |
-
24 |
25 |
26 | Default cloud directory
27 |
28 |
29 |
30 | -
31 |
32 |
33 | <span style=" font-style:italic;"><a href="https://qfield.cloud/">QFieldCloud</a> allows your team to focus on what's important, making sure you efficiently get quality field data. Thanks to its integration with QField, you and your team will be able to start surveying and digitising data collaboratively in no time.</span>
34 |
35 |
36 | true
37 |
38 |
39 | true
40 |
41 |
42 |
43 | -
44 |
45 |
46 | -
47 |
48 |
49 | Stay signed in
50 |
51 |
52 | Store QFieldCloud credentials and automatically sign in on QGIS startup.
53 |
54 |
55 |
56 |
57 |
58 |
59 | -
60 |
61 |
62 | Packaging Settings
63 |
64 |
65 |
-
66 |
67 |
68 | <span style=" font-style:italic;">QField continues to give users full control by allowing projects to be packaged and synchronized via USB cable as well as through networks.</span>
69 |
70 |
71 | true
72 |
73 |
74 | true
75 |
76 |
77 |
78 | -
79 |
80 |
81 | Default packaging import directory
82 |
83 |
84 |
85 | -
86 |
87 |
88 | -
89 |
90 |
91 | Default packaging export directory
92 |
93 |
94 |
95 | -
96 |
97 |
98 | -
99 |
100 |
101 | Show packaging actions in the toolbar
102 |
103 |
104 | Store QFieldCloud credentials and automatically sign in on QGIS startup.
105 |
106 |
107 |
108 |
109 |
110 |
111 | -
112 |
113 |
114 | Qt::Vertical
115 |
116 |
117 |
118 | 20
119 | 56
120 |
121 |
122 |
123 |
124 |
125 |
126 |
127 |
128 | QgsFileWidget
129 | QWidget
130 |
131 |
132 |
133 |
134 |
135 |
136 |
--------------------------------------------------------------------------------
/qfieldsync/ui/synchronize_dialog.ui:
--------------------------------------------------------------------------------
1 |
2 |
3 | QFieldSynchronizeBase
4 |
5 |
6 |
7 | 0
8 | 0
9 | 413
10 | 435
11 |
12 |
13 |
14 | Synchronize Project
15 |
16 |
17 | -
18 |
19 |
20 | <html><head/><body><p>Select the QField Project Folder</p></body></html>
21 |
22 |
23 | Qt::RichText
24 |
25 |
26 |
27 | -
28 |
29 |
-
30 |
31 |
32 | -
33 |
34 |
35 | ...
36 |
37 |
38 |
39 |
40 |
41 | -
42 |
43 |
44 | Advanced
45 |
46 |
47 | true
48 |
49 |
50 |
51 |
52 | -
53 |
54 |
55 | Progress
56 |
57 |
58 |
-
59 |
60 |
61 | Total
62 |
63 |
64 |
65 | -
66 |
67 |
68 | 0
69 |
70 |
71 |
72 | -
73 |
74 |
75 | Layer
76 |
77 |
78 |
79 | -
80 |
81 |
82 | 0
83 |
84 |
85 |
86 | -
87 |
88 |
89 | Qt::Vertical
90 |
91 |
92 | QSizePolicy::Expanding
93 |
94 |
95 |
96 | 16
97 | 16
98 |
99 |
100 |
101 |
102 |
103 |
104 |
105 | -
106 |
107 |
108 | Qt::Horizontal
109 |
110 |
111 | QDialogButtonBox::Close|QDialogButtonBox::Save
112 |
113 |
114 |
115 |
116 |
117 |
118 |
119 | QgsCollapsibleGroupBox
120 | QGroupBox
121 |
122 | 1
123 |
124 |
125 |
126 |
127 |
128 | button_box
129 | accepted()
130 | QFieldSynchronizeBase
131 | accept()
132 |
133 |
134 | 20
135 | 20
136 |
137 |
138 | 20
139 | 20
140 |
141 |
142 |
143 |
144 | button_box
145 | rejected()
146 | QFieldSynchronizeBase
147 | reject()
148 |
149 |
150 | 20
151 | 20
152 |
153 |
154 | 20
155 | 20
156 |
157 |
158 |
159 |
160 |
161 |
--------------------------------------------------------------------------------
/qfieldsync/utils/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/opengisch/qfieldsync/561b20451d9d4b152fb6e845bc8e5951340f51f9/qfieldsync/utils/__init__.py
--------------------------------------------------------------------------------
/qfieldsync/utils/cloud_utils.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 | """
3 | /***************************************************************************
4 | QFieldSync
5 | -------------------
6 | begin : 2020-07-13
7 | git sha : $Format:%H$
8 | copyright : (C) 2020 by OPENGIS.ch
9 | email : info@opengis.ch
10 | ***************************************************************************/
11 |
12 | /***************************************************************************
13 | * *
14 | * This program is free software; you can redistribute it and/or modify *
15 | * it under the terms of the GNU General Public License as published by *
16 | * the Free Software Foundation; either version 2 of the License, or *
17 | * (at your option) any later version. *
18 | * *
19 | ***************************************************************************/
20 | """
21 |
22 |
23 | import re
24 | from enum import Enum
25 | from pathlib import Path
26 | from typing import Tuple
27 |
28 | from libqfieldsync.utils.qgis import get_qgis_files_within_dir
29 | from qgis.PyQt.QtCore import QObject
30 |
31 |
32 | class LocalDirFeedback(Enum):
33 | Error = "error"
34 | Warning = "warning"
35 | Success = "success"
36 |
37 |
38 | def to_cloud_title(title):
39 | return re.sub("[^A-Za-z0-9-_]", "_", title)
40 |
41 |
42 | def closure(cb):
43 | def wrapper(*closure_args, **closure_kwargs):
44 | def call(*args, **kwargs):
45 | return cb(*closure_args, *args, **closure_kwargs, **kwargs)
46 |
47 | return call
48 |
49 | return wrapper
50 |
51 |
52 | def local_dir_feedback(
53 | local_dir: str,
54 | no_path_status: LocalDirFeedback = LocalDirFeedback.Error,
55 | not_dir_status: LocalDirFeedback = LocalDirFeedback.Error,
56 | not_existing_status: LocalDirFeedback = LocalDirFeedback.Warning,
57 | no_project_status: LocalDirFeedback = LocalDirFeedback.Success,
58 | single_project_status: LocalDirFeedback = LocalDirFeedback.Success,
59 | multiple_projects_status: LocalDirFeedback = LocalDirFeedback.Error,
60 | relative_status: LocalDirFeedback = LocalDirFeedback.Error,
61 | ) -> Tuple[LocalDirFeedback, str]:
62 | dummy = QObject()
63 | if not local_dir:
64 | return no_path_status, dummy.tr(
65 | "Please select local directory where the project to be stored."
66 | )
67 | elif not Path(local_dir).is_absolute():
68 | return relative_status, dummy.tr(
69 | "The entered path is a relative path. Please enter an absolute directory path."
70 | )
71 | elif Path(local_dir).exists() and not Path(local_dir).is_dir():
72 | return not_dir_status, dummy.tr(
73 | "The entered path is not an directory. Please enter a valid directory path."
74 | )
75 | elif not Path(local_dir).exists():
76 | return not_existing_status, dummy.tr(
77 | "The entered path is not an existing directory. It will be created after you submit this form."
78 | )
79 | elif len(get_qgis_files_within_dir(Path(local_dir))) == 0:
80 | message = dummy.tr("The entered path does not contain a QGIS project file yet.")
81 | status = no_project_status
82 |
83 | if single_project_status == LocalDirFeedback.Success:
84 | message += " "
85 | message += dummy.tr("You can always add one later.")
86 |
87 | return status, message
88 | elif len(get_qgis_files_within_dir(Path(local_dir))) == 1:
89 | message = dummy.tr("The entered path contains one QGIS project file.")
90 | status = single_project_status
91 |
92 | if single_project_status == LocalDirFeedback.Success:
93 | message += " "
94 | message += dummy.tr("Exactly as it should be.")
95 |
96 | return status, message
97 | else:
98 | return multiple_projects_status, dummy.tr(
99 | "Multiple project files have been found in the directory. Please leave exactly one QGIS project in the root directory."
100 | )
101 |
--------------------------------------------------------------------------------
/qfieldsync/utils/file_utils.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | """
4 | /***************************************************************************
5 | QFieldSync
6 | -------------------
7 | begin : 2016
8 | copyright : (C) 2016 by OPENGIS.ch
9 | email : info@opengis.ch
10 | ***************************************************************************/
11 |
12 | /***************************************************************************
13 | * *
14 | * This program is free software; you can redistribute it and/or modify *
15 | * it under the terms of the GNU General Public License as published by *
16 | * the Free Software Foundation; either version 2 of the License, or *
17 | * (at your option) any later version. *
18 | * *
19 | ***************************************************************************/
20 | """
21 | from enum import Enum
22 | from pathlib import Path
23 | from typing import List, TypedDict, Union
24 |
25 | PathLike = Union[Path, str]
26 |
27 |
28 | class DirectoryTreeType(str, Enum):
29 | FILE = "file"
30 | DIRECTORY = "directory"
31 |
32 |
33 | class DirectoryTreeDict(TypedDict):
34 | type: DirectoryTreeType
35 | path: Path
36 | content: List["DirectoryTreeDict"]
37 |
38 |
39 | def path_to_dict(path: PathLike, dirs_only: bool = False) -> DirectoryTreeDict:
40 | path = Path(path)
41 | node: DirectoryTreeDict = {
42 | "path": path,
43 | "content": [],
44 | }
45 |
46 | if path.is_dir():
47 | node["type"] = DirectoryTreeType.DIRECTORY
48 |
49 | glob_pattern = "*/" if dirs_only else "*"
50 | for subpath in path.glob(glob_pattern):
51 | if dirs_only and not subpath.is_dir():
52 | continue
53 |
54 | if ".qfieldsync" in str(subpath):
55 | continue
56 |
57 | node["content"].append(path_to_dict(subpath, dirs_only=dirs_only))
58 | elif not dirs_only:
59 | node["type"] = DirectoryTreeType.FILE
60 |
61 | node["content"].sort(key=lambda node: node["path"].name)
62 |
63 | return node
64 |
--------------------------------------------------------------------------------
/qfieldsync/utils/permissions.py:
--------------------------------------------------------------------------------
1 | from qfieldsync.core.cloud_project import CloudProject
2 |
3 |
4 | def can_change_project_owner(project: CloudProject) -> bool:
5 | if project.user_role == "admin":
6 | return True
7 |
8 | return False
9 |
10 |
11 | def can_delete_project(project: CloudProject) -> bool:
12 | if project.user_role == "admin":
13 | return True
14 |
15 | return False
16 |
--------------------------------------------------------------------------------
/qfieldsync/utils/qgis_utils.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | """
4 | /***************************************************************************
5 | QFieldSync
6 | -------------------
7 | begin : 2016
8 | copyright : (C) 2016 by OPENGIS.ch
9 | email : info@opengis.ch
10 | ***************************************************************************/
11 |
12 | /***************************************************************************
13 | * *
14 | * This program is free software; you can redistribute it and/or modify *
15 | * it under the terms of the GNU General Public License as published by *
16 | * the Free Software Foundation; either version 2 of the License, or *
17 | * (at your option) any later version. *
18 | * *
19 | ***************************************************************************/
20 | """
21 |
22 | from typing import List
23 |
24 | from libqfieldsync.project import ProjectConfiguration
25 | from libqfieldsync.utils.file_utils import get_project_in_folder
26 | from libqfieldsync.utils.qgis import open_project
27 | from qgis.core import QgsProject
28 |
29 |
30 | def import_checksums_of_project(dirname: str) -> List[str]:
31 | project = QgsProject.instance()
32 | qgs_file = get_project_in_folder(dirname)
33 | open_project(qgs_file)
34 | original_project_path = ProjectConfiguration(project).original_project_path
35 | open_project(original_project_path)
36 | return ProjectConfiguration(project).imported_files_checksums
37 |
--------------------------------------------------------------------------------
/qfieldsync/utils/qt_utils.py:
--------------------------------------------------------------------------------
1 | # -*- coding: utf-8 -*-
2 |
3 | """
4 | /***************************************************************************
5 | QFieldSync
6 | -------------------
7 | begin : 2016
8 | copyright : (C) 2016 by OPENGIS.ch
9 | email : info@opengis.ch
10 | ***************************************************************************/
11 |
12 | /***************************************************************************
13 | * *
14 | * This program is free software; you can redistribute it and/or modify *
15 | * it under the terms of the GNU General Public License as published by *
16 | * the Free Software Foundation; either version 2 of the License, or *
17 | * (at your option) any later version. *
18 | * *
19 | ***************************************************************************/
20 | """
21 | import os
22 | from functools import partial
23 | from typing import Callable, Optional
24 |
25 | from qgis.PyQt import QtWidgets
26 | from qgis.PyQt.QtCore import QRectF, QSize, Qt
27 | from qgis.PyQt.QtGui import QIcon, QPainter, QPainterPath, QPixmap, QTextDocument
28 | from qgis.PyQt.QtSvg import QSvgRenderer
29 | from qgis.PyQt.QtWidgets import QTreeWidgetItem
30 |
31 | from .file_utils import DirectoryTreeDict, DirectoryTreeType
32 |
33 |
34 | def selectFolder(line_edit_widget):
35 | line_edit_widget.setText(
36 | QtWidgets.QFileDialog.getExistingDirectory(directory=line_edit_widget.text())
37 | )
38 |
39 |
40 | def make_folder_selector(widget):
41 | return partial(selectFolder, line_edit_widget=widget)
42 |
43 |
44 | def make_icon(icon_name):
45 | return QIcon(os.path.join(os.path.dirname(__file__), "..", "resources", icon_name))
46 |
47 |
48 | def make_pixmap(icon_name):
49 | return QPixmap(
50 | os.path.join(os.path.dirname(__file__), "..", "resources", icon_name)
51 | )
52 |
53 |
54 | def rounded_pixmap(img_path: str, diameter: int) -> QPixmap:
55 | width, height = diameter, diameter
56 | size = QSize(height, width)
57 |
58 | target_pixmap = QPixmap(size)
59 | target_pixmap.fill(Qt.transparent)
60 |
61 | painter = QPainter(target_pixmap)
62 | painter.setRenderHint(QPainter.Antialiasing, True)
63 | painter.setRenderHint(QPainter.HighQualityAntialiasing, True)
64 | painter.setRenderHint(QPainter.SmoothPixmapTransform, True)
65 |
66 | path = QPainterPath()
67 | path.addRoundedRect(
68 | 0,
69 | 0,
70 | width,
71 | height,
72 | width / 2,
73 | height / 2,
74 | )
75 |
76 | painter.setClipPath(path)
77 |
78 | if img_path.endswith(".svg"):
79 | renderer = QSvgRenderer(img_path)
80 | renderer.render(painter, QRectF(0, 0, width, height))
81 | else:
82 | pixmap = QPixmap()
83 | pixmap = QPixmap(img_path)
84 |
85 | pixmap = pixmap.scaled(
86 | width,
87 | height,
88 | Qt.KeepAspectRatioByExpanding,
89 | Qt.SmoothTransformation,
90 | )
91 |
92 | painter.drawPixmap(0, 0, pixmap)
93 |
94 | return target_pixmap
95 |
96 |
97 | def strip_html(text: str) -> str:
98 | # strip HTML tags
99 | doc = QTextDocument()
100 | doc.setHtml(text)
101 | text = doc.toPlainText()
102 |
103 | return text
104 |
105 |
106 | def build_file_tree_widget_from_dict(
107 | parent_item: QTreeWidgetItem,
108 | node: DirectoryTreeDict,
109 | build_item_cb: Optional[Callable] = None,
110 | ):
111 | item = QTreeWidgetItem()
112 |
113 | if node["type"] == DirectoryTreeType.DIRECTORY:
114 | for subnode in node["content"]:
115 | build_file_tree_widget_from_dict(item, subnode, build_item_cb)
116 |
117 | item.setText(0, node["path"].name)
118 |
119 | should_add = None
120 | if build_item_cb:
121 | should_add = build_item_cb(item, node)
122 |
123 | if should_add is None or should_add:
124 | parent_item.addChild(item)
125 |
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | flake8==2.5.5
2 | pep8-naming
3 | flake8-respect-noqa
4 | nose2
5 | pytest
6 | future
7 | transifex-client
8 |
9 | # NOTE `libqfielsync` version should be defined in the `*.tar.gz` format, not `git+https://` to make `wheel` happy
10 | libqfieldsync @ https://github.com/opengisch/libqfieldsync/archive/9320bb884ffbf3a6e09f3788fb268ee65b52878b.tar.gz
11 |
--------------------------------------------------------------------------------
/scripts/prepare-commit.sh:
--------------------------------------------------------------------------------
1 | #!/bin/bash
2 |
3 | ROOT_DIR=$(git rev-parse --show-toplevel)
4 |
5 | autopep8 -r --in-place --ignore=E261,E265,E402,E501 $ROOT_DIR/qfieldsync/core
6 | autopep8 -r --in-place --ignore=E261,E265,E402,E501 $ROOT_DIR/qfieldsync/gui
7 |
--------------------------------------------------------------------------------
/tox.ini:
--------------------------------------------------------------------------------
1 | [flake8]
2 | ignore = N802,E501
3 | exclude=
4 | .git,
5 | qfieldsync/ui,
6 | qfieldsync/resources_*,
7 | qfieldsync/test*,
8 |
--------------------------------------------------------------------------------