diff --git a/src/app/qgsclipboard.cpp b/src/app/qgsclipboard.cpp index 1f15f8496df..bd1e65f71ae 100644 --- a/src/app/qgsclipboard.cpp +++ b/src/app/qgsclipboard.cpp @@ -206,48 +206,95 @@ QgsFeatureList QgsClipboard::stringToFeatureList( const QString &string, const Q return features; // otherwise try to read in as WKT - const QStringList values = string.split( '\n' ); - if ( values.isEmpty() || string.isEmpty() ) + if ( string.isEmpty() || string.split( '\n' ).count() == 0 ) return features; - const QgsFields sourceFields = retrieveFields(); + // Poor man's csv parser + bool isInsideQuotes {false}; + QgsAttributes attrs; + QgsGeometry geom; + QString attrVal; + bool isFirstLine {string.startsWith( QStringLiteral( "wkt_geom" ) )}; + // it seems there is no other way to check for header + const bool hasHeader{string.startsWith( QStringLiteral( "wkt_geom" ) )}; + QgsGeometry geometry; + bool setFields {fields.isEmpty()}; + QgsFields fieldsFromClipboard; - const auto constValues = values; - for ( const QString &row : constValues ) + auto parseFunc = [ & ]( const QChar & c ) { - // Assume that it's just WKT for now. because GeoJSON is managed by - // previous QgsOgrUtils::stringToFeatureList call - // Get the first value of a \t separated list. WKT clipboard pasted - // feature has first element the WKT geom. - // This split is to fix the following issue: https://github.com/qgis/QGIS/issues/24769 - // Value separators are set in generateClipboardText - QStringList fieldValues = row.split( '\t' ); - if ( fieldValues.isEmpty() ) - continue; - QgsFeature feature; - feature.setFields( sourceFields ); - feature.initAttributes( fieldValues.size() - 1 ); - - //skip header line - if ( fieldValues.at( 0 ) == QLatin1String( "wkt_geom" ) ) + // parse geom only if it wasn't successfully set before + if ( geometry.isNull() ) { - continue; + geometry = QgsGeometry::fromWkt( attrVal ); } - for ( int i = 1; i < fieldValues.size(); ++i ) + if ( isFirstLine ) // ... name { - feature.setAttribute( i - 1, fieldValues.at( i ) ); + if ( attrVal != QStringLiteral( "wkt_geom" ) ) // ignore this one + { + fieldsFromClipboard.append( QgsField{attrVal, QVariant::String } ); + } + } + else // ... or value + { + attrs.append( attrVal ); } - const QgsGeometry geometry = QgsGeometry::fromWkt( fieldValues[0] ); - if ( !geometry.isNull() ) + // end of record, create a new feature if it's not the header + if ( c == QChar( '\n' ) ) { - feature.setGeometry( geometry ); + if ( isFirstLine ) + { + isFirstLine = false; + } + else + { + QgsFeature feature{setFields ? fieldsFromClipboard : fields}; + feature.setGeometry( geometry ); + if ( hasHeader || !geometry.isNull() ) + { + attrs.pop_front(); + } + feature.setAttributes( attrs ); + features.append( feature ); + geometry = QgsGeometry(); + attrs.clear(); + } } + attrVal.clear(); + }; - features.append( feature ); + for ( auto c = string.constBegin(); c < string.constEnd(); ++c ) + { + if ( *c == QChar( '\n' ) || *c == QChar( '\t' ) ) + { + if ( isInsideQuotes ) + { + attrVal.append( *c ); + } + else + { + parseFunc( *c ); + } + } + else if ( *c == QChar( '\"' ) ) + { + isInsideQuotes = !isInsideQuotes; + } + else + { + attrVal.append( *c ); + } } + + // handle missing newline + if ( !string.endsWith( QChar( '\n' ) ) ) + { + parseFunc( QChar( '\n' ) ); + } + return features; } diff --git a/tests/src/app/testqgisappclipboard.cpp b/tests/src/app/testqgisappclipboard.cpp index 96a848c8183..9695d636000 100644 --- a/tests/src/app/testqgisappclipboard.cpp +++ b/tests/src/app/testqgisappclipboard.cpp @@ -121,6 +121,7 @@ void TestQgisAppClipboard::copyPaste() void TestQgisAppClipboard::copyToText() { + //set clipboard to some QgsFeatures QgsFields fields; fields.append( QgsField( QStringLiteral( "int_field" ), QVariant::Int ) ); @@ -221,13 +222,28 @@ void TestQgisAppClipboard::copyToText() settings.setEnumValue( QStringLiteral( "/qgis/copyFeatureFormat" ), QgsClipboard::AttributesWithWKT ); mQgisApp->clipboard()->generateClipboardText( result, resultHtml ); 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\"" ) ); + } void TestQgisAppClipboard::pasteWkt() { + + // test issue GH #44989 + 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() ); + QCOMPARE( features.length(), 3 ); + QVERIFY( features.at( 0 ).hasGeometry() && !features.at( 0 ).geometry().isNull() ); + QVERIFY( features.at( 1 ).hasGeometry() && !features.at( 1 ).geometry().isNull() ); + QVERIFY( features.at( 2 ).hasGeometry() && !features.at( 2 ).geometry().isNull() ); + QCOMPARE( features.at( 0 ).fields().count(), 2 ); + QCOMPARE( features.at( 0 ).attributeCount(), 2 ); + QCOMPARE( features.at( 1 ).fields().count(), 2 ); + QCOMPARE( features.at( 1 ).attributeCount(), 2 ); + QCOMPARE( features.at( 2 ).fields().count(), 2 ); + QCOMPARE( features.at( 2 ).attributeCount(), 2 ); + mQgisApp->clipboard()->setText( QStringLiteral( "POINT (125 10)\nPOINT (111 30)" ) ); - QgsFeatureList features = mQgisApp->clipboard()->copyOf(); + features = mQgisApp->clipboard()->copyOf(); QCOMPARE( features.length(), 2 ); QVERIFY( features.at( 0 ).hasGeometry() && !features.at( 0 ).geometry().isNull() ); QCOMPARE( features.at( 0 ).geometry().constGet()->wkbType(), QgsWkbTypes::Point ); @@ -248,7 +264,8 @@ void TestQgisAppClipboard::pasteWkt() features = mQgisApp->clipboard()->copyOf(); QCOMPARE( features.length(), 2 ); - QVERIFY( features.at( 0 ).hasGeometry() && !features.at( 0 ).geometry().isNull() ); + QVERIFY( features.at( 0 ).hasGeometry() ); + QVERIFY( !features.at( 0 ).geometry().isNull() ); QCOMPARE( features.at( 0 ).geometry().constGet()->wkbType(), QgsWkbTypes::Point ); featureGeom = features.at( 0 ).geometry(); point = dynamic_cast< const QgsPoint * >( featureGeom.constGet() ); @@ -262,7 +279,7 @@ void TestQgisAppClipboard::pasteWkt() QCOMPARE( point->y(), 10.0 ); //clipboard should support features without geometry - mQgisApp->clipboard()->setText( QStringLiteral( "\tMNL\t11\t282\tkm\t\t\t\n\tMNL\t11\t347.80000000000001\tkm\t\t\t" ) ); + mQgisApp->clipboard()->setText( QStringLiteral( "MNL\t11\t282\tkm\t\t\t\nMNL\t11\t347.80000000000001\tkm\t\t\t" ) ); features = mQgisApp->clipboard()->copyOf(); QCOMPARE( features.length(), 2 ); QVERIFY( !features.at( 0 ).hasGeometry() ); @@ -375,6 +392,7 @@ void TestQgisAppClipboard::pasteGeoJson() mQgisApp->clipboard()->setText( QStringLiteral( "{\n\"type\": \"Feature\",\"geometry\": {\"type\": \"Point\",\"coordinates\": [125, 10]},\"properties\": {\"name\": \"Dinagat Islands\"}}" ) ); const QgsFeatureList features = mQgisApp->clipboard()->copyOf( fields ); + QCOMPARE( features.length(), 1 ); QVERIFY( features.at( 0 ).hasGeometry() && !features.at( 0 ).geometry().isNull() ); QCOMPARE( features.at( 0 ).geometry().constGet()->wkbType(), QgsWkbTypes::Point );