Skip to content

Commit 303a6f2

Browse files
committedSep 18, 2021
Fix multiline wkt paste from clipboard
Fixes #44989
1 parent 5ea353b commit 303a6f2

File tree

2 files changed

+97
-32
lines changed

2 files changed

+97
-32
lines changed
 

‎src/app/qgsclipboard.cpp

Lines changed: 76 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -206,48 +206,95 @@ QgsFeatureList QgsClipboard::stringToFeatureList( const QString &string, const Q
206206
return features;
207207

208208
// otherwise try to read in as WKT
209-
const QStringList values = string.split( '\n' );
210-
if ( values.isEmpty() || string.isEmpty() )
209+
if ( string.isEmpty() || string.split( '\n' ).count() == 0 )
211210
return features;
212211

213-
const QgsFields sourceFields = retrieveFields();
214-
215-
const auto constValues = values;
216-
for ( const QString &row : constValues )
212+
// Poor man's csv parser
213+
bool isInsideQuotes {false};
214+
QgsAttributes attrs;
215+
QgsGeometry geom;
216+
QString attrVal;
217+
bool isFirstLine {string.startsWith( QStringLiteral( "wkt_geom" ) )};
218+
// it seems there is no other way to check for header
219+
const bool hasHeader{string.startsWith( QStringLiteral( "wkt_geom" ) )};
220+
QgsGeometry geometry;
221+
bool setFields {fields.isEmpty()};
222+
QgsFields fieldsFromClipboard;
223+
224+
auto parseFunc = [ & ]( const QChar & c )
217225
{
218-
// Assume that it's just WKT for now. because GeoJSON is managed by
219-
// previous QgsOgrUtils::stringToFeatureList call
220-
// Get the first value of a \t separated list. WKT clipboard pasted
221-
// feature has first element the WKT geom.
222-
// This split is to fix the following issue: https://github.com/qgis/QGIS/issues/24769
223-
// Value separators are set in generateClipboardText
224-
QStringList fieldValues = row.split( '\t' );
225-
if ( fieldValues.isEmpty() )
226-
continue;
227-
228-
QgsFeature feature;
229-
feature.setFields( sourceFields );
230-
feature.initAttributes( fieldValues.size() - 1 );
231-
232-
//skip header line
233-
if ( fieldValues.at( 0 ) == QLatin1String( "wkt_geom" ) )
226+
227+
// parse geom only if it wasn't successfully set before
228+
if ( geometry.isNull() )
234229
{
235-
continue;
230+
geometry = QgsGeometry::fromWkt( attrVal );
236231
}
237232

238-
for ( int i = 1; i < fieldValues.size(); ++i )
233+
if ( isFirstLine ) // ... name
239234
{
240-
feature.setAttribute( i - 1, fieldValues.at( i ) );
235+
if ( attrVal != QStringLiteral( "wkt_geom" ) ) // ignore this one
236+
{
237+
fieldsFromClipboard.append( QgsField{attrVal, QVariant::String } );
238+
}
239+
}
240+
else // ... or value
241+
{
242+
attrs.append( attrVal );
241243
}
242244

243-
const QgsGeometry geometry = QgsGeometry::fromWkt( fieldValues[0] );
244-
if ( !geometry.isNull() )
245+
// end of record, create a new feature if it's not the header
246+
if ( c == QChar( '\n' ) )
245247
{
246-
feature.setGeometry( geometry );
248+
if ( isFirstLine )
249+
{
250+
isFirstLine = false;
251+
}
252+
else
253+
{
254+
QgsFeature feature{setFields ? fieldsFromClipboard : fields};
255+
feature.setGeometry( geometry );
256+
if ( hasHeader || !geometry.isNull() )
257+
{
258+
attrs.pop_front();
259+
}
260+
feature.setAttributes( attrs );
261+
features.append( feature );
262+
geometry = QgsGeometry();
263+
attrs.clear();
264+
}
247265
}
266+
attrVal.clear();
267+
};
248268

249-
features.append( feature );
269+
for ( auto c = string.constBegin(); c < string.constEnd(); ++c )
270+
{
271+
if ( *c == QChar( '\n' ) || *c == QChar( '\t' ) )
272+
{
273+
if ( isInsideQuotes )
274+
{
275+
attrVal.append( *c );
276+
}
277+
else
278+
{
279+
parseFunc( *c );
280+
}
281+
}
282+
else if ( *c == QChar( '\"' ) )
283+
{
284+
isInsideQuotes = !isInsideQuotes;
285+
}
286+
else
287+
{
288+
attrVal.append( *c );
289+
}
290+
}
291+
292+
// handle missing newline
293+
if ( !string.endsWith( QChar( '\n' ) ) )
294+
{
295+
parseFunc( QChar( '\n' ) );
250296
}
297+
251298
return features;
252299
}
253300

‎tests/src/app/testqgisappclipboard.cpp

Lines changed: 21 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,7 @@ void TestQgisAppClipboard::copyPaste()
121121

122122
void TestQgisAppClipboard::copyToText()
123123
{
124+
124125
//set clipboard to some QgsFeatures
125126
QgsFields fields;
126127
fields.append( QgsField( QStringLiteral( "int_field" ), QVariant::Int ) );
@@ -221,13 +222,28 @@ void TestQgisAppClipboard::copyToText()
221222
settings.setEnumValue( QStringLiteral( "/qgis/copyFeatureFormat" ), QgsClipboard::AttributesWithWKT );
222223
mQgisApp->clipboard()->generateClipboardText( result, resultHtml );
223224
QCOMPARE( result, QString( "wkt_geom\tint_field\tstring_field\nPoint (5 6)\t1\tSingle line text\nPoint (7 8)\t2\t\"Unix Multiline \nText\"\nPoint (9 10)\t3\t\"Windows Multiline \r\nText\"" ) );
225+
224226
}
225227

226228
void TestQgisAppClipboard::pasteWkt()
227229
{
230+
231+
// test issue GH #44989
232+
QgsFeatureList features = mQgisApp->clipboard()->stringToFeatureList( QStringLiteral( "wkt_geom\tint_field\tstring_field\nPoint (5 6)\t1\tSingle line text\nPoint (7 8)\t2\t\"Unix Multiline \nText\"\nPoint (9 10)\t3\t\"Windows Multiline \r\nText\"" ), QgsFields() );
233+
QCOMPARE( features.length(), 3 );
234+
QVERIFY( features.at( 0 ).hasGeometry() && !features.at( 0 ).geometry().isNull() );
235+
QVERIFY( features.at( 1 ).hasGeometry() && !features.at( 1 ).geometry().isNull() );
236+
QVERIFY( features.at( 2 ).hasGeometry() && !features.at( 2 ).geometry().isNull() );
237+
QCOMPARE( features.at( 0 ).fields().count(), 2 );
238+
QCOMPARE( features.at( 0 ).attributeCount(), 2 );
239+
QCOMPARE( features.at( 1 ).fields().count(), 2 );
240+
QCOMPARE( features.at( 1 ).attributeCount(), 2 );
241+
QCOMPARE( features.at( 2 ).fields().count(), 2 );
242+
QCOMPARE( features.at( 2 ).attributeCount(), 2 );
243+
228244
mQgisApp->clipboard()->setText( QStringLiteral( "POINT (125 10)\nPOINT (111 30)" ) );
229245

230-
QgsFeatureList features = mQgisApp->clipboard()->copyOf();
246+
features = mQgisApp->clipboard()->copyOf();
231247
QCOMPARE( features.length(), 2 );
232248
QVERIFY( features.at( 0 ).hasGeometry() && !features.at( 0 ).geometry().isNull() );
233249
QCOMPARE( features.at( 0 ).geometry().constGet()->wkbType(), QgsWkbTypes::Point );
@@ -248,7 +264,8 @@ void TestQgisAppClipboard::pasteWkt()
248264
features = mQgisApp->clipboard()->copyOf();
249265
QCOMPARE( features.length(), 2 );
250266

251-
QVERIFY( features.at( 0 ).hasGeometry() && !features.at( 0 ).geometry().isNull() );
267+
QVERIFY( features.at( 0 ).hasGeometry() );
268+
QVERIFY( !features.at( 0 ).geometry().isNull() );
252269
QCOMPARE( features.at( 0 ).geometry().constGet()->wkbType(), QgsWkbTypes::Point );
253270
featureGeom = features.at( 0 ).geometry();
254271
point = dynamic_cast< const QgsPoint * >( featureGeom.constGet() );
@@ -262,7 +279,7 @@ void TestQgisAppClipboard::pasteWkt()
262279
QCOMPARE( point->y(), 10.0 );
263280

264281
//clipboard should support features without geometry
265-
mQgisApp->clipboard()->setText( QStringLiteral( "\tMNL\t11\t282\tkm\t\t\t\n\tMNL\t11\t347.80000000000001\tkm\t\t\t" ) );
282+
mQgisApp->clipboard()->setText( QStringLiteral( "MNL\t11\t282\tkm\t\t\t\nMNL\t11\t347.80000000000001\tkm\t\t\t" ) );
266283
features = mQgisApp->clipboard()->copyOf();
267284
QCOMPARE( features.length(), 2 );
268285
QVERIFY( !features.at( 0 ).hasGeometry() );
@@ -375,6 +392,7 @@ void TestQgisAppClipboard::pasteGeoJson()
375392
mQgisApp->clipboard()->setText( QStringLiteral( "{\n\"type\": \"Feature\",\"geometry\": {\"type\": \"Point\",\"coordinates\": [125, 10]},\"properties\": {\"name\": \"Dinagat Islands\"}}" ) );
376393

377394
const QgsFeatureList features = mQgisApp->clipboard()->copyOf( fields );
395+
378396
QCOMPARE( features.length(), 1 );
379397
QVERIFY( features.at( 0 ).hasGeometry() && !features.at( 0 ).geometry().isNull() );
380398
QCOMPARE( features.at( 0 ).geometry().constGet()->wkbType(), QgsWkbTypes::Point );

0 commit comments

Comments
 (0)
Please sign in to comment.