Skip to content

Commit

Permalink
Browse files Browse the repository at this point in the history
[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 Nov 1, 2018
1 parent f0436df commit 09c2daa
Show file tree
Hide file tree
Showing 6 changed files with 312 additions and 1 deletion.
2 changes: 2 additions & 0 deletions src/app/qgisapp.cpp
Expand Up @@ -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 );
Expand Down
126 changes: 126 additions & 0 deletions src/app/qgsappbrowserproviders.cpp
Expand Up @@ -17,6 +17,8 @@
#include "qgisapp.h"
#include "qgsstyleexportimportdialog.h"
#include "qgsstyle.h"
#include "qgslayertreenode.h"
#include "qgslayertree.h"
#include <QDesktopServices>

//
Expand Down Expand Up @@ -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;
}
45 changes: 45 additions & 0 deletions src/app/qgsappbrowserproviders.h
Expand Up @@ -15,6 +15,7 @@
#ifndef QGSAPPBROWSERPROVIDERS_H
#define QGSAPPBROWSERPROVIDERS_H

#include "qgis_app.h"
#include "qgsdataitemprovider.h"
#include "qgsdataprovider.h"
#include "qgscustomdrophandler.h"
Expand Down Expand Up @@ -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
2 changes: 1 addition & 1 deletion src/gui/qgsbrowserdockwidget.cpp
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions tests/src/app/CMakeLists.txt
Expand Up @@ -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)
Expand Down
137 changes: 137 additions & 0 deletions tests/src/app/testqgsappbrowserproviders.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.