Skip to content
Permalink
Browse files

[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
  • Loading branch information
nyalldawson committed Oct 30, 2018
1 parent f0436df commit 09c2daa1c1837fb88c9dfe4707f3cf8db42a2b83
@@ -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 );
@@ -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;
}
@@ -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
@@ -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
@@ -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)
@@ -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"

0 comments on commit 09c2daa

Please sign in to comment.
You can’t perform that action at this time.