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..08ec4ee0c4 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,64 @@ AppInterface::AppInterface( QgisMobileapp *app ) { } +QObject *AppInterface::findItemByObjectName( const QString &name ) const +{ + if ( !mApp->rootObjects().isEmpty() ) + { + return mApp->rootObjects().at( 0 )->findChild( name ); + } + return nullptr; +} + +void AppInterface::addItemToPluginsToolbar( QQuickItem *item ) const +{ + if ( !mApp->rootObjects().isEmpty() ) + { + QQuickItem *toolbar = mApp->rootObjects().at( 0 )->findChild( QStringLiteral( "pluginsToolbar" ) ); + item->setParentItem( toolbar ); + } +} + +void AppInterface::addItemToMainMenuActionsToolbar( QQuickItem *item ) const +{ + if ( !mApp->rootObjects().isEmpty() ) + { + 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 ); + } +} + +QObject *AppInterface::mainWindow() const +{ + if ( !mApp->rootObjects().isEmpty() ) + { + return mApp->rootObjects().at( 0 ); + } + return nullptr; +} + +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 70c4a66f2a..a817da3cfe 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,36 @@ class AppInterface : public QObject */ Q_INVOKABLE void clearProject() const; + /** + * Returns the item matching the provided object \a name + */ + Q_INVOKABLE QObject *findItemByObjectName( const QString &name ) const; + + /** + * Adds an \a item in the plugins toolbar container + */ + 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; + + /** + * Adds an \a item in the main menu action toolbar container + */ + Q_INVOKABLE void addItemToCanvasActionsToolbar( QQuickItem *item ) const; + + /** + * Returns the main window. + */ + Q_INVOKABLE QObject *mainWindow() const; + + /** + * Returns the main map canvas. + */ + Q_INVOKABLE QObject *mapCanvas() const; + 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..37dea8505a --- /dev/null +++ b/src/core/pluginmanager.cpp @@ -0,0 +1,136 @@ +/*************************************************************************** + 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 +#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 ) +{ + 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( pluginName ); + return; + } + } + + if ( mLoadedPlugins.contains( pluginPath ) ) + { + unloadPlugin( pluginPath ); + } + + 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 ) ); +} + +void PluginManager::unloadPlugin( const QString &pluginPath ) +{ + if ( mLoadedPlugins.contains( pluginPath ) ) + { + if ( mLoadedPlugins[pluginPath] ) + { + mLoadedPlugins[pluginPath]->deleteLater(); + mLoadedPlugins.remove( 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 ) + { + QSettings settings; + settings.beginGroup( QStringLiteral( "/qfield/plugins/%1" ).arg( mPermissionRequestPluginPath ) ); + settings.setValue( QStringLiteral( "permissionGranted" ), true ); + settings.endGroup(); + } + + loadPlugin( mPermissionRequestPluginPath, QString(), 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(); +} + +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 new file mode 100644 index 0000000000..066050e0a1 --- /dev/null +++ b/src/core/pluginmanager.h @@ -0,0 +1,52 @@ +/*************************************************************************** + 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: + explicit PluginManager( QQmlEngine *engine ); + ~PluginManager() override = default; + + void loadPlugin( const QString &pluginPath, const QString &pluginName, bool skipPermissionCheck = false ); + void unloadPlugin( const QString &pluginPath ); + + 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 ); + + private slots: + void handleWarnings( const QList &warnings ); + + private: + QQmlEngine *mEngine = nullptr; + QMap> mLoadedPlugins; + + QString mPermissionRequestPluginPath; +}; + +#endif // PLUGINMANAGER_H diff --git a/src/core/qgismobileapp.cpp b/src/core/qgismobileapp.cpp index 06f52f2123..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,15 +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 ); - // 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 = new PluginManager( 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() ); @@ -367,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" ); @@ -386,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" ); @@ -408,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", "" ); @@ -424,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" ); @@ -465,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 @@ -522,8 +518,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, "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`." ); qmlRegisterUncreatableType( "org.qfield", 1, 0, "TrackingModel", "The TrackingModel is available as context property `trackingModel`." ); @@ -545,8 +542,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 ); rootContext()->setContextProperty( "settings", &mSettings ); rootContext()->setContextProperty( "appVersion", qfield::appVersion ); rootContext()->setContextProperty( "appVersionStr", qfield::appVersionStr ); @@ -556,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( "LocatorModelNoGroup", QgsLocatorModel::NoGroup ); + 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 ); @@ -701,6 +698,10 @@ bool QgisMobileapp::loadProjectFile( const QString &path, const QString &name ) { saveProjectPreviewImage(); + if ( !mProjectFilePath.isEmpty() ) + { + mPluginManager->unloadPlugin( PluginManager::findProjectPlugin( mProjectFilePath ) ); + } mAuthRequestHandler->clearStoredRealms(); mProjectFilePath = path; @@ -1164,6 +1165,12 @@ void QgisMobileapp::readProjectFile() emit loadProjectEnded( mProjectFilePath, mProjectFileName ); connect( mMapCanvas, &QgsQuickMapCanvasMap::mapCanvasRefreshed, this, &QgisMobileapp::onMapCanvasRefreshed ); + + const QString projectPluginPath = PluginManager::findProjectPlugin( mProjectFilePath ); + if ( !projectPluginPath.isEmpty() ) + { + mPluginManager->loadPlugin( projectPluginPath, tr( "Project Plugin" ) ); + } } QString QgisMobileapp::readProjectEntry( const QString &scope, const QString &key, const QString &def ) const @@ -1384,7 +1391,12 @@ bool QgisMobileapp::event( QEvent *event ) void QgisMobileapp::clearProject() { + if ( !mProjectFilePath.isEmpty() ) + { + mPluginManager->unloadPlugin( PluginManager::findProjectPlugin( mProjectFilePath ) ); + } mAuthRequestHandler->clearStoredRealms(); + mProjectFileName = QString(); mProjectFilePath = QString(); mProject->clear(); diff --git a/src/core/qgismobileapp.h b/src/core/qgismobileapp.h index d120d37a47..bbf8efc4af 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" @@ -229,9 +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; + + PluginManager *mPluginManager = nullptr; // Dummy objects. We are not able to call static functions from QML, so we need something here. QgsCoordinateReferenceSystem mCrsFactory; 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/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/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 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 diff --git a/src/qml/qgismobileapp.qml b/src/qml/qgismobileapp.qml index 68d803b3f3..d70fabddcb 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; @@ -264,6 +266,7 @@ ApplicationWindow { Item { id: mapCanvas + objectName: "mapCanvas" clip: true DragHandler { @@ -1142,6 +1145,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" ) @@ -1150,9 +1163,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 } @@ -2236,20 +2249,21 @@ ApplicationWindow { bottomMargin: sceneBottomMargin width: { - var actionRowResult = actionsRow.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); } - return Math.max(actionRowResult, result + padding * 2); + return Math.max(toolbarWidth, result + padding * 2); } Row { - id: actionsRow + id: mainMenuActionsToolbar + objectName: "mainMenuActionsToolbar" leftPadding: 2 rightPadding: 2 spacing: 2 @@ -2265,7 +2279,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() @@ -2283,7 +2297,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() @@ -2300,7 +2314,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() @@ -2318,7 +2332,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) { @@ -2343,7 +2357,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) { @@ -2662,6 +2676,8 @@ ApplicationWindow { Menu { id: canvasMenu + objectName: "canvasMenu" + title: qsTr( "Map Canvas Options" ) font: Theme.defaultFont @@ -2687,16 +2703,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); + const toolbarWidth = canvasMenuActionsToolbar.childrenRect.width + 4 + let result = 0; + let padding = 0; + // Skip first Row item + for (let i = 1; i < count; ++i) { + const 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: "" @@ -2872,7 +2904,7 @@ ApplicationWindow { } } - onObjectAdded: (index, object) => { canvasMenu.insertMenu(index+9, object) } + onObjectAdded: (index, object) => { canvasMenu.insertMenu(index + 11, object) } onObjectRemoved: (index, object) => { canvasMenu.removeMenu(object) } } } @@ -3826,7 +3858,7 @@ ApplicationWindow { WelcomeScreen { id: welcomeScreen - objectName: 'welcomeScreen' + objectName: "welcomeScreen" visible: !iface.hasProjectOnLaunch() model: RecentProjectListModel { @@ -4012,4 +4044,52 @@ 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: '' + + Column { + Label { + width: parent.width + wrapMode: Text.WordWrap + text: qsTr( "Do you grant permission to activate `%1`?" ).arg( pluginPermissionDialog.title ) + } + + CheckBox { + id: permanentCheckBox + 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(pluginName) { + pluginPermissionDialog.title = pluginName + pluginPermissionDialog.open() + } + } }