From d1108e3e6af98e7304e20b98a15792141d70d32b Mon Sep 17 00:00:00 2001 From: Mathieu Pellerin Date: Sat, 13 Apr 2024 17:10:34 +0700 Subject: [PATCH 01/17] Set focus on opened code reader/drawing popup so ESC can close that instead of the app --- src/qml/QFieldSketcher.qml | 7 +++++-- src/qml/imports/QFieldControls/CodeReader.qml | 4 ++++ 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/src/qml/QFieldSketcher.qml b/src/qml/QFieldSketcher.qml index 88c5c29de9..6046e4dfa3 100644 --- a/src/qml/QFieldSketcher.qml +++ b/src/qml/QFieldSketcher.qml @@ -22,15 +22,18 @@ Popup { closePolicy: Popup.CloseOnEscape dim: true + onOpened: { + contentItem.forceActiveFocus() + } + Settings { id: settings property color strokeColor: "#000000" } - Page { + Pane { width: parent.width height: parent.height - padding: 0 DrawingCanvas { id: drawingCanvas diff --git a/src/qml/imports/QFieldControls/CodeReader.qml b/src/qml/imports/QFieldControls/CodeReader.qml index a82846aec4..0ac27ed04a 100644 --- a/src/qml/imports/QFieldControls/CodeReader.qml +++ b/src/qml/imports/QFieldControls/CodeReader.qml @@ -44,6 +44,10 @@ Popup { } } + onOpened: { + contentItem.forceActiveFocus() + } + onAboutToHide: { if (cameraLoader.active) { cameraLoader.item.camera.torchMode = Camera.TorchOff From b682c7a2d35ee007247ec262cd4621498fb5f0ad Mon Sep 17 00:00:00 2001 From: Mathieu Pellerin Date: Sun, 14 Apr 2024 09:27:57 +0700 Subject: [PATCH 02/17] Some more focus improvements and safeguard against tracker going RIP --- src/qml/DashBoard.qml | 4 ++++ src/qml/Legend.qml | 1 + src/qml/TrackerSettings.qml | 5 +++-- 3 files changed, 8 insertions(+), 2 deletions(-) diff --git a/src/qml/DashBoard.qml b/src/qml/DashBoard.qml index 6a1bd72e03..338a001d15 100644 --- a/src/qml/DashBoard.qml +++ b/src/qml/DashBoard.qml @@ -27,6 +27,10 @@ Drawer { } } + onOpened: { + contentItem.forceActiveFocus() + } + width: Math.min( 300, mainWindow.width) height: parent.height edge: Qt.LeftEdge diff --git a/src/qml/Legend.qml b/src/qml/Legend.qml index 4154e991ab..ca931267bd 100644 --- a/src/qml/Legend.qml +++ b/src/qml/Legend.qml @@ -54,6 +54,7 @@ ListView { onPressAndHold: { itemProperties.index = legend.model.index(index, 0) itemProperties.open() + itemProperties.forceActiveFocus() } onReleased: (mouse) => { if (mouse.button === Qt.RightButton) { diff --git a/src/qml/TrackerSettings.qml b/src/qml/TrackerSettings.qml index ade17cf582..34be61b4a3 100644 --- a/src/qml/TrackerSettings.qml +++ b/src/qml/TrackerSettings.qml @@ -18,7 +18,7 @@ Popup { width: parent.width - Theme.popupScreenEdgeMargin height: parent.height - Theme.popupScreenEdgeMargin * 2 modal: true - closePolicy: Popup.CloseOnEscape + closePolicy: Popup.NoAutoClose property var tracker: undefined @@ -549,6 +549,7 @@ Popup { active: false onLoaded: { item.open() + item.forceActiveFocus() } } @@ -565,7 +566,7 @@ Popup { width: parent.width - Theme.popupScreenEdgeMargin height: parent.height - Theme.popupScreenEdgeMargin * 2 modal: true - closePolicy: Popup.CloseOnEscape + closePolicy: Popup.NoAutoClose FeatureForm { id: form From f176fc5e1cc2431262b04378bac5a873fd211a31 Mon Sep 17 00:00:00 2001 From: Mathieu Pellerin Date: Sun, 14 Apr 2024 12:15:30 +0700 Subject: [PATCH 03/17] Plugin manager skeleton --- src/core/CMakeLists.txt | 2 ++ src/core/appinterface.cpp | 10 +++++++ src/core/appinterface.h | 3 +++ src/core/pluginmanager.cpp | 53 ++++++++++++++++++++++++++++++++++++++ src/core/pluginmanager.h | 39 ++++++++++++++++++++++++++++ src/core/qgismobileapp.cpp | 17 +++++------- src/core/qgismobileapp.h | 3 +++ src/qml/qgismobileapp.qml | 3 ++- 8 files changed, 119 insertions(+), 11 deletions(-) create mode 100644 src/core/pluginmanager.cpp create mode 100644 src/core/pluginmanager.h diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index 0da1545fb9..7b7c5fc490 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -79,6 +79,7 @@ set(QFIELD_CORE_SRCS networkreply.cpp orderedrelationmodel.cpp peliasgeocoder.cpp + pluginmanager.cpp resourcesource.cpp printlayoutlistmodel.cpp projectinfo.cpp @@ -189,6 +190,7 @@ set(QFIELD_CORE_HDRS networkreply.h orderedrelationmodel.h peliasgeocoder.h + pluginmanager.h resourcesource.h printlayoutlistmodel.h projectinfo.h diff --git a/src/core/appinterface.cpp b/src/core/appinterface.cpp index 86b7e9ec3d..9d9180e0ef 100644 --- a/src/core/appinterface.cpp +++ b/src/core/appinterface.cpp @@ -27,6 +27,7 @@ #include #include #include +#include #include #include #include @@ -40,6 +41,15 @@ AppInterface::AppInterface( QgisMobileapp *app ) { } +QQuickItem *AppInterface::findItemByObjectName( const QString &name ) +{ + if ( !mApp->rootObjects().isEmpty() ) + { + return mApp->rootObjects().at( 0 )->findChild( name ); + } + return nullptr; +} + void AppInterface::removeRecentProject( const QString &path ) { return mApp->removeRecentProject( path ); diff --git a/src/core/appinterface.h b/src/core/appinterface.h index 70c4a66f2a..eca906b99a 100644 --- a/src/core/appinterface.h +++ b/src/core/appinterface.h @@ -25,6 +25,7 @@ class QgisMobileapp; class QgsRectangle; class QgsFeature; +class QQuickItem; class AppInterface : public QObject { @@ -94,6 +95,8 @@ class AppInterface : public QObject */ Q_INVOKABLE void clearProject() const; + Q_INVOKABLE QQuickItem *findItemByObjectName( const QString &name ); + static void setInstance( AppInterface *instance ) { sAppInterface = instance; } static AppInterface *instance() { return sAppInterface; } diff --git a/src/core/pluginmanager.cpp b/src/core/pluginmanager.cpp new file mode 100644 index 0000000000..5135ee60d6 --- /dev/null +++ b/src/core/pluginmanager.cpp @@ -0,0 +1,53 @@ +/*************************************************************************** + pluginmanager.h - PluginManager + + --------------------- + begin : 14.05.2024 + copyright : (C) 2024 by Mathieu Pellerin + email : mathieu (at) opengis.ch + *************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ + +#include "pluginmanager.h" + +#include +#include +#include +#include +#include + +PluginManager::PluginManager( QQmlEngine *engine ) + : QObject( engine ) + , mEngine( engine ) +{ +} + +void PluginManager::loadPlugin( const QString &pluginPath ) +{ + if ( mLoadedPlugins.contains( pluginPath ) ) + { + unloadPlugin( pluginPath ); + } + + QQmlComponent component( mEngine, pluginPath, this ); + QObject *object = component.create( mEngine->rootContext() ); + mLoadedPlugins.insert( pluginPath, QPointer( object ) ); +} + +void PluginManager::unloadPlugin( const QString &pluginPath ) +{ + if ( mLoadedPlugins.contains( pluginPath ) ) + { + if ( mLoadedPlugins[pluginPath] ) + { + mLoadedPlugins[pluginPath]->deleteLater(); + mLoadedPlugins.remove( pluginPath ); + } + } +} diff --git a/src/core/pluginmanager.h b/src/core/pluginmanager.h new file mode 100644 index 0000000000..3db4707fb5 --- /dev/null +++ b/src/core/pluginmanager.h @@ -0,0 +1,39 @@ +/*************************************************************************** + pluginmanager.h - PluginManager + + --------------------- + begin : 14.05.2024 + copyright : (C) 2024 by Mathieu Pellerin + email : mathieu (at) opengis.ch + *************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ + +#ifndef PLUGINMANAGER_H +#define PLUGINMANAGER_H + +#include +#include + +class PluginManager : public QObject +{ + Q_OBJECT + + public: + PluginManager( QQmlEngine *engine ); + ~PluginManager() = default; + + void loadPlugin( const QString &pluginPath ); + void unloadPlugin( const QString &pluginPath ); + + private: + QQmlEngine *mEngine = nullptr; + QMap> mLoadedPlugins; +}; + +#endif // PLUGINMANAGER_H diff --git a/src/core/qgismobileapp.cpp b/src/core/qgismobileapp.cpp index 06f52f2123..353a76a524 100644 --- a/src/core/qgismobileapp.cpp +++ b/src/core/qgismobileapp.cpp @@ -270,12 +270,7 @@ QgisMobileapp::QgisMobileapp( QgsApplication *app, QObject *parent ) mBookmarkModel = std::make_unique( QgsApplication::bookmarkManager(), mProject->bookmarkManager(), nullptr ); mDrawingTemplateModel = std::make_unique( this ); - // Transition from 1.8 to 1.8.1+ - const QString deviceAddress = settings.value( QStringLiteral( "positioningDevice" ), QString() ).toString(); - if ( deviceAddress == QStringLiteral( "internal" ) ) - { - settings.setValue( QStringLiteral( "positioningDevice" ), QString() ); - } + mPluginManager = std::make_unique( this ); // cppcheck-suppress leakReturnValNotUsed initDeclarative(); @@ -317,7 +312,6 @@ QgisMobileapp::QgisMobileapp( QgsApplication *app, QObject *parent ) mMapCanvas = rootObjects().first()->findChild(); Q_ASSERT_X( mMapCanvas, "QML Init", "QgsQuickMapCanvasMap not found. It is likely that we failed to load the QML files. Check debug output for related messages." ); - mMapCanvas->mapSettings()->setProject( mProject ); mBookmarkModel->setMapSettings( mMapCanvas->mapSettings() ); @@ -522,8 +516,9 @@ void QgisMobileapp::initDeclarative() REGISTER_SINGLETON( "org.qfield", PositioningUtils, "PositioningUtils" ); REGISTER_SINGLETON( "org.qfield", CoordinateReferenceSystemUtils, "CoordinateReferenceSystemUtils" ); - qmlRegisterUncreatableType( "org.qgis", 1, 0, "QgisInterface", "QgisInterface is only provided by the environment and cannot be created ad-hoc" ); + qmlRegisterUncreatableType( "org.qgis", 1, 0, "AppInterface", "AppInterface is only provided by the environment and cannot be created ad-hoc" ); qmlRegisterUncreatableType( "org.qgis", 1, 0, "Settings", "" ); + qmlRegisterUncreatableType( "org.qfield", 1, 0, "PluginManager", "" ); qmlRegisterUncreatableType( "org.qfield", 1, 0, "PlatformUtilities", "" ); qmlRegisterUncreatableType( "org.qfield", 1, 0, "FlatLayerTreeModel", "The FlatLayerTreeModel is available as context property `flatLayerTree`." ); qmlRegisterUncreatableType( "org.qfield", 1, 0, "TrackingModel", "The TrackingModel is available as context property `trackingModel`." ); @@ -545,8 +540,8 @@ void QgisMobileapp::initDeclarative() rootContext()->setContextProperty( "systemFontPointSize", PlatformUtilities::instance()->systemFontPointSize() ); rootContext()->setContextProperty( "mouseDoubleClickInterval", QApplication::styleHints()->mouseDoubleClickInterval() ); rootContext()->setContextProperty( "qgisProject", mProject ); - rootContext()->setContextProperty( "bookmarkModel", mBookmarkModel.get() ); rootContext()->setContextProperty( "iface", mIface ); + rootContext()->setContextProperty( "pluginManager", mPluginManager.get() ); rootContext()->setContextProperty( "settings", &mSettings ); rootContext()->setContextProperty( "appVersion", qfield::appVersion ); rootContext()->setContextProperty( "appVersionStr", qfield::appVersionStr ); @@ -556,7 +551,7 @@ void QgisMobileapp::initDeclarative() rootContext()->setContextProperty( "CrsFactory", QVariant::fromValue( mCrsFactory ) ); rootContext()->setContextProperty( "UnitTypes", QVariant::fromValue( mUnitTypes ) ); rootContext()->setContextProperty( "ExifTools", QVariant::fromValue( mExifTools ) ); - rootContext()->setContextProperty( "LocatorModelNoGroup", QgsLocatorModel::NoGroup ); + rootContext()->setContextProperty( "bookmarkModel", mBookmarkModel.get() ); rootContext()->setContextProperty( "gpkgFlusher", mGpkgFlusher.get() ); rootContext()->setContextProperty( "layerObserver", mLayerObserver.get() ); rootContext()->setContextProperty( "featureHistory", mFeatureHistory.get() ); @@ -646,6 +641,8 @@ void QgisMobileapp::onAfterFirstRendering() if ( mFirstRenderingFlag ) { + mPluginManager->loadPlugin( QStringLiteral( "file:///home/webmaster/Desktop/tinystep.qml" ) ); + if ( PlatformUtilities::instance()->hasQgsProject() ) { PlatformUtilities::instance()->checkWriteExternalStoragePermissions(); diff --git a/src/core/qgismobileapp.h b/src/core/qgismobileapp.h index d120d37a47..ba67bc8230 100644 --- a/src/core/qgismobileapp.h +++ b/src/core/qgismobileapp.h @@ -32,6 +32,7 @@ #include "appcoordinateoperationhandlers.h" #include "bookmarkmodel.h" #include "drawingtemplatemodel.h" +#include "pluginmanager.h" #include "qfield_core_export.h" #include "qfieldappauthrequesthandler.h" #include "qgsgpkgflusher.h" @@ -233,6 +234,8 @@ class QFIELD_CORE_EXPORT QgisMobileapp : public QQmlApplicationEngine std::unique_ptr mDrawingTemplateModel; std::unique_ptr mMessageLogModel; + std::unique_ptr mPluginManager; + // Dummy objects. We are not able to call static functions from QML, so we need something here. QgsCoordinateReferenceSystem mCrsFactory; QgsUnitTypes mUnitTypes; diff --git a/src/qml/qgismobileapp.qml b/src/qml/qgismobileapp.qml index 68d803b3f3..85d0c9477d 100644 --- a/src/qml/qgismobileapp.qml +++ b/src/qml/qgismobileapp.qml @@ -264,6 +264,7 @@ ApplicationWindow { Item { id: mapCanvas + objectName: "mapCanvas" clip: true DragHandler { @@ -3826,7 +3827,7 @@ ApplicationWindow { WelcomeScreen { id: welcomeScreen - objectName: 'welcomeScreen' + objectName: "welcomeScreen" visible: !iface.hasProjectOnLaunch() model: RecentProjectListModel { From 37bff8e7591242ff582612e8a0afff87078e726d Mon Sep 17 00:00:00 2001 From: Mathieu Pellerin Date: Sun, 14 Apr 2024 17:50:27 +0700 Subject: [PATCH 04/17] Add project plugin handling --- src/core/pluginmanager.cpp | 65 ++++++++++++++++++++++++++++++++++++-- src/core/pluginmanager.h | 12 ++++++- src/core/qgismobileapp.cpp | 17 ++++++++-- src/qml/qgismobileapp.qml | 50 +++++++++++++++++++++++++++++ 4 files changed, 138 insertions(+), 6 deletions(-) diff --git a/src/core/pluginmanager.cpp b/src/core/pluginmanager.cpp index 5135ee60d6..5b5ffa1d80 100644 --- a/src/core/pluginmanager.cpp +++ b/src/core/pluginmanager.cpp @@ -16,11 +16,12 @@ #include "pluginmanager.h" +#include +#include #include #include #include -#include -#include +#include PluginManager::PluginManager( QQmlEngine *engine ) : QObject( engine ) @@ -28,8 +29,28 @@ PluginManager::PluginManager( QQmlEngine *engine ) { } -void PluginManager::loadPlugin( const QString &pluginPath ) +void PluginManager::loadPlugin( const QString &pluginPath, bool skipPermissionCheck ) { + if ( !skipPermissionCheck ) + { + QSettings settings; + settings.beginGroup( QStringLiteral( "/qfield/plugins/%1" ).arg( pluginPath ) ); + const QStringList keys = settings.childKeys(); + if ( keys.contains( QStringLiteral( "permissionGranted" ) ) ) + { + if ( !settings.value( QStringLiteral( "permissionGranted" ) ).toBool() ) + { + return; + } + } + else + { + mPermissionRequestPluginPath = pluginPath; + emit pluginPermissionRequested(); + return; + } + } + if ( mLoadedPlugins.contains( pluginPath ) ) { unloadPlugin( pluginPath ); @@ -51,3 +72,41 @@ void PluginManager::unloadPlugin( const QString &pluginPath ) } } } + +const QString PluginManager::findProjectPlugin( const QString &projectPath ) const +{ + QFileInfo fi( projectPath ); + const QString pluginPath = QStringLiteral( "%1/%2.qml" ).arg( fi.absolutePath(), fi.completeBaseName() ); + if ( QFileInfo::exists( pluginPath ) ) + { + return pluginPath; + } + return QString(); +} + +void PluginManager::grantRequestedPluginPermission( bool permanent ) +{ + if ( permanent ) + { + QSettings settings; + settings.beginGroup( QStringLiteral( "/qfield/plugins/%1" ).arg( mPermissionRequestPluginPath ) ); + settings.setValue( QStringLiteral( "permissionGranted" ), true ); + settings.endGroup(); + } + + loadPlugin( mPermissionRequestPluginPath, true ); + mPermissionRequestPluginPath.clear(); +} + +void PluginManager::denyRequestedPluginPermission( bool permanent ) +{ + if ( permanent ) + { + QSettings settings; + settings.beginGroup( QStringLiteral( "/qfield/plugins/%1" ).arg( mPermissionRequestPluginPath ) ); + settings.setValue( QStringLiteral( "permissionGranted" ), false ); + settings.endGroup(); + } + + mPermissionRequestPluginPath.clear(); +} diff --git a/src/core/pluginmanager.h b/src/core/pluginmanager.h index 3db4707fb5..a579693ab1 100644 --- a/src/core/pluginmanager.h +++ b/src/core/pluginmanager.h @@ -28,12 +28,22 @@ class PluginManager : public QObject PluginManager( QQmlEngine *engine ); ~PluginManager() = default; - void loadPlugin( const QString &pluginPath ); + void loadPlugin( const QString &pluginPath, bool skipPermissionCheck = false ); void unloadPlugin( const QString &pluginPath ); + const QString findProjectPlugin( const QString &projectPath ) const; + + Q_INVOKABLE void grantRequestedPluginPermission( bool permanent = false ); + Q_INVOKABLE void denyRequestedPluginPermission( bool permanent = false ); + + signals: + void pluginPermissionRequested(); + private: QQmlEngine *mEngine = nullptr; QMap> mLoadedPlugins; + + QString mPermissionRequestPluginPath; }; #endif // PLUGINMANAGER_H diff --git a/src/core/qgismobileapp.cpp b/src/core/qgismobileapp.cpp index 353a76a524..64e49651a9 100644 --- a/src/core/qgismobileapp.cpp +++ b/src/core/qgismobileapp.cpp @@ -641,8 +641,6 @@ void QgisMobileapp::onAfterFirstRendering() if ( mFirstRenderingFlag ) { - mPluginManager->loadPlugin( QStringLiteral( "file:///home/webmaster/Desktop/tinystep.qml" ) ); - if ( PlatformUtilities::instance()->hasQgsProject() ) { PlatformUtilities::instance()->checkWriteExternalStoragePermissions(); @@ -698,6 +696,10 @@ bool QgisMobileapp::loadProjectFile( const QString &path, const QString &name ) { saveProjectPreviewImage(); + if ( !mProjectFilePath.isEmpty() ) + { + mPluginManager->unloadPlugin( mPluginManager->findProjectPlugin( mProjectFilePath ) ); + } mAuthRequestHandler->clearStoredRealms(); mProjectFilePath = path; @@ -1161,6 +1163,12 @@ void QgisMobileapp::readProjectFile() emit loadProjectEnded( mProjectFilePath, mProjectFileName ); connect( mMapCanvas, &QgsQuickMapCanvasMap::mapCanvasRefreshed, this, &QgisMobileapp::onMapCanvasRefreshed ); + + const QString projectPluginPath = mPluginManager->findProjectPlugin( mProjectFilePath ); + if ( !projectPluginPath.isEmpty() ) + { + mPluginManager->loadPlugin( projectPluginPath ); + } } QString QgisMobileapp::readProjectEntry( const QString &scope, const QString &key, const QString &def ) const @@ -1381,7 +1389,12 @@ bool QgisMobileapp::event( QEvent *event ) void QgisMobileapp::clearProject() { + if ( !mProjectFilePath.isEmpty() ) + { + mPluginManager->unloadPlugin( mPluginManager->findProjectPlugin( mProjectFilePath ) ); + } mAuthRequestHandler->clearStoredRealms(); + mProjectFileName = QString(); mProjectFilePath = QString(); mProject->clear(); diff --git a/src/qml/qgismobileapp.qml b/src/qml/qgismobileapp.qml index 85d0c9477d..df8b51dd0a 100644 --- a/src/qml/qgismobileapp.qml +++ b/src/qml/qgismobileapp.qml @@ -4013,4 +4013,54 @@ ApplicationWindow { id: screenLocker enabled: false } + + Dialog { + id: pluginPermissionDialog + parent: mainWindow.contentItem + + visible: false + modal: true + font: Theme.defaultFont + + z: 10000 // 1000s are embedded feature forms, user a higher value to insure the dialog will always show above embedded feature forms + x: ( mainWindow.width - width ) / 2 + y: ( mainWindow.height - height ) / 2 + + title: qsTr( "Plugin Permission" ) + + Column { + width: parent.width + + Label { + width: parent.width + wrapMode: Text.WordWrap + text: qsTr( "The project contains a plugin, do you want to activate it?" ) + } + + CheckBox { + id: permanentCheckBox + width: parent.width + text: qsTr('Remember my choice') + font: Theme.defaultFont + } + } + + onAccepted: { + pluginManager.grantRequestedPluginPermission(permanentCheckBox.checked) + } + + onRejected: { + pluginManager.denyRequestedPluginPermission(permanentCheckBox.checked) + } + + standardButtons: Dialog.Yes | Dialog.No + } + + Connections { + target: pluginManager + + function onPluginPermissionRequested() { + pluginPermissionDialog.open() + } + } } From cb5f9fe5513f703bd3a5b48629fc7e32655141fe Mon Sep 17 00:00:00 2001 From: Mathieu Pellerin Date: Sun, 14 Apr 2024 17:57:35 +0700 Subject: [PATCH 05/17] Generalize permissio dialog description --- src/core/pluginmanager.cpp | 6 +++--- src/core/pluginmanager.h | 4 ++-- src/core/qgismobileapp.cpp | 2 +- src/qml/qgismobileapp.qml | 10 ++++------ 4 files changed, 10 insertions(+), 12 deletions(-) diff --git a/src/core/pluginmanager.cpp b/src/core/pluginmanager.cpp index 5b5ffa1d80..57379dde54 100644 --- a/src/core/pluginmanager.cpp +++ b/src/core/pluginmanager.cpp @@ -29,7 +29,7 @@ PluginManager::PluginManager( QQmlEngine *engine ) { } -void PluginManager::loadPlugin( const QString &pluginPath, bool skipPermissionCheck ) +void PluginManager::loadPlugin( const QString &pluginPath, const QString &pluginName, bool skipPermissionCheck ) { if ( !skipPermissionCheck ) { @@ -46,7 +46,7 @@ void PluginManager::loadPlugin( const QString &pluginPath, bool skipPermissionCh else { mPermissionRequestPluginPath = pluginPath; - emit pluginPermissionRequested(); + emit pluginPermissionRequested( pluginName ); return; } } @@ -94,7 +94,7 @@ void PluginManager::grantRequestedPluginPermission( bool permanent ) settings.endGroup(); } - loadPlugin( mPermissionRequestPluginPath, true ); + loadPlugin( mPermissionRequestPluginPath, QString(), true ); mPermissionRequestPluginPath.clear(); } diff --git a/src/core/pluginmanager.h b/src/core/pluginmanager.h index a579693ab1..f92983063d 100644 --- a/src/core/pluginmanager.h +++ b/src/core/pluginmanager.h @@ -28,7 +28,7 @@ class PluginManager : public QObject PluginManager( QQmlEngine *engine ); ~PluginManager() = default; - void loadPlugin( const QString &pluginPath, bool skipPermissionCheck = false ); + void loadPlugin( const QString &pluginPath, const QString &pluginName, bool skipPermissionCheck = false ); void unloadPlugin( const QString &pluginPath ); const QString findProjectPlugin( const QString &projectPath ) const; @@ -37,7 +37,7 @@ class PluginManager : public QObject Q_INVOKABLE void denyRequestedPluginPermission( bool permanent = false ); signals: - void pluginPermissionRequested(); + void pluginPermissionRequested( const QString &pluginName ); private: QQmlEngine *mEngine = nullptr; diff --git a/src/core/qgismobileapp.cpp b/src/core/qgismobileapp.cpp index 64e49651a9..136d428dc4 100644 --- a/src/core/qgismobileapp.cpp +++ b/src/core/qgismobileapp.cpp @@ -1167,7 +1167,7 @@ void QgisMobileapp::readProjectFile() const QString projectPluginPath = mPluginManager->findProjectPlugin( mProjectFilePath ); if ( !projectPluginPath.isEmpty() ) { - mPluginManager->loadPlugin( projectPluginPath ); + mPluginManager->loadPlugin( projectPluginPath, tr( "Project Plugin" ) ); } } diff --git a/src/qml/qgismobileapp.qml b/src/qml/qgismobileapp.qml index df8b51dd0a..d415074e6c 100644 --- a/src/qml/qgismobileapp.qml +++ b/src/qml/qgismobileapp.qml @@ -4026,20 +4026,17 @@ ApplicationWindow { x: ( mainWindow.width - width ) / 2 y: ( mainWindow.height - height ) / 2 - title: qsTr( "Plugin Permission" ) + title: '' Column { - width: parent.width - Label { width: parent.width wrapMode: Text.WordWrap - text: qsTr( "The project contains a plugin, do you want to activate it?" ) + text: qsTr( "Do you grant permission to activate this plugin?" ) } CheckBox { id: permanentCheckBox - width: parent.width text: qsTr('Remember my choice') font: Theme.defaultFont } @@ -4059,7 +4056,8 @@ ApplicationWindow { Connections { target: pluginManager - function onPluginPermissionRequested() { + function onPluginPermissionRequested(pluginName) { + pluginPermissionDialog.title = pluginName pluginPermissionDialog.open() } } From 174229b4f7c25a20bbd00f4349ab6aae86493fef Mon Sep 17 00:00:00 2001 From: Mathieu Pellerin Date: Sun, 14 Apr 2024 18:41:20 +0700 Subject: [PATCH 06/17] Better separate QGIS and QField-specific type registration --- src/core/qgismobileapp.cpp | 54 ++++++++++++++++++++------------------ 1 file changed, 28 insertions(+), 26 deletions(-) diff --git a/src/core/qgismobileapp.cpp b/src/core/qgismobileapp.cpp index 136d428dc4..8a96a95ae0 100644 --- a/src/core/qgismobileapp.cpp +++ b/src/core/qgismobileapp.cpp @@ -361,6 +361,8 @@ void QgisMobileapp::initDeclarative() #endif addImportPath( QStringLiteral( "qrc:/qml/imports" ) ); + qRegisterMetaType( "QVariant::Type" ); + // Register QGIS QML types qmlRegisterType( "org.qgis", 1, 0, "SnappingUtils" ); qmlRegisterType( "org.qgis", 1, 0, "MapLayerModel" ); @@ -380,19 +382,9 @@ void QgisMobileapp::initDeclarative() qRegisterMetaType( "QgsSnappingConfig" ); qRegisterMetaType( "QgsRelation" ); qRegisterMetaType( "QgsPolymorphicRelation" ); - qRegisterMetaType( "PlatformUtilities::Capabilities" ); qRegisterMetaType( "QgsField" ); - qRegisterMetaType( "QVariant::Type" ); qRegisterMetaType( "QgsDefaultValue" ); qRegisterMetaType( "QgsFieldConstraints" ); - qRegisterMetaType( "GeometryOperationResult" ); - qRegisterMetaType( "QFieldCloudConnection::ConnectionStatus" ); - qRegisterMetaType( "CloudUserInformation" ); - qRegisterMetaType( "QFieldCloudProjectsModel::ProjectStatus" ); - qRegisterMetaType( "QFieldCloudProjectsModel::ProjectCheckout" ); - qRegisterMetaType( "QFieldCloudProjectsModel::ProjectModification" ); - qRegisterMetaType( "Tracker::MeasureType" ); - qRegisterMetaType( "Positioning::ElevationCorrectionMode" ); qRegisterMetaType( "Qgis::GeometryType" ); qRegisterMetaType( "Qgis::WkbType" ); @@ -402,6 +394,7 @@ void QgisMobileapp::initDeclarative() qRegisterMetaType( "Qgis::AngleUnit" ); qRegisterMetaType( "Qgis::DeviceConnectionStatus" ); qRegisterMetaType( "Qgis::SnappingMode" ); + qmlRegisterUncreatableType( "org.qgis", 1, 0, "Qgis", "" ); qmlRegisterUncreatableType( "org.qgis", 1, 0, "Project", "" ); @@ -418,22 +411,31 @@ void QgisMobileapp::initDeclarative() qmlRegisterType( "org.qgis", 1, 0, "MapSettings" ); qmlRegisterType( "org.qfield", 1, 0, "CoordinateTransformer" ); qmlRegisterType( "org.qgis", 1, 0, "ElevationProfileCanvas" ); - qmlRegisterType( "org.qgis", 1, 0, "MapTransform" ); // Register QField QML types - qmlRegisterType( "org.qgis", 1, 0, "MultiFeatureListModel" ); - qmlRegisterType( "org.qgis", 1, 0, "FeatureListModel" ); - qmlRegisterType( "org.qgis", 1, 0, "FeatureListModelSelection" ); - qmlRegisterType( "org.qgis", 1, 0, "FeaturelistExtentController" ); - qmlRegisterType( "org.qgis", 1, 0, "Geometry" ); - qmlRegisterType( "org.qgis", 1, 0, "ModelHelper" ); - qmlRegisterType( "org.qgis", 1, 0, "RubberbandShape" ); - qmlRegisterType( "org.qgis", 1, 0, "RubberbandModel" ); - qmlRegisterType( "org.qgis", 1, 0, "ResourceSource" ); - qmlRegisterType( "org.qgis", 1, 0, "ProjectInfo" ); - qmlRegisterType( "org.qgis", 1, 0, "ProjectSource" ); - qmlRegisterType( "org.qgis", 1, 0, "ViewStatus" ); + qRegisterMetaType( "PlatformUtilities::Capabilities" ); + qRegisterMetaType( "GeometryOperationResult" ); + qRegisterMetaType( "QFieldCloudConnection::ConnectionStatus" ); + qRegisterMetaType( "CloudUserInformation" ); + qRegisterMetaType( "QFieldCloudProjectsModel::ProjectStatus" ); + qRegisterMetaType( "QFieldCloudProjectsModel::ProjectCheckout" ); + qRegisterMetaType( "QFieldCloudProjectsModel::ProjectModification" ); + qRegisterMetaType( "Tracker::MeasureType" ); + qRegisterMetaType( "Positioning::ElevationCorrectionMode" ); + + qmlRegisterType( "org.qfield", 1, 0, "MultiFeatureListModel" ); + qmlRegisterType( "org.qfield", 1, 0, "FeatureListModel" ); + qmlRegisterType( "org.qfield", 1, 0, "FeatureListModelSelection" ); + qmlRegisterType( "org.qfield", 1, 0, "FeaturelistExtentController" ); + qmlRegisterType( "org.qfield", 1, 0, "Geometry" ); + qmlRegisterType( "org.qfield", 1, 0, "ModelHelper" ); + qmlRegisterType( "org.qfield", 1, 0, "RubberbandShape" ); + qmlRegisterType( "org.qfield", 1, 0, "RubberbandModel" ); + qmlRegisterType( "org.qfield", 1, 0, "ResourceSource" ); + qmlRegisterType( "org.qfield", 1, 0, "ProjectInfo" ); + qmlRegisterType( "org.qfield", 1, 0, "ProjectSource" ); + qmlRegisterType( "org.qfield", 1, 0, "ViewStatus" ); qmlRegisterType( "org.qfield", 1, 0, "DigitizingLogger" ); qmlRegisterType( "org.qfield", 1, 0, "AttributeFormModel" ); @@ -459,7 +461,7 @@ void QgisMobileapp::initDeclarative() qmlRegisterType( "org.qfield", 1, 0, "RecentProjectListModel" ); qmlRegisterType( "org.qfield", 1, 0, "ReferencingFeatureListModel" ); qmlRegisterType( "org.qfield", 1, 0, "OrderedRelationModel" ); - qmlRegisterType( "org.qgis", 1, 0, "FeatureCheckListModel" ); + qmlRegisterType( "org.qfield", 1, 0, "FeatureCheckListModel" ); qmlRegisterType( "org.qfield", 1, 0, "GeometryEditorsModel" ); qmlRegisterType( "org.qfield", 1, 0, "ExpressionEvaluator" ); #ifdef WITH_BLUETOOTH @@ -516,8 +518,8 @@ void QgisMobileapp::initDeclarative() REGISTER_SINGLETON( "org.qfield", PositioningUtils, "PositioningUtils" ); REGISTER_SINGLETON( "org.qfield", CoordinateReferenceSystemUtils, "CoordinateReferenceSystemUtils" ); - qmlRegisterUncreatableType( "org.qgis", 1, 0, "AppInterface", "AppInterface is only provided by the environment and cannot be created ad-hoc" ); - qmlRegisterUncreatableType( "org.qgis", 1, 0, "Settings", "" ); + qmlRegisterUncreatableType( "org.qfield", 1, 0, "AppInterface", "AppInterface is only provided by the environment and cannot be created ad-hoc" ); + qmlRegisterUncreatableType( "org.qfield", 1, 0, "SettingsInterface", "" ); qmlRegisterUncreatableType( "org.qfield", 1, 0, "PluginManager", "" ); qmlRegisterUncreatableType( "org.qfield", 1, 0, "PlatformUtilities", "" ); qmlRegisterUncreatableType( "org.qfield", 1, 0, "FlatLayerTreeModel", "The FlatLayerTreeModel is available as context property `flatLayerTree`." ); From 6e3b2291a3c1dd257851699e4a8d2ba254a39de2 Mon Sep 17 00:00:00 2001 From: Mathieu Pellerin Date: Mon, 15 Apr 2024 10:09:52 +0700 Subject: [PATCH 07/17] Add iface method to insert item in a plugins-dedicated toolbar --- src/core/appinterface.cpp | 11 ++++++++++- src/core/appinterface.h | 10 +++++++++- src/qml/qgismobileapp.qml | 16 +++++++++++++--- 3 files changed, 32 insertions(+), 5 deletions(-) diff --git a/src/core/appinterface.cpp b/src/core/appinterface.cpp index 9d9180e0ef..0f1c54cb19 100644 --- a/src/core/appinterface.cpp +++ b/src/core/appinterface.cpp @@ -41,7 +41,7 @@ AppInterface::AppInterface( QgisMobileapp *app ) { } -QQuickItem *AppInterface::findItemByObjectName( const QString &name ) +QQuickItem *AppInterface::findItemByObjectName( const QString &name ) const { if ( !mApp->rootObjects().isEmpty() ) { @@ -50,6 +50,15 @@ QQuickItem *AppInterface::findItemByObjectName( const QString &name ) return nullptr; } +void AppInterface::addItemToPluginsToolbar( QQuickItem *item ) const +{ + if ( !mApp->rootObjects().isEmpty() ) + { + QQuickItem *pluginsToolbar = mApp->rootObjects().at( 0 )->findChild( QStringLiteral( "pluginsToolbar" ) ); + item->setParentItem( pluginsToolbar ); + } +} + void AppInterface::removeRecentProject( const QString &path ) { return mApp->removeRecentProject( path ); diff --git a/src/core/appinterface.h b/src/core/appinterface.h index eca906b99a..ca3fcda417 100644 --- a/src/core/appinterface.h +++ b/src/core/appinterface.h @@ -95,7 +95,15 @@ class AppInterface : public QObject */ Q_INVOKABLE void clearProject() const; - Q_INVOKABLE QQuickItem *findItemByObjectName( const QString &name ); + /** + * Returns the item matching the provided object \a name + */ + Q_INVOKABLE QQuickItem *findItemByObjectName( const QString &name ) const; + + /** + * Adds an \a item in the plugins toolbar container + */ + Q_INVOKABLE void addItemToPluginsToolbar( QQuickItem *item ) const; static void setInstance( AppInterface *instance ) { sAppInterface = instance; } static AppInterface *instance() { return sAppInterface; } diff --git a/src/qml/qgismobileapp.qml b/src/qml/qgismobileapp.qml index d415074e6c..a8d45b535d 100644 --- a/src/qml/qgismobileapp.qml +++ b/src/qml/qgismobileapp.qml @@ -1143,6 +1143,16 @@ ApplicationWindow { source: featureForm } + Column { + id: pluginsToolbar + objectName: "pluginsToolbar" + + anchors.right: locatorItem.right + anchors.top: locatorItem.bottom + anchors.topMargin: 4 + spacing: 4 + } + QfToolButton { id: alertIcon iconSource: Theme.getThemeVectorIcon( "ic_alert_black_24dp" ) @@ -1151,9 +1161,9 @@ ApplicationWindow { visible: !screenLocker.enabled && messageLog.unreadMessages - anchors.right: locatorItem.right - anchors.top: locatorItem.top - anchors.topMargin: 52 + anchors.right: pluginsToolbar.right + anchors.top: pluginsToolbar.bottom + anchors.topMargin: 4 onClicked: messageLog.visible = true } From ad5cfb2712059ac67312ac218b669675fe603fe8 Mon Sep 17 00:00:00 2001 From: Mathieu Pellerin Date: Mon, 15 Apr 2024 10:20:18 +0700 Subject: [PATCH 08/17] Add iface method to insert item in the main menu actions toolbar --- src/core/appinterface.cpp | 11 +++++++++++ src/core/appinterface.h | 5 +++++ src/qml/qgismobileapp.qml | 15 ++++++++------- 3 files changed, 24 insertions(+), 7 deletions(-) diff --git a/src/core/appinterface.cpp b/src/core/appinterface.cpp index 0f1c54cb19..176be01fe1 100644 --- a/src/core/appinterface.cpp +++ b/src/core/appinterface.cpp @@ -59,6 +59,17 @@ void AppInterface::addItemToPluginsToolbar( QQuickItem *item ) const } } +void AppInterface::addItemToMainMenuActionsToolbar( QQuickItem *item ) const +{ + if ( !mApp->rootObjects().isEmpty() ) + { + QQuickItem *pluginsToolbar = mApp->rootObjects().at( 0 )->findChild( QStringLiteral( "mainMenuActionsToolbar" ) ); + item->setParentItem( pluginsToolbar ); + const QList childItems = pluginsToolbar->childItems(); + item->stackBefore( childItems.at( childItems.size() - 3 ) ); + } +} + void AppInterface::removeRecentProject( const QString &path ) { return mApp->removeRecentProject( path ); diff --git a/src/core/appinterface.h b/src/core/appinterface.h index ca3fcda417..1ad3e031a2 100644 --- a/src/core/appinterface.h +++ b/src/core/appinterface.h @@ -105,6 +105,11 @@ class AppInterface : public QObject */ Q_INVOKABLE void addItemToPluginsToolbar( QQuickItem *item ) const; + /** + * Adds an \a item in the main menu action toolbar container + */ + Q_INVOKABLE void addItemToMainMenuActionsToolbar( QQuickItem *item ) const; + static void setInstance( AppInterface *instance ) { sAppInterface = instance; } static AppInterface *instance() { return sAppInterface; } diff --git a/src/qml/qgismobileapp.qml b/src/qml/qgismobileapp.qml index a8d45b535d..6b0c98c4ad 100644 --- a/src/qml/qgismobileapp.qml +++ b/src/qml/qgismobileapp.qml @@ -2247,7 +2247,7 @@ ApplicationWindow { bottomMargin: sceneBottomMargin width: { - var actionRowResult = actionsRow.childrenRect.width + 4 + var actionRowResult = mainMenuActionToolbar.childrenRect.width + 4 var result = 0; var padding = 0; // Skip first Row item @@ -2260,7 +2260,8 @@ ApplicationWindow { } Row { - id: actionsRow + id: mainMenuActionToolbar + objectName: "mainMenuActionsToolbar" leftPadding: 2 rightPadding: 2 spacing: 2 @@ -2276,7 +2277,7 @@ ApplicationWindow { round: true iconSource: Theme.getThemeVectorIcon( "ic_home_black_24dp" ) iconColor: Theme.mainTextColor - bgcolor: hovered ? actionsRow.hoveredColor : "#00ffffff" + bgcolor: hovered ? parent.hoveredColor : "#00ffffff" onClicked: { mainMenu.close() @@ -2294,7 +2295,7 @@ ApplicationWindow { round: true iconSource: Theme.getThemeVectorIcon( "ic_measurement_black_24dp" ) iconColor: Theme.mainTextColor - bgcolor: hovered ? actionsRow.hoveredColor : "#00ffffff" + bgcolor: hovered ? parent.hoveredColor : "#00ffffff" onClicked: { mainMenu.close() @@ -2311,7 +2312,7 @@ ApplicationWindow { round: true iconSource: Theme.getThemeVectorIcon( "ic_lock_black_24dp" ) iconColor: Theme.mainTextColor - bgcolor: hovered ? actionsRow.hoveredColor : "#00ffffff" + bgcolor: hovered ? parent.hoveredColor : "#00ffffff" onClicked: { mainMenu.close() @@ -2329,7 +2330,7 @@ ApplicationWindow { round: true iconSource: Theme.getThemeVectorIcon( "ic_undo_black_24dp" ) iconColor: isEnabled ? Theme.mainTextColor : Theme.mainTextDisabledColor - bgcolor: isEnabled && hovered ? actionsRow.hoveredColor : "#00ffffff" + bgcolor: isEnabled && hovered ? parent.hoveredColor : "#00ffffff" onClicked: { if (isEnabled) { @@ -2354,7 +2355,7 @@ ApplicationWindow { round: true iconSource: Theme.getThemeVectorIcon( "ic_redo_black_24dp" ) iconColor: isEnabled ? Theme.mainTextColor : Theme.mainTextDisabledColor - bgcolor: isEnabled && hovered ? actionsRow.hoveredColor : "#00ffffff" + bgcolor: isEnabled && hovered ? parent.hoveredColor : "#00ffffff" onClicked: { if (isEnabled) { From 1f0776ef5e3f5f896d8c04f6c7d545112b48c0f8 Mon Sep 17 00:00:00 2001 From: Mathieu Pellerin Date: Mon, 15 Apr 2024 10:47:39 +0700 Subject: [PATCH 09/17] Add iface method to insert item in a new map canvas actions toolbar --- src/core/appinterface.cpp | 25 ++++++++++++++++++------- src/core/appinterface.h | 7 ++++++- src/qml/qgismobileapp.qml | 38 ++++++++++++++++++++++++++++---------- 3 files changed, 52 insertions(+), 18 deletions(-) diff --git a/src/core/appinterface.cpp b/src/core/appinterface.cpp index 176be01fe1..0a6f641134 100644 --- a/src/core/appinterface.cpp +++ b/src/core/appinterface.cpp @@ -41,11 +41,11 @@ AppInterface::AppInterface( QgisMobileapp *app ) { } -QQuickItem *AppInterface::findItemByObjectName( const QString &name ) const +QObject *AppInterface::findItemByObjectName( const QString &name ) const { if ( !mApp->rootObjects().isEmpty() ) { - return mApp->rootObjects().at( 0 )->findChild( name ); + return mApp->rootObjects().at( 0 )->findChild( name ); } return nullptr; } @@ -54,8 +54,8 @@ void AppInterface::addItemToPluginsToolbar( QQuickItem *item ) const { if ( !mApp->rootObjects().isEmpty() ) { - QQuickItem *pluginsToolbar = mApp->rootObjects().at( 0 )->findChild( QStringLiteral( "pluginsToolbar" ) ); - item->setParentItem( pluginsToolbar ); + QQuickItem *toolbar = mApp->rootObjects().at( 0 )->findChild( QStringLiteral( "pluginsToolbar" ) ); + item->setParentItem( toolbar ); } } @@ -63,13 +63,24 @@ void AppInterface::addItemToMainMenuActionsToolbar( QQuickItem *item ) const { if ( !mApp->rootObjects().isEmpty() ) { - QQuickItem *pluginsToolbar = mApp->rootObjects().at( 0 )->findChild( QStringLiteral( "mainMenuActionsToolbar" ) ); - item->setParentItem( pluginsToolbar ); - const QList childItems = pluginsToolbar->childItems(); + QQuickItem *toolbar = mApp->rootObjects().at( 0 )->findChild( QStringLiteral( "mainMenuActionsToolbar" ) ); + item->setParentItem( toolbar ); + + // Place the item to the left of the Undo/Redo buttons + const QList childItems = toolbar->childItems(); item->stackBefore( childItems.at( childItems.size() - 3 ) ); } } +void AppInterface::addItemToCanvasActionsToolbar( QQuickItem *item ) const +{ + if ( !mApp->rootObjects().isEmpty() ) + { + QQuickItem *toolbar = mApp->rootObjects().at( 0 )->findChild( QStringLiteral( "canvasMenuActionsToolbar" ) ); + item->setParentItem( toolbar ); + } +} + void AppInterface::removeRecentProject( const QString &path ) { return mApp->removeRecentProject( path ); diff --git a/src/core/appinterface.h b/src/core/appinterface.h index 1ad3e031a2..69f80ad4b0 100644 --- a/src/core/appinterface.h +++ b/src/core/appinterface.h @@ -98,7 +98,7 @@ class AppInterface : public QObject /** * Returns the item matching the provided object \a name */ - Q_INVOKABLE QQuickItem *findItemByObjectName( const QString &name ) const; + Q_INVOKABLE QObject *findItemByObjectName( const QString &name ) const; /** * Adds an \a item in the plugins toolbar container @@ -110,6 +110,11 @@ class AppInterface : public QObject */ Q_INVOKABLE void addItemToMainMenuActionsToolbar( QQuickItem *item ) const; + /** + * Adds an \a item in the main menu action toolbar container + */ + Q_INVOKABLE void addItemToCanvasActionsToolbar( QQuickItem *item ) const; + static void setInstance( AppInterface *instance ) { sAppInterface = instance; } static AppInterface *instance() { return sAppInterface; } diff --git a/src/qml/qgismobileapp.qml b/src/qml/qgismobileapp.qml index 6b0c98c4ad..d4aec88453 100644 --- a/src/qml/qgismobileapp.qml +++ b/src/qml/qgismobileapp.qml @@ -2247,7 +2247,7 @@ ApplicationWindow { bottomMargin: sceneBottomMargin width: { - var actionRowResult = mainMenuActionToolbar.childrenRect.width + 4 + var toolbarWidth = mainMenuActionToolbar.childrenRect.width + 4 var result = 0; var padding = 0; // Skip first Row item @@ -2256,7 +2256,7 @@ ApplicationWindow { result = Math.max(item.contentItem.implicitWidth, result); padding = Math.max(item.padding, padding); } - return Math.max(actionRowResult, result + padding * 2); + return Math.max(toolbarWidth, result + padding * 2); } Row { @@ -2674,6 +2674,8 @@ ApplicationWindow { Menu { id: canvasMenu + objectName: "canvasMenu" + title: qsTr( "Map Canvas Options" ) font: Theme.defaultFont @@ -2699,16 +2701,32 @@ ApplicationWindow { bottomMargin: sceneBottomMargin width: { - var result = 0; - var padding = 0; - for (var i = 0; i < count; ++i) { - var item = itemAt(i); - result = Math.max(item.contentItem.implicitWidth, result); - padding = Math.max(item.padding, padding); - } - return Math.min( result + padding * 2,mainWindow.width - 20); + var toolbarWidth = canvasMenuActionsToolbar.childrenRect.width + 4 + var result = 0; + var padding = 0; + // Skip first Row item + for (var i = 1; i < count; ++i) { + var item = itemAt(i); + result = Math.max(item.contentItem.implicitWidth, result); + padding = Math.max(item.padding, padding); + } + return Math.min(Math.max(toolbarWidth, result + padding * 2), mainWindow.width - 20); } + Row { + id: canvasMenuActionsToolbar + objectName: "canvasMenuActionsToolbar" + leftPadding: 2 + rightPadding: 2 + spacing: 2 + height: children.length > 0 ? addBookmarkItem.height : 0 + clip: true + + property color hoveredColor: Qt.hsla(Theme.mainTextColor.hslHue, Theme.mainTextColor.hslSaturation, Theme.mainTextColor.hslLightness, 0.2) + } + + MenuSeparator { width: parent.width; height: canvasMenuActionsToolbar.children.length > 0 ? undefined : 0 } + MenuItem { id: xItem text: "" From bc43828afa578cdc664c3dbf4819304a7f4659c7 Mon Sep 17 00:00:00 2001 From: Mathieu Pellerin Date: Mon, 15 Apr 2024 11:18:19 +0700 Subject: [PATCH 10/17] Fix identified feature placement in canvas menu --- src/qml/qgismobileapp.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/qml/qgismobileapp.qml b/src/qml/qgismobileapp.qml index d4aec88453..cedad427b6 100644 --- a/src/qml/qgismobileapp.qml +++ b/src/qml/qgismobileapp.qml @@ -2902,7 +2902,7 @@ ApplicationWindow { } } - onObjectAdded: (index, object) => { canvasMenu.insertMenu(index+9, object) } + onObjectAdded: (index, object) => { canvasMenu.insertMenu(index + 11, object) } onObjectRemoved: (index, object) => { canvasMenu.removeMenu(object) } } } From d3f2fea23ef365d0aa53b3d3e387b45557456653 Mon Sep 17 00:00:00 2001 From: Mathieu Pellerin Date: Mon, 15 Apr 2024 11:32:32 +0700 Subject: [PATCH 11/17] Add an iface.mapCanvas() to get to the main map canvas easily --- src/core/appinterface.cpp | 9 +++++++++ src/core/appinterface.h | 5 +++++ 2 files changed, 14 insertions(+) diff --git a/src/core/appinterface.cpp b/src/core/appinterface.cpp index 0a6f641134..035dff0f5d 100644 --- a/src/core/appinterface.cpp +++ b/src/core/appinterface.cpp @@ -81,6 +81,15 @@ void AppInterface::addItemToCanvasActionsToolbar( QQuickItem *item ) const } } +QObject *AppInterface::mapCanvas() const +{ + if ( !mApp->rootObjects().isEmpty() ) + { + return mApp->rootObjects().at( 0 )->findChild( "mapCanvas" ); + } + return nullptr; +} + void AppInterface::removeRecentProject( const QString &path ) { return mApp->removeRecentProject( path ); diff --git a/src/core/appinterface.h b/src/core/appinterface.h index 69f80ad4b0..db099f2d76 100644 --- a/src/core/appinterface.h +++ b/src/core/appinterface.h @@ -115,6 +115,11 @@ class AppInterface : public QObject */ Q_INVOKABLE void addItemToCanvasActionsToolbar( QQuickItem *item ) const; + /** + * Returns the main map canvas. + */ + Q_INVOKABLE QObject *mapCanvas() const; + static void setInstance( AppInterface *instance ) { sAppInterface = instance; } static AppInterface *instance() { return sAppInterface; } From 55ea0d6f24cb8e67574a21e6ee065fbf989c33cb Mon Sep 17 00:00:00 2001 From: Mathieu Pellerin Date: Mon, 15 Apr 2024 11:55:47 +0700 Subject: [PATCH 12/17] Make cppcheck happy and adopt some suggestions --- src/core/pluginmanager.cpp | 22 +++++++++++----------- src/core/pluginmanager.h | 8 ++++---- src/core/qgismobileapp.cpp | 6 +++--- 3 files changed, 18 insertions(+), 18 deletions(-) diff --git a/src/core/pluginmanager.cpp b/src/core/pluginmanager.cpp index 57379dde54..068975bf9d 100644 --- a/src/core/pluginmanager.cpp +++ b/src/core/pluginmanager.cpp @@ -73,17 +73,6 @@ void PluginManager::unloadPlugin( const QString &pluginPath ) } } -const QString PluginManager::findProjectPlugin( const QString &projectPath ) const -{ - QFileInfo fi( projectPath ); - const QString pluginPath = QStringLiteral( "%1/%2.qml" ).arg( fi.absolutePath(), fi.completeBaseName() ); - if ( QFileInfo::exists( pluginPath ) ) - { - return pluginPath; - } - return QString(); -} - void PluginManager::grantRequestedPluginPermission( bool permanent ) { if ( permanent ) @@ -110,3 +99,14 @@ void PluginManager::denyRequestedPluginPermission( bool permanent ) mPermissionRequestPluginPath.clear(); } + +QString PluginManager::findProjectPlugin( const QString &projectPath ) +{ + const QFileInfo fi( projectPath ); + const QString pluginPath = QStringLiteral( "%1/%2.qml" ).arg( fi.absolutePath(), fi.completeBaseName() ); + if ( QFileInfo::exists( pluginPath ) ) + { + return pluginPath; + } + return QString(); +} diff --git a/src/core/pluginmanager.h b/src/core/pluginmanager.h index f92983063d..f07fa5df91 100644 --- a/src/core/pluginmanager.h +++ b/src/core/pluginmanager.h @@ -25,17 +25,17 @@ class PluginManager : public QObject Q_OBJECT public: - PluginManager( QQmlEngine *engine ); - ~PluginManager() = default; + explicit PluginManager( QQmlEngine *engine ); + ~PluginManager() override = default; void loadPlugin( const QString &pluginPath, const QString &pluginName, bool skipPermissionCheck = false ); void unloadPlugin( const QString &pluginPath ); - const QString findProjectPlugin( const QString &projectPath ) const; - Q_INVOKABLE void grantRequestedPluginPermission( bool permanent = false ); Q_INVOKABLE void denyRequestedPluginPermission( bool permanent = false ); + static QString findProjectPlugin( const QString &projectPath ); + signals: void pluginPermissionRequested( const QString &pluginName ); diff --git a/src/core/qgismobileapp.cpp b/src/core/qgismobileapp.cpp index 8a96a95ae0..6e3c0ef76b 100644 --- a/src/core/qgismobileapp.cpp +++ b/src/core/qgismobileapp.cpp @@ -700,7 +700,7 @@ bool QgisMobileapp::loadProjectFile( const QString &path, const QString &name ) if ( !mProjectFilePath.isEmpty() ) { - mPluginManager->unloadPlugin( mPluginManager->findProjectPlugin( mProjectFilePath ) ); + mPluginManager->unloadPlugin( PluginManager::findProjectPlugin( mProjectFilePath ) ); } mAuthRequestHandler->clearStoredRealms(); @@ -1166,7 +1166,7 @@ void QgisMobileapp::readProjectFile() connect( mMapCanvas, &QgsQuickMapCanvasMap::mapCanvasRefreshed, this, &QgisMobileapp::onMapCanvasRefreshed ); - const QString projectPluginPath = mPluginManager->findProjectPlugin( mProjectFilePath ); + const QString projectPluginPath = PluginManager::findProjectPlugin( mProjectFilePath ); if ( !projectPluginPath.isEmpty() ) { mPluginManager->loadPlugin( projectPluginPath, tr( "Project Plugin" ) ); @@ -1393,7 +1393,7 @@ void QgisMobileapp::clearProject() { if ( !mProjectFilePath.isEmpty() ) { - mPluginManager->unloadPlugin( mPluginManager->findProjectPlugin( mProjectFilePath ) ); + mPluginManager->unloadPlugin( PluginManager::findProjectPlugin( mProjectFilePath ) ); } mAuthRequestHandler->clearStoredRealms(); From 29606bb527d2c0e9445f36d097cf1e1ba2c72736 Mon Sep 17 00:00:00 2001 From: Mathieu Pellerin Date: Mon, 15 Apr 2024 14:53:51 +0700 Subject: [PATCH 13/17] Let it be --- src/qml/qgismobileapp.qml | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/qml/qgismobileapp.qml b/src/qml/qgismobileapp.qml index cedad427b6..68a42ee566 100644 --- a/src/qml/qgismobileapp.qml +++ b/src/qml/qgismobileapp.qml @@ -2247,12 +2247,12 @@ ApplicationWindow { bottomMargin: sceneBottomMargin width: { - var toolbarWidth = mainMenuActionToolbar.childrenRect.width + 4 - var result = 0; - var padding = 0; + const toolbarWidth = mainMenuActionsToolbar.childrenRect.width + 4 + let result = 0; + let padding = 0; // Skip first Row item - for (var i = 1; i < count; ++i) { - var item = itemAt(i); + for (let i = 1; i < count; ++i) { + const item = itemAt(i); result = Math.max(item.contentItem.implicitWidth, result); padding = Math.max(item.padding, padding); } @@ -2260,7 +2260,7 @@ ApplicationWindow { } Row { - id: mainMenuActionToolbar + id: mainMenuActionsToolbar objectName: "mainMenuActionsToolbar" leftPadding: 2 rightPadding: 2 @@ -2701,12 +2701,12 @@ ApplicationWindow { bottomMargin: sceneBottomMargin width: { - var toolbarWidth = canvasMenuActionsToolbar.childrenRect.width + 4 - var result = 0; - var padding = 0; + const toolbarWidth = canvasMenuActionsToolbar.childrenRect.width + 4 + let result = 0; + let padding = 0; // Skip first Row item - for (var i = 1; i < count; ++i) { - var item = itemAt(i); + for (let i = 1; i < count; ++i) { + const item = itemAt(i); result = Math.max(item.contentItem.implicitWidth, result); padding = Math.max(item.padding, padding); } From b79b2774d78929abd832e03dec1e543626e31496 Mon Sep 17 00:00:00 2001 From: Mathieu Pellerin Date: Mon, 15 Apr 2024 15:56:03 +0700 Subject: [PATCH 14/17] Add a method to reach the main window (needed for popup et al) --- src/core/appinterface.cpp | 9 +++++++++ src/core/appinterface.h | 5 +++++ src/qml/qgismobileapp.qml | 2 ++ 3 files changed, 16 insertions(+) diff --git a/src/core/appinterface.cpp b/src/core/appinterface.cpp index 035dff0f5d..08ec4ee0c4 100644 --- a/src/core/appinterface.cpp +++ b/src/core/appinterface.cpp @@ -81,6 +81,15 @@ void AppInterface::addItemToCanvasActionsToolbar( QQuickItem *item ) const } } +QObject *AppInterface::mainWindow() const +{ + if ( !mApp->rootObjects().isEmpty() ) + { + return mApp->rootObjects().at( 0 ); + } + return nullptr; +} + QObject *AppInterface::mapCanvas() const { if ( !mApp->rootObjects().isEmpty() ) diff --git a/src/core/appinterface.h b/src/core/appinterface.h index db099f2d76..a817da3cfe 100644 --- a/src/core/appinterface.h +++ b/src/core/appinterface.h @@ -115,6 +115,11 @@ class AppInterface : public QObject */ Q_INVOKABLE void addItemToCanvasActionsToolbar( QQuickItem *item ) const; + /** + * Returns the main window. + */ + Q_INVOKABLE QObject *mainWindow() const; + /** * Returns the main map canvas. */ diff --git a/src/qml/qgismobileapp.qml b/src/qml/qgismobileapp.qml index 68a42ee566..db778e060f 100644 --- a/src/qml/qgismobileapp.qml +++ b/src/qml/qgismobileapp.qml @@ -200,6 +200,8 @@ ApplicationWindow { */ Positioning { id: positionSource + objectName: "positionSource" + deviceId: positioningSettings.positioningDevice property bool currentness: false; From a12e4554d472d0479b4f68a6f0ca14c606f5af09 Mon Sep 17 00:00:00 2001 From: Mathieu Pellerin Date: Mon, 15 Apr 2024 18:36:14 +0700 Subject: [PATCH 15/17] Show plugin errors and warnings in the message log to help authors and users --- src/core/pluginmanager.cpp | 24 ++++++++++++++++++++++++ src/core/pluginmanager.h | 3 +++ 2 files changed, 27 insertions(+) diff --git a/src/core/pluginmanager.cpp b/src/core/pluginmanager.cpp index 068975bf9d..37dea8505a 100644 --- a/src/core/pluginmanager.cpp +++ b/src/core/pluginmanager.cpp @@ -22,11 +22,13 @@ #include #include #include +#include PluginManager::PluginManager( QQmlEngine *engine ) : QObject( engine ) , mEngine( engine ) { + connect( mEngine, &QQmlEngine::warnings, this, &PluginManager::handleWarnings ); } void PluginManager::loadPlugin( const QString &pluginPath, const QString &pluginName, bool skipPermissionCheck ) @@ -57,6 +59,14 @@ void PluginManager::loadPlugin( const QString &pluginPath, const QString &plugin } QQmlComponent component( mEngine, pluginPath, this ); + if ( component.status() == QQmlComponent::Status::Error ) + { + for ( const QQmlError &error : component.errors() ) + { + QgsMessageLog::logMessage( error.toString(), QStringLiteral( "Plugin Manager" ), Qgis::MessageLevel::Critical ); + } + return; + } QObject *object = component.create( mEngine->rootContext() ); mLoadedPlugins.insert( pluginPath, QPointer( object ) ); } @@ -73,6 +83,20 @@ void PluginManager::unloadPlugin( const QString &pluginPath ) } } +void PluginManager::handleWarnings( const QList &warnings ) +{ + for ( const QQmlError &warning : warnings ) + { + if ( warning.url().isLocalFile() ) + { + if ( mLoadedPlugins.keys().contains( warning.url().toLocalFile() ) ) + { + QgsMessageLog::logMessage( warning.toString(), QStringLiteral( "Plugin Manager" ), Qgis::MessageLevel::Warning ); + } + } + } +} + void PluginManager::grantRequestedPluginPermission( bool permanent ) { if ( permanent ) diff --git a/src/core/pluginmanager.h b/src/core/pluginmanager.h index f07fa5df91..066050e0a1 100644 --- a/src/core/pluginmanager.h +++ b/src/core/pluginmanager.h @@ -39,6 +39,9 @@ class PluginManager : public QObject signals: void pluginPermissionRequested( const QString &pluginName ); + private slots: + void handleWarnings( const QList &warnings ); + private: QQmlEngine *mEngine = nullptr; QMap> mLoadedPlugins; From 3e75141239ea144392ff56047a8a16cbb43e94c0 Mon Sep 17 00:00:00 2001 From: Mathieu Pellerin Date: Tue, 16 Apr 2024 11:52:09 +0700 Subject: [PATCH 16/17] Repeat plugin name in the permission dialog's description label --- src/qml/qgismobileapp.qml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/qml/qgismobileapp.qml b/src/qml/qgismobileapp.qml index db778e060f..d70fabddcb 100644 --- a/src/qml/qgismobileapp.qml +++ b/src/qml/qgismobileapp.qml @@ -4063,7 +4063,7 @@ ApplicationWindow { Label { width: parent.width wrapMode: Text.WordWrap - text: qsTr( "Do you grant permission to activate this plugin?" ) + text: qsTr( "Do you grant permission to activate `%1`?" ).arg( pluginPermissionDialog.title ) } CheckBox { From 8bb80737902cb3780ab66281c99e30d4ff997f9c Mon Sep 17 00:00:00 2001 From: Mathieu Pellerin Date: Tue, 16 Apr 2024 12:09:11 +0700 Subject: [PATCH 17/17] Rely on QObject parenting --- src/core/qgismobileapp.cpp | 16 ++++++++-------- src/core/qgismobileapp.h | 8 ++++---- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/core/qgismobileapp.cpp b/src/core/qgismobileapp.cpp index 6e3c0ef76b..4c00b2c1c1 100644 --- a/src/core/qgismobileapp.cpp +++ b/src/core/qgismobileapp.cpp @@ -201,7 +201,7 @@ QgisMobileapp::QgisMobileapp( QgsApplication *app, QObject *parent ) palette.setColor( QPalette::LinkVisited, QColor( 128, 204, 40 ) ); app->setPalette( palette ); - mMessageLogModel = std::make_unique( this ); + mMessageLogModel = new MessageLogModel( this ); QSettings settings; if ( PlatformUtilities::instance()->capabilities() & PlatformUtilities::AdjustBrightness ) @@ -267,10 +267,10 @@ QgisMobileapp::QgisMobileapp( QgsApplication *app, QObject *parent ) mLocalFilesImageProvider = new LocalFilesImageProvider(); mProjectsImageProvider = new ProjectsImageProvider(); - mBookmarkModel = std::make_unique( QgsApplication::bookmarkManager(), mProject->bookmarkManager(), nullptr ); - mDrawingTemplateModel = std::make_unique( this ); + mBookmarkModel = new BookmarkModel( QgsApplication::bookmarkManager(), mProject->bookmarkManager(), this ); + mDrawingTemplateModel = new DrawingTemplateModel( this ); - mPluginManager = std::make_unique( this ); + mPluginManager = new PluginManager( this ); // cppcheck-suppress leakReturnValNotUsed initDeclarative(); @@ -543,7 +543,7 @@ void QgisMobileapp::initDeclarative() rootContext()->setContextProperty( "mouseDoubleClickInterval", QApplication::styleHints()->mouseDoubleClickInterval() ); rootContext()->setContextProperty( "qgisProject", mProject ); rootContext()->setContextProperty( "iface", mIface ); - rootContext()->setContextProperty( "pluginManager", mPluginManager.get() ); + rootContext()->setContextProperty( "pluginManager", mPluginManager ); rootContext()->setContextProperty( "settings", &mSettings ); rootContext()->setContextProperty( "appVersion", qfield::appVersion ); rootContext()->setContextProperty( "appVersionStr", qfield::appVersionStr ); @@ -553,12 +553,12 @@ void QgisMobileapp::initDeclarative() rootContext()->setContextProperty( "CrsFactory", QVariant::fromValue( mCrsFactory ) ); rootContext()->setContextProperty( "UnitTypes", QVariant::fromValue( mUnitTypes ) ); rootContext()->setContextProperty( "ExifTools", QVariant::fromValue( mExifTools ) ); - rootContext()->setContextProperty( "bookmarkModel", mBookmarkModel.get() ); + rootContext()->setContextProperty( "bookmarkModel", mBookmarkModel ); rootContext()->setContextProperty( "gpkgFlusher", mGpkgFlusher.get() ); rootContext()->setContextProperty( "layerObserver", mLayerObserver.get() ); rootContext()->setContextProperty( "featureHistory", mFeatureHistory.get() ); - rootContext()->setContextProperty( "messageLogModel", mMessageLogModel.get() ); - rootContext()->setContextProperty( "drawingTemplateModel", mDrawingTemplateModel.get() ); + rootContext()->setContextProperty( "messageLogModel", mMessageLogModel ); + rootContext()->setContextProperty( "drawingTemplateModel", mDrawingTemplateModel ); rootContext()->setContextProperty( "qfieldAuthRequestHandler", mAuthRequestHandler ); diff --git a/src/core/qgismobileapp.h b/src/core/qgismobileapp.h index ba67bc8230..bbf8efc4af 100644 --- a/src/core/qgismobileapp.h +++ b/src/core/qgismobileapp.h @@ -230,11 +230,11 @@ class QFIELD_CORE_EXPORT QgisMobileapp : public QQmlApplicationEngine std::unique_ptr mFeatureHistory; QFieldAppAuthRequestHandler *mAuthRequestHandler = nullptr; - std::unique_ptr mBookmarkModel; - std::unique_ptr mDrawingTemplateModel; - std::unique_ptr mMessageLogModel; + BookmarkModel *mBookmarkModel = nullptr; + DrawingTemplateModel *mDrawingTemplateModel = nullptr; + MessageLogModel *mMessageLogModel = nullptr; - std::unique_ptr mPluginManager; + PluginManager *mPluginManager = nullptr; // Dummy objects. We are not able to call static functions from QML, so we need something here. QgsCoordinateReferenceSystem mCrsFactory;