Skip to content

Commit

Permalink
Add a template list to the welcome screen
Browse files Browse the repository at this point in the history
  • Loading branch information
m-kuhn committed May 27, 2019
1 parent e78a9f7 commit 6eb5b3e
Show file tree
Hide file tree
Showing 8 changed files with 241 additions and 6 deletions.
3 changes: 3 additions & 0 deletions python/core/auto_generated/qgsproject.sip.in
Original file line number Diff line number Diff line change
Expand Up @@ -976,6 +976,8 @@ Returns the current auxiliary storage.
.. versionadded:: 3.0
%End



const QgsProjectMetadata &metadata() const;
%Docstring
Returns a reference to the project's metadata store.
Expand Down Expand Up @@ -1392,6 +1394,7 @@ Emitted when the project dirty status changes.
.. versionadded:: 3.2
%End


public slots:

void setSnappingConfig( const QgsSnappingConfig &snappingConfig );
Expand Down
2 changes: 2 additions & 0 deletions src/app/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,7 @@ SET(QGIS_APP_SRCS
qgsstatusbarcoordinateswidget.cpp
qgsstatusbarmagnifierwidget.cpp
qgsstatusbarscalewidget.cpp
qgstemplateprojectsmodel.cpp
qgsvectorlayerloadstyledialog.cpp
qgsversioninfo.cpp
qgsrecentprojectsitemsmodel.cpp
Expand Down Expand Up @@ -310,6 +311,7 @@ SET (QGIS_APP_MOC_HDRS
qgsstatusbarcoordinateswidget.h
qgsstatusbarmagnifierwidget.h
qgsstatusbarscalewidget.h
qgstemplateprojectsmodel.h
qgsvectorlayerloadstyledialog.h
qgsversioninfo.h
qgsrecentprojectsitemsmodel.h
Expand Down
5 changes: 3 additions & 2 deletions src/app/qgisapp.h
Original file line number Diff line number Diff line change
Expand Up @@ -1062,6 +1062,9 @@ class APP_EXPORT QgisApp : public QMainWindow, private Ui::MainWindow
*/
void triggerCrashHandler();

//! Create a new file from a template project
bool fileNewFromTemplate( const QString &fileName );

protected:

//! Handle state changes (WindowTitleChange)
Expand Down Expand Up @@ -1316,8 +1319,6 @@ class APP_EXPORT QgisApp : public QMainWindow, private Ui::MainWindow
void fileOpenAfterLaunch();
//! After project read, set any auto-opened project as successful
void fileOpenedOKAfterLaunch();
//! Create a new file from a template project
bool fileNewFromTemplate( const QString &fileName );
void fileNewFromTemplateAction( QAction *qAction );
void fileNewFromDefaultTemplate();
//! Calculate new rasters from existing ones
Expand Down
103 changes: 103 additions & 0 deletions src/app/qgstemplateprojectsmodel.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
/***************************************************************************
----------------------------------------------------
date : 16.5.2019
copyright : (C) 2019 by Matthias Kuhn
email : matthias@opengis.ch
***************************************************************************
* *
* 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 "qgstemplateprojectsmodel.h"
#include "qgsziputils.h"
#include "qgssettings.h"
#include "qgsapplication.h"
#include "qgis.h"
#include "qgsrecentprojectsitemsmodel.h"

#include <QStandardPaths>
#include <QDir>
#include <QCryptographicHash>
#include <QPainter>

#include <memory>


QgsTemplateProjectsModel::QgsTemplateProjectsModel( QObject *parent )
: QStandardItemModel( parent )
{
const QStringList paths = QStandardPaths::standardLocations( QStandardPaths::AppDataLocation );
QString templateDirName = QgsSettings().value( QStringLiteral( "qgis/projectTemplateDir" ),
QgsApplication::qgisSettingsDirPath() + QStringLiteral( "project_templates" ) ).toString();

for ( const QString &templatePath : paths )
{
const QString path = templatePath + QDir::separator() + QStringLiteral( "project_templates" );
scanDirectory( path );
mFileSystemWatcher.addPath( path );
}

scanDirectory( templateDirName );
mFileSystemWatcher.addPath( templateDirName );

connect( &mFileSystemWatcher, &QFileSystemWatcher::directoryChanged, this, &QgsTemplateProjectsModel::scanDirectory );

setColumnCount( 1 );
}

void QgsTemplateProjectsModel::scanDirectory( const QString &path )
{
QDir dir = QDir( path );
const QFileInfoList files = dir.entryInfoList( QStringList() << QStringLiteral( "*.qgs" ) << QStringLiteral( "*.qgz" ) );

// Remove any template from this directory)
for ( int i = rowCount() - 1; i >= 0; --i )
{
if ( index( i, 0 ).data( QgsRecentProjectItemsModel::NativePathRole ).toString().startsWith( path ) )
{
removeRow( i );
}
}

// Refill with templates from this directory
for ( const QFileInfo &file : files )
{
std::unique_ptr<QStandardItem> item = qgis::make_unique<QStandardItem>( file.fileName() ) ;

const QString fileId = QCryptographicHash::hash( file.filePath().toUtf8(), QCryptographicHash::Sha224 ).toHex();

QStringList files;
QDir().mkpath( mTemporaryDir.filePath( fileId ) );

QgsZipUtils::unzip( file.filePath(), mTemporaryDir.filePath( fileId ), files );

QString filename( mTemporaryDir.filePath( fileId ) + QDir::separator() + QStringLiteral( "preview.png" ) );

QImage thumbnail( filename );
if ( !thumbnail.isNull() )
{
//nicely round corners so users don't get paper cuts
QImage previewImage( thumbnail.size(), QImage::Format_ARGB32 );
previewImage.fill( Qt::transparent );
QPainter previewPainter( &previewImage );
previewPainter.setRenderHint( QPainter::Antialiasing, true );
previewPainter.setPen( Qt::NoPen );
previewPainter.setBrush( Qt::black );
previewPainter.drawRoundedRect( 0, 0, previewImage.width(), previewImage.height(), 8, 8 );
previewPainter.setCompositionMode( QPainter::CompositionMode_SourceIn );
previewPainter.drawImage( 0, 0, thumbnail );
previewPainter.end();
item->setData( QPixmap::fromImage( previewImage ), Qt::DecorationRole );
}
item->setData( file.baseName(), QgsRecentProjectItemsModel::TitleRole );
item->setData( file.filePath(), QgsRecentProjectItemsModel::NativePathRole );

item->setFlags( Qt::ItemFlag::ItemIsSelectable | Qt::ItemFlag::ItemIsEnabled ) ;
appendRow( item.release() );
}
}
41 changes: 41 additions & 0 deletions src/app/qgstemplateprojectsmodel.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
/***************************************************************************
----------------------------------------------------
date : 16.5.2019
copyright : (C) 2019 by Matthias Kuhn
email : matthias@opengis.ch
***************************************************************************
* *
* 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. *
* *
***************************************************************************/

#ifndef QGSTEMPLATEPROJECTSMODEL_H
#define QGSTEMPLATEPROJECTSMODEL_H

#include <QStandardItemModel>
#include <QFileSystemWatcher>
#include <QTemporaryDir>

class QgsTemplateProjectsModel : public QStandardItemModel
{
Q_OBJECT

public:
QgsTemplateProjectsModel( QObject *parent = nullptr );

private slots:

private:
void scanDirectory( const QString &path );

QFileSystemWatcher mFileSystemWatcher;

QTemporaryDir mTemporaryDir;
};


#endif // QGSTEMPLATEPROJECTSMODEL_H
54 changes: 50 additions & 4 deletions src/app/qgswelcomepage.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -23,12 +23,14 @@
#include "qgsnative.h"
#include "qgsstringutils.h"
#include "qgsfileutils.h"
#include "qgstemplateprojectsmodel.h"

#include <QHBoxLayout>
#include <QVBoxLayout>
#include <QListView>
#include <QDesktopServices>
#include <QTextBrowser>
#include <QMessageBox>

QgsWelcomePage::QgsWelcomePage( bool skipVersionCheck, QWidget *parent )
: QWidget( parent )
Expand Down Expand Up @@ -68,26 +70,29 @@ QgsWelcomePage::QgsWelcomePage( bool skipVersionCheck, QWidget *parent )

centerLayout->addWidget( mRecentProjectsListView, 1, 0 );

layout->addWidget(centerContainer);
layout->addWidget( centerContainer );

QLabel *templatesTitle = new QLabel( QStringLiteral( "<div style='font-size:%1px;font-weight:bold'>%2</div>" ).arg( titleSize ).arg( tr( "Templates" ) ) );
templatesTitle->setContentsMargins( titleSize / 2, titleSize / 6, 0, 0 );
centerLayout->addWidget( templatesTitle, 0, 1 );

mTemplateProjectsModel = new QStandardItemModel;
mTemplateProjectsModel = new QgsTemplateProjectsModel( this );
mTemplateProjectsListView = new QListView();
mTemplateProjectsListView->setResizeMode( QListView::Adjust );
mTemplateProjectsListView->setModel( mTemplateProjectsModel );
mTemplateProjectsListView->setItemDelegate( new QgsRecentProjectItemDelegate( mTemplateProjectsListView ) );
mTemplateProjectsListView->setContextMenuPolicy( Qt::CustomContextMenu );
connect( mTemplateProjectsListView, &QListView::customContextMenuRequested, this, &QgsWelcomePage::showContextMenuForTemplates );
centerLayout->addWidget( mTemplateProjectsListView, 1, 1 );

mVersionInformation = new QTextBrowser;
mVersionInformation->setSizePolicy( QSizePolicy::Expanding, QSizePolicy::Maximum );
mVersionInformation->setReadOnly( true );
mVersionInformation->setOpenExternalLinks( true );
mVersionInformation->setStyleSheet( QStringLiteral("QTextEdit { background-color: #dff0d8; border: 1px solid #8e998a; padding-top: 0.25em; max-height: 1.75em; min-height: 1.75em; } "
mVersionInformation->setStyleSheet( QStringLiteral( "QTextEdit { background-color: #dff0d8; border: 1px solid #8e998a; padding-top: 0.25em; max-height: 1.75em; min-height: 1.75em; } "
"QScrollBar { background-color: rgba(0,0,0,0); } "
"QScrollBar::add-page,QScrollBar::sub-page,QScrollBar::handle { background-color: rgba(0,0,0,0); color: rgba(0,0,0,0); } "
"QScrollBar::up-arrow,QScrollBar::down-arrow { color: rgb(0,0,0); } ") );
"QScrollBar::up-arrow,QScrollBar::down-arrow { color: rgb(0,0,0); } " ) );

mainLayout->addWidget( mVersionInformation );
mVersionInformation->setVisible( false );
Expand All @@ -101,6 +106,7 @@ QgsWelcomePage::QgsWelcomePage( bool skipVersionCheck, QWidget *parent )
}

connect( mRecentProjectsListView, &QAbstractItemView::activated, this, &QgsWelcomePage::recentProjectItemActivated );
connect( mTemplateProjectsListView, &QAbstractItemView::activated, this, &QgsWelcomePage::templateProjectItemActivated );
}

QgsWelcomePage::~QgsWelcomePage()
Expand All @@ -118,6 +124,11 @@ void QgsWelcomePage::recentProjectItemActivated( const QModelIndex &index )
QgisApp::instance()->openProject( mRecentProjectsModel->data( index, Qt::ToolTipRole ).toString() );
}

void QgsWelcomePage::templateProjectItemActivated( const QModelIndex &index )
{
QgisApp::instance()->fileNewFromTemplate( index.data( QgsRecentProjectItemsModel::NativePathRole ).toString() );
}

void QgsWelcomePage::versionInfoReceived()
{
QgsVersionInfo *versionInfo = qobject_cast<QgsVersionInfo *>( sender() );
Expand Down Expand Up @@ -204,4 +215,39 @@ void QgsWelcomePage::showContextMenuForProjects( QPoint point )
menu->addAction( removeProjectAction );

menu->popup( mapToGlobal( point ) );
connect( menu, &QMenu::aboutToHide, menu, &QMenu::deleteLater );
}

void QgsWelcomePage::showContextMenuForTemplates( QPoint point )
{
QModelIndex index = mTemplateProjectsListView->indexAt( point );
if ( !index.isValid() )
return;

QFileInfo fileInfo( index.data( QgsRecentProjectItemsModel::NativePathRole ).toString() );

QMenu *menu = new QMenu();

if ( fileInfo.isWritable() )
{
QAction *deleteFileAction = new QAction( tr( "Delete template" ) );
connect( deleteFileAction, &QAction::triggered, this, [this, fileInfo, index]
{
QMessageBox msgBox( this );
msgBox.setWindowTitle( tr( "Delete template" ) );
msgBox.setText( tr( "Do you want to delete the template %1? This action can not be undone." ).arg( index.data( QgsRecentProjectItemsModel::TitleRole ).toString() ) );
auto deleteButton = msgBox.addButton( tr( "Delete" ), QMessageBox::YesRole );
msgBox.addButton( QMessageBox::Cancel );
msgBox.setIcon( QMessageBox::Question );
msgBox.exec();
if ( msgBox.clickedButton() == deleteButton )
{
QFile file( fileInfo.filePath() );
file.remove();
}
} );
menu->addAction( deleteFileAction );
}

menu->popup( mTemplateProjectsListView->mapToGlobal( point ) );
}
3 changes: 3 additions & 0 deletions src/app/qgswelcomepage.h
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
#include <QWidget>
#include <QTextBrowser>
#include <QStandardItemModel>
#include <QFileSystemWatcher>

#include "qgsrecentprojectsitemsmodel.h"

Expand All @@ -43,8 +44,10 @@ class QgsWelcomePage : public QWidget

private slots:
void recentProjectItemActivated( const QModelIndex &index );
void templateProjectItemActivated( const QModelIndex &index );
void versionInfoReceived();
void showContextMenuForProjects( QPoint point );
void showContextMenuForTemplates( QPoint point );

private:
QgsRecentProjectItemsModel *mRecentProjectsModel = nullptr;
Expand Down
36 changes: 36 additions & 0 deletions src/core/qgsproject.h
Original file line number Diff line number Diff line change
Expand Up @@ -966,6 +966,27 @@ class CORE_EXPORT QgsProject : public QObject, public QgsExpressionContextGenera
*/
QgsAuxiliaryStorage *auxiliaryStorage();

/**
* Returns the path to an attached file known by \a fileName.
*
* \note Not available in Python bindings
* \note Attached files are only supported by QGZ file based projects
* \see collectAttachedFiles()
* \since QGIS 3.8
*/
QString attachedFile( const QString &fileName ) const SIP_SKIP;

/**
* Returns a map of all attached files with relative paths and real paths.
*
* \note Not available in Python bindings
* \note Attached files are only supported by QGZ file based projects
* \see collectAttachedFiles()
* \see attachedFile( fileName )
* \since QGIS 3.8
*/
QgsStringMap attachedFiles() const SIP_SKIP;

/**
* Returns a reference to the project's metadata store.
* \see setMetadata()
Expand Down Expand Up @@ -1329,6 +1350,21 @@ class CORE_EXPORT QgsProject : public QObject, public QgsExpressionContextGenera
*/
void isDirtyChanged( bool dirty );

/**
* Emitted whenever the project is saved to a qgz file.
* This can be used to package additional files into the qgz file by modifying the \a files map.
*
* Map keys represent relative paths inside the qgz file, map values represent the path to
* the source file.
*
* \note Not available in Python bindings
* \note Only will be emitted with QGZ project files
* \see attachedFiles()
* \see attachedFile( fileName )
* \since QGIS 3.8
*/
void collectAttachedFiles( QgsStringMap &files SIP_INOUT ) SIP_SKIP;

public slots:

/**
Expand Down

0 comments on commit 6eb5b3e

Please sign in to comment.