[FEATURE] Allow exploration of QGS project file contents directly

within browser

Allows QGIS project file items inside the browser to be expanded,
showing the full layer tree (including groups) contained within
that project. Layers are shown as normal layer items, allowing
them to be easily added to the current project via drag and drop
or double click. Additionally, because they are treated just
the same as any other layer items in the browser, they can be
drag and dropped within the browser to e.g. directly copy the
layer to a geopackage file!

TODO: apply layer symbology from project file when adding a
layer from a different project to the current project
This commit is contained in:
Nyall Dawson 2018-10-30 14:03:07 +10:00
parent f0436df618
commit 09c2daa1c1
6 changed files with 312 additions and 1 deletions

View File

@ -1161,6 +1161,8 @@ QgisApp::QgisApp( QSplashScreen *splash, bool restorePlugins, bool skipVersionCh
registerCustomDropHandler( new QgsPyDropHandler() );
#endif
QgsApplication::dataItemProviderRegistry()->addProvider( new QgsProjectDataItemProvider() );
// Create the plugin registry and load plugins
// load any plugins that were running in the last session
mSplash->showMessage( tr( "Restoring loaded plugins" ), Qt::AlignHCenter | Qt::AlignBottom );

View File

@ -17,6 +17,8 @@
#include "qgisapp.h"
#include "qgsstyleexportimportdialog.h"
#include "qgsstyle.h"
#include "qgslayertreenode.h"
#include "qgslayertree.h"
#include <QDesktopServices>
//
@ -399,3 +401,127 @@ bool QgsStyleXmlDropHandler::handleFileDrop( const QString &file )
return false;
}
//
// QgsProjectRootDataItem
//
QgsProjectRootDataItem::QgsProjectRootDataItem( QgsDataItem *parent, const QString &path )
: QgsProjectItem( parent, QFileInfo( path ).completeBaseName(), path )
{
mCapabilities = Collapse | Fertile; // collapse by default to avoid costly population on startup
setState( NotPopulated );
}
QVector<QgsDataItem *> QgsProjectRootDataItem::createChildren()
{
QVector<QgsDataItem *> childItems;
QgsProject p;
if ( !p.read( mPath ) )
{
childItems.append( new QgsErrorItem( nullptr, p.error(), mPath + "/error" ) );
return childItems;
}
// recursively create groups and layer items for project's layer tree
std::function<void( QgsDataItem *parentItem, QgsLayerTreeGroup *group )> addNodes;
addNodes = [this, &addNodes, &childItems]( QgsDataItem * parentItem, QgsLayerTreeGroup * group )
{
const QList< QgsLayerTreeNode * > children = group->children();
for ( QgsLayerTreeNode *child : children )
{
switch ( child->nodeType() )
{
case QgsLayerTreeNode::NodeLayer:
{
if ( QgsLayerTreeLayer *layerNode = qobject_cast< QgsLayerTreeLayer * >( child ) )
{
QgsMapLayer *layer = layerNode->layer();
#if 0 // TODO
QString style;
if ( layer )
{
QString errorMsg;
QDomDocument doc( QStringLiteral( "qgis" ) );
QgsReadWriteContext context;
context.setPathResolver( p.pathResolver() );
layer->exportNamedStyle( doc, errorMsg, context );
style = doc.toString();
}
#endif
QgsLayerItem *layerItem = new QgsLayerItem( nullptr, layerNode->name(),
layer ? layer->source() : QString(),
layer ? layer->source() : QString(),
layer ? QgsLayerItem::typeFromMapLayer( layer ) : QgsLayerItem::NoType,
layer ? layer->dataProvider()->name() : QString() );
layerItem->setState( Populated ); // children are not expected
layerItem->setToolTip( layer ? layer->source() : QString() );
if ( parentItem == this )
childItems << layerItem;
else
parentItem->addChildItem( layerItem, true );
}
break;
}
case QgsLayerTreeNode::NodeGroup:
{
if ( QgsLayerTreeGroup *groupNode = qobject_cast< QgsLayerTreeGroup * >( child ) )
{
QgsProjectLayerTreeGroupItem *groupItem = new QgsProjectLayerTreeGroupItem( nullptr, groupNode->name() );
addNodes( groupItem, groupNode );
groupItem->setState( Populated );
if ( parentItem == this )
childItems << groupItem;
else
parentItem->addChildItem( groupItem, true );
}
}
break;
}
}
};
addNodes( this, p.layerTreeRoot() );
return childItems;
}
//
// QgsProjectLayerTreeGroupItem
//
QgsProjectLayerTreeGroupItem::QgsProjectLayerTreeGroupItem( QgsDataItem *parent, const QString &name )
: QgsDataCollectionItem( parent, name )
{
mIconName = QStringLiteral( "mActionFolder.svg" );
mCapabilities = NoCapabilities;
setToolTip( name );
}
//
// QgsProjectDataItemProvider
//
QString QgsProjectDataItemProvider::name()
{
return QStringLiteral( "project_item" );
}
int QgsProjectDataItemProvider::capabilities()
{
return QgsDataProvider::File;
}
QgsDataItem *QgsProjectDataItemProvider::createDataItem( const QString &path, QgsDataItem *parentItem )
{
QFileInfo fileInfo( path );
if ( fileInfo.suffix().compare( QLatin1String( "qgs" ), Qt::CaseInsensitive ) == 0 || fileInfo.suffix().compare( QLatin1String( "qgz" ), Qt::CaseInsensitive ) == 0 )
{
return new QgsProjectRootDataItem( parentItem, path );
}
return nullptr;
}

View File

@ -15,6 +15,7 @@
#ifndef QGSAPPBROWSERPROVIDERS_H
#define QGSAPPBROWSERPROVIDERS_H
#include "qgis_app.h"
#include "qgsdataitemprovider.h"
#include "qgsdataprovider.h"
#include "qgscustomdrophandler.h"
@ -189,4 +190,48 @@ class QgsStyleXmlDropHandler : public QgsCustomDropHandler
bool handleFileDrop( const QString &file ) override;
};
/**
* Custom data item for qgs/qgz QGIS project files, with more functionality than default browser project
* file handling. Specifically allows browsing of the project's layer structure within the browser
*/
class APP_EXPORT QgsProjectRootDataItem : public QgsProjectItem
{
public:
/**
* Constructor for QgsProjectRootDataItem, with the specified
* project \a path.
*/
QgsProjectRootDataItem( QgsDataItem *parent, const QString &path );
QVector<QgsDataItem *> createChildren() override;
};
/**
* Represents a layer tree group node within a QGIS project file.
*/
class APP_EXPORT QgsProjectLayerTreeGroupItem : public QgsDataCollectionItem
{
public:
/**
* Constructor for QgsProjectLayerTreeGroupItem, with the specified group \a name.
*/
QgsProjectLayerTreeGroupItem( QgsDataItem *parent, const QString &name );
};
/**
* Custom data item provider for showing qgs/qgz QGIS project files within the browser,
* including the ability to browser the whole project's layer tree structure directly
* within the browser.
*/
class APP_EXPORT QgsProjectDataItemProvider : public QgsDataItemProvider
{
public:
QString name() override;
int capabilities() override;
QgsDataItem *createDataItem( const QString &path, QgsDataItem *parentItem ) override;
};
#endif // QGSAPPBROWSERPROVIDERS_H

View File

@ -175,7 +175,7 @@ void QgsBrowserDockWidget::itemDoubleClicked( const QModelIndex &index )
return;
else
{
// double click not handled by browser model, so use as default view expand behavior
// double-click not handled by browser model, so use as default view expand behavior
if ( mBrowserView->isExpanded( index ) )
mBrowserView->collapse( index );
else

View File

@ -93,6 +93,7 @@ IF (WITH_BINDINGS)
ADD_QGIS_TEST(apppythontest testqgisapppython.cpp)
ENDIF ()
ADD_QGIS_TEST(qgisapp testqgisapp.cpp)
ADD_QGIS_TEST(appbrowserproviders testqgsappbrowserproviders.cpp)
ADD_QGIS_TEST(qgisappclipboard testqgisappclipboard.cpp)
ADD_QGIS_TEST(attributetabletest testqgsattributetable.cpp)
ADD_QGIS_TEST(applocatorfilters testqgsapplocatorfilters.cpp)

View File

@ -0,0 +1,137 @@
/***************************************************************************
testqgsappbrowserproviders.cpp
--------------------------------------
Date : October 30 2018
Copyright : (C) 2018 Nyall Dawson
Email : nyall dot dawson at gmail dot com
***************************************************************************
* *
* 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. *
* *
***************************************************************************/
#include "qgstest.h"
#include <QObject>
#include <QString>
#include <QStringList>
//qgis includes...
#include "qgsdataitem.h"
#include "qgsapplication.h"
#include "qgslogger.h"
#include "qgsdataitemprovider.h"
#include "qgsdataitemproviderregistry.h"
#include "qgssettings.h"
#include "qgsappbrowserproviders.h"
class TestQgsAppBrowserProviders : public QObject
{
Q_OBJECT
public:
TestQgsAppBrowserProviders();
private slots:
void initTestCase();// will be called before the first testfunction is executed.
void cleanupTestCase();// will be called after the last testfunction was executed.
void init() {} // will be called before each testfunction is executed.
void cleanup() {} // will be called after every testfunction.
void testProjectItemCreation();
private:
QgsDirectoryItem *mDirItem = nullptr;
QString mScanItemsSetting;
QString mTestDataDir;
};
TestQgsAppBrowserProviders::TestQgsAppBrowserProviders() = default;
void TestQgsAppBrowserProviders::initTestCase()
{
//
// Runs once before any tests are run
//
// init QGIS's paths - true means that all path will be inited from prefix
QgsApplication::init();
QgsApplication::initQgis();
QgsApplication::showSettings();
QString dataDir( TEST_DATA_DIR ); //defined in CmakeLists.txt
mTestDataDir = dataDir + '/';
// Set up the QgsSettings environment
QCoreApplication::setOrganizationName( QStringLiteral( "QGIS" ) );
QCoreApplication::setOrganizationDomain( QStringLiteral( "qgis.org" ) );
QCoreApplication::setApplicationName( QStringLiteral( "QGIS-TEST" ) );
// save current scanItemsSetting value
QgsSettings settings;
mScanItemsSetting = settings.value( QStringLiteral( "/qgis/scanItemsInBrowser2" ), QVariant( "" ) ).toString();
//create a directory item that will be used in all tests...
mDirItem = new QgsDirectoryItem( nullptr, QStringLiteral( "Test" ), TEST_DATA_DIR );
}
void TestQgsAppBrowserProviders::cleanupTestCase()
{
// restore scanItemsSetting
QgsSettings settings;
settings.setValue( QStringLiteral( "/qgis/scanItemsInBrowser2" ), mScanItemsSetting );
if ( mDirItem )
delete mDirItem;
QgsApplication::exitQgis();
}
void TestQgsAppBrowserProviders::testProjectItemCreation()
{
QgsDirectoryItem *dirItem = new QgsDirectoryItem( nullptr, QStringLiteral( "Test" ), mTestDataDir + QStringLiteral( "qgis_server/" ) );
QVector<QgsDataItem *> children = dirItem->createChildren();
// now, add a specific provider which handles project files
QgsApplication::dataItemProviderRegistry()->addProvider( new QgsProjectDataItemProvider() );
dirItem = new QgsDirectoryItem( nullptr, QStringLiteral( "Test" ), mTestDataDir + QStringLiteral( "qgis_server/" ) );
children = dirItem->createChildren();
for ( QgsDataItem *child : children )
{
if ( child->type() == QgsDataItem::Project && child->path() == mTestDataDir + QStringLiteral( "qgis_server/test_project.qgs" ) )
{
child->populate( true );
QCOMPARE( child->children().count(), 4 );
QVERIFY( dynamic_cast< QgsProjectLayerTreeGroupItem * >( child->children().at( 0 ) ) );
QCOMPARE( child->children().at( 0 )->name(), QStringLiteral( "groupwithoutshortname" ) );
QCOMPARE( child->children().at( 0 )->children().count(), 1 );
QVERIFY( dynamic_cast< QgsLayerItem * >( child->children().at( 0 )->children().at( 0 ) ) );
QCOMPARE( child->children().at( 0 )->children().at( 0 )->name(), QStringLiteral( "testlayer3" ) );
QVERIFY( dynamic_cast< QgsProjectLayerTreeGroupItem * >( child->children().at( 1 ) ) );
QCOMPARE( child->children().at( 1 )->name(), QStringLiteral( "groupwithshortname" ) );
QCOMPARE( child->children().at( 1 )->children().count(), 1 );
QVERIFY( dynamic_cast< QgsLayerItem * >( child->children().at( 1 )->children().at( 0 ) ) );
QCOMPARE( child->children().at( 1 )->children().at( 0 )->name(), QStringLiteral( "testlayer2" ) );
QVERIFY( dynamic_cast< QgsLayerItem * >( child->children().at( 2 ) ) );
QCOMPARE( child->children().at( 2 )->name(), QStringLiteral( "testlayer" ) );
QVERIFY( dynamic_cast< QgsLayerItem * >( child->children().at( 3 ) ) );
QCOMPARE( child->children().at( 3 )->name(), QStringLiteral( "testlayer \u00E8\u00E9" ) );
delete dirItem;
return;
}
}
delete dirItem;
QVERIFY( false ); // should not be reached
}
QGSTEST_MAIN( TestQgsAppBrowserProviders )
#include "testqgsappbrowserproviders.moc"