Skip to content

Commit 9b964c4

Browse files
authored
Add qgis patch to support qgis 4 projects (#4379)
The new patch includes changes done in #62446 & #65114. They were slightly modified for QGIS 3.40.
1 parent 07ab7ba commit 9b964c4

File tree

3 files changed

+1123
-0
lines changed

3 files changed

+1123
-0
lines changed

vcpkg/ports/qgis/portfile.cmake

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,8 @@ vcpkg_from_github(
1616
cmakelists.patch
1717
crssync.patch
1818
libxml2.patch
19+
qgis4-project-properties.patch
20+
qgis4_url_encoding.patch
1921
)
2022

2123
file(REMOVE ${SOURCE_PATH}/cmake/FindQtKeychain.cmake)
Lines changed: 274 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,274 @@
1+
commit 74549aad26c3358101e88477d9dfa1caae013d72
2+
Author: Jürgen E. Fischer <jef@norbit.de>
3+
Date: Fri Jun 20 15:58:30 2025 +0200
4+
5+
Reapply "Allow free naming of project properties (#60855)"
6+
7+
This reverts commit fb11239112adfc321b3bbacbb20da888a7a37c23.
8+
9+
diff --git a/src/core/project/qgsproject.cpp b/src/core/project/qgsproject.cpp
10+
index f78f9e53bef..cd6f78edaaf 100644
11+
--- a/src/core/project/qgsproject.cpp
12+
+++ b/src/core/project/qgsproject.cpp
13+
@@ -116,21 +116,6 @@ QStringList makeKeyTokens_( const QString &scope, const QString &key )
14+
// be sure to include the canonical root node
15+
keyTokens.push_front( QStringLiteral( "properties" ) );
16+
17+
- //check validy of keys since an invalid xml name will will be dropped upon saving the xml file. If not valid, we print a message to the console.
18+
- for ( int i = 0; i < keyTokens.size(); ++i )
19+
- {
20+
- const QString keyToken = keyTokens.at( i );
21+
-
22+
- //invalid chars in XML are found at http://www.w3.org/TR/REC-xml/#NT-NameChar
23+
- //note : it seems \x10000-\xEFFFF is valid, but it when added to the regexp, a lot of unwanted characters remain
24+
- const thread_local QRegularExpression sInvalidRegexp = QRegularExpression( QStringLiteral( "([^:A-Z_a-z\\x{C0}-\\x{D6}\\x{D8}-\\x{F6}\\x{F8}-\\x{2FF}\\x{370}-\\x{37D}\\x{37F}-\\x{1FFF}\\x{200C}-\\x{200D}\\x{2070}-\\x{218F}\\x{2C00}-\\x{2FEF}\\x{3001}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFFD}\\-\\.0-9\\x{B7}\\x{0300}-\\x{036F}\\x{203F}-\\x{2040}]|^[^:A-Z_a-z\\x{C0}-\\x{D6}\\x{D8}-\\x{F6}\\x{F8}-\\x{2FF}\\x{370}-\\x{37D}\\x{37F}-\\x{1FFF}\\x{200C}-\\x{200D}\\x{2070}-\\x{218F}\\x{2C00}-\\x{2FEF}\\x{3001}-\\x{D7FF}\\x{F900}-\\x{FDCF}\\x{FDF0}-\\x{FFFD}])" ) );
25+
- if ( keyToken.contains( sInvalidRegexp ) )
26+
- {
27+
- const QString errorString = QObject::tr( "Entry token invalid : '%1'. The token will not be saved to file." ).arg( keyToken );
28+
- QgsMessageLog::logMessage( errorString, QString(), Qgis::MessageLevel::Critical );
29+
- }
30+
- }
31+
-
32+
return keyTokens;
33+
}
34+
35+
@@ -1322,20 +1307,20 @@ void dump_( const QgsProjectPropertyKey &topQgsPropertyKey )
36+
* scope. "layers" is a list containing three string values.
37+
*
38+
* \code{.xml}
39+
- * <properties>
40+
- * <fsplugin>
41+
- * <foo type="int" >42</foo>
42+
- * <baz type="int" >1</baz>
43+
- * <layers type="QStringList" >
44+
+ * <properties name="properties">
45+
+ * <properties name="fsplugin">
46+
+ * <properties name="foo" type="int" >42</properties>
47+
+ * <properties name="baz" type="int" >1</properties>
48+
+ * <properties name="layers" type="QStringList">
49+
* <value>railroad</value>
50+
* <value>airport</value>
51+
- * </layers>
52+
- * <xyqzzy type="int" >1</xyqzzy>
53+
- * <bar type="double" >123.456</bar>
54+
- * <feature_types type="QStringList" >
55+
+ * </properties>
56+
+ * <properties name="xyqzzy" type="int" >1</properties>
57+
+ * <properties name="bar" type="double" >123.456</properties>
58+
+ * <properties name="feature_types" type="QStringList">
59+
* <value>type</value>
60+
- * </feature_types>
61+
- * </fsplugin>
62+
+ * </properties>
63+
+ * </properties>
64+
* </properties>
65+
* \endcode
66+
*
67+
@@ -3992,10 +3977,25 @@ bool QgsProject::createEmbeddedLayer( const QString &layerId, const QString &pro
68+
const QDomElement propertiesElem = sProjectDocument.documentElement().firstChildElement( QStringLiteral( "properties" ) );
69+
if ( !propertiesElem.isNull() )
70+
{
71+
- const QDomElement absElem = propertiesElem.firstChildElement( QStringLiteral( "Paths" ) ).firstChildElement( QStringLiteral( "Absolute" ) );
72+
- if ( !absElem.isNull() )
73+
+ QDomElement e = propertiesElem.firstChildElement( QStringLiteral( "Paths" ) );
74+
+ if ( e.isNull() )
75+
+ {
76+
+ e = propertiesElem.firstChildElement( QStringLiteral( "properties" ) );
77+
+ while ( !e.isNull() && e.attribute( QStringLiteral( "name" ) ) != QStringLiteral( "Paths" ) )
78+
+ e = e.nextSiblingElement( QStringLiteral( "properties" ) );
79+
+
80+
+ e = e.firstChildElement( QStringLiteral( "properties" ) );
81+
+ while ( !e.isNull() && e.attribute( QStringLiteral( "name" ) ) != QStringLiteral( "Absolute" ) )
82+
+ e = e.nextSiblingElement( QStringLiteral( "properties" ) );
83+
+ }
84+
+ else
85+
+ {
86+
+ e = e.firstChildElement( QStringLiteral( "Absolute" ) );
87+
+ }
88+
+
89+
+ if ( !e.isNull() )
90+
{
91+
- useAbsolutePaths = absElem.text().compare( QLatin1String( "true" ), Qt::CaseInsensitive ) == 0;
92+
+ useAbsolutePaths = e.text().compare( QLatin1String( "true" ), Qt::CaseInsensitive ) == 0;
93+
}
94+
}
95+
96+
diff --git a/src/core/project/qgsprojectproperty.cpp b/src/core/project/qgsprojectproperty.cpp
97+
index ff8024a5260..1af598012b4 100644
98+
--- a/src/core/project/qgsprojectproperty.cpp
99+
+++ b/src/core/project/qgsprojectproperty.cpp
100+
@@ -233,15 +233,15 @@ bool QgsProjectPropertyValue::readXml( const QDomNode &keyNode )
101+
102+
// keyElement is created by parent QgsProjectPropertyKey
103+
bool QgsProjectPropertyValue::writeXml( QString const &nodeName,
104+
- QDomElement &keyElement,
105+
- QDomDocument &document )
106+
+ QDomElement &keyElement,
107+
+ QDomDocument &document )
108+
{
109+
- QDomElement valueElement = document.createElement( nodeName );
110+
+ QDomElement valueElement = document.createElement( QStringLiteral( "properties" ) );
111+
112+
// remember the type so that we can rebuild it when the project is read in
113+
+ valueElement.setAttribute( QStringLiteral( "name" ), nodeName );
114+
valueElement.setAttribute( QStringLiteral( "type" ), mValue.typeName() );
115+
116+
-
117+
// we handle string lists differently from other types in that we
118+
// create a sequence of repeated elements to cover all the string list
119+
// members; each value will be in a <value></value> tag.
120+
@@ -362,33 +362,41 @@ bool QgsProjectPropertyKey::readXml( const QDomNode &keyNode )
121+
122+
while ( i < subkeys.count() )
123+
{
124+
+ const QDomNode subkey = subkeys.item( i );
125+
+ QString name;
126+
+
127+
+ if ( subkey.nodeName() == QStringLiteral( "properties" ) &&
128+
+ subkey.hasAttributes() && // if we have attributes
129+
+ subkey.isElement() && // and we're an element
130+
+ subkey.toElement().hasAttribute( QStringLiteral( "name" ) ) ) // and we have a "name" attribute
131+
+ name = subkey.toElement().attribute( QStringLiteral( "name" ) );
132+
+ else
133+
+ name = subkey.nodeName();
134+
+
135+
// if the current node is an element that has a "type" attribute,
136+
// then we know it's a leaf node; i.e., a subkey _value_, and not
137+
// a subkey
138+
- if ( subkeys.item( i ).hasAttributes() && // if we have attributes
139+
- subkeys.item( i ).isElement() && // and we're an element
140+
- subkeys.item( i ).toElement().hasAttribute( QStringLiteral( "type" ) ) ) // and we have a "type" attribute
141+
+ if ( subkey.hasAttributes() && // if we have attributes
142+
+ subkey.isElement() && // and we're an element
143+
+ subkey.toElement().hasAttribute( QStringLiteral( "type" ) ) ) // and we have a "type" attribute
144+
{
145+
// then we're a key value
146+
- delete mProperties.take( subkeys.item( i ).nodeName() );
147+
- mProperties.insert( subkeys.item( i ).nodeName(), new QgsProjectPropertyValue );
148+
+ //
149+
+ delete mProperties.take( name );
150+
+ mProperties.insert( name, new QgsProjectPropertyValue );
151+
152+
- QDomNode subkey = subkeys.item( i );
153+
-
154+
- if ( !mProperties[subkeys.item( i ).nodeName()]->readXml( subkey ) )
155+
+ if ( !mProperties[name]->readXml( subkey ) )
156+
{
157+
- QgsDebugError( QStringLiteral( "unable to parse key value %1" ).arg( subkeys.item( i ).nodeName() ) );
158+
+ QgsDebugError( QStringLiteral( "unable to parse key value %1" ).arg( name ) );
159+
}
160+
}
161+
else // otherwise it's a subkey, so just recurse on down the remaining keys
162+
{
163+
- addKey( subkeys.item( i ).nodeName() );
164+
-
165+
- QDomNode subkey = subkeys.item( i );
166+
+ addKey( name );
167+
168+
- if ( !mProperties[subkeys.item( i ).nodeName()]->readXml( subkey ) )
169+
+ if ( !mProperties[name]->readXml( subkey ) )
170+
{
171+
- QgsDebugError( QStringLiteral( "unable to parse subkey %1" ).arg( subkeys.item( i ).nodeName() ) );
172+
+ QgsDebugError( QStringLiteral( "unable to parse subkey %1" ).arg( name ) );
173+
}
174+
}
175+
176+
@@ -408,7 +416,8 @@ bool QgsProjectPropertyKey::writeXml( QString const &nodeName, QDomElement &elem
177+
// If it's an _empty_ node (i.e., one with no properties) we need to emit
178+
// an empty place holder; else create new Dom elements as necessary.
179+
180+
- QDomElement keyElement = document.createElement( nodeName ); // Dom element for this property key
181+
+ QDomElement keyElement = document.createElement( "properties" ); // Dom element for this property key
182+
+ keyElement.toElement().setAttribute( QStringLiteral( "name" ), nodeName );
183+
184+
if ( ! mProperties.isEmpty() )
185+
{
186+
diff --git a/tests/src/python/test_qgsproject.py b/tests/src/python/test_qgsproject.py
187+
index 237553260f6..d44d4438006 100644
188+
--- a/tests/src/python/test_qgsproject.py
189+
+++ b/tests/src/python/test_qgsproject.py
190+
@@ -65,84 +65,6 @@ class TestQgsProject(QgisTestCase):
191+
QgisTestCase.__init__(self, methodName)
192+
self.messageCaught = False
193+
194+
- def test_makeKeyTokens_(self):
195+
- # see http://www.w3.org/TR/REC-xml/#d0e804 for a list of valid characters
196+
-
197+
- invalidTokens = []
198+
- validTokens = []
199+
-
200+
- # all test tokens will be generated by prepending or inserting characters to this token
201+
- validBase = "valid"
202+
-
203+
- # some invalid characters, not allowed anywhere in a token
204+
- # note that '/' must not be added here because it is taken as a separator by makeKeyTokens_()
205+
- invalidChars = "+*,;<>|!$%()=?#\x01"
206+
-
207+
- # generate the characters that are allowed at the start of a token (and at every other position)
208+
- validStartChars = ":_"
209+
- charRanges = [
210+
- (ord("a"), ord("z")),
211+
- (ord("A"), ord("Z")),
212+
- (0x00F8, 0x02FF),
213+
- (0x0370, 0x037D),
214+
- (0x037F, 0x1FFF),
215+
- (0x200C, 0x200D),
216+
- (0x2070, 0x218F),
217+
- (0x2C00, 0x2FEF),
218+
- (0x3001, 0xD7FF),
219+
- (0xF900, 0xFDCF),
220+
- (0xFDF0, 0xFFFD),
221+
- # (0x10000, 0xEFFFF), while actually valid, these are not yet accepted by makeKeyTokens_()
222+
- ]
223+
- for r in charRanges:
224+
- for c in range(r[0], r[1]):
225+
- validStartChars += chr(c)
226+
-
227+
- # generate the characters that are only allowed inside a token, not at the start
228+
- validInlineChars = "-.\xB7"
229+
- charRanges = [
230+
- (ord("0"), ord("9")),
231+
- (0x0300, 0x036F),
232+
- (0x203F, 0x2040),
233+
- ]
234+
- for r in charRanges:
235+
- for c in range(r[0], r[1]):
236+
- validInlineChars += chr(c)
237+
-
238+
- # test forbidden start characters
239+
- for c in invalidChars + validInlineChars:
240+
- invalidTokens.append(c + validBase)
241+
-
242+
- # test forbidden inline characters
243+
- for c in invalidChars:
244+
- invalidTokens.append(validBase[:4] + c + validBase[4:])
245+
-
246+
- # test each allowed start character
247+
- for c in validStartChars:
248+
- validTokens.append(c + validBase)
249+
-
250+
- # test each allowed inline character
251+
- for c in validInlineChars:
252+
- validTokens.append(validBase[:4] + c + validBase[4:])
253+
-
254+
- logger = QgsApplication.messageLog()
255+
- logger.messageReceived.connect(self.catchMessage)
256+
- prj = QgsProject.instance()
257+
-
258+
- for token in validTokens:
259+
- self.messageCaught = False
260+
- prj.readEntry("test", token)
261+
- myMessage = f"valid token '{token}' not accepted"
262+
- assert not self.messageCaught, myMessage
263+
-
264+
- for token in invalidTokens:
265+
- self.messageCaught = False
266+
- prj.readEntry("test", token)
267+
- myMessage = f"invalid token '{token}' accepted"
268+
- assert self.messageCaught, myMessage
269+
-
270+
- logger.messageReceived.disconnect(self.catchMessage)
271+
-
272+
def catchMessage(self):
273+
self.messageCaught = True
274+

0 commit comments

Comments
 (0)