From e1ede700a897c899be8aabf20abe7f953209cb10 Mon Sep 17 00:00:00 2001 From: Alessandro Pasotti Date: Wed, 22 Feb 2017 11:22:10 +0100 Subject: [PATCH] [feature] QgsSettings QGIS QSettings replacement (#4160) * [feature] QgsSettings QGIS QSettings replacement This is the new QgsSettings class that adds an (optional) Global Settings additional ini file where a system administrator can store default values for the settings and provide pre-configuration. If an ini file named qgis_global_settings.ini is found in the pkgDataPath directory it is automatically loaded, this path can be overriden by a command line argument ( --globalsettingsfile path ) and through an environment variable (QGIS_GLOBAL_SETTINGS_FILE). --- python/core/core.sip | 1 + python/core/qgssettings.sip | 181 +++++++++++++++++ src/app/main.cpp | 44 ++++- src/app/qgisapp.cpp | 4 +- src/core/CMakeLists.txt | 2 + src/core/qgssettings.cpp | 259 +++++++++++++++++++++++++ src/core/qgssettings.h | 198 +++++++++++++++++++ src/providers/wms/qgswmsconnection.cpp | 12 +- tests/src/python/CMakeLists.txt | 1 + tests/src/python/test_qgssettings.py | 223 +++++++++++++++++++++ 10 files changed, 913 insertions(+), 12 deletions(-) create mode 100644 python/core/qgssettings.sip create mode 100644 src/core/qgssettings.cpp create mode 100644 src/core/qgssettings.h create mode 100644 tests/src/python/test_qgssettings.py diff --git a/python/core/core.sip b/python/core/core.sip index 2c20ac01ac9c..06cdc023ffff 100644 --- a/python/core/core.sip +++ b/python/core/core.sip @@ -15,6 +15,7 @@ %Include qgsapplication.sip %Include qgsaction.sip +%Include qgssettings.sip %Include qgsactionmanager.sip %Include qgsactionscope.sip %Include qgsactionscoperegistry.sip diff --git a/python/core/qgssettings.sip b/python/core/qgssettings.sip new file mode 100644 index 000000000000..86bce0ea9857 --- /dev/null +++ b/python/core/qgssettings.sip @@ -0,0 +1,181 @@ +/*************************************************************************** + qgssettings.sip + -------------------------------------- + Date : January 2017 + Copyright : (C) 2017 by Alessandro Pasotti + Email : apasotti at boundlessgeo dot com + *************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ + +/** \ingroup core + * \class QgsSettings + * + * This class is a composition of two QSettings instances: + * - the main QSettings instance is the standard User Settings and + * - the second one (Global Settings) is meant to provide read-only + * pre-configuration and defaults to the first one. + * + * Unlike the original QSettings, the keys of QgsSettings are case insensitive. + * + * For a given settings key, the function call to value(key, default) will return + * the first existing setting in the order specified below: + * - User Settings + * - Global Settings + * - Default Value + * + * The path to the Global Settings storage can be set before constructing the QgsSettings + * objects, with a static call to: + * static bool setGlobalSettingsPath( QString path ); + * + * QgsSettings provides some shortcuts to get/set namespaced settings from/to a specific section: + * - Core + * - Gui + * - Server + * - Plugins + * - Misc + * + * @note added in QGIS 3 + */ +class QgsSettings : public QObject +{ + + %TypeHeaderCode + #include + %End + + public: + + //! Sections for namespaced settings + enum Section + { + NoSection, + Core, + Gui, + Server, + Plugins, + Misc + }; + + /** Construct a QgsSettings object for accessing settings of the application + * called application from the organization called organization, and with parent parent. + */ + explicit QgsSettings( const QString &organization, + const QString &application = QString(), QObject *parent = 0 ); + + /** Construct a QgsSettings object for accessing settings of the application called application + * from the organization called organization, and with parent parent. + * If scope is QSettings::UserScope, the QSettings object searches user-specific settings first, + * before it searches system-wide settings as a fallback. If scope is QSettings::SystemScope, + * the QSettings object ignores user-specific settings and provides access to system-wide settings. + * + * The storage format is set to QSettings::NativeFormat (i.e. calling setDefaultFormat() before + * calling this constructor has no effect). + * + * If no application name is given, the QSettings object will only access the organization-wide + * locations. + */ + QgsSettings( QSettings::Scope scope, const QString &organization, + const QString &application = QString(), QObject *parent = 0 ); + + /** Construct a QgsSettings object for accessing settings of the application called application + * from the organization called organization, and with parent parent. + * If scope is QSettings::UserScope, the QSettings object searches user-specific settings first, + * before it searches system-wide settings as a fallback. If scope is QSettings::SystemScope, + * the QSettings object ignores user-specific settings and provides access to system-wide settings. + * If format is QSettings::NativeFormat, the native API is used for storing settings. If format + * is QSettings::IniFormat, the INI format is used. + * + * If no application name is given, the QSettings object will only access the organization-wide + * locations. + */ + QgsSettings( QSettings::Format format, QSettings::Scope scope, const QString &organization, + const QString &application = QString(), QObject *parent = 0 ); + + /** Construct a QgsSettings object for accessing the settings stored in the file called fileName, + * with parent parent. If the file doesn't already exist, it is created. + * + * If format is QSettings::NativeFormat, the meaning of fileName depends on the platform. On Unix, + * fileName is the name of an INI file. On macOS and iOS, fileName is the name of a .plist file. + * On Windows, fileName is a path in the system registry. + * + * If format is QSettings::IniFormat, fileName is the name of an INI file. + * + * Warning: This function is provided for convenience. It works well for accessing INI or .plist + * files generated by Qt, but might fail on some syntaxes found in such files originated by + * other programs. In particular, be aware of the following limitations: + * - QgsSettings provides no way of reading INI "path" entries, i.e., entries with unescaped slash characters. + * (This is because these entries are ambiguous and cannot be resolved automatically.) + * - In INI files, QSettings uses the @ character as a metacharacter in some contexts, to encode + * Qt-specific data types (e.g., \@Rect), and might therefore misinterpret it when it occurs + * in pure INI files. + */ + QgsSettings( const QString &fileName, QSettings::Format format, QObject *parent = 0 ); + + /** Constructs a QgsSettings object for accessing settings of the application and organization + * set previously with a call to QCoreApplication::setOrganizationName(), + * QCoreApplication::setOrganizationDomain(), and QCoreApplication::setApplicationName(). + * + * The scope is QSettings::UserScope and the format is defaultFormat() (QSettings::NativeFormat + * by default). Use setDefaultFormat() before calling this constructor to change the default + * format used by this constructor. + */ + explicit QgsSettings( QObject *parent = 0 ); + ~QgsSettings(); + + /** Appends prefix to the current group. + * The current group is automatically prepended to all keys specified to QSettings. + * In addition, query functions such as childGroups(), childKeys(), and allKeys() + * are based on the group. By default, no group is set. + */ + void beginGroup( const QString &prefix ); + //! Resets the group to what it was before the corresponding beginGroup() call. + void endGroup(); + //! Returns a list of all keys, including subkeys, that can be read using the QSettings object. + QStringList allKeys() const; + //! Returns a list of all top-level keys that can be read using the QSettings object. + QStringList childKeys() const; + //! Returns a list of all key top-level groups that contain keys that can be read using the QSettings object. + QStringList childGroups() const; + //! Return the path to the Global Settings QSettings storage file + static QString globalSettingsPath(); + //! Set the Global Settings QSettings storage file + static bool setGlobalSettingsPath( QString path ); + //! Adds prefix to the current group and starts reading from an array. Returns the size of the array. + int beginReadArray( const QString &prefix ); + //! Closes the array that was started using beginReadArray() or beginWriteArray(). + void endArray(); + //! Sets the current array index to i. Calls to functions such as setValue(), value(), remove(), and contains() will operate on the array entry at that index. + void setArrayIndex( int i ); + //! Sets the value of setting key to value. If the key already exists, the previous value is overwritten. + //! An optional Section argument can be used to set a value to a specific Section. + //! @note keys are case insensitive + void setValue(const QString &key, const QVariant &value, const QgsSettings::Section section = QgsSettings::Section::NoSection ); + /** Returns the value for setting key. If the setting doesn't exist, it will be + * searched in the Global Settings and if not found, returns defaultValue. + * If no default value is specified, a default QVariant is returned. + * An optional Section argument can be used to get a value from a specific Section. + */ + QVariant value( const QString &key, const QVariant &defaultValue = QVariant(), + const QgsSettings::Section section = QgsSettings::Section::NoSection ) const; + //! Removes the setting key and any sub-settings of key. + void remove(const QString &key); + //! Return the sanitized and prefixed key + QString prefixedKey( const QString &key, const Section section ) const; + //! Returns the path where settings written using this QSettings object are stored. + QString fileName() const; + //! Writes any unsaved changes to permanent storage, and reloads any settings that have been + //! changed in the meantime by another application. + //! This function is called automatically from QSettings's destructor and by the event + //! loop at regular intervals, so you normally don't need to call it yourself. + void sync(); + //! Returns true if there exists a setting called key; returns false otherwise. + //! If a group is set using beginGroup(), key is taken to be relative to that group. + bool contains(const QString &key) const; + +}; diff --git a/src/app/main.cpp b/src/app/main.cpp index 08814a79fd77..aa37d7acc2e0 100644 --- a/src/app/main.cpp +++ b/src/app/main.cpp @@ -78,6 +78,7 @@ typedef SInt32 SRefCon; #endif #include "qgscustomization.h" +#include "qgssettings.h" #include "qgsfontutils.h" #include "qgspluginregistry.h" #include "qgsmessagelog.h" @@ -119,7 +120,8 @@ void usage( const QString& appName ) << QStringLiteral( "\t[--noversioncheck]\tdon't check for new version of QGIS at startup\n" ) << QStringLiteral( "\t[--noplugins]\tdon't restore plugins on startup\n" ) << QStringLiteral( "\t[--nocustomization]\tdon't apply GUI customization\n" ) - << QStringLiteral( "\t[--customizationfile]\tuse the given ini file as GUI customization\n" ) + << QStringLiteral( "\t[--customizationfile path]\tuse the given ini file as GUI customization\n" ) + << QStringLiteral( "\t[--globalsettingsfile path]\tuse the given ini file as Global Settings (defaults)\n" ) << QStringLiteral( "\t[--optionspath path]\tuse the given QSettings path\n" ) << QStringLiteral( "\t[--configpath path]\tuse the given path for all user configuration\n" ) << QStringLiteral( "\t[--authdbdirectory path] use the given directory for authentication database\n" ) @@ -556,6 +558,7 @@ int main( int argc, char *argv[] ) QString pythonfile; QString customizationfile; + QString globalsettingsfile; #if defined(ANDROID) QgsDebugMsg( QString( "Android: All params stripped" ) );// Param %1" ).arg( argv[0] ) ); @@ -642,6 +645,10 @@ int main( int argc, char *argv[] ) { customizationfile = QDir::toNativeSeparators( QFileInfo( args[++i] ).absoluteFilePath() ); } + else if ( i + 1 < argc && ( arg == QLatin1String( "--globalsettingsfile" ) || arg == QLatin1String( "-g" ) ) ) + { + globalsettingsfile = QDir::toNativeSeparators( QFileInfo( args[++i] ).absoluteFilePath() ); + } else if ( arg == QLatin1String( "--defaultui" ) || arg == QLatin1String( "-d" ) ) { myRestoreDefaultWindowState = true; @@ -813,6 +820,35 @@ int main( int argc, char *argv[] ) QCoreApplication::setApplicationName( QgsApplication::QGIS_APPLICATION_NAME ); QCoreApplication::setAttribute( Qt::AA_DontShowIconsInMenus, false ); + // SetUp the QgsSettings Global Settings: + // - use the path specified with --globalsettings path, + // - use the environment if not found + // - use a default location as a fallback + if ( globalsettingsfile.isEmpty( ) ) + { + globalsettingsfile = getenv( "QGIS_GLOBAL_SETTINGS_FILE" ); + } + if ( globalsettingsfile.isEmpty( ) ) + { + QString default_globalsettingsfile = QgsApplication::pkgDataPath( ) + "/qgis_global_settings.ini"; + if ( QFile::exists( default_globalsettingsfile ) ) + { + globalsettingsfile = default_globalsettingsfile; + } + } + if ( !globalsettingsfile.isEmpty() ) + { + if ( ! QgsSettings::setGlobalSettingsPath( globalsettingsfile ) ) + { + QgsMessageLog::logMessage( QString( "Invalid globalsettingsfile path: %1" ).arg( globalsettingsfile ), QStringLiteral( "QGIS" ) ); + } + else + { + QgsMessageLog::logMessage( QString( "Successfully loaded globalsettingsfile path: %1" ).arg( globalsettingsfile ), QStringLiteral( "QGIS" ) ); + } + } + + // TODO: use QgsSettings QSettings* customizationsettings = nullptr; if ( !optionpath.isEmpty() || !configpath.isEmpty() ) { @@ -866,7 +902,8 @@ int main( int argc, char *argv[] ) } #endif - QSettings mySettings; + + QgsSettings mySettings; // update any saved setting for older themes to new default 'gis' theme (2013-04-15) if ( mySettings.contains( QStringLiteral( "/Themes" ) ) ) @@ -880,7 +917,6 @@ int main( int argc, char *argv[] ) } } - // custom environment variables QMap systemEnvVars = QgsApplication::systemEnvVars(); bool useCustomVars = mySettings.value( QStringLiteral( "qgis/customEnvVarsUse" ), QVariant( false ) ).toBool(); @@ -1072,7 +1108,7 @@ int main( int argc, char *argv[] ) // set max. thread count // this should be done in QgsApplication::init() but it doesn't know the settings dir. - QgsApplication::setMaxThreads( QSettings().value( QStringLiteral( "/qgis/max_threads" ), -1 ).toInt() ); + QgsApplication::setMaxThreads( mySettings.value( QStringLiteral( "/qgis/max_threads" ), -1 ).toInt() ); QgisApp *qgis = new QgisApp( mypSplash, myRestorePlugins, mySkipVersionCheck ); // "QgisApp" used to find canonical instance qgis->setObjectName( QStringLiteral( "QgisApp" ) ); diff --git a/src/app/qgisapp.cpp b/src/app/qgisapp.cpp index 61141d877e73..1c90ef7c27c8 100644 --- a/src/app/qgisapp.cpp +++ b/src/app/qgisapp.cpp @@ -50,7 +50,6 @@ #include #include #include -#include #include #include #include @@ -70,6 +69,7 @@ #include #include +#include #include #include #include @@ -673,7 +673,7 @@ QgisApp::QgisApp( QSplashScreen *splash, bool restorePlugins, bool skipVersionCh mSplash->showMessage( tr( "Setting up the GUI" ), Qt::AlignHCenter | Qt::AlignBottom ); qApp->processEvents(); - QSettings settings; + QgsSettings settings; startProfile( QStringLiteral( "Building style sheet" ) ); // set up stylesheet builder and apply saved or default style options diff --git a/src/core/CMakeLists.txt b/src/core/CMakeLists.txt index e79fd5574b6e..ea7a86f78763 100644 --- a/src/core/CMakeLists.txt +++ b/src/core/CMakeLists.txt @@ -257,6 +257,7 @@ SET(QGIS_CORE_SRCS qgsvirtuallayerdefinitionutils.cpp qgsmapthemecollection.cpp qgsxmlutils.cpp + qgssettings.cpp composer/qgsaddremoveitemcommand.cpp composer/qgsaddremovemultiframecommand.cpp @@ -539,6 +540,7 @@ SET(QGIS_CORE_MOC_HDRS qgsmapthemecollection.h qgswebpage.h qgswebview.h + qgssettings.h annotations/qgsannotation.h annotations/qgsannotationmanager.h diff --git a/src/core/qgssettings.cpp b/src/core/qgssettings.cpp new file mode 100644 index 000000000000..90640b77fbad --- /dev/null +++ b/src/core/qgssettings.cpp @@ -0,0 +1,259 @@ +/*************************************************************************** + qgssettings.cpp + -------------------------------------- + Date : January 2017 + Copyright : (C) 2017 by Alessandro Pasotti + Email : apasotti at boundlessgeo dot com + *************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ + + +#include + +#include "qgssettings.h" +#include +#include +#include + +QString QgsSettings::sGlobalSettingsPath = QString(); + +bool QgsSettings::setGlobalSettingsPath( QString path ) +{ + if ( QFileInfo::exists( path ) ) + { + sGlobalSettingsPath = path; + return true; + } + return false; +} + +void QgsSettings::init() +{ + if ( ! sGlobalSettingsPath.isEmpty( ) ) + { + mGlobalSettings = new QSettings( sGlobalSettingsPath, QSettings::IniFormat ); + mGlobalSettings->setIniCodec( "UTF-8" ); + } +} + + +QgsSettings::QgsSettings( const QString &organization, const QString &application, QObject *parent ) +{ + mUserSettings = new QSettings( organization, application, parent ); + init( ); +} + +QgsSettings::QgsSettings( QSettings::Scope scope, const QString &organization, + const QString &application, QObject *parent ) +{ + mUserSettings = new QSettings( scope, organization, application, parent ); + init( ); +} + +QgsSettings::QgsSettings( QSettings::Format format, QSettings::Scope scope, + const QString &organization, const QString &application, QObject *parent ) +{ + mUserSettings = new QSettings( format, scope, organization, application, parent ); + init( ); +} + +QgsSettings::QgsSettings( const QString &fileName, QSettings::Format format, QObject *parent ) +{ + mUserSettings = new QSettings( fileName, format, parent ); + init( ); +} + +QgsSettings::QgsSettings( QObject *parent ) +{ + mUserSettings = new QSettings( parent ); + init( ); +} + +QgsSettings::~QgsSettings() +{ + delete mUserSettings; + delete mGlobalSettings; +} + + +void QgsSettings::beginGroup( const QString &prefix ) +{ + mUserSettings->beginGroup( prefix ); + if ( mGlobalSettings ) + { + mGlobalSettings->beginGroup( prefix ); + } +} + +void QgsSettings::endGroup() +{ + mUserSettings->endGroup( ); + if ( mGlobalSettings ) + { + mGlobalSettings->endGroup( ); + } +} + + +QStringList QgsSettings::allKeys() const +{ + QStringList keys = mUserSettings->allKeys( ); + if ( mGlobalSettings ) + { + for ( auto &s : mGlobalSettings->allKeys() ) + { + if ( ! keys.contains( s ) ) + { + keys.append( s ); + } + } + } + return keys; +} + + +QStringList QgsSettings::childKeys() const +{ + QStringList keys = mUserSettings->childKeys( ); + if ( mGlobalSettings ) + { + for ( auto &s : mGlobalSettings->childKeys() ) + { + if ( ! keys.contains( s ) ) + { + keys.append( s ); + } + } + } + return keys; +} + +QStringList QgsSettings::childGroups() const +{ + QStringList keys = mUserSettings->childGroups( ); + if ( mGlobalSettings ) + { + for ( auto &s : mGlobalSettings->childGroups() ) + { + if ( ! keys.contains( s ) ) + { + keys.append( s ); + } + } + } + return keys; +} + +QVariant QgsSettings::value( const QString &key, const QVariant &defaultValue, const QgsSettings::Section section ) const +{ + QString pKey = prefixedKey( key, section ); + if ( ! mUserSettings->value( pKey ).isNull() ) + { + return mUserSettings->value( pKey ); + } + if ( mGlobalSettings ) + { + return mGlobalSettings->value( pKey, defaultValue ); + } + return defaultValue; +} + +bool QgsSettings::contains( const QString &key ) const +{ + return mUserSettings->contains( key ) || + ( mGlobalSettings && mGlobalSettings->contains( key ) ); +} + +QString QgsSettings::fileName() const +{ + return mUserSettings->fileName( ); +} + +void QgsSettings::sync() +{ + return mUserSettings->sync( ); +} + +void QgsSettings::remove( const QString &key ) +{ + mGlobalSettings->remove( key ); +} + +QString QgsSettings::prefixedKey( const QString &key, const Section section ) const +{ + QString prefix; + switch ( section ) + { + case Section::Core : + prefix = "core"; + break; + case Section::Server : + prefix = "server"; + break; + case Section::Gui : + prefix = "gui"; + break; + case Section::Plugins : + prefix = "plugins"; + break; + case Section::Misc : + prefix = "misc"; + break; + case Section::NoSection: + default: + return sanitizeKey( key ); + } + return prefix + "/" + sanitizeKey( key ); +} + + +int QgsSettings::beginReadArray( const QString &prefix ) +{ + int size = mUserSettings->beginReadArray( prefix ); + if ( 0 == size && mGlobalSettings ) + { + size = mGlobalSettings->beginReadArray( prefix ); + mUsingGlobalArray = ( size > 0 ); + } + return size; +} + +void QgsSettings::endArray() +{ + mUserSettings->endArray(); + if ( mGlobalSettings ) + { + mGlobalSettings->endArray(); + } + mUsingGlobalArray = false; +} + +void QgsSettings::setArrayIndex( int i ) +{ + if ( mGlobalSettings && mUsingGlobalArray ) + { + mGlobalSettings->setArrayIndex( i ); + } + else + { + mUserSettings->setArrayIndex( i ); + } +} + +void QgsSettings::setValue( const QString &key, const QVariant &value , const QgsSettings::Section section ) +{ + // TODO: add valueChanged signal + mUserSettings->setValue( prefixedKey( key, section ), value ); +} + +// To lower case and clean the path +QString QgsSettings::sanitizeKey( QString key ) const +{ + return QDir::cleanPath( key.toLower() ); +} diff --git a/src/core/qgssettings.h b/src/core/qgssettings.h new file mode 100644 index 000000000000..eb092cd10e9e --- /dev/null +++ b/src/core/qgssettings.h @@ -0,0 +1,198 @@ +/*************************************************************************** + qgssettings.h + -------------------------------------- + Date : January 2017 + Copyright : (C) 2017 by Alessandro Pasotti + Email : apasotti at boundlessgeo dot com + *************************************************************************** + * * + * This program is free software; you can redistribute it and/or modify * + * it under the terms of the GNU General Public License as published by * + * the Free Software Foundation; either version 2 of the License, or * + * (at your option) any later version. * + * * + ***************************************************************************/ + + +#ifndef QGSSETTINGS_H +#define QGSSETTINGS_H + +#include +#include "qgis_core.h" + +/** \ingroup core + * \class QgsSettings + * + * This class is a composition of two QSettings instances: + * - the main QSettings instance is the standard User Settings and + * - the second one (Global Settings) is meant to provide read-only + * pre-configuration and defaults to the first one. + * + * Unlike the original QSettings, the keys of QgsSettings are case insensitive. + * + * For a given settings key, the function call to value(key, default) will return + * the first existing setting in the order specified below: + * - User Settings + * - Global Settings + * - Default Value + * + * The path to the Global Settings storage can be set before constructing the QgsSettings + * objects, with a static call to: + * static bool setGlobalSettingsPath( QString path ); + * + * QgsSettings provides some shortcuts to get/set namespaced settings from/to a specific section: + * - Core + * - Gui + * - Server + * - Plugins + * - Misc + * + * @note added in QGIS 3 + */ +class CORE_EXPORT QgsSettings : public QObject +{ + Q_OBJECT + public: + + //! Sections for namespaced settings + enum Section + { + NoSection, + Core, + Gui, + Server, + Plugins, + Misc + }; + + /** Construct a QgsSettings object for accessing settings of the application + * called application from the organization called organization, and with parent parent. + */ + explicit QgsSettings( const QString &organization, + const QString &application = QString(), QObject *parent = nullptr ); + + /** Construct a QgsSettings object for accessing settings of the application called application + * from the organization called organization, and with parent parent. + * If scope is QSettings::UserScope, the QSettings object searches user-specific settings first, + * before it searches system-wide settings as a fallback. If scope is QSettings::SystemScope, + * the QSettings object ignores user-specific settings and provides access to system-wide settings. + * + * The storage format is set to QSettings::NativeFormat (i.e. calling setDefaultFormat() before + * calling this constructor has no effect). + * + * If no application name is given, the QSettings object will only access the organization-wide + * locations. + */ + QgsSettings( QSettings::Scope scope, const QString &organization, + const QString &application = QString(), QObject *parent = nullptr ); + + /** Construct a QgsSettings object for accessing settings of the application called application + * from the organization called organization, and with parent parent. + * If scope is QSettings::UserScope, the QSettings object searches user-specific settings first, + * before it searches system-wide settings as a fallback. If scope is QSettings::SystemScope, + * the QSettings object ignores user-specific settings and provides access to system-wide settings. + * If format is QSettings::NativeFormat, the native API is used for storing settings. If format + * is QSettings::IniFormat, the INI format is used. + * + * If no application name is given, the QSettings object will only access the organization-wide + * locations. + */ + QgsSettings( QSettings::Format format, QSettings::Scope scope, const QString &organization, + const QString &application = QString(), QObject *parent = nullptr ); + + /** Construct a QgsSettings object for accessing the settings stored in the file called fileName, + * with parent parent. If the file doesn't already exist, it is created. + * + * If format is QSettings::NativeFormat, the meaning of fileName depends on the platform. On Unix, + * fileName is the name of an INI file. On macOS and iOS, fileName is the name of a .plist file. + * On Windows, fileName is a path in the system registry. + * + * If format is QSettings::IniFormat, fileName is the name of an INI file. + * + * Warning: This function is provided for convenience. It works well for accessing INI or .plist + * files generated by Qt, but might fail on some syntaxes found in such files originated by + * other programs. In particular, be aware of the following limitations: + * - QgsSettings provides no way of reading INI "path" entries, i.e., entries with unescaped slash characters. + * (This is because these entries are ambiguous and cannot be resolved automatically.) + * - In INI files, QSettings uses the @ character as a metacharacter in some contexts, to encode + * Qt-specific data types (e.g., \@Rect), and might therefore misinterpret it when it occurs + * in pure INI files. + */ + QgsSettings( const QString &fileName, QSettings::Format format, QObject *parent = nullptr ); + + /** Constructs a QgsSettings object for accessing settings of the application and organization + * set previously with a call to QCoreApplication::setOrganizationName(), + * QCoreApplication::setOrganizationDomain(), and QCoreApplication::setApplicationName(). + * + * The scope is QSettings::UserScope and the format is defaultFormat() (QSettings::NativeFormat + * by default). Use setDefaultFormat() before calling this constructor to change the default + * format used by this constructor. + */ + explicit QgsSettings( QObject *parent = 0 ); + ~QgsSettings(); + + /** Appends prefix to the current group. + * The current group is automatically prepended to all keys specified to QSettings. + * In addition, query functions such as childGroups(), childKeys(), and allKeys() + * are based on the group. By default, no group is set. + */ + void beginGroup( const QString &prefix ); + //! Resets the group to what it was before the corresponding beginGroup() call. + void endGroup(); + //! Returns a list of all keys, including subkeys, that can be read using the QSettings object. + QStringList allKeys() const; + //! Returns a list of all top-level keys that can be read using the QSettings object. + QStringList childKeys() const; + //! Returns a list of all key top-level groups that contain keys that can be read using the QSettings object. + QStringList childGroups() const; + //! Return the path to the Global Settings QSettings storage file + static QString globalSettingsPath() { return sGlobalSettingsPath; } + //! Set the Global Settings QSettings storage file + static bool setGlobalSettingsPath( QString path ); + //! Adds prefix to the current group and starts reading from an array. Returns the size of the array. + int beginReadArray( const QString &prefix ); + //! Closes the array that was started using beginReadArray() or beginWriteArray(). + void endArray(); + //! Sets the current array index to i. Calls to functions such as setValue(), value(), + //! remove(), and contains() will operate on the array entry at that index. + void setArrayIndex( int i ); + //! Sets the value of setting key to value. If the key already exists, the previous value is overwritten. + //! An optional Section argument can be used to set a value to a specific Section. + //! @note keys are case insensitive + void setValue( const QString &key, const QVariant &value, const Section section = Section::NoSection ); + + /** Returns the value for setting key. If the setting doesn't exist, it will be + * searched in the Global Settings and if not found, returns defaultValue. + * If no default value is specified, a default QVariant is returned. + * An optional Section argument can be used to get a value from a specific Section. + */ + QVariant value( const QString &key, const QVariant &defaultValue = QVariant(), + const Section section = Section::NoSection ) const; + //! Returns true if there exists a setting called key; returns false otherwise. + //! If a group is set using beginGroup(), key is taken to be relative to that group. + bool contains( const QString &key ) const; + //! Returns the path where settings written using this QSettings object are stored. + QString fileName() const; + //! Writes any unsaved changes to permanent storage, and reloads any settings that have been + //! changed in the meantime by another application. + //! This function is called automatically from QSettings's destructor and by the event + //! loop at regular intervals, so you normally don't need to call it yourself. + void sync(); + //! Removes the setting key and any sub-settings of key. + void remove( const QString &key ); + //! Return the sanitized and prefixed key + QString prefixedKey( const QString &key, const Section section ) const; + + private: + + static QString sGlobalSettingsPath; + void init( ); + QString sanitizeKey( QString key ) const; + QSettings* mUserSettings = nullptr; + QSettings* mGlobalSettings = nullptr; + bool mUsingGlobalArray = false; + Q_DISABLE_COPY( QgsSettings ) + +}; + +#endif // QGSSETTINGS_H diff --git a/src/providers/wms/qgswmsconnection.cpp b/src/providers/wms/qgswmsconnection.cpp index 3484c6137030..4a2a3a0b809c 100644 --- a/src/providers/wms/qgswmsconnection.cpp +++ b/src/providers/wms/qgswmsconnection.cpp @@ -26,11 +26,11 @@ #include "qgsproviderregistry.h" #include "qgswmsconnection.h" #include "qgsnetworkaccessmanager.h" +#include "qgssettings.h" #include #include #include -#include #include #include @@ -41,7 +41,7 @@ QgsWMSConnection::QgsWMSConnection( const QString& connName ) { QgsDebugMsg( "theConnName = " + connName ); - QSettings settings; + QgsSettings settings; QString key = "/Qgis/connections-wms/" + mConnName; QString credentialsKey = "/Qgis/WMS/" + mConnName; @@ -123,26 +123,26 @@ QgsDataSourceUri QgsWMSConnection::uri() QStringList QgsWMSConnection::connectionList() { - QSettings settings; + QgsSettings settings; settings.beginGroup( QStringLiteral( "/Qgis/connections-wms" ) ); return settings.childGroups(); } QString QgsWMSConnection::selectedConnection() { - QSettings settings; + QgsSettings settings; return settings.value( QStringLiteral( "/Qgis/connections-wms/selected" ) ).toString(); } void QgsWMSConnection::setSelectedConnection( const QString& name ) { - QSettings settings; + QgsSettings settings; settings.setValue( QStringLiteral( "/Qgis/connections-wms/selected" ), name ); } void QgsWMSConnection::deleteConnection( const QString& name ) { - QSettings settings; + QgsSettings settings; settings.remove( "/Qgis/connections-wms/" + name ); settings.remove( "/Qgis/WMS/" + name ); } diff --git a/tests/src/python/CMakeLists.txt b/tests/src/python/CMakeLists.txt index c79af1218d1d..4516e6d57d91 100644 --- a/tests/src/python/CMakeLists.txt +++ b/tests/src/python/CMakeLists.txt @@ -145,6 +145,7 @@ ADD_PYTHON_TEST(PyQgsLayerDependencies test_layer_dependencies.py) ADD_PYTHON_TEST(PyQgsVersionCompare test_versioncompare.py) ADD_PYTHON_TEST(PyQgsDBManagerGpkg test_db_manager_gpkg.py) ADD_PYTHON_TEST(PyQgsFileDownloader test_qgsfiledownloader.py) +ADD_PYTHON_TEST(PyQgsSettings test_qgssettings.py) IF (NOT WIN32) ADD_PYTHON_TEST(PyQgsLogger test_qgslogger.py) diff --git a/tests/src/python/test_qgssettings.py b/tests/src/python/test_qgssettings.py new file mode 100644 index 000000000000..1d1d793072a8 --- /dev/null +++ b/tests/src/python/test_qgssettings.py @@ -0,0 +1,223 @@ +# -*- coding: utf-8 -*- +""" +Test the QgsSettings class + +Run with: ctest -V -R PyQgsSettings + +.. note:: This program is free software; you can redistribute it and/or modify +it under the terms of the GNU General Public License as published by +the Free Software Foundation; either version 2 of the License, or +(at your option) any later version. +""" +from __future__ import print_function +from future import standard_library +import os +import tempfile +from qgis.core import (QgsSettings,) +from qgis.testing import start_app, unittest +from qgis.PyQt.QtCore import QSettings + +standard_library.install_aliases() + +__author__ = 'Alessandro Pasotti' +__date__ = '02/02/2017' +__copyright__ = 'Copyright 2017, The QGIS Project' +# This will get replaced with a git SHA1 when you do a git archive +__revision__ = '$Format:%H$' + + +start_app() + + +class TestQgsSettings(unittest.TestCase): + + cnt = 0 + + def setUp(self): + self.cnt += 1 + h, path = tempfile.mkstemp('.ini') + assert QgsSettings.setGlobalSettingsPath(path) + self.settings = QgsSettings('testqgissettings', 'testqgissettings%s' % self.cnt) + self.globalsettings = QSettings(self.settings.globalSettingsPath(), QSettings.IniFormat) + + def tearDown(self): + settings_file = self.settings.fileName() + settings_default_file = self.settings.globalSettingsPath() + del(self.settings) + try: + os.unlink(settings_file) + except: + pass + try: + os.unlink(settings_default_file) + except: + pass + + def addToDefaults(self, key, value): + self.globalsettings.setValue(key, value) + self.globalsettings.sync() + + def addArrayToDefaults(self, prefix, key, values): + defaults = QSettings(self.settings.globalSettingsPath(), QSettings.IniFormat) + self.globalsettings.beginWriteArray(prefix) + i = 0 + for v in values: + self.globalsettings.setArrayIndex(i) + self.globalsettings.setValue(key, v) + i += 1 + self.globalsettings.endArray() + self.globalsettings.sync() + + def test_basic_functionality(self): + self.assertEqual(self.settings.value('testqgissettings/doesnotexists', 'notexist'), 'notexist') + self.settings.setValue('testqgissettings/name', 'qgisrocks') + self.settings.sync() + self.assertEqual(self.settings.value('testqgissettings/name'), 'qgisrocks') + + def test_defaults(self): + self.assertIsNone(self.settings.value('testqgissettings/name')) + self.addToDefaults('testqgissettings/name', 'qgisrocks') + self.assertEqual(self.settings.value('testqgissettings/name'), 'qgisrocks') + + def test_allkeys(self): + self.assertEqual(self.settings.allKeys(), []) + self.addToDefaults('testqgissettings/name', 'qgisrocks') + self.addToDefaults('testqgissettings/name2', 'qgisrocks2') + self.settings.setValue('nepoti/eman', 'osaple') + + self.assertEqual(3, len(self.settings.allKeys())) + self.assertIn('testqgissettings/name', self.settings.allKeys()) + self.assertIn('nepoti/eman', self.settings.allKeys()) + self.assertEqual('qgisrocks', self.settings.value('testqgissettings/name')) + self.assertEqual('qgisrocks2', self.settings.value('testqgissettings/name2')) + self.assertEqual('qgisrocks', self.globalsettings.value('testqgissettings/name')) + self.assertEqual('osaple', self.settings.value('nepoti/eman')) + self.assertEqual(3, len(self.settings.allKeys())) + self.assertEqual(2, len(self.globalsettings.allKeys())) + + def test_precedence(self): + self.assertEqual(self.settings.allKeys(), []) + self.addToDefaults('testqgissettings/names/name1', 'qgisrocks1') + self.settings.setValue('testqgissettings/names/name1', 'qgisrocks-1') + + self.assertEqual(self.settings.value('testqgissettings/names/name1'), 'qgisrocks-1') + + def test_uft8(self): + self.assertEqual(self.settings.allKeys(), []) + self.addToDefaults('testqgissettings/names/namèé↓1', 'qgisrocks↓1') + self.assertEqual(self.settings.value('testqgissettings/names/namèé↓1'), 'qgisrocks↓1') + + self.settings.setValue('testqgissettings/names/namèé↓2', 'qgisrocks↓2') + self.assertEqual(self.settings.value('testqgissettings/names/namèé↓2'), 'qgisrocks↓2') + self.settings.setValue('testqgissettings/names/namèé↓1', 'qgisrocks↓-1') + self.assertEqual(self.settings.value('testqgissettings/names/namèé↓1'), 'qgisrocks↓-1') + + def test_groups(self): + self.assertEqual(self.settings.allKeys(), []) + self.addToDefaults('testqgissettings/names/name1', 'qgisrocks1') + self.addToDefaults('testqgissettings/names/name2', 'qgisrocks2') + self.addToDefaults('testqgissettings/names/name3', 'qgisrocks3') + self.addToDefaults('testqgissettings/name', 'qgisrocks') + + self.settings.beginGroup('testqgissettings') + self.assertEqual(['names'], self.settings.childGroups()) + + self.settings.setValue('surnames/name1', 'qgisrocks-1') + self.assertEqual(['surnames', 'names'], self.settings.childGroups()) + + self.settings.setValue('names/name1', 'qgisrocks-1') + self.assertEqual('qgisrocks-1', self.settings.value('names/name1')) + self.settings.endGroup() + self.settings.beginGroup('testqgissettings/names') + self.settings.setValue('name4', 'qgisrocks-4') + keys = list(self.settings.childKeys()) + keys.sort() + self.assertEqual(keys, ['name1', 'name2', 'name3', 'name4']) + self.settings.endGroup() + self.assertEqual('qgisrocks-1', self.settings.value('testqgissettings/names/name1')) + self.assertEqual('qgisrocks-4', self.settings.value('testqgissettings/names/name4')) + + def test_array(self): + self.assertEqual(self.settings.allKeys(), []) + self.addArrayToDefaults('testqgissettings', 'key', ['qgisrocks1', 'qgisrocks2', 'qgisrocks3']) + self.assertEqual(self.settings.allKeys(), ['testqgissettings/1/key', 'testqgissettings/2/key', 'testqgissettings/3/key', 'testqgissettings/size']) + self.assertEqual(self.globalsettings.allKeys(), ['testqgissettings/1/key', 'testqgissettings/2/key', 'testqgissettings/3/key', 'testqgissettings/size']) + + self.assertEqual(3, self.globalsettings.beginReadArray('testqgissettings')) + self.globalsettings.endArray() + self.assertEqual(3, self.settings.beginReadArray('testqgissettings')) + + values = [] + for i in range(3): + self.settings.setArrayIndex(i) + values.append(self.settings.value("key")) + + self.assertEqual(values, ['qgisrocks1', 'qgisrocks2', 'qgisrocks3']) + + def test_section_getters_setters(self): + self.assertEqual(self.settings.allKeys(), []) + + self.settings.setValue('key1', 'core1', QgsSettings.Core) + self.settings.setValue('key2', 'core2', QgsSettings.Core) + + self.settings.setValue('key1', 'server1', QgsSettings.Server) + self.settings.setValue('key2', 'server2', QgsSettings.Server) + + self.settings.setValue('key1', 'gui1', QgsSettings.Gui) + self.settings.setValue('key2', 'gui2', QgsSettings.Gui) + + self.settings.setValue('key1', 'plugins1', QgsSettings.Plugins) + self.settings.setValue('key2', 'plugins2', QgsSettings.Plugins) + + self.settings.setValue('key1', 'misc1', QgsSettings.Misc) + self.settings.setValue('key2', 'misc2', QgsSettings.Misc) + + # Test that the values are namespaced + self.assertEqual(self.settings.value('core/key1'), 'core1') + self.assertEqual(self.settings.value('core/key2'), 'core2') + + self.assertEqual(self.settings.value('server/key1'), 'server1') + self.assertEqual(self.settings.value('server/key2'), 'server2') + + self.assertEqual(self.settings.value('gui/key1'), 'gui1') + self.assertEqual(self.settings.value('gui/key2'), 'gui2') + + self.assertEqual(self.settings.value('plugins/key1'), 'plugins1') + self.assertEqual(self.settings.value('plugins/key2'), 'plugins2') + + self.assertEqual(self.settings.value('misc/key1'), 'misc1') + self.assertEqual(self.settings.value('misc/key2'), 'misc2') + + # Test getters + self.assertEqual(self.settings.value('key1', None, QgsSettings.Core), 'core1') + self.assertEqual(self.settings.value('key2', None, QgsSettings.Core), 'core2') + + self.assertEqual(self.settings.value('key1', None, QgsSettings.Server), 'server1') + self.assertEqual(self.settings.value('key2', None, QgsSettings.Server), 'server2') + + self.assertEqual(self.settings.value('key1', None, QgsSettings.Gui), 'gui1') + self.assertEqual(self.settings.value('key2', None, QgsSettings.Gui), 'gui2') + + self.assertEqual(self.settings.value('key1', None, QgsSettings.Plugins), 'plugins1') + self.assertEqual(self.settings.value('key2', None, QgsSettings.Plugins), 'plugins2') + + self.assertEqual(self.settings.value('key1', None, QgsSettings.Misc), 'misc1') + self.assertEqual(self.settings.value('key2', None, QgsSettings.Misc), 'misc2') + + # Test default values on Section getter + self.assertEqual(self.settings.value('key_not_exist', 'misc_not_exist', QgsSettings.Misc), 'misc_not_exist') + + def test_contains(self): + self.assertEqual(self.settings.allKeys(), []) + self.addToDefaults('testqgissettings/name', 'qgisrocks1') + self.addToDefaults('testqgissettings/name2', 'qgisrocks2') + + self.assertTrue(self.settings.contains('testqgissettings/name')) + self.assertTrue(self.settings.contains('testqgissettings/name2')) + + self.settings.setValue('testqgissettings/name3', 'qgisrocks3') + self.assertTrue(self.settings.contains('testqgissettings/name3')) + + +if __name__ == '__main__': + unittest.main()