diff --git a/python/core/__init__.py b/python/core/__init__.py index 6854c3c0bb9..4d2db7e66cb 100644 --- a/python/core/__init__.py +++ b/python/core/__init__.py @@ -35,6 +35,7 @@ from .additions.qgsdefaultvalue import _isValid from .additions.qgsfeature import mapping_feature from .additions.qgsfunction import register_function, qgsfunction from .additions.qgsgeometry import _geometryNonZero, mapping_geometry +from .additions.qgssettings import _qgssettings_enum_value, _qgssettings_flag_value from .additions.qgstaskwrapper import QgsTaskWrapper from .additions.readwritecontextentercategory import ReadWriteContextEnterCategory @@ -48,27 +49,36 @@ QgsProcessingFeatureSourceDefinition.__repr__ = processing_source_repr QgsProcessingOutputLayerDefinition.__repr__ = processing_output_layer_repr QgsProject.blockDirtying = ProjectDirtyBlocker QgsReadWriteContext.enterCategory = ReadWriteContextEnterCategory +QgsSettings.enumValue = _qgssettings_enum_value +QgsSettings.flagValue = _qgssettings_flag_value QgsTask.fromFunction = fromFunction # ----------------- # DO NOT EDIT BELOW # These are automatically added by calling sipify.pl script -QgsTolerance.UnitType.baseClass = QgsTolerance + QgsAuthManager.MessageLevel.baseClass = QgsAuthManager QgsDataItem.Type.baseClass = QgsDataItem QgsDataItem.State.baseClass = QgsDataItem QgsLayerItem.LayerType.baseClass = QgsLayerItem QgsDataProvider.DataCapability.baseClass = QgsDataProvider QgsDataSourceUri.SslMode.baseClass = QgsDataSourceUri +QgsFieldProxyModel.Filters.baseClass = QgsFieldProxyModel +Filters = QgsFieldProxyModel # dirty hack since SIP seems to introduce the flags in module +QgsMapLayerProxyModel.Filters.baseClass = QgsMapLayerProxyModel +Filters = QgsMapLayerProxyModel # dirty hack since SIP seems to introduce the flags in module QgsNetworkContentFetcherRegistry.FetchingMode.baseClass = QgsNetworkContentFetcherRegistry QgsSnappingConfig.SnappingMode.baseClass = QgsSnappingConfig QgsSnappingConfig.SnappingType.baseClass = QgsSnappingConfig +QgsTolerance.UnitType.baseClass = QgsTolerance QgsUnitTypes.DistanceUnit.baseClass = QgsUnitTypes QgsUnitTypes.AreaUnit.baseClass = QgsUnitTypes QgsUnitTypes.AngleUnit.baseClass = QgsUnitTypes QgsUnitTypes.RenderUnit.baseClass = QgsUnitTypes QgsUnitTypes.LayoutUnit.baseClass = QgsUnitTypes QgsVectorSimplifyMethod.SimplifyHint.baseClass = QgsVectorSimplifyMethod +QgsVectorSimplifyMethod.SimplifyHints.baseClass = QgsVectorSimplifyMethod +SimplifyHints = QgsVectorSimplifyMethod # dirty hack since SIP seems to introduce the flags in module QgsVectorSimplifyMethod.SimplifyAlgorithm.baseClass = QgsVectorSimplifyMethod QgsRasterProjector.Precision.baseClass = QgsRasterProjector QgsAbstractGeometry.SegmentationToleranceType.baseClass = QgsAbstractGeometry diff --git a/python/core/additions/metaenum.py b/python/core/additions/metaenum.py index 75c46e16874..ed5153bfd37 100644 --- a/python/core/additions/metaenum.py +++ b/python/core/additions/metaenum.py @@ -18,11 +18,27 @@ """ -def metaEnumFromValue(enumValue, raiseException=True, baseClass=None): - return metaEnumFromType(enumValue.__class__, raiseException, baseClass) +def metaEnumFromValue(enumValue, baseClass=None, raiseException=True): + """ + Returns the QMetaEnum for an enum value. + The enum must have declared using the Q_ENUM macro + :param enumValue: the enum value + :param baseClass: the enum base class. If not given, it will try to get it by using `enumValue.__class__.baseClass` + :param raiseException: if False, no exception will be raised and None will be return in case of failure + :return: the QMetaEnum if it succeeds, None otherwise + """ + return metaEnumFromType(enumValue.__class__, baseClass, raiseException) -def metaEnumFromType(enumClass, raiseException=True, baseClass=None): +def metaEnumFromType(enumClass, baseClass=None, raiseException=True): + """ + Returns the QMetaEnum for an enum type. + The enum must have declared using the Q_ENUM macro + :param enumClass: the enum class + :param baseClass: the enum base class. If not given, it will try to get it by using `enumValue.__class__.baseClass` + :param raiseException: if False, no exception will be raised and None will be return in case of failure + :return: the QMetaEnum if it succeeds, None otherwise + """ if enumClass == int: if raiseException: raise TypeError("enumClass is an int, while it should be an enum") @@ -32,7 +48,7 @@ def metaEnumFromType(enumClass, raiseException=True, baseClass=None): if baseClass is None: try: baseClass = enumClass.baseClass - return metaEnumFromType(enumClass, raiseException, baseClass) + return metaEnumFromType(enumClass, baseClass, raiseException) except AttributeError: if raiseException: raise ValueError("Enum type does not implement baseClass method. Provide the base class as argument.") diff --git a/python/core/additions/qgssettings.py b/python/core/additions/qgssettings.py new file mode 100644 index 00000000000..6ab13870dac --- /dev/null +++ b/python/core/additions/qgssettings.py @@ -0,0 +1,101 @@ +# -*- coding: utf-8 -*- + +""" +*************************************************************************** + qgssettings.py + --------------------- + Date : May 2018 + Copyright : (C) 2018 by Denis Rouzaud + Email : denis@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. * +* * +*************************************************************************** +""" + +from .metaenum import metaEnumFromValue +import qgis + + +def _qgssettings_enum_value(self, key, enumDefaultValue, section=None): + """ + Return the setting value for a setting based on an enum. + This forces the output to be a valid and existing entry of the enum. + Hence if the setting value is incorrect, the given default value is returned. + This tries first with setting as a string (as the enum) and then as an integer value. + + :param self: the QgsSettings object + :param key: the setting key + :param enumDefaultValue: the default value as an enum value + :param section: optional section + :return: the setting value + + .. note:: The enum needs to be declared with Q_ENUM. + + """ + if section is None: + section = self.NoSection + + meta_enum = metaEnumFromValue(enumDefaultValue) + if meta_enum is None or not meta_enum.isValid(): + # this should not happen + raise ValueError("could not get the meta enum for given enum default value (type: {})".format(type(enumDefaultValue))) + + str_val = self.value(key, meta_enum.valueToKey(enumDefaultValue)) + # need a new meta enum as QgsSettings.value is making a copy and leads to seg fault (proaby a PyQt issue) + meta_enum_2 = metaEnumFromValue(enumDefaultValue) + (enu_val, ok) = meta_enum_2.keyToValue(str_val) + + if not ok: + enu_val = enumDefaultValue + + return enu_val + + +def _qgssettings_flag_value(self, key, flagDefaultValue, section=None): + """ + Return the setting value for a setting based on a flag. + This forces the output to be a valid and existing entry of the enum. + Hence if the setting value is incorrect, the given default value is returned. + This tries first with setting as a string (as the enum) and then as an integer value. + + :param self: the QgsSettings object + :param key: the setting key + :param flagDefaultValue: the default value as a flag value + :param section: optional section + :return: the setting value + + .. note:: The flag needs to be declared with Q_FLAG (not Q_FLAGS). + + """ + if section is None: + section = self.NoSection + + # There is an issue in SIP, flags.__class__ does not return the proper class + # (e.g. Filters instead of QgsMapLayerProxyModel.Filters) + # dirty hack to get the parent class + __import__(flagDefaultValue.__module__) + baseClass = None + exec("baseClass={module}.{flag_class}".format(module=flagDefaultValue.__module__.replace('_', ''), + flag_class=flagDefaultValue.__class__.__name__)) + + meta_enum = metaEnumFromValue(flagDefaultValue, baseClass) + if meta_enum is None or not meta_enum.isValid(): + # this should not happen + raise ValueError("could not get the meta enum for given enum default value (type: {})".format(type(flagDefaultValue))) + + str_val = self.value(key, meta_enum.valueToKey(flagDefaultValue)) + # need a new meta enum as QgsSettings.value is making a copy and leads to seg fault (proaby a PyQt issue) + meta_enum_2 = metaEnumFromValue(flagDefaultValue) + (flag_val, ok) = meta_enum_2.keysToValue(str_val) + + if not ok: + flag_val = flagDefaultValue + else: + flag_val = flagDefaultValue.__class__(flag_val) + + return flag_val diff --git a/python/gui/__init__.py b/python/gui/__init__.py index 00396d6111c..51aad36fdb4 100644 --- a/python/gui/__init__.py +++ b/python/gui/__init__.py @@ -30,12 +30,20 @@ from qgis._gui import * # DO NOT EDIT BELOW # These are automatically added by calling sipify.pl script QgsAuthSettingsWidget.WarningType.baseClass = QgsAuthSettingsWidget +QgsAdvancedDigitizingDockWidget.CadCapacities.baseClass = QgsAdvancedDigitizingDockWidget +CadCapacities = QgsAdvancedDigitizingDockWidget # dirty hack since SIP seems to introduce the flags in module QgsColorButton.Behavior.baseClass = QgsColorButton QgsColorTextWidget.ColorTextFormat.baseClass = QgsColorTextWidget QgsFilterLineEdit.ClearMode.baseClass = QgsFilterLineEdit QgsFloatingWidget.AnchorPoint.baseClass = QgsFloatingWidget QgsFontButton.Mode.baseClass = QgsFontButton +QgsMapLayerAction.Targets.baseClass = QgsMapLayerAction +Targets = QgsMapLayerAction # dirty hack since SIP seems to introduce the flags in module +QgsMapLayerAction.Flags.baseClass = QgsMapLayerAction +Flags = QgsMapLayerAction # dirty hack since SIP seems to introduce the flags in module QgsMapToolIdentify.IdentifyMode.baseClass = QgsMapToolIdentify +QgsMapToolIdentify.LayerType.baseClass = QgsMapToolIdentify +LayerType = QgsMapToolIdentify # dirty hack since SIP seems to introduce the flags in module QgsAttributeTableFilterModel.FilterMode.baseClass = QgsAttributeTableFilterModel QgsAttributeTableFilterModel.ColumnType.baseClass = QgsAttributeTableFilterModel QgsDualView.ViewMode.baseClass = QgsDualView diff --git a/scripts/sipify.pl b/scripts/sipify.pl index e96a539a29a..d3962e992fb 100755 --- a/scripts/sipify.pl +++ b/scripts/sipify.pl @@ -612,10 +612,11 @@ while ($LINE_IDX < $LINE_COUNT){ } next; } - if ($LINE =~ m/Q_ENUM\(\s*(\w+)\s*\)/ ){ + if ($LINE =~ m/Q_(ENUM|FLAG)\(\s*(\w+)\s*\)/ ){ if ($LINE !~ m/SIP_SKIP/){ - my $enum_helper = "$ACTUAL_CLASS.$1.baseClass = $ACTUAL_CLASS"; - dbg_info("Q_ENUM $enum_helper"); + my $is_flag = $1 eq 'FLAG' ? 1 : 0; + my $enum_helper = "$ACTUAL_CLASS.$2.baseClass = $ACTUAL_CLASS"; + dbg_info("Q_ENUM/Q_FLAG $enum_helper"); if ($python_output ne ''){ my $pl; open(FH, '+<', $python_output) or die $!; @@ -626,6 +627,11 @@ while ($LINE_IDX < $LINE_COUNT){ } } if ($enum_helper ne ''){ + if ($is_flag == 1){ + # SIP seems to introduce the flags in the module rather than in the class itself + # as a dirty hack, inject directly in module, hopefully we don't have flags with the same name.... + $enum_helper .= "\n$2 = $ACTUAL_CLASS # dirty hack since SIP seems to introduce the flags in module"; + } print FH "$enum_helper\n"; } close(FH); diff --git a/src/core/qgssettings.h b/src/core/qgssettings.h index 536ff7fb17a..38de49dff80 100644 --- a/src/core/qgssettings.h +++ b/src/core/qgssettings.h @@ -227,6 +227,7 @@ class CORE_EXPORT QgsSettings : public QObject * Hence if the setting value is incorrect, the given default value is returned. * This tries first with setting as a string (as the enum) and then as an integer value. * \note The enum needs to be declared with Q_ENUM, and flags with Q_FLAG (not Q_FLAGS). + * \note for Python bindings, a custom implementation is achieved in Python directly * \see setEnumValue * \see flagValue */ @@ -304,6 +305,7 @@ class CORE_EXPORT QgsSettings : public QObject * Hence if the setting value is incorrect, the given default value is returned. * This tries first with setting as a string (using a byte array) and then as an integer value. * \note The flag needs to be declared with Q_FLAG (not Q_FLAGS). + * \note for Python bindings, a custom implementation is achieved in Python directly. * \see setFlagValue * \see enumValue */ diff --git a/tests/src/python/test_core_additions.py b/tests/src/python/test_core_additions.py index 6eea3ff7dcd..d6621253326 100644 --- a/tests/src/python/test_core_additions.py +++ b/tests/src/python/test_core_additions.py @@ -24,16 +24,19 @@ start_app() class TestCoreAdditions(unittest.TestCase): - def testEnum(self): + def testMetaEnum(self): me = metaEnumFromValue(QgsTolerance.Pixels) self.assertIsNotNone(me) self.assertEqual(me.valueToKey(QgsTolerance.Pixels), 'Pixels') # if using same variable twice (e.g. me = me2), this seg faults - me2 = metaEnumFromValue(QgsTolerance.Pixels, True, QgsTolerance) + me2 = metaEnumFromValue(QgsTolerance.Pixels, QgsTolerance) self.assertIsNotNone(me) self.assertEqual(me2.valueToKey(QgsTolerance.Pixels), 'Pixels') + # do not raise error + self.assertIsNone(metaEnumFromValue(1, QgsTolerance, False)) + # do not provide an int with self.assertRaises(TypeError): metaEnumFromValue(1) diff --git a/tests/src/python/test_qgssettings.py b/tests/src/python/test_qgssettings.py index 80b4c1b34a5..13ea62487e1 100644 --- a/tests/src/python/test_qgssettings.py +++ b/tests/src/python/test_qgssettings.py @@ -12,7 +12,7 @@ the Free Software Foundation; either version 2 of the License, or import os import tempfile -from qgis.core import (QgsSettings,) +from qgis.core import QgsSettings, QgsTolerance, QgsMapLayerProxyModel from qgis.testing import start_app, unittest from qgis.PyQt.QtCore import QSettings @@ -391,6 +391,23 @@ class TestQgsSettings(unittest.TestCase): self.settings.remove('testQgisSettings/temp', section=QgsSettings.Core) self.assertEqual(self.settings.value('testqQgisSettings/temp', section=QgsSettings.Core), None) + def test_enumValue(self): + self.settings.setValue('enum', 'LayerUnits') + self.assertEqual(self.settings.enumValue('enum', QgsTolerance.Pixels), QgsTolerance.LayerUnits) + self.settings.setValue('enum', 'dummy_setting') + self.assertEqual(self.settings.enumValue('enum', QgsTolerance.Pixels), QgsTolerance.Pixels) + self.assertEqual(type(self.settings.enumValue('enum', QgsTolerance.Pixels)), QgsTolerance.UnitType) + + def test_flagValue(self): + pointAndLine = QgsMapLayerProxyModel.Filters(QgsMapLayerProxyModel.PointLayer | QgsMapLayerProxyModel.LineLayer) + pointAndPolygon = QgsMapLayerProxyModel.Filters(QgsMapLayerProxyModel.PointLayer | QgsMapLayerProxyModel.PolygonLayer) + + self.settings.setValue('flag', 'PointLayer|PolygonLayer') + self.assertEqual(self.settings.flagValue('flag', pointAndLine), pointAndPolygon) + self.settings.setValue('flag', 'dummy_setting') + self.assertEqual(self.settings.flagValue('flag', pointAndLine), pointAndLine) + self.assertEqual(type(self.settings.flagValue('enum', pointAndLine)), QgsMapLayerProxyModel.Filters) + if __name__ == '__main__': unittest.main()