From 7ae0fbfa467072494cebdadfcb422a94206c4b52 Mon Sep 17 00:00:00 2001 From: Denis Rouzaud Date: Thu, 23 Jan 2025 14:43:46 +0100 Subject: [PATCH] improve QgsDockableWidgetHelper API uses dynamic settings to save the state (docked, area, geometry) now if the dock is floating, its geometry is also reset --- src/app/qgsattributetabledialog.cpp | 9 +++-- src/gui/qgsdockablewidgethelper.cpp | 63 +++++++++++++++++++++++++---- src/gui/qgsdockablewidgethelper.h | 60 ++++++++++++++++++++++++++- src/gui/qgsgui.h | 1 + 4 files changed, 120 insertions(+), 13 deletions(-) diff --git a/src/app/qgsattributetabledialog.cpp b/src/app/qgsattributetabledialog.cpp index baf6312395f..e32904efa5a 100644 --- a/src/app/qgsattributetabledialog.cpp +++ b/src/app/qgsattributetabledialog.cpp @@ -57,6 +57,7 @@ #include "qgsdockwidget.h" #include "qgssettingsregistrycore.h" + QgsExpressionContext QgsAttributeTableDialog::createExpressionContext() const { QgsExpressionContext expContext; @@ -283,9 +284,11 @@ QgsAttributeTableDialog::QgsAttributeTableDialog( QgsVectorLayer *layer, QgsAttr // info from table to application connect( this, &QgsAttributeTableDialog::saveEdits, this, [=] { QgisApp::instance()->saveEdits(); } ); - const bool isDocked = initiallyDocked ? *initiallyDocked : settings.value( QStringLiteral( "qgis/dockAttributeTable" ), false ).toBool(); - toggleShortcuts( !isDocked ); - mDockableWidgetHelper = new QgsDockableWidgetHelper( isDocked, windowTitle(), this, QgisApp::instance(), Qt::BottomDockWidgetArea, QStringList(), !initiallyDocked, QStringLiteral( "Windows/BetterAttributeTable/geometry" ) ); + QgsDockableWidgetHelper::OpeningMode openingMode = QgsDockableWidgetHelper::OpeningMode::RespectSetting; + if ( initiallyDocked ) + openingMode = *initiallyDocked ? QgsDockableWidgetHelper::OpeningMode::ForceDocked : QgsDockableWidgetHelper::OpeningMode::ForceDialog; + mDockableWidgetHelper = new QgsDockableWidgetHelper( windowTitle(), this, QgisApp::instance(), QStringLiteral( "attribute-table" ), QStringList(), openingMode, true, Qt::BottomDockWidgetArea ); + toggleShortcuts( !mDockableWidgetHelper->isDocked() ); connect( mDockableWidgetHelper, &QgsDockableWidgetHelper::closed, this, [=]() { close(); } ); diff --git a/src/gui/qgsdockablewidgethelper.cpp b/src/gui/qgsdockablewidgethelper.cpp index bb21929e7f5..eebf5ef3bb9 100644 --- a/src/gui/qgsdockablewidgethelper.cpp +++ b/src/gui/qgsdockablewidgethelper.cpp @@ -26,6 +26,9 @@ ///@cond PRIVATE +const QgsSettingsEntryBool *QgsDockableWidgetHelper::sSettingsIsDocked = new QgsSettingsEntryBool( QStringLiteral( "is-docked" ), QgsDockableWidgetHelper::sTtreeDockConfigs, false ); +const QgsSettingsEntryVariant *QgsDockableWidgetHelper::sSettingsWindowGeometry = new QgsSettingsEntryVariant( QStringLiteral( "geometry" ), QgsDockableWidgetHelper::sTtreeDockConfigs ); +const QgsSettingsEntryEnumFlag *QgsDockableWidgetHelper::sSettingsDockArea = new QgsSettingsEntryEnumFlag( QStringLiteral( "area" ), QgsDockableWidgetHelper::sTtreeDockConfigs, Qt::RightDockWidgetArea ); std::function QgsDockableWidgetHelper::sAddTabifiedDockWidgetFunction = []( Qt::DockWidgetArea, QDockWidget *, const QStringList &, bool ) {}; std::function QgsDockableWidgetHelper::sAppStylesheetFunction = [] { return QString(); }; @@ -40,11 +43,33 @@ QgsDockableWidgetHelper::QgsDockableWidgetHelper( bool isDocked, const QString & , mWindowTitle( windowTitle ) , mOwnerWindow( ownerWindow ) , mTabifyWith( tabifyWith ) - , mRaiseTab( raiseTab ) , mWindowGeometrySettingsKey( windowGeometrySettingsKey ) , mUuid( QUuid::createUuid().toString() ) - , mUsePersistentWidget( usePersistentWidget ) { + mOptions.setFlag( Option::RaiseTab, raiseTab ); + mOptions.setFlag( Option::PermanentWidget, usePersistentWidget ); + toggleDockMode( isDocked ); +} + +QgsDockableWidgetHelper::QgsDockableWidgetHelper( const QString &windowTitle, QWidget *widget, QMainWindow *ownerWindow, const QString &dockId, const QStringList &tabifyWith, OpeningMode openingMode, bool defaultIsDocked, Qt::DockWidgetArea defaultDockArea, Options options ) + : QObject( nullptr ) + , mWidget( widget ) + , mDialogGeometry( 0, 0, 0, 0 ) + , mWindowTitle( windowTitle ) + , mOwnerWindow( ownerWindow ) + , mTabifyWith( tabifyWith ) + , mOptions( options ) + , mUuid( QUuid::createUuid().toString() ) + , mSettingKeyDockId( dockId ) +{ + bool isDocked = sSettingsIsDocked->valueWithDefaultOverride( defaultIsDocked, mSettingKeyDockId ); + if ( openingMode == OpeningMode::ForceDocked ) + isDocked = true; + else if ( openingMode == OpeningMode::ForceDialog ) + isDocked = false; + + mDockArea = sSettingsDockArea->valueWithDefaultOverride( defaultDockArea, mSettingKeyDockId ); + mIsDockFloating = mDockArea == Qt::DockWidgetArea::NoDockWidgetArea; toggleDockMode( isDocked ); } @@ -53,6 +78,8 @@ QgsDockableWidgetHelper::~QgsDockableWidgetHelper() if ( mDock ) { mDockGeometry = mDock->geometry(); + if ( !mSettingKeyDockId.isEmpty() ) + sSettingsWindowGeometry->setValue( mDock->saveGeometry(), mSettingKeyDockId ); mIsDockFloating = mDock->isFloating(); if ( mOwnerWindow ) mDockArea = mOwnerWindow->dockWidgetArea( mDock ); @@ -73,6 +100,8 @@ QgsDockableWidgetHelper::~QgsDockableWidgetHelper() { QgsSettings().setValue( mWindowGeometrySettingsKey, mDialog->saveGeometry() ); } + if ( !mSettingKeyDockId.isEmpty() ) + sSettingsWindowGeometry->setValue( mDialog->saveGeometry(), mSettingKeyDockId ); mDialog->layout()->removeWidget( mWidget ); mDialog->deleteLater(); @@ -243,6 +272,8 @@ void QgsDockableWidgetHelper::toggleDockMode( bool docked ) } mIsDocked = docked; + if ( !mSettingKeyDockId.isEmpty() ) + sSettingsIsDocked->setValue( mIsDocked, mSettingKeyDockId ); // If there is no widget set, do not create a dock or a dialog if ( !mWidget ) @@ -258,6 +289,13 @@ void QgsDockableWidgetHelper::toggleDockMode( bool docked ) mDock->setProperty( "dock_uuid", mUuid ); setupDockWidget(); + if ( !mSettingKeyDockId.isEmpty() ) + { + connect( mDock, &QgsDockWidget::dockLocationChanged, this, [=]( Qt::DockWidgetArea area ) { + sSettingsDockArea->setValue( area, mSettingKeyDockId ); + } ); + } + connect( mDock, &QgsDockWidget::closed, this, [=]() { mDockGeometry = mDock->geometry(); mIsDockFloating = mDock->isFloating(); @@ -266,7 +304,7 @@ void QgsDockableWidgetHelper::toggleDockMode( bool docked ) emit closed(); } ); - if ( mUsePersistentWidget ) + if ( mOptions.testFlag( Option::PermanentWidget ) ) mDock->installEventFilter( this ); connect( mDock, &QgsDockWidget::visibilityChanged, this, &QgsDockableWidgetHelper::visibilityChanged ); @@ -278,7 +316,7 @@ void QgsDockableWidgetHelper::toggleDockMode( bool docked ) // going from dock -> window // note -- we explicitly DO NOT set the parent for the dialog, as we want these treated as // proper top level windows and have their own taskbar entries. See https://github.com/qgis/QGIS/issues/49286 - if ( mUsePersistentWidget ) + if ( mOptions.testFlag( Option::PermanentWidget ) ) mDialog = new QgsNonRejectableDialog( nullptr, Qt::Window ); else mDialog = new QDialog( nullptr, Qt::Window ); @@ -287,7 +325,7 @@ void QgsDockableWidgetHelper::toggleDockMode( bool docked ) mDialog->setWindowTitle( mWindowTitle ); mDialog->setObjectName( mObjectName ); - if ( mUsePersistentWidget ) + if ( mOptions.testFlag( Option::PermanentWidget ) ) mDialog->installEventFilter( this ); QVBoxLayout *vl = new QVBoxLayout(); @@ -299,6 +337,10 @@ void QgsDockableWidgetHelper::toggleDockMode( bool docked ) QgsSettings settings; mDialog->restoreGeometry( settings.value( mWindowGeometrySettingsKey ).toByteArray() ); } + else if ( !mSettingKeyDockId.isEmpty() ) + { + mDialog->restoreGeometry( sSettingsWindowGeometry->value( mSettingKeyDockId ).toByteArray() ); + } else { if ( !mDockGeometry.isEmpty() ) @@ -390,6 +432,7 @@ void QgsDockableWidgetHelper::setupDockWidget( const QStringList &tabSiblings ) return; mDock->setFloating( mIsDockFloating ); + // default dock geometry if ( mDockGeometry.isEmpty() && mOwnerWindow ) { const QFontMetrics fm( mOwnerWindow->font() ); @@ -400,9 +443,9 @@ void QgsDockableWidgetHelper::setupDockWidget( const QStringList &tabSiblings ) { sAddTabifiedDockWidgetFunction( mDockArea, mDock, tabSiblings, false ); } - else if ( mRaiseTab ) + else if ( mOptions.testFlag( Option::RaiseTab ) ) { - sAddTabifiedDockWidgetFunction( mDockArea, mDock, mTabifyWith, mRaiseTab ); + sAddTabifiedDockWidgetFunction( mDockArea, mDock, mTabifyWith, true ); } else if ( mOwnerWindow ) { @@ -411,7 +454,11 @@ void QgsDockableWidgetHelper::setupDockWidget( const QStringList &tabSiblings ) // can only resize properly and set the dock geometry after pending events have been processed, // so queue the geometry setting on the end of the event loop - QMetaObject::invokeMethod( mDock, [this] { mDock->setGeometry( mDockGeometry ); }, Qt::QueuedConnection ); + QMetaObject::invokeMethod( mDock, [this] { + if (mIsDockFloating && sSettingsWindowGeometry->exists(mSettingKeyDockId)) + mDock->restoreGeometry( sSettingsWindowGeometry->value(mSettingKeyDockId).toByteArray() ); + else + mDock->setGeometry( mDockGeometry ); }, Qt::QueuedConnection ); } QToolButton *QgsDockableWidgetHelper::createDockUndockToolButton() diff --git a/src/gui/qgsdockablewidgethelper.h b/src/gui/qgsdockablewidgethelper.h index b5fa201a201..cbb16c9a6be 100644 --- a/src/gui/qgsdockablewidgethelper.h +++ b/src/gui/qgsdockablewidgethelper.h @@ -24,6 +24,10 @@ #include #include +#include "qgssettingsentryimpl.h" +#include "qgssettingsentryenumflag.h" +#include "qgsgui.h" + #define SIP_NO_FILE class QgsDockWidget; @@ -50,8 +54,29 @@ class GUI_EXPORT QgsNonRejectableDialog : public QDialog */ class GUI_EXPORT QgsDockableWidgetHelper : public QObject { + static inline QgsSettingsTreeNode *sTtreeDockConfigs = QgsGui::sTtreeWidgetGeometry->createNamedListNode( QStringLiteral( "docks" ) ) SIP_SKIP; + + static const QgsSettingsEntryBool *sSettingsIsDocked SIP_SKIP; + static const QgsSettingsEntryVariant *sSettingsWindowGeometry SIP_SKIP; + static const QgsSettingsEntryEnumFlag *sSettingsDockArea SIP_SKIP; + Q_OBJECT public: + enum class OpeningMode : int + { + RespectSetting, //! Respect the setting used + ForceDocked, //! Force the widget to be docked, despite its settings + ForceDialog, //! Force the widget to be shown in a dialog, despite its settings + }; + + enum class Option : int + { + RaiseTab = 1 << 1, //!< Raise Tab + PermanentWidget = 1 << 2, //!< The widget (either as a dock or window) cannot be destroyed and must be hidden instead + }; + Q_ENUM( Option ) + Q_DECLARE_FLAGS( Options, Option ) + /** * Constructs an object that is responsible of making a docked widget or a window titled \a windowTitle that holds the \a widget * The ownership of \a widget is returned to \a ownerWindow once the object is destroyed. @@ -59,8 +84,35 @@ class GUI_EXPORT QgsDockableWidgetHelper : public QObject * If \a usePersistentWidget is TRUE then the \a widget (either as a dock or window) cannot be destroyed and must be hidden instead. */ QgsDockableWidgetHelper( bool isDocked, const QString &windowTitle, QWidget *widget, QMainWindow *ownerWindow, Qt::DockWidgetArea defaultDockArea = Qt::NoDockWidgetArea, const QStringList &tabifyWith = QStringList(), bool raiseTab = false, const QString &windowGeometrySettingsKey = QString(), bool usePersistentWidget = false ); + + /** + * Constructs an object that is responsible of making a docked widget or a window titled \a windowTitle that holds the \a widget + * The ownership of \a widget is returned to \a ownerWindow once the object is destroyed. + * + * With a unique \a dockId, the status (docked, area and geometry) are saved in the settings and re-used on creation. + * + * If \a usePersistentWidget is TRUE then the \a widget (either as a dock or window) cannot be destroyed and must be hidden instead. + * + * \since QGIS 3.42 + */ + QgsDockableWidgetHelper( + const QString &windowTitle, + QWidget *widget, + QMainWindow *ownerWindow, + const QString &dockId, + const QStringList &tabifyWith = QStringList(), + OpeningMode openingMode = OpeningMode::RespectSetting, + bool defaultIsDocked = false, + Qt::DockWidgetArea defaultDockArea = Qt::DockWidgetArea::RightDockWidgetArea, + Options options = Options() + ); + ~QgsDockableWidgetHelper(); + //! Returns if the widget is docked + //! \since 3.42 + bool isDocked() const { return mIsDocked; } + //! Reads the dimensions of both the dock widget and the top level window void writeXml( QDomElement &viewDom ); void readXml( const QDomElement &viewDom ); @@ -140,16 +192,20 @@ class GUI_EXPORT QgsDockableWidgetHelper : public QObject QMainWindow *mOwnerWindow = nullptr; QStringList mTabifyWith; - bool mRaiseTab = false; + Options mOptions; QString mWindowGeometrySettingsKey; // Unique identifier of dock QString mUuid; - bool mUsePersistentWidget = false; + + const QString mSettingKeyDockId; }; +Q_DECLARE_OPERATORS_FOR_FLAGS( QgsDockableWidgetHelper::Options ) + + ///@endcond #endif // QGSDOCKABLEWIDGETHELPER_H diff --git a/src/gui/qgsgui.h b/src/gui/qgsgui.h index 797acd09755..0f7db9d6084 100644 --- a/src/gui/qgsgui.h +++ b/src/gui/qgsgui.h @@ -65,6 +65,7 @@ class GUI_EXPORT QgsGui : public QObject Q_OBJECT public: + static inline QgsSettingsTreeNode *sTtreeWidgetGeometry = QgsSettingsTree::sTreeApp->createChildNode( QStringLiteral( "widget-geometry" ) ) SIP_SKIP; static inline QgsSettingsTreeNode *sTtreeWidgetLastUsedValues = QgsSettingsTree::sTreeApp->createChildNode( QStringLiteral( "widget-last-used-values" ) ) SIP_SKIP; /**