diff --git a/python/gui/auto_generated/qgsnewdatabasetablenamewidget.sip.in b/python/gui/auto_generated/qgsnewdatabasetablenamewidget.sip.in index 598112e221a..dd17fc27cd8 100644 --- a/python/gui/auto_generated/qgsnewdatabasetablenamewidget.sip.in +++ b/python/gui/auto_generated/qgsnewdatabasetablenamewidget.sip.in @@ -46,17 +46,23 @@ Constructs a new QgsNewDatabaseTableNameWidget :param parent: optional parent for this widget %End - QString schema(); + QString schema() const; %Docstring -Returns the currently selected schema for the new table +Returns the currently selected schema or file path (in case of filesystem-based DBs like spatialite or GPKG) for the new table %End - QString table(); + QString uri() const; +%Docstring +Returns the (possibly blank) string representation of the new table data source URI. +The URI might be invalid in case the widget is not in a valid state. +%End + + QString table() const; %Docstring Returns the current name of the new table %End - QString dataProviderKey(); + QString dataProviderKey() const; %Docstring Returns the currently selected data item provider key %End @@ -66,11 +72,13 @@ Returns the currently selected data item provider key Returns ``True`` if the widget contains a valid new table name %End - QString validationError(); + QString validationError() const; %Docstring Returns the validation error or an empty string is the widget status is valid %End + + signals: void validationChanged( bool isValid ); @@ -82,7 +90,7 @@ This signal is emitted whenever the validation status of the widget changes. void schemaNameChanged( const QString &schemaName ); %Docstring -This signal is emitted when the user selects a schema. +This signal is emitted when the user selects a schema (or file path for filesystem-based DBs like spatialite or GPKG). :param schemaName: the name of the selected schema %End @@ -102,6 +110,13 @@ that has a different data provider than the previously selected one. :param providerKey: the data provider key of the selected schema %End + void uriChanged( const QString &uri ); +%Docstring +This signal is emitted when the URI of the new table changes, whether or not it is a valid one. + +:param uri: URI string representation +%End + }; diff --git a/src/gui/qgsnewdatabasetablenamewidget.cpp b/src/gui/qgsnewdatabasetablenamewidget.cpp index efd70b2f1c7..8d84d8fef4d 100644 --- a/src/gui/qgsnewdatabasetablenamewidget.cpp +++ b/src/gui/qgsnewdatabasetablenamewidget.cpp @@ -23,6 +23,10 @@ #include "qgsproviderregistry.h" #include "qgsprovidermetadata.h" + +// List of data item provider keys that are filesystem based +QStringList QgsNewDatabaseTableNameWidget::FILESYSTEM_BASED_DATAITEM_PROVIDERS { QStringLiteral( "GPKG" ), QStringLiteral( "SPATIALITE" ) }; + QgsNewDatabaseTableNameWidget::QgsNewDatabaseTableNameWidget( QgsBrowserGuiModel *browserModel, const QStringList &providersFilter, @@ -44,8 +48,6 @@ QgsNewDatabaseTableNameWidget::QgsNewDatabaseTableNameWidget( setupUi( this ); - mValidationResults->setStyleSheet( QStringLiteral( "* { font-weight: bold; color: red; }" ) ); - QStringList hiddenProviders { QStringLiteral( "special:Favorites" ), @@ -63,8 +65,8 @@ QgsNewDatabaseTableNameWidget::QgsNewDatabaseTableNameWidget( hiddenProviders.push_back( provider->name() ); continue; } - QgsProviderMetadata *md { QgsProviderRegistry::instance()->providerMetadata( provider->dataProviderKey() ) }; - if ( ! md ) + QgsProviderMetadata *metadata { QgsProviderRegistry::instance()->providerMetadata( provider->dataProviderKey() ) }; + if ( ! metadata ) { hiddenProviders.push_back( provider->name() ); continue; @@ -90,7 +92,6 @@ QgsNewDatabaseTableNameWidget::QgsNewDatabaseTableNameWidget( mBrowserProxyModel.setDataItemProviderKeyFilter( hiddenProviders ); mBrowserProxyModel.setShowLayers( false ); mBrowserTreeView->setHeaderHidden( true ); - mBrowserTreeView->setExpandsOnDoubleClick( false ); mBrowserTreeView->setModel( &mBrowserProxyModel ); mBrowserTreeView->setBrowserModel( mBrowserModel ); @@ -99,6 +100,7 @@ QgsNewDatabaseTableNameWidget::QgsNewDatabaseTableNameWidget( { mTableName = mNewTableName->text(); emit tableNameChanged( mTableName ); + updateUri(); validate(); } ); @@ -106,40 +108,39 @@ QgsNewDatabaseTableNameWidget::QgsNewDatabaseTableNameWidget( { if ( index.isValid() ) { - const QgsDataItem *dataItem( mBrowserProxyModel.dataItem( index ) ); - if ( dataItem ) + if ( const QgsDataItem *dataItem = mBrowserProxyModel.dataItem( index ) ) { - const QgsDataCollectionItem *collectionItem = qobject_cast( dataItem ); - if ( collectionItem ) + if ( const QgsDataCollectionItem *collectionItem = qobject_cast( dataItem ) ) { const QString providerKey { QgsApplication::dataItemProviderRegistry()->dataProviderKey( dataItem->providerKey() ) }; - if ( mShownProviders.contains( providerKey ) ) + bool validationRequired { false }; + const QString oldSchema { mSchemaName }; + + if ( mDataProviderKey != providerKey ) { - bool validationRequired { false }; - const QString oldSchema { mSchemaName }; + mSchemaName.clear(); + mDataProviderKey = providerKey; + emit providerKeyChanged( providerKey ); + validationRequired = true; + } - if ( mDataProviderKey != providerKey ) + if ( collectionItem->layerCollection( ) ) + { + mIsFilePath = FILESYSTEM_BASED_DATAITEM_PROVIDERS.contains( collectionItem->providerKey() ); + // Data items for filesystem based items are in the form gpkg://path/to/file.gpkg + mSchemaName = mIsFilePath ? collectionItem->path().remove( QRegularExpression( QStringLiteral( "^[A-z]+:/" ) ) ) : collectionItem->name(); // it may be cleared + mConnectionName = mIsFilePath ? collectionItem->name() : collectionItem->parent()->name(); + if ( oldSchema != mSchemaName ) { - mSchemaName.clear(); - emit providerKeyChanged( providerKey ); - mDataProviderKey = providerKey; - validate(); + emit schemaNameChanged( mSchemaName ); + validationRequired = true; } + } - if ( collectionItem->layerCollection( ) ) - { - mSchemaName = collectionItem->name(); // it may be cleared - if ( oldSchema != collectionItem->name() ) - { - emit schemaNameChanged( mSchemaName ); - validationRequired = true; - } - } - - if ( validationRequired ) - { - validate(); - } + if ( validationRequired ) + { + updateUri(); + validate(); } } } @@ -147,20 +148,59 @@ QgsNewDatabaseTableNameWidget::QgsNewDatabaseTableNameWidget( } ); validate(); - } -QString QgsNewDatabaseTableNameWidget::schema() +void QgsNewDatabaseTableNameWidget::updateUri() +{ + const QString oldUri { mUri }; + QgsProviderMetadata *md { QgsProviderRegistry::instance()->providerMetadata( mDataProviderKey ) }; + if ( md ) + { + QgsAbstractProviderConnection *conn { md->findConnection( mConnectionName ) }; + if ( conn ) + { + QVariantMap uriParts { md->decodeUri( conn->uri() ) }; + uriParts[ QStringLiteral( "layerName" ) ] = mTableName; + uriParts[ QStringLiteral( "schema" ) ] = mSchemaName; + uriParts[ QStringLiteral( "table" ) ] = mTableName; + if ( mIsFilePath ) + { + uriParts[ QStringLiteral( "dbname" ) ] = mSchemaName; + } + mUri = md->encodeUri( uriParts ); + } + else + { + mUri = QString(); + } + } + else + { + mUri = QString(); + } + + if ( mUri != oldUri ) + { + emit uriChanged( mUri ); + } +} + +QString QgsNewDatabaseTableNameWidget::schema() const { return mSchemaName; } -QString QgsNewDatabaseTableNameWidget::table() +QString QgsNewDatabaseTableNameWidget::uri() const +{ + return mUri; +} + +QString QgsNewDatabaseTableNameWidget::table() const { return mTableName; } -QString QgsNewDatabaseTableNameWidget::dataProviderKey() +QString QgsNewDatabaseTableNameWidget::dataProviderKey() const { return mDataProviderKey; } @@ -177,6 +217,9 @@ void QgsNewDatabaseTableNameWidget::validate() mValidationError.clear(); + // Whether to show it red + bool isError { false }; + if ( ! mIsValid ) { if ( mTableName.isEmpty() && mSchemaName.isEmpty() ) @@ -187,6 +230,7 @@ void QgsNewDatabaseTableNameWidget::validate() ! mSchemaName.isEmpty() && tableNames( ).contains( mTableName ) ) { + isError = true; mValidationError = tr( "A table named '%1' already exists" ).arg( mTableName ); } else if ( mSchemaName.isEmpty() ) @@ -206,6 +250,11 @@ void QgsNewDatabaseTableNameWidget::validate() mValidationError = tr( "Select a database schema and enter a unique name for the new table" ); } } + + mValidationResults->setStyleSheet( isError ? + QStringLiteral( "* { color: red; }" ) : + QString() ); + mValidationResults->setText( mValidationError ); mValidationResults->setVisible( ! mIsValid ); if ( wasValid != mIsValid ) @@ -226,26 +275,29 @@ QStringList QgsNewDatabaseTableNameWidget::tableNames() const QString dataProviderKey { QgsApplication::dataItemProviderRegistry()->dataProviderKey( dataItem->providerKey() ) }; if ( ! dataProviderKey.isEmpty() ) { - QgsProviderMetadata *md { QgsProviderRegistry::instance()->providerMetadata( dataProviderKey ) }; - if ( md ) + QgsProviderMetadata *metadata { QgsProviderRegistry::instance()->providerMetadata( dataProviderKey ) }; + if ( metadata ) { - QgsDataItem *parentDataItem { dataItem->parent() }; + QgsDataItem *parentDataItem { mIsFilePath ? dataItem : dataItem->parent() }; if ( parentDataItem ) { - QgsAbstractProviderConnection *conn { md->findConnection( parentDataItem->name() ) }; - const QString cacheKey { conn->uri() + dataItem->name() }; - if ( mTableNamesCache.contains( cacheKey ) ) + QgsAbstractProviderConnection *conn { metadata->findConnection( parentDataItem->name() ) }; + if ( conn ) { - tableNames = mTableNamesCache.value( cacheKey ); - } - else if ( conn && static_cast( conn ) ) - { - const auto tables { static_cast( conn )->tables( dataItem->name() ) }; - for ( const auto &tp : tables ) + const QString cacheKey { conn->uri() + dataItem->name() }; + if ( mTableNamesCache.contains( cacheKey ) ) { - tableNames.push_back( tp.tableName() ); + tableNames = mTableNamesCache.value( cacheKey ); + } + else if ( conn && static_cast( conn ) ) + { + const auto tables { static_cast( conn )->tables( dataItem->name() ) }; + for ( const auto &tp : tables ) + { + tableNames.push_back( tp.tableName() ); + } + mTableNamesCache[ cacheKey ] = tableNames; } - mTableNamesCache[ cacheKey ] = tableNames; } } } @@ -260,7 +312,7 @@ bool QgsNewDatabaseTableNameWidget::isValid() const return mIsValid; } -QString QgsNewDatabaseTableNameWidget::validationError() +QString QgsNewDatabaseTableNameWidget::validationError() const { return mValidationError; } diff --git a/src/gui/qgsnewdatabasetablenamewidget.h b/src/gui/qgsnewdatabasetablenamewidget.h index 66c32de17fc..f31569efc5f 100644 --- a/src/gui/qgsnewdatabasetablenamewidget.h +++ b/src/gui/qgsnewdatabasetablenamewidget.h @@ -59,19 +59,25 @@ class GUI_EXPORT QgsNewDatabaseTableNameWidget : public QWidget, private Ui::Qgs QWidget *parent = nullptr ); /** - * Returns the currently selected schema for the new table + * Returns the currently selected schema or file path (in case of filesystem-based DBs like spatialite or GPKG) for the new table */ - QString schema(); + QString schema() const; + + /** + * Returns the (possibly blank) string representation of the new table data source URI. + * The URI might be invalid in case the widget is not in a valid state. + */ + QString uri() const; /** * Returns the current name of the new table */ - QString table(); + QString table() const; /** * Returns the currently selected data item provider key */ - QString dataProviderKey(); + QString dataProviderKey() const; /** * Returns TRUE if the widget contains a valid new table name @@ -81,7 +87,9 @@ class GUI_EXPORT QgsNewDatabaseTableNameWidget : public QWidget, private Ui::Qgs /** * Returns the validation error or an empty string is the widget status is valid */ - QString validationError(); + QString validationError() const; + + signals: @@ -93,7 +101,7 @@ class GUI_EXPORT QgsNewDatabaseTableNameWidget : public QWidget, private Ui::Qgs void validationChanged( bool isValid ); /** - * This signal is emitted when the user selects a schema. + * This signal is emitted when the user selects a schema (or file path for filesystem-based DBs like spatialite or GPKG). * * \param schemaName the name of the selected schema */ @@ -114,16 +122,28 @@ class GUI_EXPORT QgsNewDatabaseTableNameWidget : public QWidget, private Ui::Qgs */ void providerKeyChanged( const QString &providerKey ); + /** + * This signal is emitted when the URI of the new table changes, whether or not it is a valid one. + * + * \param uri URI string representation + */ + void uriChanged( const QString &uri ); + private: - QgsBrowserProxyModel mBrowserProxyModel; - QgsBrowserGuiModel *mBrowserModel = nullptr; + void updateUri(); void validate(); QStringList tableNames(); + + QgsBrowserProxyModel mBrowserProxyModel; + QgsBrowserGuiModel *mBrowserModel = nullptr; QString mDataProviderKey; QString mTableName; QString mSchemaName; + QString mConnectionName; + bool mIsFilePath = false; + QString mUri; //! List of data provider keys of shown providers QSet mShownProviders; bool mIsValid = false; @@ -131,6 +151,7 @@ class GUI_EXPORT QgsNewDatabaseTableNameWidget : public QWidget, private Ui::Qgs //! Table names cache QMap mTableNamesCache; + static QStringList FILESYSTEM_BASED_DATAITEM_PROVIDERS; // For testing: friend class TestQgsNewDatabaseTableNameWidget; diff --git a/tests/src/gui/testqgsnewdatabasetablewidget.cpp b/tests/src/gui/testqgsnewdatabasetablewidget.cpp index 7102196e7b8..fc97c504372 100644 --- a/tests/src/gui/testqgsnewdatabasetablewidget.cpp +++ b/tests/src/gui/testqgsnewdatabasetablewidget.cpp @@ -18,6 +18,7 @@ #include #include #include +#include #include "qgsnewdatabasetablenamewidget.h" #include "qgsprovidermetadata.h" @@ -37,20 +38,50 @@ class TestQgsNewDatabaseTableNameWidget: public QObject void cleanup(); // will be called after every testfunction. void testWidgetFilters(); - void testWidgetSignals(); + void testWidgetSignalsPostgres(); + void testWidgetSignalsGeopackage(); + private: + + std::unique_ptr mPgConn; + std::unique_ptr mGpkgConn; + QTemporaryDir mDir; + QString mGpkgPath; }; void TestQgsNewDatabaseTableNameWidget::initTestCase() { + + QCoreApplication::setOrganizationName( QStringLiteral( "QGIS" ) ); + QCoreApplication::setOrganizationDomain( QStringLiteral( "qgis.org" ) ); + QCoreApplication::setApplicationName( QStringLiteral( "QGIS-TEST-NEW-DBTABLE-WIDGET" ) ); + QgsApplication::init(); QgsApplication::initQgis(); + // Add some connections to test with QgsProviderMetadata *md { QgsProviderRegistry::instance()->providerMetadata( QStringLiteral( "postgres" ) ) }; - QgsAbstractProviderConnection *conn { md->createConnection( qgetenv( "QGIS_PGTEST_DB" ) ) }; - md->saveConnection( conn, QStringLiteral( "PG_1" ) ); - conn = md->createConnection( qgetenv( " QGIS_PGTEST_DB" ) ); - md->saveConnection( conn, QStringLiteral( "PG_2" ) ); + mPgConn.reset( md->createConnection( qgetenv( "QGIS_PGTEST_DB" ), { } ) ); + md->saveConnection( mPgConn.get(), QStringLiteral( "PG_1" ) ); + md->saveConnection( mPgConn.get(), QStringLiteral( "PG_2" ) ); + + md = QgsProviderRegistry::instance()->providerMetadata( QStringLiteral( "ogr" ) ); + QString errCause; + QMap m; + mGpkgPath = mDir.filePath( QStringLiteral( "test.gpkg" ) ); + QMap options { { QStringLiteral( "layerName" ), QString( "test_layer" ) } }; + QVERIFY( md->createEmptyLayer( mGpkgPath, + QgsFields(), + QgsWkbTypes::Type::Point, + QgsCoordinateReferenceSystem::fromEpsgId( 4326 ), + true, + m, + errCause, + &options ) == QgsVectorLayerExporter::ExportError::NoError ); + QVERIFY( errCause.isEmpty() ); + mGpkgConn.reset( md->createConnection( mDir.filePath( QStringLiteral( "test.gpkg" ) ), { } ) ); + md->saveConnection( mGpkgConn.get(), QStringLiteral( "GPKG_1" ) ); + } void TestQgsNewDatabaseTableNameWidget::cleanupTestCase() @@ -77,7 +108,7 @@ void TestQgsNewDatabaseTableNameWidget::testWidgetFilters() } -void TestQgsNewDatabaseTableNameWidget::testWidgetSignals() +void TestQgsNewDatabaseTableNameWidget::testWidgetSignalsPostgres() { std::unique_ptr w { qgis::make_unique( nullptr, QStringList{ "postgres" } ) }; @@ -92,6 +123,7 @@ void TestQgsNewDatabaseTableNameWidget::testWidgetSignals() QSignalSpy schemaSpy( w.get(), SIGNAL( schemaNameChanged( QString ) ) ); QSignalSpy tableSpy( w.get(), SIGNAL( tableNameChanged( QString ) ) ); QSignalSpy providerSpy( w.get(), SIGNAL( providerKeyChanged( QString ) ) ); + QSignalSpy uriSpy( w.get(), SIGNAL( uriChanged( QString ) ) ); index = w->mBrowserProxyModel.mapToSource( w->mBrowserProxyModel.index( 0, 0 ) ); QVERIFY( index.isValid() ); @@ -103,6 +135,7 @@ void TestQgsNewDatabaseTableNameWidget::testWidgetSignals() QVERIFY( ! w->isValid() ); QCOMPARE( providerSpy.count(), 1 ); + QCOMPARE( uriSpy.count(), 0 ); QCOMPARE( tableSpy.count(), 0 ); QCOMPARE( schemaSpy.count(), 0 ); QCOMPARE( validationSpy.count(), 0 ); @@ -110,7 +143,7 @@ void TestQgsNewDatabaseTableNameWidget::testWidgetSignals() QCOMPARE( arguments.at( 0 ).toString(), QString( "postgres" ) ); // Find qgis_test schema item - index = w->mBrowserModel->findPath( QStringLiteral( "pg:/PG_1/qgis_test" ), Qt::MatchFlag::MatchStartsWith ); + index = w->mBrowserModel->findPath( QStringLiteral( "pg:/PG_1/qgis_test" ) ); QVERIFY( index.isValid() ); w->mBrowserTreeView->scrollTo( w->mBrowserProxyModel.mapFromSource( index ) ); rect = w->mBrowserTreeView->visualRect( w->mBrowserProxyModel.mapFromSource( index ) ); @@ -121,8 +154,11 @@ void TestQgsNewDatabaseTableNameWidget::testWidgetSignals() QCOMPARE( validationSpy.count(), 0 ); QCOMPARE( schemaSpy.count(), 1 ); + QCOMPARE( uriSpy.count(), 1 ); arguments = schemaSpy.takeLast(); QCOMPARE( arguments.at( 0 ).toString(), QString( "qgis_test" ) ); + arguments = uriSpy.takeLast(); + QVERIFY( ! arguments.at( 0 ).toString().isEmpty() ); w->mNewTableName->setText( QStringLiteral( "someNewTableData" ) ); QCOMPARE( tableSpy.count(), 1 ); @@ -137,6 +173,7 @@ void TestQgsNewDatabaseTableNameWidget::testWidgetSignals() QCOMPARE( w->table(), QString( "someNewTableData" ) ); QCOMPARE( w->schema(), QString( "qgis_test" ) ); QCOMPARE( w->dataProviderKey(), QString( "postgres" ) ); + QVERIFY( w->uri().contains( R"("qgis_test"."someNewTableData")" ) ); // Test unique and make it invalid again so we get a status change w->mNewTableName->setText( QStringLiteral( "someData" ) ); @@ -149,7 +186,7 @@ void TestQgsNewDatabaseTableNameWidget::testWidgetSignals() QCOMPARE( arguments.at( 0 ).toBool(), false ); // Now select another schema - index = w->mBrowserModel->findPath( QStringLiteral( "pg:/PG_1/public" ), Qt::MatchFlag::MatchStartsWith ); + index = w->mBrowserModel->findPath( QStringLiteral( "pg:/PG_1/public" ) ); QVERIFY( index.isValid() ); w->mBrowserTreeView->scrollTo( w->mBrowserProxyModel.mapFromSource( index ) ); rect = w->mBrowserTreeView->visualRect( w->mBrowserProxyModel.mapFromSource( index ) ); @@ -168,6 +205,61 @@ void TestQgsNewDatabaseTableNameWidget::testWidgetSignals() QCOMPARE( w->table(), QString( "someData" ) ); QCOMPARE( w->schema(), QString( "public" ) ); QCOMPARE( w->dataProviderKey(), QString( "postgres" ) ); + QVERIFY( w->uri().contains( R"("public"."someData")" ) ); +} + +void TestQgsNewDatabaseTableNameWidget::testWidgetSignalsGeopackage() +{ + std::unique_ptr w { qgis::make_unique( nullptr, QStringList{ "ogr" } ) }; + + auto index = w->mBrowserModel->findPath( QStringLiteral( "pg:/PG_1" ) ); + QVERIFY( index.isValid() ); + w->mBrowserModel->dataItem( index )->populate( true ); + w->mBrowserTreeView->expandAll(); + + QVERIFY( ! w->isValid() ); + + QSignalSpy validationSpy( w.get(), SIGNAL( validationChanged( bool ) ) ); + QSignalSpy schemaSpy( w.get(), SIGNAL( schemaNameChanged( QString ) ) ); + QSignalSpy tableSpy( w.get(), SIGNAL( tableNameChanged( QString ) ) ); + QSignalSpy providerSpy( w.get(), SIGNAL( providerKeyChanged( QString ) ) ); + QSignalSpy uriSpy( w.get(), SIGNAL( uriChanged( QString ) ) ); + + /* + QDialog d; + QVBoxLayout l; + l.addWidget( w.get() ); + d.setLayout( &l ); + d.exec(); + //*/ + + uriSpy.clear(); + index = w->mBrowserModel->findPath( QStringLiteral( "gpkg:/%1" ).arg( mGpkgPath ) ); + QVERIFY( index.isValid() ); + w->mBrowserTreeView->scrollTo( w->mBrowserProxyModel.mapFromSource( index ) ); + auto rect = w->mBrowserTreeView->visualRect( w->mBrowserProxyModel.mapFromSource( index ) ); + QVERIFY( rect.isValid() ); + QTest::mouseClick( w->mBrowserTreeView->viewport(), Qt::LeftButton, 0, rect.center() ); + + QVERIFY( ! w->isValid() ); + QCOMPARE( schemaSpy.count(), 1 ); + auto arguments = schemaSpy.takeLast(); + QCOMPARE( arguments.at( 0 ).toString(), mGpkgPath ); + QCOMPARE( uriSpy.count(), 1 ); + arguments = uriSpy.takeLast(); + QCOMPARE( arguments.at( 0 ).toString(), mGpkgPath ); + + w->mNewTableName->setText( QStringLiteral( "newTableName" ) ); + QVERIFY( w->isValid() ); + QCOMPARE( validationSpy.count(), 1 ); + arguments = validationSpy.takeLast(); + QCOMPARE( arguments.at( 0 ).toBool(), true ); + + // Test getters + QCOMPARE( w->table(), QString( "newTableName" ) ); + QCOMPARE( w->schema(), mGpkgPath ); + QCOMPARE( w->dataProviderKey(), QString( "ogr" ) ); + QCOMPARE( w->uri(), mGpkgPath + QStringLiteral( "|layername=newTableName" ) ); }