Create elevation profile manager dialog

This commit is contained in:
Nyall Dawson 2025-07-29 13:17:17 +10:00
parent d6d8e307c0
commit a665d48588
9 changed files with 553 additions and 17 deletions

View File

@ -187,6 +187,7 @@ set(QGIS_APP_SRCS
elevation/qgselevationprofileexportsettingswidget.cpp
elevation/qgselevationprofileimageexportdialog.cpp
elevation/qgselevationprofilepdfexportdialog.cpp
elevation/qgselevationprofilemanagerdialog.cpp
elevation/qgselevationprofiletoolidentify.cpp
elevation/qgselevationprofiletoolmeasure.cpp
elevation/qgselevationprofilewidget.cpp

View File

@ -0,0 +1,267 @@
/***************************************************************************
qgselevationprofilemanagerdialog.cpp
-----------------------
begin : July 2025
copyright : (C) 2025 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 "qgselevationprofilemanagerdialog.h"
#include "moc_qgselevationprofilemanagerdialog.cpp"
#include "qgisapp.h"
#include "qgselevationprofilemanager.h"
#include "qgselevationprofilemanagermodel.h"
#include "qgsproject.h"
#include "qgsgui.h"
#include "qgselevationprofile.h"
#include "qgsnewnamedialog.h"
#include <QDesktopServices>
#include <QDialog>
#include <QDir>
#include <QFileDialog>
#include <QInputDialog>
#include <QListWidgetItem>
#include <QMessageBox>
#include <QUrl>
QgsElevationProfileManagerDialog::QgsElevationProfileManagerDialog( QWidget *parent, Qt::WindowFlags f )
: QDialog( parent, f )
{
setupUi( this );
QgsGui::enableAutoGeometryRestore( this );
mModel = new QgsElevationProfileManagerModel( QgsProject::instance()->elevationProfileManager(), this );
mProxyModel = new QgsElevationProfileManagerProxyModel( mProfileListView );
mProxyModel->setSourceModel( mModel );
mProfileListView->setModel( mProxyModel );
mSearchLineEdit->setShowSearchIcon( true );
mSearchLineEdit->setShowClearButton( true );
mSearchLineEdit->setFocus();
connect( mSearchLineEdit, &QgsFilterLineEdit::textChanged, mProxyModel, &QgsElevationProfileManagerProxyModel::setFilterString );
connect( mButtonBox, &QDialogButtonBox::rejected, this, &QWidget::close );
connect( mProfileListView->selectionModel(), &QItemSelectionModel::selectionChanged, this, &QgsElevationProfileManagerDialog::toggleButtons );
connect( mProfileListView, &QListView::doubleClicked, this, &QgsElevationProfileManagerDialog::itemDoubleClicked );
connect( mShowButton, &QAbstractButton::clicked, this, &QgsElevationProfileManagerDialog::showClicked );
connect( mRemoveButton, &QAbstractButton::clicked, this, &QgsElevationProfileManagerDialog::removeClicked );
connect( mRenameButton, &QAbstractButton::clicked, this, &QgsElevationProfileManagerDialog::renameClicked );
#ifdef Q_OS_MAC
// Create action to select this window
mWindowAction = new QAction( windowTitle(), this );
connect( mWindowAction, &QAction::triggered, this, &QgsElevationProfileManagerDialog::activate );
#endif
toggleButtons();
}
void QgsElevationProfileManagerDialog::toggleButtons()
{
// Nothing selected: no button.
if ( mProfileListView->selectionModel()->selectedRows().isEmpty() )
{
mShowButton->setEnabled( false );
mRemoveButton->setEnabled( false );
mRenameButton->setEnabled( false );
}
// toggle everything if one profile is selected
else if ( mProfileListView->selectionModel()->selectedRows().count() == 1 )
{
mShowButton->setEnabled( true );
mRemoveButton->setEnabled( true );
mRenameButton->setEnabled( true );
}
// toggle only show and remove buttons in other cases
else
{
mShowButton->setEnabled( true );
mRemoveButton->setEnabled( true );
mRenameButton->setEnabled( false );
}
}
void QgsElevationProfileManagerDialog::activate()
{
raise();
setWindowState( windowState() & ~Qt::WindowMinimized );
activateWindow();
}
bool QgsElevationProfileManagerDialog::uniqueProfileTitle( QWidget *parent, QString &title, const QString &currentTitle )
{
if ( !parent )
{
parent = this;
}
bool titleValid = false;
QString newTitle = QString( currentTitle );
QString chooseMsg = tr( "Enter a unique elevation profile title" );
QString titleMsg = chooseMsg;
QStringList profileNames;
const QList<QgsElevationProfile *> profiles = QgsProject::instance()->elevationProfileManager()->profiles();
profileNames.reserve( profiles.size() + 1 );
for ( QgsElevationProfile *l : profiles )
{
profileNames << l->name();
}
const QString windowTitle = tr( "Rename Elevation Profile" );
while ( !titleValid )
{
QgsNewNameDialog dlg( tr( "elevation profile" ), newTitle, QStringList(), profileNames, Qt::CaseSensitive, parent );
dlg.setWindowTitle( windowTitle );
dlg.setHintString( titleMsg );
dlg.setOverwriteEnabled( false );
dlg.setAllowEmptyName( true );
dlg.setConflictingNameWarning( tr( "Title already exists!" ) );
if ( dlg.exec() != QDialog::Accepted )
{
return false;
}
newTitle = dlg.name();
if ( newTitle.isEmpty() )
{
titleMsg = chooseMsg + "\n\n" + tr( "Title can not be empty!" );
}
else if ( profileNames.indexOf( newTitle, 1 ) >= 0 )
{
profileNames[0] = QString(); // clear non-unique name
titleMsg = chooseMsg + "\n\n" + tr( "Title already exists!" );
}
else
{
titleValid = true;
}
}
title = newTitle;
return true;
}
#ifdef Q_OS_MAC
void QgsElevationProfileManagerDialog::showEvent( QShowEvent *event )
{
if ( !event->spontaneous() )
{
QgisApp::instance()->addWindow( mWindowAction );
}
}
void QgsElevationProfileManagerDialog::changeEvent( QEvent *event )
{
QDialog::changeEvent( event );
switch ( event->type() )
{
case QEvent::ActivationChange:
if ( QApplication::activeWindow() == this )
{
mWindowAction->setChecked( true );
}
break;
default:
break;
}
}
#endif
void QgsElevationProfileManagerDialog::removeClicked()
{
const QModelIndexList profileItems = mProfileListView->selectionModel()->selectedRows();
if ( profileItems.isEmpty() )
{
return;
}
QString title;
QString message;
if ( profileItems.count() == 1 )
{
title = tr( "Remove Elevation Profile" );
message = tr( "Do you really want to remove the elevation profile “%1”?" ).arg( mProfileListView->model()->data( profileItems.at( 0 ), Qt::DisplayRole ).toString() );
}
else
{
title = tr( "Remove Elevation Profiles" );
message = tr( "Do you really want to remove all selected elevation profiles?" );
}
if ( QMessageBox::warning( this, title, message, QMessageBox::Ok | QMessageBox::Cancel ) != QMessageBox::Ok )
{
return;
}
QList<QgsElevationProfile *> profileList;
// Find the profiles that need to be deleted
for ( const QModelIndex &index : profileItems )
{
if ( QgsElevationProfile *l = mModel->profileFromIndex( mProxyModel->mapToSource( index ) ) )
{
profileList << l;
}
}
// Once we have the profile list, we can delete all of them !
for ( QgsElevationProfile *l : std::as_const( profileList ) )
{
QgsProject::instance()->elevationProfileManager()->removeProfile( l );
}
}
void QgsElevationProfileManagerDialog::showClicked()
{
const QModelIndexList profileItems = mProfileListView->selectionModel()->selectedRows();
for ( const QModelIndex &index : profileItems )
{
if ( QgsElevationProfile *l = mModel->profileFromIndex( mProxyModel->mapToSource( index ) ) )
{
Q_UNUSED( l );
}
}
}
void QgsElevationProfileManagerDialog::renameClicked()
{
if ( mProfileListView->selectionModel()->selectedRows().isEmpty() )
{
return;
}
QgsElevationProfile *currentProfile = mModel->profileFromIndex( mProxyModel->mapToSource( mProfileListView->selectionModel()->selectedRows().at( 0 ) ) );
if ( !currentProfile )
return;
QString currentTitle = currentProfile->name();
QString newTitle;
if ( !uniqueProfileTitle( this, newTitle, currentTitle ) )
{
return;
}
currentProfile->setName( newTitle );
}
void QgsElevationProfileManagerDialog::itemDoubleClicked( const QModelIndex &index )
{
if ( QgsElevationProfile *l = mModel->profileFromIndex( mProxyModel->mapToSource( index ) ) )
{
Q_UNUSED( l );
}
}

View File

@ -0,0 +1,67 @@
/***************************************************************************
qgselevationprofilemanagerdialog.h
-----------------------
begin : July 2025
copyright : (C) 2025 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 QGSELEVATIONPROFILEMANAGERDIALOG_H
#define QGSELEVATIONPROFILEMANAGERDIALOG_H
#include <QItemDelegate>
#include <QSortFilterProxyModel>
#include "ui_qgselevationprofilemanagerbase.h"
class QListWidgetItem;
class QgsElevationProfile;
class QgsElevationProfileManager;
class QgsElevationProfileManagerModel;
class QgsElevationProfileManagerProxyModel;
/**
* A dialog that allows management of elevation profiles within a project.
*/
class QgsElevationProfileManagerDialog : public QDialog, private Ui::QgsElevationProfileManagerBase
{
Q_OBJECT
public:
QgsElevationProfileManagerDialog( QWidget *parent = nullptr, Qt::WindowFlags f = Qt::WindowFlags() );
public slots:
//! Raise, unminimize and activate this window
void activate();
private:
bool uniqueProfileTitle( QWidget *parent, QString &title, const QString &currentTitle );
QgsElevationProfileManagerModel *mModel = nullptr;
QgsElevationProfileManagerProxyModel *mProxyModel = 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 removeClicked();
void showClicked();
void renameClicked();
void itemDoubleClicked( const QModelIndex &index );
};
#endif // QGSELEVATIONPROFILEMANAGERDIALOG_H

View File

@ -2998,6 +2998,7 @@ void QgisApp::createActions()
mStatisticalSummaryDockWidget->setToggleVisibilityAction( mActionStatisticalSummary );
connect( mActionManage3DMapViews, &QAction::triggered, this, &QgisApp::show3DMapViewsManager );
connect( mActionElevationProfile, &QAction::triggered, this, &QgisApp::createNewElevationProfile );
connect( mActionManageElevationProfiles, &QAction::triggered, this, &QgisApp::showElevationProfileManager );
// Layer Menu Items
@ -13011,13 +13012,18 @@ void QgisApp::reloadConnections()
void QgisApp::showLayoutManager()
{
static_cast<QgsAppWindowManager *>( QgsGui::windowManager() )->openApplicationDialog( QgsAppWindowManager::DialogLayoutManager );
static_cast<QgsAppWindowManager *>( QgsGui::windowManager() )->openApplicationDialog( QgsAppWindowManager::ApplicationDialog::LayoutManager );
}
void QgisApp::showElevationProfileManager()
{
static_cast<QgsAppWindowManager *>( QgsGui::windowManager() )->openApplicationDialog( QgsAppWindowManager::ApplicationDialog::ElevationProfileManager );
}
void QgisApp::show3DMapViewsManager()
{
#ifdef HAVE_3D
static_cast<QgsAppWindowManager *>( QgsGui::windowManager() )->openApplicationDialog( QgsAppWindowManager::Dialog3DMapViewsManager );
static_cast<QgsAppWindowManager *>( QgsGui::windowManager() )->openApplicationDialog( QgsAppWindowManager::ApplicationDialog::Dialog3DMapViewsManager );
#endif
}

View File

@ -1351,6 +1351,11 @@ class APP_EXPORT QgisApp : public QMainWindow, private Ui::MainWindow
*/
void show3DMapViewsManager();
/**
* Shows the elevation profile manager dialog.
*/
void showElevationProfileManager();
//! shows the snapping Options
void snappingOptions();

View File

@ -15,10 +15,9 @@
#include "qgsappwindowmanager.h"
#include "qgsstylemanagerdialog.h"
#include "qgsstyle.h"
#include "qgisapp.h"
#include "qgslayoutmanagerdialog.h"
#include "qgsrasterlayer.h"
#include "elevation/qgselevationprofilemanagerdialog.h"
#ifdef HAVE_3D
#include "qgs3dviewsmanagerdialog.h"
@ -30,6 +29,8 @@ QgsAppWindowManager::~QgsAppWindowManager()
delete mStyleManagerDialog;
if ( mLayoutManagerDialog )
delete mLayoutManagerDialog;
if ( mElevationProfileManagerDialog )
delete mElevationProfileManagerDialog;
}
QWidget *QgsAppWindowManager::openStandardDialog( QgsWindowManagerInterface::StandardDialog dialog )
@ -55,7 +56,7 @@ QWidget *QgsAppWindowManager::openApplicationDialog( QgsAppWindowManager::Applic
{
switch ( dialog )
{
case DialogLayoutManager:
case ApplicationDialog::LayoutManager:
{
if ( !mLayoutManagerDialog )
{
@ -66,7 +67,7 @@ QWidget *QgsAppWindowManager::openApplicationDialog( QgsAppWindowManager::Applic
mLayoutManagerDialog->activate();
return mLayoutManagerDialog;
}
case Dialog3DMapViewsManager:
case ApplicationDialog::Dialog3DMapViewsManager:
{
#ifdef HAVE_3D
if ( !m3DMapViewsManagerDialog )
@ -82,6 +83,17 @@ QWidget *QgsAppWindowManager::openApplicationDialog( QgsAppWindowManager::Applic
return m3DMapViewsManagerDialog;
#endif
}
case ApplicationDialog::ElevationProfileManager:
{
if ( !mElevationProfileManagerDialog )
{
mElevationProfileManagerDialog = new QgsElevationProfileManagerDialog( QgisApp::instance(), Qt::Window );
mElevationProfileManagerDialog->setAttribute( Qt::WA_DeleteOnClose );
}
mElevationProfileManagerDialog->show();
mElevationProfileManagerDialog->activate();
return mElevationProfileManagerDialog;
}
}
return nullptr;
}

View File

@ -23,6 +23,7 @@
class QgsStyleManagerDialog;
class QgsLayoutManagerDialog;
class Qgs3DViewsManagerDialog;
class QgsElevationProfileManagerDialog;
/**
* \ingroup gui
@ -32,10 +33,11 @@ class QgsAppWindowManager : public QgsWindowManagerInterface
{
public:
//! Application-only QGIS dialogs
enum ApplicationDialog
enum class ApplicationDialog : int
{
DialogLayoutManager = 0, //!< Layout manager dialog
LayoutManager = 0, //!< Layout manager dialog
Dialog3DMapViewsManager = 1, //!< 3D map views manager dialog
ElevationProfileManager = 2, //!< Elevation profile manager dialog
};
QgsAppWindowManager() = default;
@ -57,6 +59,7 @@ class QgsAppWindowManager : public QgsWindowManagerInterface
QPointer<QgsStyleManagerDialog> mStyleManagerDialog;
QPointer<QgsLayoutManagerDialog> mLayoutManagerDialog;
QPointer<Qgs3DViewsManagerDialog> m3DMapViewsManagerDialog;
QPointer<QgsElevationProfileManagerDialog> mElevationProfileManagerDialog;
};

View File

@ -160,6 +160,13 @@
<addaction name="mActionTemporalController"/>
<addaction name="mActionElevationController"/>
</widget>
<widget class="QMenu" name="menuElevation_Profiles">
<property name="title">
<string>Elevation Profiles</string>
</property>
<addaction name="mActionElevationProfile"/>
<addaction name="mActionManageElevationProfiles"/>
</widget>
<addaction name="mActionNewMapCanvas"/>
<addaction name="m3DMapViewsMenu"/>
<addaction name="mActionPan"/>
@ -171,7 +178,7 @@
<addaction name="mActionIdentify"/>
<addaction name="mMenuMeasure"/>
<addaction name="mActionStatisticalSummary"/>
<addaction name="mActionElevationProfile"/>
<addaction name="menuElevation_Profiles"/>
<addaction name="separator"/>
<addaction name="mActionZoomFullExtent"/>
<addaction name="mActionZoomToSelected"/>
@ -912,7 +919,7 @@
<string>Ctrl+Q</string>
</property>
<property name="menuRole">
<enum>QAction::MenuRole::QuitRole</enum>
<enum>QAction::QuitRole</enum>
</property>
</action>
<action name="mActionUndo">
@ -1903,7 +1910,7 @@ Shift+O to turn segments into straight or curve lines.</string>
<string>&amp;Options…</string>
</property>
<property name="menuRole">
<enum>QAction::MenuRole::NoRole</enum>
<enum>QAction::NoRole</enum>
</property>
</action>
<action name="mActionCustomProjection">
@ -1924,7 +1931,7 @@ Shift+O to turn segments into straight or curve lines.</string>
<string>Keyboard Shortcuts…</string>
</property>
<property name="menuRole">
<enum>QAction::MenuRole::NoRole</enum>
<enum>QAction::NoRole</enum>
</property>
</action>
<action name="mActionLocalHistogramStretch">
@ -1994,7 +2001,7 @@ Shift+O to turn segments into straight or curve lines.</string>
<string>About</string>
</property>
<property name="menuRole">
<enum>QAction::MenuRole::AboutRole</enum>
<enum>QAction::AboutRole</enum>
</property>
</action>
<action name="mActionSponsors">
@ -3317,7 +3324,7 @@ Shows placeholders for labels which could not be placed, e.g. due to overlaps wi
</action>
<action name="mActionManage3DMapViews">
<property name="text">
<string>Manage 3D Map Views</string>
<string>Manage 3D Map Views</string>
</property>
</action>
<action name="mActionDigitizeWithSegment">
@ -3350,10 +3357,10 @@ Shows placeholders for labels which could not be placed, e.g. due to overlaps wi
<normaloff>:/images/themes/default/mActionNewElevationProfile.svg</normaloff>:/images/themes/default/mActionNewElevationProfile.svg</iconset>
</property>
<property name="text">
<string>Elevation Profile</string>
<string>New Elevation Profile</string>
</property>
<property name="toolTip">
<string>Open an interactive elevation profile chart</string>
<string>Create a new interactive elevation profile chart</string>
</property>
</action>
<action name="mActionDonate">
@ -3409,7 +3416,15 @@ Shows placeholders for labels which could not be placed, e.g. due to overlaps wi
<string>Database Query History</string>
</property>
<property name="menuRole">
<enum>QAction::MenuRole::NoRole</enum>
<enum>QAction::NoRole</enum>
</property>
</action>
<action name="mActionManageElevationProfiles">
<property name="text">
<string>Manage Elevation Profiles…</string>
</property>
<property name="toolTip">
<string>Manage Elevation Profiles</string>
</property>
</action>
</widget>

View File

@ -0,0 +1,160 @@
<?xml version="1.0" encoding="UTF-8"?>
<ui version="4.0">
<class>QgsElevationProfileManagerBase</class>
<widget class="QDialog" name="QgsElevationProfileManagerBase">
<property name="geometry">
<rect>
<x>0</x>
<y>0</y>
<width>425</width>
<height>300</height>
</rect>
</property>
<property name="minimumSize">
<size>
<width>425</width>
<height>300</height>
</size>
</property>
<property name="windowTitle">
<string>Elevation Profile Manager</string>
</property>
<property name="windowIcon">
<iconset resource="../../images/images.qrc">
<normaloff>:/images/themes/default/mActionLayoutManager.svg</normaloff>:/images/themes/default/mActionLayoutManager.svg</iconset>
</property>
<layout class="QVBoxLayout" name="verticalLayout">
<item>
<layout class="QGridLayout" name="gridLayout_2" rowstretch="0,0">
<item row="1" column="0">
<widget class="QToolButton" name="mShowButton">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>&amp;Show</string>
</property>
</widget>
</item>
<item row="1" column="1">
<widget class="QToolButton" name="mRemoveButton">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>&amp;Remove…</string>
</property>
</widget>
</item>
<item row="1" column="2">
<widget class="QToolButton" name="mRenameButton">
<property name="sizePolicy">
<sizepolicy hsizetype="Preferred" vsizetype="Fixed">
<horstretch>0</horstretch>
<verstretch>0</verstretch>
</sizepolicy>
</property>
<property name="text">
<string>Re&amp;name…</string>
</property>
</widget>
</item>
<item row="0" column="0" colspan="3">
<layout class="QVBoxLayout" name="verticalLayout_2" stretch="0,1">
<property name="spacing">
<number>0</number>
</property>
<property name="topMargin">
<number>0</number>
</property>
<item>
<widget class="QgsFilterLineEdit" name="mSearchLineEdit">
<property name="placeholderText">
<string>Search…</string>
</property>
</widget>
</item>
<item>
<widget class="QListView" name="mProfileListView">
<property name="selectionMode">
<enum>QAbstractItemView::ExtendedSelection</enum>
</property>
<property name="selectionBehavior">
<enum>QAbstractItemView::SelectRows</enum>
</property>
</widget>
</item>
</layout>
</item>
</layout>
</item>
<item>
<widget class="QDialogButtonBox" name="mButtonBox">
<property name="orientation">
<enum>Qt::Horizontal</enum>
</property>
<property name="standardButtons">
<set>QDialogButtonBox::Close</set>
</property>
</widget>
</item>
</layout>
</widget>
<customwidgets>
<customwidget>
<class>QgsFilterLineEdit</class>
<extends>QLineEdit</extends>
<header>qgsfilterlineedit.h</header>
</customwidget>
</customwidgets>
<tabstops>
<tabstop>mSearchLineEdit</tabstop>
<tabstop>mProfileListView</tabstop>
<tabstop>mShowButton</tabstop>
<tabstop>mRemoveButton</tabstop>
<tabstop>mRenameButton</tabstop>
</tabstops>
<resources>
<include location="../../images/images.qrc"/>
</resources>
<connections>
<connection>
<sender>mButtonBox</sender>
<signal>accepted()</signal>
<receiver>QgsElevationProfileManagerBase</receiver>
<slot>accept()</slot>
<hints>
<hint type="sourcelabel">
<x>266</x>
<y>295</y>
</hint>
<hint type="destinationlabel">
<x>157</x>
<y>274</y>
</hint>
</hints>
</connection>
<connection>
<sender>mButtonBox</sender>
<signal>rejected()</signal>
<receiver>QgsElevationProfileManagerBase</receiver>
<slot>reject()</slot>
<hints>
<hint type="sourcelabel">
<x>266</x>
<y>295</y>
</hint>
<hint type="destinationlabel">
<x>286</x>
<y>274</y>
</hint>
</hints>
</connection>
</connections>
</ui>