diff --git a/src/app/CMakeLists.txt b/src/app/CMakeLists.txt index ff930b2a662..76832125a87 100644 --- a/src/app/CMakeLists.txt +++ b/src/app/CMakeLists.txt @@ -107,6 +107,7 @@ set(QGIS_APP_SRCS qgsmaptooltextannotation.cpp annotations/qgsannotationitempropertieswidget.cpp + annotations/qgsannotationlayerproperties.cpp decorations/qgsdecorationitem.cpp decorations/qgsdecorationtitle.cpp diff --git a/src/app/annotations/qgsannotationlayerproperties.cpp b/src/app/annotations/qgsannotationlayerproperties.cpp new file mode 100644 index 00000000000..21c8eb5bd68 --- /dev/null +++ b/src/app/annotations/qgsannotationlayerproperties.cpp @@ -0,0 +1,298 @@ +/*************************************************************************** + qgsannotationlayerproperties.cpp + -------------------------------------- + Date : September 2021 + Copyright : (C) 2021 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 "qgsannotationlayerproperties.h" + +#include "qgsfileutils.h" +#include "qgshelp.h" +#include "qgsmaplayerstylemanager.h" +#include "qgsmaplayerstyleguiutils.h" +#include "qgsgui.h" +#include "qgsnative.h" +#include "qgsapplication.h" +#include "qgsmetadatawidget.h" +#include "qgsmaplayerloadstyledialog.h" +#include "qgsmaplayerconfigwidgetfactory.h" +#include "qgsmaplayerconfigwidget.h" +#include "qgsdatumtransformdialog.h" +#include +#include +#include +#include +#include + +QgsAnnotationLayerProperties::QgsAnnotationLayerProperties( QgsAnnotationLayer *layer, QgsMapCanvas *canvas, QgsMessageBar *, QWidget *parent, Qt::WindowFlags flags ) + : QgsOptionsDialogBase( QStringLiteral( "AnnotationLayerProperties" ), parent, flags ) + , mLayer( layer ) + , mMapCanvas( canvas ) +{ + setupUi( this ); + + connect( this, &QDialog::accepted, this, &QgsAnnotationLayerProperties::apply ); + connect( this, &QDialog::rejected, this, &QgsAnnotationLayerProperties::onCancel ); + connect( buttonBox->button( QDialogButtonBox::Apply ), &QAbstractButton::clicked, this, &QgsAnnotationLayerProperties::apply ); + connect( buttonBox, &QDialogButtonBox::helpRequested, this, &QgsAnnotationLayerProperties::showHelp ); + + connect( mCrsSelector, &QgsProjectionSelectionWidget::crsChanged, this, &QgsAnnotationLayerProperties::crsChanged ); + + // QgsOptionsDialogBase handles saving/restoring of geometry, splitter and current tab states, + // switching vertical tabs between icon/text to icon-only modes (splitter collapsed to left), + // and connecting QDialogButtonBox's accepted/rejected signals to dialog's accept/reject slots + initOptionsBase( false ); + + mOptsPage_Information->setContentsMargins( 0, 0, 0, 0 ); + + // update based on layer's current state + syncToLayer(); + + QgsSettings settings; + if ( !settings.contains( QStringLiteral( "/Windows/AnnotationLayerProperties/tab" ) ) ) + { + settings.setValue( QStringLiteral( "Windows/AnnotationLayerProperties/tab" ), + mOptStackedWidget->indexOf( mOptsPage_Information ) ); + } + + QString title = tr( "Layer Properties - %1" ).arg( mLayer->name() ); + + mBtnStyle = new QPushButton( tr( "Style" ) ); + QMenu *menuStyle = new QMenu( this ); + menuStyle->addAction( tr( "Load Style…" ), this, &QgsAnnotationLayerProperties::loadStyle ); + menuStyle->addAction( tr( "Save Style…" ), this, &QgsAnnotationLayerProperties::saveStyleAs ); + menuStyle->addSeparator(); + menuStyle->addAction( tr( "Save as Default" ), this, &QgsAnnotationLayerProperties::saveDefaultStyle ); + menuStyle->addAction( tr( "Restore Default" ), this, &QgsAnnotationLayerProperties::loadDefaultStyle ); + mBtnStyle->setMenu( menuStyle ); + connect( menuStyle, &QMenu::aboutToShow, this, &QgsAnnotationLayerProperties::aboutToShowStyleMenu ); + + buttonBox->addButton( mBtnStyle, QDialogButtonBox::ResetRole ); + + if ( !mLayer->styleManager()->isDefault( mLayer->styleManager()->currentStyle() ) ) + title += QStringLiteral( " (%1)" ).arg( mLayer->styleManager()->currentStyle() ); + restoreOptionsBaseUi( title ); +} + +void QgsAnnotationLayerProperties::addPropertiesPageFactory( const QgsMapLayerConfigWidgetFactory *factory ) +{ + if ( !factory->supportsLayer( mLayer ) || !factory->supportLayerPropertiesDialog() ) + { + return; + } + + QgsMapLayerConfigWidget *page = factory->createWidget( mLayer, mMapCanvas, false, this ); + mConfigWidgets << page; + + const QString beforePage = factory->layerPropertiesPagePositionHint(); + if ( beforePage.isEmpty() ) + addPage( factory->title(), factory->title(), factory->icon(), page ); + else + insertPage( factory->title(), factory->title(), factory->icon(), page, beforePage ); + + page->syncToLayer( mLayer ); +} + +void QgsAnnotationLayerProperties::apply() +{ + mLayer->setName( mLayerOrigNameLineEdit->text() ); + + for ( QgsMapLayerConfigWidget *w : mConfigWidgets ) + w->apply(); + + mLayer->triggerRepaint(); +} + +void QgsAnnotationLayerProperties::onCancel() +{ + if ( mOldStyle.xmlData() != mLayer->styleManager()->style( mLayer->styleManager()->currentStyle() ).xmlData() ) + { + // need to reset style to previous - style applied directly to the layer (not in apply()) + QString myMessage; + QDomDocument doc( QStringLiteral( "qgis" ) ); + int errorLine, errorColumn; + doc.setContent( mOldStyle.xmlData(), false, &myMessage, &errorLine, &errorColumn ); + mLayer->importNamedStyle( doc, myMessage ); + syncToLayer(); + } +} + +void QgsAnnotationLayerProperties::syncToLayer() +{ + // populate the general information + mLayerOrigNameLineEdit->setText( mLayer->name() ); + + /* + * Information Tab + */ + QString myStyle = QgsApplication::reportStyleSheet(); + myStyle.append( QStringLiteral( "body { margin: 10px; }\n " ) ); + mInformationTextBrowser->clear(); + mInformationTextBrowser->document()->setDefaultStyleSheet( myStyle ); + mInformationTextBrowser->setHtml( mLayer->htmlMetadata() ); + mInformationTextBrowser->setOpenLinks( false ); + connect( mInformationTextBrowser, &QTextBrowser::anchorClicked, this, &QgsAnnotationLayerProperties::urlClicked ); + + mCrsSelector->setCrs( mLayer->crs() ); + + for ( QgsMapLayerConfigWidget *w : mConfigWidgets ) + w->syncToLayer( mLayer ); +} + + +void QgsAnnotationLayerProperties::loadDefaultStyle() +{ + bool defaultLoadedFlag = false; + const QString myMessage = mLayer->loadDefaultStyle( defaultLoadedFlag ); + // reset if the default style was loaded OK only + if ( defaultLoadedFlag ) + { + syncToLayer(); + } + else + { + // otherwise let the user know what went wrong + QMessageBox::information( this, + tr( "Default Style" ), + myMessage + ); + } +} + +void QgsAnnotationLayerProperties::saveDefaultStyle() +{ + apply(); // make sure the style to save is up-to-date + + // a flag passed by reference + bool defaultSavedFlag = false; + // after calling this the above flag will be set true for success + // or false if the save operation failed + const QString myMessage = mLayer->saveDefaultStyle( defaultSavedFlag ); + if ( !defaultSavedFlag ) + { + // let the user know what went wrong + QMessageBox::information( this, + tr( "Default Style" ), + myMessage + ); + } +} + +void QgsAnnotationLayerProperties::loadStyle() +{ + QgsSettings settings; + const QString lastUsedDir = settings.value( QStringLiteral( "style/lastStyleDir" ), QDir::homePath() ).toString(); + + QString fileName = QFileDialog::getOpenFileName( + this, + tr( "Load layer properties from style file" ), + lastUsedDir, + tr( "QGIS Layer Style File" ) + " (*.qml)" ); + if ( fileName.isEmpty() ) + return; + + // ensure the user never omits the extension from the file name + if ( !fileName.endsWith( QLatin1String( ".qml" ), Qt::CaseInsensitive ) ) + fileName += QLatin1String( ".qml" ); + + mOldStyle = mLayer->styleManager()->style( mLayer->styleManager()->currentStyle() ); + + bool defaultLoadedFlag = false; + const QString message = mLayer->loadNamedStyle( fileName, defaultLoadedFlag ); + if ( defaultLoadedFlag ) + { + settings.setValue( QStringLiteral( "style/lastStyleDir" ), QFileInfo( fileName ).absolutePath() ); + syncToLayer(); + } + else + { + QMessageBox::information( this, tr( "Load Style" ), message ); + } +} + +void QgsAnnotationLayerProperties::saveStyleAs() +{ + QgsSettings settings; + const QString lastUsedDir = settings.value( QStringLiteral( "style/lastStyleDir" ), QDir::homePath() ).toString(); + + QString outputFileName = QFileDialog::getSaveFileName( + this, + tr( "Save layer properties as style file" ), + lastUsedDir, + tr( "QGIS Layer Style File" ) + " (*.qml)" ); + if ( outputFileName.isEmpty() ) + return; + + // ensure the user never omits the extension from the file name + outputFileName = QgsFileUtils::ensureFileNameHasExtension( outputFileName, QStringList() << QStringLiteral( "qml" ) ); + + apply(); // make sure the style to save is up-to-date + + // then export style + bool defaultLoadedFlag = false; + QString message; + message = mLayer->saveNamedStyle( outputFileName, defaultLoadedFlag ); + + if ( defaultLoadedFlag ) + { + settings.setValue( QStringLiteral( "style/lastStyleDir" ), QFileInfo( outputFileName ).absolutePath() ); + } + else + QMessageBox::information( this, tr( "Save Style" ), message ); +} + +void QgsAnnotationLayerProperties::aboutToShowStyleMenu() +{ + QMenu *m = qobject_cast( sender() ); + + QgsMapLayerStyleGuiUtils::instance()->removesExtraMenuSeparators( m ); + // re-add style manager actions! + m->addSeparator(); + QgsMapLayerStyleGuiUtils::instance()->addStyleManagerActions( m, mLayer ); +} + +void QgsAnnotationLayerProperties::showHelp() +{ + const QVariant helpPage = mOptionsStackedWidget->currentWidget()->property( "helpPage" ); + + if ( helpPage.isValid() ) + { + QgsHelp::openHelp( helpPage.toString() ); + } + else + { + QgsHelp::openHelp( QStringLiteral( "working_with_vector_tiles/vector_tiles_properties.html" ) ); + } +} + +void QgsAnnotationLayerProperties::urlClicked( const QUrl &url ) +{ + const QFileInfo file( url.toLocalFile() ); + if ( file.exists() && !file.isDir() ) + QgsGui::instance()->nativePlatformInterface()->openFileExplorerAndSelectFile( url.toLocalFile() ); + else + QDesktopServices::openUrl( url ); +} + +void QgsAnnotationLayerProperties::crsChanged( const QgsCoordinateReferenceSystem &crs ) +{ + QgsDatumTransformDialog::run( crs, QgsProject::instance()->crs(), this, mMapCanvas, tr( "Select transformation for the layer" ) ); + mLayer->setCrs( crs ); +} + +void QgsAnnotationLayerProperties::optionsStackedWidget_CurrentChanged( int index ) +{ + QgsOptionsDialogBase::optionsStackedWidget_CurrentChanged( index ); + + mBtnStyle->setVisible( true ); +} + diff --git a/src/app/annotations/qgsannotationlayerproperties.h b/src/app/annotations/qgsannotationlayerproperties.h new file mode 100644 index 00000000000..6f80ddb0ce0 --- /dev/null +++ b/src/app/annotations/qgsannotationlayerproperties.h @@ -0,0 +1,80 @@ +/*************************************************************************** + qgsannotationlayerproperties.h + -------------------------------------- + Date : September 2021 + Copyright : (C) 2021 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 QGSANNOTATIONLAYERPROPERTIES_H +#define QGSANNOTATIONLAYERPROPERTIES_H + +#include "qgsoptionsdialogbase.h" + +#include "ui_qgsannotationlayerpropertiesbase.h" + +#include "qgsmaplayerstylemanager.h" + +#include "qgsannotationlayer.h" + +class QgsMapLayer; +class QgsMapCanvas; +class QgsMessageBar; +class QgsAnnotationLayer; +class QgsMetadataWidget; +class QgsMapLayerConfigWidgetFactory; +class QgsMapLayerConfigWidget; + + +class QgsAnnotationLayerProperties : public QgsOptionsDialogBase, private Ui::QgsAnnotationLayerPropertiesBase +{ + Q_OBJECT + public: + QgsAnnotationLayerProperties( QgsAnnotationLayer *layer, QgsMapCanvas *canvas, QgsMessageBar *messageBar, QWidget *parent = nullptr, Qt::WindowFlags = QgsGuiUtils::ModalDialogFlags ); + + void addPropertiesPageFactory( const QgsMapLayerConfigWidgetFactory *factory ); + + private slots: + void apply(); + void onCancel(); + + void loadDefaultStyle(); + void saveDefaultStyle(); + void loadStyle(); + void saveStyleAs(); + void aboutToShowStyleMenu(); + void showHelp(); + void urlClicked( const QUrl &url ); + void crsChanged( const QgsCoordinateReferenceSystem &crs ); + + protected slots: + void optionsStackedWidget_CurrentChanged( int index ) override SIP_SKIP ; + + private: + void syncToLayer(); + + private: + QgsAnnotationLayer *mLayer = nullptr; + + QPushButton *mBtnStyle = nullptr; + + QgsMapCanvas *mMapCanvas = nullptr; + + /** + * Previous layer style. Used to reset style to previous state if new style + * was loaded but dialog is canceled. + */ + QgsMapLayerStyle mOldStyle; + + QList mConfigWidgets; + +}; + +#endif // QGSANNOTATIONLAYERPROPERTIES_H diff --git a/src/app/qgisapp.cpp b/src/app/qgisapp.cpp index 13ae1d3045a..6d7c23f5295 100644 --- a/src/app/qgisapp.cpp +++ b/src/app/qgisapp.cpp @@ -433,6 +433,7 @@ Q_GUI_EXPORT extern int qt_defaultDpiX(); #include "qgssublayersdialog.h" #include "ogr/qgsvectorlayersaveasdialog.h" #include "qgsannotationitemguiregistry.h" +#include "annotations/qgsannotationlayerproperties.h" #include "qgscreateannotationitemmaptool.h" #include "pointcloud/qgspointcloudelevationpropertieswidget.h" @@ -16720,6 +16721,25 @@ void QgisApp::showLayerProperties( QgsMapLayer *mapLayer, const QString &page ) case QgsMapLayerType::AnnotationLayer: { + QgsAnnotationLayerProperties annotationLayerPropertiesDialog( qobject_cast( mapLayer ), mMapCanvas, visibleMessageBar(), this ); + + if ( !page.isEmpty() ) + annotationLayerPropertiesDialog.setCurrentPage( page ); + else + annotationLayerPropertiesDialog.restoreLastPage(); + + for ( const QgsMapLayerConfigWidgetFactory *factory : std::as_const( providerFactories ) ) + { + annotationLayerPropertiesDialog.addPropertiesPageFactory( factory ); + } + + mMapStyleWidget->blockUpdates( true ); + if ( annotationLayerPropertiesDialog.exec() ) + { + activateDeactivateLayerRelatedActions( mapLayer ); + mMapStyleWidget->updateCurrentWidgetLayer(); + } + mMapStyleWidget->blockUpdates( false ); // delete since dialog cannot be reused without updating code break; } diff --git a/src/ui/annotations/qgsannotationlayerpropertiesbase.ui b/src/ui/annotations/qgsannotationlayerpropertiesbase.ui new file mode 100644 index 00000000000..1d1325d6942 --- /dev/null +++ b/src/ui/annotations/qgsannotationlayerpropertiesbase.ui @@ -0,0 +1,380 @@ + + + QgsAnnotationLayerPropertiesBase + + + + 0 + 0 + 815 + 557 + + + + + 700 + 0 + + + + Annotation Layer Properties + + + + 0 + + + 0 + + + + + Qt::Horizontal + + + false + + + + + 0 + 0 + + + + QFrame::NoFrame + + + QFrame::Raised + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + + + + 58 + 0 + + + + + 150 + 16777215 + + + + Qt::ScrollBarAlwaysOff + + + QAbstractItemView::NoEditTriggers + + + + 32 + 32 + + + + Qt::ElideNone + + + QListView::Adjust + + + true + + + + Information + + + Information + + + + :/images/themes/default/propertyicons/metadata.svg:/images/themes/default/propertyicons/metadata.svg + + + + + Source + + + Source + + + + :/images/themes/default/propertyicons/system.svg:/images/themes/default/propertyicons/system.svg + + + + + + + + + + 1 + 0 + + + + QFrame::NoFrame + + + QFrame::Raised + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + 0 + 0 + + + + QFrame::Plain + + + 1 + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + + + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Settings + + + + + + + + Layer name + + + + + + + + + + + + + + + Qt::StrongFocus + + + Assigned Coordinate Reference System (CRS) + + + false + + + vectorgeneral + + + + 6 + + + + + Qt::StrongFocus + + + + + + + <html><head/><body><p><span style=" font-weight:600;">Changing this option does not modify the original data source or perform any reprojection of points. Rather, it can be used to override the layer's CRS within this project if it could not be detected or has been incorrectly detected.</span></p></body></html> + + + Qt::RichText + + + true + + + + + + + Qt::Vertical + + + + + + + + + + Qt::Vertical + + + + 20 + 40 + + + + + + + + + + + + + + + + + 0 + 0 + + + + QFrame::NoFrame + + + QFrame::Raised + + + + 0 + + + 0 + + + 0 + + + 0 + + + + + Qt::Horizontal + + + QDialogButtonBox::Apply|QDialogButtonBox::Cancel|QDialogButtonBox::Ok + + + + + + + + + + + QgsFilterLineEdit + QLineEdit +
qgsfilterlineedit.h
+
+ + QgsCollapsibleGroupBox + QGroupBox +
qgscollapsiblegroupbox.h
+ 1 +
+ + QgsProjectionSelectionWidget + QWidget +
qgsprojectionselectionwidget.h
+ 1 +
+
+ + mSearchLineEdit + mOptionsListWidget + + + + + + + mOptionsListWidget + currentRowChanged(int) + mOptionsStackedWidget + setCurrentIndex(int) + + + 86 + 325 + + + 794 + 14 + + + + +