From a30dd8a153ef8eb536ce10a25dffbcc7b4fe7573 Mon Sep 17 00:00:00 2001 From: Nyall Dawson Date: Tue, 5 Dec 2017 11:06:27 +1000 Subject: [PATCH] Port composer manager dialog to layouts Not exposed via GUI yet, but can be shown by entering iface.showLayoutManager() in the Python console --- python/gui/qgisinterface.sip | 6 + src/app/CMakeLists.txt | 2 + src/app/composer/qgscomposermanager.cpp | 38 +- src/app/composer/qgscomposermanager.h | 6 +- src/app/layout/qgslayoutdesignerdialog.cpp | 7 +- src/app/layout/qgslayoutmanagerdialog.cpp | 575 +++++++++++++++++++++ src/app/layout/qgslayoutmanagerdialog.h | 124 +++++ src/app/qgisapp.cpp | 14 +- src/app/qgisapp.h | 8 + src/app/qgisappinterface.cpp | 5 + src/app/qgisappinterface.h | 1 + src/gui/qgisinterface.h | 6 + src/ui/layout/qgslayoutmanagerbase.ui | 202 ++++++++ 13 files changed, 970 insertions(+), 24 deletions(-) create mode 100644 src/app/layout/qgslayoutmanagerdialog.cpp create mode 100644 src/app/layout/qgslayoutmanagerdialog.h create mode 100644 src/ui/layout/qgslayoutmanagerbase.ui diff --git a/python/gui/qgisinterface.sip b/python/gui/qgisinterface.sip index d4189b8a43e2..43b3694ead94 100644 --- a/python/gui/qgisinterface.sip +++ b/python/gui/qgisinterface.sip @@ -927,6 +927,12 @@ Adds a widget to the user input tool bar. The composition remains unaffected. .. versionadded:: 3.0 .. seealso:: :py:func:`openComposer()` +%End + + virtual void showLayoutManager() = 0; +%Docstring + Opens the layout manager dialog. +.. versionadded:: 3.0 %End virtual QgsLayoutDesignerInterface *openLayoutDesigner( QgsLayout *layout ) = 0; diff --git a/src/app/CMakeLists.txt b/src/app/CMakeLists.txt index a273d95319ac..ec8120ea0da5 100644 --- a/src/app/CMakeLists.txt +++ b/src/app/CMakeLists.txt @@ -188,6 +188,7 @@ SET(QGIS_APP_SRCS layout/qgslayoutlabelwidget.cpp layout/qgslayoutlegendlayersdialog.cpp layout/qgslayoutlegendwidget.cpp + layout/qgslayoutmanagerdialog.cpp layout/qgslayoutmapwidget.cpp layout/qgslayoutmapgridwidget.cpp layout/qgslayoutpagepropertieswidget.cpp @@ -402,6 +403,7 @@ SET (QGIS_APP_MOC_HDRS layout/qgslayoutlabelwidget.h layout/qgslayoutlegendwidget.h layout/qgslayoutlegendlayersdialog.h + layout/qgslayoutmanagerdialog.h layout/qgslayoutmapwidget.h layout/qgslayoutmapgridwidget.h layout/qgslayoutpagepropertieswidget.h diff --git a/src/app/composer/qgscomposermanager.cpp b/src/app/composer/qgscomposermanager.cpp index 8177c9bc8de7..3a7ebbdd861a 100644 --- a/src/app/composer/qgscomposermanager.cpp +++ b/src/app/composer/qgscomposermanager.cpp @@ -47,8 +47,8 @@ QgsComposerManager::QgsComposerManager( QWidget *parent, Qt::WindowFlags f ): QD QgsSettings settings; restoreGeometry( settings.value( QStringLiteral( "Windows/ComposerManager/geometry" ) ).toByteArray() ); - mModel = new QgsLayoutManagerModel( QgsProject::instance()->layoutManager(), - this ); + mModel = new QgsComposerManagerModel( QgsProject::instance()->layoutManager(), + this ); mComposerListView->setModel( mModel ); connect( mButtonBox, &QDialogButtonBox::rejected, this, &QWidget::close ); @@ -440,27 +440,27 @@ void QgsComposerManager::renameClicked() } // -// QgsLayoutManagerModel +// QgsComposerManagerModel // -QgsLayoutManagerModel::QgsLayoutManagerModel( QgsLayoutManager *manager, QObject *parent ) +QgsComposerManagerModel::QgsComposerManagerModel( QgsLayoutManager *manager, QObject *parent ) : QAbstractListModel( parent ) , mLayoutManager( manager ) { - connect( mLayoutManager, &QgsLayoutManager::compositionAboutToBeAdded, this, &QgsLayoutManagerModel::compositionAboutToBeAdded ); - connect( mLayoutManager, &QgsLayoutManager::compositionAdded, this, &QgsLayoutManagerModel::compositionAdded ); - connect( mLayoutManager, &QgsLayoutManager::compositionAboutToBeRemoved, this, &QgsLayoutManagerModel::compositionAboutToBeRemoved ); - connect( mLayoutManager, &QgsLayoutManager::compositionRemoved, this, &QgsLayoutManagerModel::compositionRemoved ); - connect( mLayoutManager, &QgsLayoutManager::compositionRenamed, this, &QgsLayoutManagerModel::compositionRenamed ); + connect( mLayoutManager, &QgsLayoutManager::compositionAboutToBeAdded, this, &QgsComposerManagerModel::compositionAboutToBeAdded ); + connect( mLayoutManager, &QgsLayoutManager::compositionAdded, this, &QgsComposerManagerModel::compositionAdded ); + connect( mLayoutManager, &QgsLayoutManager::compositionAboutToBeRemoved, this, &QgsComposerManagerModel::compositionAboutToBeRemoved ); + connect( mLayoutManager, &QgsLayoutManager::compositionRemoved, this, &QgsComposerManagerModel::compositionRemoved ); + connect( mLayoutManager, &QgsLayoutManager::compositionRenamed, this, &QgsComposerManagerModel::compositionRenamed ); } -int QgsLayoutManagerModel::rowCount( const QModelIndex &parent ) const +int QgsComposerManagerModel::rowCount( const QModelIndex &parent ) const { Q_UNUSED( parent ); return mLayoutManager->compositions().count(); } -QVariant QgsLayoutManagerModel::data( const QModelIndex &index, int role ) const +QVariant QgsComposerManagerModel::data( const QModelIndex &index, int role ) const { if ( index.row() < 0 || index.row() >= rowCount( QModelIndex() ) ) return QVariant(); @@ -480,7 +480,7 @@ QVariant QgsLayoutManagerModel::data( const QModelIndex &index, int role ) const } } -bool QgsLayoutManagerModel::setData( const QModelIndex &index, const QVariant &value, int role ) +bool QgsComposerManagerModel::setData( const QModelIndex &index, const QVariant &value, int role ) { if ( !index.isValid() || role != Qt::EditRole ) { @@ -520,7 +520,7 @@ bool QgsLayoutManagerModel::setData( const QModelIndex &index, const QVariant &v return true; } -Qt::ItemFlags QgsLayoutManagerModel::flags( const QModelIndex &index ) const +Qt::ItemFlags QgsComposerManagerModel::flags( const QModelIndex &index ) const { Qt::ItemFlags flags = QAbstractListModel::flags( index ); @@ -534,18 +534,18 @@ Qt::ItemFlags QgsLayoutManagerModel::flags( const QModelIndex &index ) const } } -QgsComposition *QgsLayoutManagerModel::compositionFromIndex( const QModelIndex &index ) const +QgsComposition *QgsComposerManagerModel::compositionFromIndex( const QModelIndex &index ) const { return qobject_cast< QgsComposition * >( qvariant_cast( data( index, CompositionRole ) ) ); } -void QgsLayoutManagerModel::compositionAboutToBeAdded( const QString & ) +void QgsComposerManagerModel::compositionAboutToBeAdded( const QString & ) { int row = mLayoutManager->compositions().count(); beginInsertRows( QModelIndex(), row, row ); } -void QgsLayoutManagerModel::compositionAboutToBeRemoved( const QString &name ) +void QgsComposerManagerModel::compositionAboutToBeRemoved( const QString &name ) { QgsComposition *c = mLayoutManager->compositionByName( name ); int row = mLayoutManager->compositions().indexOf( c ); @@ -553,17 +553,17 @@ void QgsLayoutManagerModel::compositionAboutToBeRemoved( const QString &name ) beginRemoveRows( QModelIndex(), row, row ); } -void QgsLayoutManagerModel::compositionAdded( const QString & ) +void QgsComposerManagerModel::compositionAdded( const QString & ) { endInsertRows(); } -void QgsLayoutManagerModel::compositionRemoved( const QString & ) +void QgsComposerManagerModel::compositionRemoved( const QString & ) { endRemoveRows(); } -void QgsLayoutManagerModel::compositionRenamed( QgsComposition *composition, const QString & ) +void QgsComposerManagerModel::compositionRenamed( QgsComposition *composition, const QString & ) { int row = mLayoutManager->compositions().indexOf( composition ); QModelIndex index = createIndex( row, 0 ); diff --git a/src/app/composer/qgscomposermanager.h b/src/app/composer/qgscomposermanager.h index 8e5df71d56b1..352013089992 100644 --- a/src/app/composer/qgscomposermanager.h +++ b/src/app/composer/qgscomposermanager.h @@ -26,7 +26,7 @@ class QgsComposer; class QgsComposition; class QgsLayoutManager; -class QgsLayoutManagerModel : public QAbstractListModel +class QgsComposerManagerModel : public QAbstractListModel { Q_OBJECT @@ -37,7 +37,7 @@ class QgsLayoutManagerModel : public QAbstractListModel CompositionRole = Qt::UserRole + 1, }; - explicit QgsLayoutManagerModel( QgsLayoutManager *manager, QObject *parent = nullptr ); + explicit QgsComposerManagerModel( QgsLayoutManager *manager, QObject *parent = nullptr ); int rowCount( const QModelIndex &parent ) const override; QVariant data( const QModelIndex &index, int role ) const override; @@ -93,7 +93,7 @@ class QgsComposerManager: public QDialog, private Ui::QgsComposerManagerBase QPushButton *mRemoveButton = nullptr; QPushButton *mRenameButton = nullptr; QPushButton *mDuplicateButton = nullptr; - QgsLayoutManagerModel *mModel = nullptr; + QgsComposerManagerModel *mModel = nullptr; #ifdef Q_OS_MAC void showEvent( QShowEvent *event ); diff --git a/src/app/layout/qgslayoutdesignerdialog.cpp b/src/app/layout/qgslayoutdesignerdialog.cpp index f9aee7e145dc..f2217fcb6cba 100644 --- a/src/app/layout/qgslayoutdesignerdialog.cpp +++ b/src/app/layout/qgslayoutdesignerdialog.cpp @@ -615,6 +615,8 @@ QgsLayout *QgsLayoutDesignerDialog::currentLayout() void QgsLayoutDesignerDialog::setCurrentLayout( QgsLayout *layout ) { mLayout = layout; + connect( mLayout, &QgsLayout::destroyed, this, &QgsLayoutDesignerDialog::close ); + mView->setCurrentLayout( layout ); // add undo/redo actions which apply to the correct layout undo stack @@ -1372,7 +1374,10 @@ void QgsLayoutDesignerDialog::showManager() // NOTE: Avoid crash where composer that spawned modal manager from toolbar ends up // being deleted by user, but event loop tries to return to layout on manager close // (does not seem to be an issue for menu action) - QTimer::singleShot( 0, QgisApp::instance()->actionShowComposerManager(), SLOT( trigger() ) ); + QTimer::singleShot( 0, this, [ = ] + { + QgisApp::instance()->showLayoutManager(); + } ); } void QgsLayoutDesignerDialog::paste() diff --git a/src/app/layout/qgslayoutmanagerdialog.cpp b/src/app/layout/qgslayoutmanagerdialog.cpp new file mode 100644 index 000000000000..9395dffa5790 --- /dev/null +++ b/src/app/layout/qgslayoutmanagerdialog.cpp @@ -0,0 +1,575 @@ +/*************************************************************************** + qgslayoutmanagerdialog.cpp + ----------------------- + begin : December 2017 + copyright : (C) 2017 by 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 "qgslayoutmanagerdialog.h" +#include "qgisapp.h" +#include "qgsapplication.h" +#include "qgsbusyindicatordialog.h" +#include "qgslayoutdesignerdialog.h" +#include "qgslayout.h" +#include "qgslogger.h" +#include "qgssettings.h" +#include "qgslayoutview.h" +#include "qgslayoutmanager.h" +#include "qgsproject.h" +#include "qgsgui.h" + +#include +#include +#include +#include +#include +#include +#include +#include + +QgsLayoutManagerDialog::QgsLayoutManagerDialog( QWidget *parent, Qt::WindowFlags f ): QDialog( parent, f ) +{ + setupUi( this ); + connect( mAddButton, &QPushButton::clicked, this, &QgsLayoutManagerDialog::mAddButton_clicked ); + connect( mTemplate, static_cast( &QComboBox::currentIndexChanged ), this, &QgsLayoutManagerDialog::mTemplate_currentIndexChanged ); + connect( mTemplatePathBtn, &QPushButton::pressed, this, &QgsLayoutManagerDialog::mTemplatePathBtn_pressed ); + connect( mTemplatesDefaultDirBtn, &QPushButton::pressed, this, &QgsLayoutManagerDialog::mTemplatesDefaultDirBtn_pressed ); + connect( mTemplatesUserDirBtn, &QPushButton::pressed, this, &QgsLayoutManagerDialog::mTemplatesUserDirBtn_pressed ); + + QgsGui::instance()->enableAutoGeometryRestore( this ); + + mModel = new QgsLayoutManagerModel( QgsProject::instance()->layoutManager(), + this ); + mLayoutListView->setModel( mModel ); + + connect( mButtonBox, &QDialogButtonBox::rejected, this, &QWidget::close ); + connect( mLayoutListView->selectionModel(), &QItemSelectionModel::selectionChanged, + this, &QgsLayoutManagerDialog::toggleButtons ); + + mShowButton = mButtonBox->addButton( tr( "&Show" ), QDialogButtonBox::ActionRole ); + connect( mShowButton, &QAbstractButton::clicked, this, &QgsLayoutManagerDialog::showClicked ); + + mDuplicateButton = mButtonBox->addButton( tr( "&Duplicate" ), QDialogButtonBox::ActionRole ); + connect( mDuplicateButton, &QAbstractButton::clicked, this, &QgsLayoutManagerDialog::duplicateClicked ); + + mRemoveButton = mButtonBox->addButton( tr( "&Remove" ), QDialogButtonBox::ActionRole ); + connect( mRemoveButton, &QAbstractButton::clicked, this, &QgsLayoutManagerDialog::removeClicked ); + + mRenameButton = mButtonBox->addButton( tr( "Re&name" ), QDialogButtonBox::ActionRole ); + connect( mRenameButton, &QAbstractButton::clicked, this, &QgsLayoutManagerDialog::renameClicked ); + +#ifdef Q_OS_MAC + // Create action to select this window + mWindowAction = new QAction( windowTitle(), this ); + connect( mWindowAction, SIGNAL( triggered() ), this, SLOT( activate() ) ); +#endif + + mTemplate->addItem( tr( "Empty layout" ) ); + mTemplate->addItem( tr( "Specific" ) ); + + mUserTemplatesDir = QgsApplication::qgisSettingsDirPath() + "/composer_templates"; + QMap userTemplateMap = defaultTemplates( true ); + this->addTemplates( userTemplateMap ); + + mDefaultTemplatesDir = QgsApplication::pkgDataPath() + "/composer_templates"; + QMap defaultTemplateMap = defaultTemplates( false ); + this->addTemplates( defaultTemplateMap ); + this->addTemplates( this->otherTemplates() ); + + QgsSettings settings; + mTemplatePathLineEdit->setText( settings.value( QStringLiteral( "UI/ComposerManager/templatePath" ), QString() ).toString() ); + + toggleButtons(); +} + +void QgsLayoutManagerDialog::toggleButtons() +{ + // Nothing selected: no button. + if ( mLayoutListView->selectionModel()->selectedRows().isEmpty() ) + { + mShowButton->setEnabled( false ); + mRemoveButton->setEnabled( false ); + mRenameButton->setEnabled( false ); + mDuplicateButton->setEnabled( false ); + } + // toggle everything if one layout is selected + else if ( mLayoutListView->selectionModel()->selectedRows().count() == 1 ) + { + mShowButton->setEnabled( true ); + mRemoveButton->setEnabled( true ); + mRenameButton->setEnabled( true ); + mDuplicateButton->setEnabled( true ); + } + // toggle only show and remove buttons in other cases + else + { + mShowButton->setEnabled( true ); + mRemoveButton->setEnabled( true ); + mRenameButton->setEnabled( false ); + mDuplicateButton->setEnabled( false ); + } +} + +void QgsLayoutManagerDialog::addTemplates( const QMap &templates ) +{ + if ( !templates.isEmpty() ) + { + mTemplate->insertSeparator( mTemplate->count() ); + QMap::const_iterator templateIt = templates.constBegin(); + for ( ; templateIt != templates.constEnd(); ++templateIt ) + { + mTemplate->addItem( templateIt.key(), templateIt.value() ); + } + } + +} + +void QgsLayoutManagerDialog::activate() +{ + raise(); + setWindowState( windowState() & ~Qt::WindowMinimized ); + activateWindow(); +} + +QMap QgsLayoutManagerDialog::defaultTemplates( bool fromUser ) const +{ + //search for default templates in $pkgDataPath/composer_templates + // user templates in $qgisSettingsDirPath/composer_templates + return templatesFromPath( fromUser ? mUserTemplatesDir : mDefaultTemplatesDir ); +} + +QMap QgsLayoutManagerDialog::otherTemplates() const +{ + QMap templateMap; + QStringList paths = QgsApplication::composerTemplatePaths(); + Q_FOREACH ( const QString &path, paths ) + { + QMap templates = templatesFromPath( path ); + QMap::const_iterator templateIt = templates.constBegin(); + for ( ; templateIt != templates.constEnd(); ++templateIt ) + { + templateMap.insert( templateIt.key(), templateIt.value() ); + } + } + return templateMap; +} + +QMap QgsLayoutManagerDialog::templatesFromPath( const QString &path ) const +{ + QMap templateMap; + + QDir templateDir( path ); + if ( !templateDir.exists() ) + { + return templateMap; + } + + QFileInfoList fileInfoList = templateDir.entryInfoList( QDir::Files ); + QFileInfoList::const_iterator infoIt = fileInfoList.constBegin(); + for ( ; infoIt != fileInfoList.constEnd(); ++infoIt ) + { + if ( infoIt->suffix().toLower() == QLatin1String( "qpt" ) ) + { + templateMap.insert( infoIt->baseName(), infoIt->absoluteFilePath() ); + } + } + return templateMap; +} + +void QgsLayoutManagerDialog::mAddButton_clicked() +{ + QFile templateFile; + bool loadingTemplate = ( mTemplate->currentIndex() > 0 ); + if ( loadingTemplate ) + { + if ( mTemplate->currentIndex() == 1 ) + { + templateFile.setFileName( mTemplatePathLineEdit->text() ); + } + else + { + templateFile.setFileName( mTemplate->currentData().toString() ); + } + + if ( !templateFile.exists() ) + { + QMessageBox::warning( this, tr( "Create layout" ), tr( "Template file “%1” not found." ).arg( templateFile.fileName() ) ); + return; + } + if ( !templateFile.open( QIODevice::ReadOnly ) ) + { + QMessageBox::warning( this, tr( "Create layout" ), tr( "Could not read template file “%1”." ).arg( templateFile.fileName() ) ); + return; + } + } + + QString title; + if ( !QgisApp::instance()->uniqueLayoutTitle( this, title, true ) ) + { + return; + } + + if ( title.isEmpty() ) + { + title = QgsProject::instance()->layoutManager()->generateUniqueTitle(); + } + + std::unique_ptr< QgsLayout > layout = qgis::make_unique< QgsLayout >( QgsProject::instance() ); + if ( loadingTemplate ) + { + QDomDocument templateDoc; + if ( templateDoc.setContent( &templateFile, false ) ) + { + bool loadedOK = false; + ( void )layout->loadFromTemplate( templateDoc, QgsReadWriteContext(), true, &loadedOK ); + if ( !loadedOK ) + { + QMessageBox::warning( this, tr( "Create layout" ), tr( "Invalid template file “%1”." ).arg( templateFile.fileName() ) ); + layout.reset(); + } + } + } + else + { + layout->initializeDefaults(); + } + + if ( layout ) + { + layout->setName( title ); + QgisApp::instance()->openLayoutDesignerDialog( layout.get() ); + QgsProject::instance()->layoutManager()->addLayout( layout.release() ); + } +} + +void QgsLayoutManagerDialog::mTemplate_currentIndexChanged( int indx ) +{ + bool specific = ( indx == 1 ); // comes just after empty template + mTemplatePathLineEdit->setEnabled( specific ); + mTemplatePathBtn->setEnabled( specific ); +} + +void QgsLayoutManagerDialog::mTemplatePathBtn_pressed() +{ + QgsSettings settings; + QString lastTmplDir = settings.value( QStringLiteral( "UI/lastComposerTemplateDir" ), QDir::homePath() ).toString(); + QString tmplPath = QFileDialog::getOpenFileName( this, + tr( "Choose template" ), + lastTmplDir, + tr( "Layout templates" ) + QStringLiteral( " (*.qpt *.QPT)" ) ); + if ( !tmplPath.isEmpty() ) + { + mTemplatePathLineEdit->setText( tmplPath ); + settings.setValue( QStringLiteral( "UI/ComposerManager/templatePath" ), tmplPath ); + QFileInfo tmplFileInfo( tmplPath ); + settings.setValue( QStringLiteral( "UI/lastComposerTemplateDir" ), tmplFileInfo.absolutePath() ); + } +} + +void QgsLayoutManagerDialog::mTemplatesDefaultDirBtn_pressed() +{ + openLocalDirectory( mDefaultTemplatesDir ); +} + +void QgsLayoutManagerDialog::mTemplatesUserDirBtn_pressed() +{ + openLocalDirectory( mUserTemplatesDir ); +} + +void QgsLayoutManagerDialog::openLocalDirectory( const QString &localDirPath ) +{ + QDir localDir; + if ( !localDir.mkpath( localDirPath ) ) + { + QMessageBox::warning( this, tr( "Open directory" ), tr( "Could not open or create local directory “%1”." ).arg( localDirPath ) ); + } + else + { + QDesktopServices::openUrl( QUrl::fromLocalFile( localDirPath ) ); + } +} + +#ifdef Q_OS_MAC +void QgsLayoutManagerDialog::showEvent( QShowEvent *event ) +{ + if ( !event->spontaneous() ) + { + QgisApp::instance()->addWindow( mWindowAction ); + } +} + +void QgsLayoutManagerDialog::changeEvent( QEvent *event ) +{ + QDialog::changeEvent( event ); + switch ( event->type() ) + { + case QEvent::ActivationChange: + if ( QApplication::activeWindow() == this ) + { + mWindowAction->setChecked( true ); + } + break; + + default: + break; + } +} +#endif + +void QgsLayoutManagerDialog::removeClicked() +{ + const QModelIndexList layoutItems = mLayoutListView->selectionModel()->selectedRows(); + if ( layoutItems.isEmpty() ) + { + return; + } + + QString title; + QString message; + if ( layoutItems.count() == 1 ) + { + title = tr( "Remove layout" ); + message = tr( "Do you really want to remove the print layout “%1”?" ).arg( + mLayoutListView->model()->data( layoutItems.at( 0 ), Qt::DisplayRole ).toString() ); + } + else + { + title = tr( "Remove layouts" ); + message = tr( "Do you really want to remove all selected print layouts?" ); + } + + if ( QMessageBox::warning( this, title, message, QMessageBox::Ok | QMessageBox::Cancel ) != QMessageBox::Ok ) + { + return; + } + + QList layoutList; + // Find the layouts that need to be deleted + for ( const QModelIndex &index : layoutItems ) + { + QgsLayout *l = mModel->layoutFromIndex( index ); + if ( l ) + { + layoutList << l; + } + } + + // Once we have the layout list, we can delete all of them ! + for ( QgsLayout *l : qgis::as_const( layoutList ) ) + { + QgsProject::instance()->layoutManager()->removeLayout( l ); + } +} + +void QgsLayoutManagerDialog::showClicked() +{ + const QModelIndexList layoutItems = mLayoutListView->selectionModel()->selectedRows(); + for ( const QModelIndex &index : layoutItems ) + { + if ( QgsLayout *l = mModel->layoutFromIndex( index ) ) + { + QgisApp::instance()->openLayoutDesignerDialog( l ); + } + } +} + +void QgsLayoutManagerDialog::duplicateClicked() +{ + if ( mLayoutListView->selectionModel()->selectedRows().isEmpty() ) + { + return; + } + + QgsLayout *currentLayout = mModel->layoutFromIndex( mLayoutListView->selectionModel()->selectedRows().at( 0 ) ); + if ( !currentLayout ) + return; + QString currentTitle = currentLayout->name(); + + QString newTitle; + if ( !QgisApp::instance()->uniqueLayoutTitle( this, newTitle, false, tr( "%1 copy" ).arg( currentTitle ) ) ) + { + return; + } + + // provide feedback, since loading of template into duplicate layout will be hidden + QDialog *dlg = new QgsBusyIndicatorDialog( tr( "Duplicating layout…" ) ); + dlg->setStyleSheet( QgisApp::instance()->styleSheet() ); + dlg->show(); + + QgsLayoutDesignerDialog *newDialog = QgisApp::instance()->duplicateLayout( currentLayout, newTitle ); + + dlg->close(); + delete dlg; + dlg = nullptr; + + if ( !newDialog ) + { + QMessageBox::warning( this, tr( "Duplicate layout" ), + tr( "Layout duplication failed." ) ); + } + else + { + newDialog->activate(); + } +} + +void QgsLayoutManagerDialog::renameClicked() +{ + if ( mLayoutListView->selectionModel()->selectedRows().isEmpty() ) + { + return; + } + + QgsLayout *currentLayout = mModel->layoutFromIndex( mLayoutListView->selectionModel()->selectedRows().at( 0 ) ); + if ( !currentLayout ) + return; + + QString currentTitle = currentLayout->name(); + QString newTitle; + if ( !QgisApp::instance()->uniqueLayoutTitle( this, newTitle, false, currentTitle ) ) + { + return; + } + currentLayout->setName( newTitle ); +} + +// +// QgsLayoutManagerModel +// + +QgsLayoutManagerModel::QgsLayoutManagerModel( QgsLayoutManager *manager, QObject *parent ) + : QAbstractListModel( parent ) + , mLayoutManager( manager ) +{ + connect( mLayoutManager, &QgsLayoutManager::layoutAboutToBeAdded, this, &QgsLayoutManagerModel::layoutAboutToBeAdded ); + connect( mLayoutManager, &QgsLayoutManager::layoutAdded, this, &QgsLayoutManagerModel::layoutAdded ); + connect( mLayoutManager, &QgsLayoutManager::layoutAboutToBeRemoved, this, &QgsLayoutManagerModel::layoutAboutToBeRemoved ); + connect( mLayoutManager, &QgsLayoutManager::layoutRemoved, this, &QgsLayoutManagerModel::layoutRemoved ); + connect( mLayoutManager, &QgsLayoutManager::layoutRenamed, this, &QgsLayoutManagerModel::layoutRenamed ); +} + +int QgsLayoutManagerModel::rowCount( const QModelIndex &parent ) const +{ + Q_UNUSED( parent ); + return mLayoutManager->layouts().count(); +} + +QVariant QgsLayoutManagerModel::data( const QModelIndex &index, int role ) const +{ + if ( index.row() < 0 || index.row() >= rowCount( QModelIndex() ) ) + return QVariant(); + + switch ( role ) + { + case Qt::DisplayRole: + case Qt::ToolTipRole: + case Qt::EditRole: + return mLayoutManager->layouts().at( index.row() )->name(); + + case LayoutRole: + return QVariant::fromValue( mLayoutManager->layouts().at( index.row() ) ); + + default: + return QVariant(); + } +} + +bool QgsLayoutManagerModel::setData( const QModelIndex &index, const QVariant &value, int role ) +{ + if ( !index.isValid() || role != Qt::EditRole ) + { + return false; + } + if ( index.row() >= mLayoutManager->layouts().count() ) + { + return false; + } + + if ( value.toString().isEmpty() ) + return false; + + QgsLayout *layout = layoutFromIndex( index ); + if ( !layout ) + return false; + + //has name changed? + bool changed = layout->name() != value.toString(); + if ( !changed ) + return true; + + //check if name already exists + QStringList layoutNames; + const QList< QgsLayout * > layouts = QgsProject::instance()->layoutManager()->layouts(); + for ( QgsLayout *l : layouts ) + { + layoutNames << l->name(); + } + if ( layoutNames.contains( value.toString() ) ) + { + //name exists! + QMessageBox::warning( nullptr, tr( "Rename layout" ), tr( "There is already a layout named “%1”." ).arg( value.toString() ) ); + return false; + } + + layout->setName( value.toString() ); + return true; +} + +Qt::ItemFlags QgsLayoutManagerModel::flags( const QModelIndex &index ) const +{ + Qt::ItemFlags flags = QAbstractListModel::flags( index ); + + if ( index.isValid() ) + { + return flags | Qt::ItemIsEditable; + } + else + { + return flags; + } +} + +QgsLayout *QgsLayoutManagerModel::layoutFromIndex( const QModelIndex &index ) const +{ + return qobject_cast< QgsLayout * >( qvariant_cast( data( index, LayoutRole ) ) ); +} + +void QgsLayoutManagerModel::layoutAboutToBeAdded( const QString & ) +{ + int row = mLayoutManager->layouts().count(); + beginInsertRows( QModelIndex(), row, row ); +} + +void QgsLayoutManagerModel::layoutAboutToBeRemoved( const QString &name ) +{ + QgsLayout *l = mLayoutManager->layoutByName( name ); + int row = mLayoutManager->layouts().indexOf( l ); + if ( row >= 0 ) + beginRemoveRows( QModelIndex(), row, row ); +} + +void QgsLayoutManagerModel::layoutAdded( const QString & ) +{ + endInsertRows(); +} + +void QgsLayoutManagerModel::layoutRemoved( const QString & ) +{ + endRemoveRows(); +} + +void QgsLayoutManagerModel::layoutRenamed( QgsLayout *layout, const QString & ) +{ + int row = mLayoutManager->layouts().indexOf( layout ); + QModelIndex index = createIndex( row, 0 ); + emit dataChanged( index, index, QVector() << Qt::DisplayRole ); +} diff --git a/src/app/layout/qgslayoutmanagerdialog.h b/src/app/layout/qgslayoutmanagerdialog.h new file mode 100644 index 000000000000..203ddb11f58f --- /dev/null +++ b/src/app/layout/qgslayoutmanagerdialog.h @@ -0,0 +1,124 @@ +/*************************************************************************** + qgslayoutmanagerdialog.h + ----------------------- + begin : December 2017 + copyright : (C) 2017 by 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. * + * * + ***************************************************************************/ + +#ifndef QGSLAYOUTMANAGERDIALOG_H +#define QGSLAYOUTMANAGERDIALOG_H + +#include + +#include "ui_qgslayoutmanagerbase.h" + +class QListWidgetItem; +class QgsLayoutDesignerDialog; +class QgsLayout; +class QgsLayoutManager; + +class QgsLayoutManagerModel : public QAbstractListModel +{ + Q_OBJECT + + public: + + enum Role + { + LayoutRole = Qt::UserRole + 1, + }; + + explicit QgsLayoutManagerModel( QgsLayoutManager *manager, QObject *parent = nullptr ); + + int rowCount( const QModelIndex &parent ) const override; + QVariant data( const QModelIndex &index, int role ) const override; + bool setData( const QModelIndex &index, const QVariant &value, int role = Qt::EditRole ) override; + Qt::ItemFlags flags( const QModelIndex &index ) const override; + QgsLayout *layoutFromIndex( const QModelIndex &index ) const; + + private slots: + void layoutAboutToBeAdded( const QString &name ); + void layoutAboutToBeRemoved( const QString &name ); + void layoutAdded( const QString &name ); + void layoutRemoved( const QString &name ); + void layoutRenamed( QgsLayout *layout, const QString &newName ); + private: + QgsLayoutManager *mLayoutManager = nullptr; +}; + +/** + * A dialog that allows management of layouts within a project. +*/ +class QgsLayoutManagerDialog: public QDialog, private Ui::QgsLayoutManagerBase +{ + Q_OBJECT + public: + QgsLayoutManagerDialog( QWidget *parent = nullptr, Qt::WindowFlags f = 0 ); + + void addTemplates( const QMap &templates ); + + public slots: + //! Raise, unminimize and activate this window + void activate(); + + private: + + /** + * Returns the default templates (key: template name, value: absolute path to template file) + * \param fromUser whether to return user templates from ~/.qgis/composer_templates + */ + QMap defaultTemplates( bool fromUser = false ) const; + QMap otherTemplates() const; + + QMap templatesFromPath( const QString &path ) const; + + /** + * Open local directory with user's system, creating it if not present + */ + void openLocalDirectory( const QString &localDirPath ); + + QString mDefaultTemplatesDir; + QString mUserTemplatesDir; + QPushButton *mShowButton = nullptr; + QPushButton *mRemoveButton = nullptr; + QPushButton *mRenameButton = nullptr; + QPushButton *mDuplicateButton = nullptr; + QgsLayoutManagerModel *mModel = nullptr; + +#ifdef Q_OS_MAC + void showEvent( QShowEvent *event ); + void changeEvent( QEvent * ); + + QAction *mWindowAction = nullptr; +#endif + + private slots: + //! Slot to update buttons state when selecting layouts + void toggleButtons(); + void mAddButton_clicked(); + //! Slot to track combobox to use specific template path + void mTemplate_currentIndexChanged( int indx ); + //! Slot to choose path to template + void mTemplatePathBtn_pressed(); + //! Slot to open default templates dir with user's system + void mTemplatesDefaultDirBtn_pressed(); + //! Slot to open user templates dir with user's system + void mTemplatesUserDirBtn_pressed(); + + void removeClicked(); + void showClicked(); + //! Duplicate layout + void duplicateClicked(); + void renameClicked(); +}; + +#endif // QGSLAYOUTMANAGERDIALOG_H diff --git a/src/app/qgisapp.cpp b/src/app/qgisapp.cpp index dabe18997f98..a54896d0839a 100644 --- a/src/app/qgisapp.cpp +++ b/src/app/qgisapp.cpp @@ -209,6 +209,7 @@ Q_GUI_EXPORT extern int qt_defaultDpiX(); #include "qgslayoutcustomdrophandler.h" #include "qgslayoutdesignerdialog.h" #include "qgslayoutmanager.h" +#include "qgslayoutmanagerdialog.h" #include "qgslayoutqptdrophandler.h" #include "qgslayoutapputils.h" #include "qgslocatorwidget.h" @@ -7380,7 +7381,7 @@ bool QgisApp::uniqueLayoutTitle( QWidget *parent, QString &title, bool acceptEmp else { titleValid = true; - newTitle = QgsProject::instance()->layoutManager()->generateUniqueComposerTitle(); + newTitle = QgsProject::instance()->layoutManager()->generateUniqueTitle(); } } else if ( cNames.indexOf( newTitle, 1 ) >= 0 ) @@ -7454,6 +7455,7 @@ QgsLayoutDesignerDialog *QgisApp::createNewLayout( QString title ) //create new layout object QgsLayout *layout = new QgsLayout( QgsProject::instance() ); layout->setName( title ); + layout->initializeDefaults(); QgsProject::instance()->layoutManager()->addLayout( layout ); return openLayoutDesignerDialog( layout ); } @@ -10109,6 +10111,16 @@ void QgisApp::reloadConnections() emit connectionsChanged( ); } +void QgisApp::showLayoutManager() +{ + if ( !mLayoutManagerDialog ) + { + mLayoutManagerDialog = new QgsLayoutManagerDialog( this, Qt::Window ); + mLayoutManagerDialog->setAttribute( Qt::WA_DeleteOnClose ); + } + mLayoutManagerDialog->show(); + mLayoutManagerDialog->activate(); +} QgsVectorLayer *QgisApp::addVectorLayer( const QString &vectorLayerPath, const QString &name, const QString &providerKey ) { diff --git a/src/app/qgisapp.h b/src/app/qgisapp.h index c61487c76e87..c30b6e66dd30 100644 --- a/src/app/qgisapp.h +++ b/src/app/qgisapp.h @@ -65,6 +65,7 @@ class QgsLayout; class QgsLayoutCustomDropHandler; class QgsLayoutDesignerDialog; class QgsLayoutDesignerInterface; +class QgsLayoutManagerDialog; class QgsMapCanvas; class QgsMapCanvasDockWidget; class QgsMapLayer; @@ -940,6 +941,12 @@ class APP_EXPORT QgisApp : public QMainWindow, private Ui::MainWindow */ void reloadConnections(); + /** + * Shows the layout manager dialog. + * \since QGIS 3.0 + */ + void showLayoutManager(); + protected: //! Handle state changes (WindowTitleChange) @@ -2112,6 +2119,7 @@ class APP_EXPORT QgisApp : public QMainWindow, private Ui::MainWindow QgsLayerStylingWidget *mMapStyleWidget = nullptr; QgsComposerManager *mComposerManager = nullptr; + QPointer< QgsLayoutManagerDialog > mLayoutManagerDialog; //! Persistent tile scale slider QgsTileScaleWidget *mpTileScaleWidget = nullptr; diff --git a/src/app/qgisappinterface.cpp b/src/app/qgisappinterface.cpp index 2540a52469cf..74c8ccaef364 100644 --- a/src/app/qgisappinterface.cpp +++ b/src/app/qgisappinterface.cpp @@ -446,6 +446,11 @@ void QgisAppInterface::closeComposer( QgsComposition *composition ) } } +void QgisAppInterface::showLayoutManager() +{ + qgis->showLayoutManager(); +} + QList QgisAppInterface::openLayoutDesigners() { QList designerInterfaceList; diff --git a/src/app/qgisappinterface.h b/src/app/qgisappinterface.h index 2d4cd1856870..ad1498dbc5d2 100644 --- a/src/app/qgisappinterface.h +++ b/src/app/qgisappinterface.h @@ -236,6 +236,7 @@ class APP_EXPORT QgisAppInterface : public QgisInterface QgsComposerInterface *openComposer( QgsComposition *composition ) override; void closeComposer( QgsComposition *composition ) override; + void showLayoutManager() override; QList openLayoutDesigners() override; QgsLayoutDesignerInterface *openLayoutDesigner( QgsLayout *layout ) override; diff --git a/src/gui/qgisinterface.h b/src/gui/qgisinterface.h index 2f56967e16de..98e0594ecd16 100644 --- a/src/gui/qgisinterface.h +++ b/src/gui/qgisinterface.h @@ -547,6 +547,12 @@ class GUI_EXPORT QgisInterface : public QObject */ virtual void closeComposer( QgsComposition *composition ) = 0; + /** + * Opens the layout manager dialog. + * \since QGIS 3.0 + */ + virtual void showLayoutManager() = 0; + /** * Opens a new layout designer dialog for the specified \a layout, or * brings an already open designer window to the foreground if one diff --git a/src/ui/layout/qgslayoutmanagerbase.ui b/src/ui/layout/qgslayoutmanagerbase.ui new file mode 100644 index 000000000000..677f90a1b69e --- /dev/null +++ b/src/ui/layout/qgslayoutmanagerbase.ui @@ -0,0 +1,202 @@ + + + QgsLayoutManagerBase + + + + 0 + 0 + 425 + 300 + + + + + 425 + 300 + + + + Layout Manager + + + + + + QAbstractItemView::ExtendedSelection + + + QAbstractItemView::SelectRows + + + + + + + + 0 + 0 + + + + Qt::StrongFocus + + + New from template + + + + 6 + + + + + + 0 + 0 + + + + Add + + + + + + + + + + + + + + + + + + + + + + + 0 + 0 + + + + Open template directory + + + + + + + + 0 + 0 + + + + user + + + + + + + + 0 + 0 + + + + default + + + + + + + Qt::Horizontal + + + + 40 + 20 + + + + + + + + + + + + + Qt::Horizontal + + + QDialogButtonBox::Close + + + + + + + + QgsCollapsibleGroupBox + QGroupBox +
qgscollapsiblegroupbox.h
+ 1 +
+
+ + mLayoutListView + mTemplateGrpBox + mTemplate + mAddButton + mTemplatePathLineEdit + mTemplatePathBtn + mTemplatesUserDirBtn + mTemplatesDefaultDirBtn + mButtonBox + + + + + mButtonBox + accepted() + QgsLayoutManagerBase + accept() + + + 266 + 295 + + + 157 + 274 + + + + + mButtonBox + rejected() + QgsLayoutManagerBase + reject() + + + 266 + 295 + + + 286 + 274 + + + + +