QGIS API Documentation 3.99.0-Master (d270888f95f)
Loading...
Searching...
No Matches
qgsdockablewidgethelper.cpp
Go to the documentation of this file.
1/***************************************************************************
2 qgsdockablewidgethelper.cpp
3 --------------------------------------
4 Date : January 2022
5 Copyright : (C) 2022 by Belgacem Nedjima
6 Email : belgacem dot nedjima at gmail dot com
7 ***************************************************************************
8 * *
9 * This program is free software; you can redistribute it and/or modify *
10 * it under the terms of the GNU General Public License as published by *
11 * the Free Software Foundation; either version 2 of the License, or *
12 * (at your option) any later version. *
13 * *
14 ***************************************************************************/
15
17
18#include "qgsapplication.h"
19#include "qgsdockwidget.h"
20
21#include <QAction>
22#include <QLayout>
23#include <QString>
24#include <QUuid>
25
26#include "moc_qgsdockablewidgethelper.cpp"
27
28using namespace Qt::StringLiterals;
29
31
32const QgsSettingsEntryBool *QgsDockableWidgetHelper::sSettingsIsDocked = new QgsSettingsEntryBool( u"is-docked"_s, QgsDockableWidgetHelper::sTtreeDockConfigs, false );
33const QgsSettingsEntryVariant *QgsDockableWidgetHelper::sSettingsDockGeometry = new QgsSettingsEntryVariant( u"dock-geometry"_s, QgsDockableWidgetHelper::sTtreeDockConfigs );
34const QgsSettingsEntryVariant *QgsDockableWidgetHelper::sSettingsDialogGeometry = new QgsSettingsEntryVariant( u"dialog-geometry"_s, QgsDockableWidgetHelper::sTtreeDockConfigs );
35const QgsSettingsEntryEnumFlag<Qt::DockWidgetArea> *QgsDockableWidgetHelper::sSettingsDockArea = new QgsSettingsEntryEnumFlag<Qt::DockWidgetArea>( u"dock-area"_s, QgsDockableWidgetHelper::sTtreeDockConfigs, Qt::RightDockWidgetArea );
36
37std::function<void( Qt::DockWidgetArea, QDockWidget *, const QStringList &, bool )> QgsDockableWidgetHelper::sAddTabifiedDockWidgetFunction = []( Qt::DockWidgetArea, QDockWidget *, const QStringList &, bool ) {};
38std::function<QString()> QgsDockableWidgetHelper::sAppStylesheetFunction = [] { return QString(); };
39QMainWindow *QgsDockableWidgetHelper::sOwnerWindow = nullptr;
40
41QgsDockableWidgetHelper::QgsDockableWidgetHelper( const QString &windowTitle, QWidget *widget, QMainWindow *ownerWindow, const QString &dockId, const QStringList &tabifyWith, OpeningMode openingMode, bool defaultIsDocked, Qt::DockWidgetArea defaultDockArea, Options options )
42 : QObject( nullptr )
43 , mWidget( widget )
44 , mDialogGeometry( 0, 0, 0, 0 )
45 , mWindowTitle( windowTitle )
46 , mOwnerWindow( ownerWindow )
47 , mTabifyWith( tabifyWith )
48 , mOptions( options )
49 , mUuid( QUuid::createUuid().toString() )
50 , mSettingKeyDockId( dockId )
51{
52 bool isDocked = sSettingsIsDocked->valueWithDefaultOverride( defaultIsDocked, mSettingKeyDockId );
53 if ( openingMode == OpeningMode::ForceDocked )
54 isDocked = true;
55 else if ( openingMode == OpeningMode::ForceDialog )
56 isDocked = false;
57
58 mDockArea = sSettingsDockArea->valueWithDefaultOverride( defaultDockArea, mSettingKeyDockId );
59 mIsDockFloating = mDockArea == Qt::DockWidgetArea::NoDockWidgetArea;
60 toggleDockMode( isDocked );
61}
62
63QgsDockableWidgetHelper::~QgsDockableWidgetHelper()
64{
65 if ( mDock )
66 {
67 if ( !mSettingKeyDockId.isEmpty() )
68 sSettingsDockGeometry->setValue( mDock->saveGeometry(), mSettingKeyDockId );
69
70 if ( mOwnerWindow )
71 mOwnerWindow->removeDockWidget( mDock );
72
73 mDock->setWidget( nullptr );
74 mWidget->setParent( nullptr );
75 // TODO -- potentially "deleteLater" would be safer here, see eg note
76 // in QgsElevationProfileWidget destructor
77 delete mDock.data();
78 mDock = nullptr;
79 }
80
81 if ( mDialog )
82 {
83 mDialogGeometry = mDialog->geometry();
84
85 if ( !mSettingKeyDockId.isEmpty() )
86 sSettingsDialogGeometry->setValue( mDialog->saveGeometry(), mSettingKeyDockId );
87
88 mDialog->layout()->removeWidget( mWidget );
89 mDialog->deleteLater();
90 mDialog = nullptr;
91 }
92}
93
94void QgsDockableWidgetHelper::writeXml( QDomElement &viewDom )
95{
96 viewDom.setAttribute( u"isDocked"_s, mIsDocked );
97
98 if ( mDock )
99 {
100 mDockGeometry = mDock->geometry();
101 mIsDockFloating = mDock->isFloating();
102 if ( mOwnerWindow )
103 mDockArea = mOwnerWindow->dockWidgetArea( mDock );
104 }
105
106 viewDom.setAttribute( u"x"_s, mDockGeometry.x() );
107 viewDom.setAttribute( u"y"_s, mDockGeometry.y() );
108 viewDom.setAttribute( u"width"_s, mDockGeometry.width() );
109 viewDom.setAttribute( u"height"_s, mDockGeometry.height() );
110 viewDom.setAttribute( u"floating"_s, mIsDockFloating );
111 viewDom.setAttribute( u"area"_s, mDockArea );
112 viewDom.setAttribute( u"uuid"_s, mUuid );
113
114 if ( mDock )
115 {
116 const QList<QDockWidget *> tabSiblings = mOwnerWindow ? mOwnerWindow->tabifiedDockWidgets( mDock ) : QList<QDockWidget *>();
117 QDomElement tabSiblingsElement = viewDom.ownerDocument().createElement( u"tab_siblings"_s );
118 for ( QDockWidget *dock : tabSiblings )
119 {
120 QDomElement siblingElement = viewDom.ownerDocument().createElement( u"sibling"_s );
121 siblingElement.setAttribute( u"uuid"_s, dock->property( "dock_uuid" ).toString() );
122 siblingElement.setAttribute( u"object_name"_s, dock->objectName() );
123 tabSiblingsElement.appendChild( siblingElement );
124 }
125 viewDom.appendChild( tabSiblingsElement );
126 }
127
128 if ( mDialog )
129 mDialogGeometry = mDialog->geometry();
130
131 viewDom.setAttribute( u"d_x"_s, mDialogGeometry.x() );
132 viewDom.setAttribute( u"d_y"_s, mDialogGeometry.y() );
133 viewDom.setAttribute( u"d_width"_s, mDialogGeometry.width() );
134 viewDom.setAttribute( u"d_height"_s, mDialogGeometry.height() );
135}
136
137void QgsDockableWidgetHelper::readXml( const QDomElement &viewDom )
138{
139 mUuid = viewDom.attribute( u"uuid"_s, mUuid );
140
141 {
142 int x = viewDom.attribute( u"d_x"_s, u"0"_s ).toInt();
143 int y = viewDom.attribute( u"d_x"_s, u"0"_s ).toInt();
144 int w = viewDom.attribute( u"d_width"_s, u"200"_s ).toInt();
145 int h = viewDom.attribute( u"d_height"_s, u"200"_s ).toInt();
146 mDialogGeometry = QRect( x, y, w, h );
147 if ( mDialog )
148 mDialog->setGeometry( mDialogGeometry );
149 }
150
151 {
152 int x = viewDom.attribute( u"x"_s, u"0"_s ).toInt();
153 int y = viewDom.attribute( u"y"_s, u"0"_s ).toInt();
154 int w = viewDom.attribute( u"width"_s, u"200"_s ).toInt();
155 int h = viewDom.attribute( u"height"_s, u"200"_s ).toInt();
156 mDockGeometry = QRect( x, y, w, h );
157 mIsDockFloating = viewDom.attribute( u"floating"_s, u"0"_s ).toInt();
158 mDockArea = static_cast<Qt::DockWidgetArea>( viewDom.attribute( u"area"_s, QString::number( Qt::RightDockWidgetArea ) ).toInt() );
159
160 if ( mDockArea == Qt::DockWidgetArea::NoDockWidgetArea && !mIsDockFloating )
161 {
162 mDockArea = Qt::RightDockWidgetArea;
163 }
164
165 QStringList tabSiblings;
166 const QDomElement tabSiblingsElement = viewDom.firstChildElement( u"tab_siblings"_s );
167 const QDomNodeList tabSiblingNodes = tabSiblingsElement.childNodes();
168 for ( int i = 0; i < tabSiblingNodes.size(); ++i )
169 {
170 const QDomElement tabSiblingElement = tabSiblingNodes.at( i ).toElement();
171 // prefer uuid if set, as it's always unique
172 QString tabId = tabSiblingElement.attribute( u"uuid"_s );
173 if ( tabId.isEmpty() )
174 tabId = tabSiblingElement.attribute( u"object_name"_s );
175 if ( !tabId.isEmpty() )
176 tabSiblings.append( tabId );
177 }
178
179 setupDockWidget( tabSiblings );
180 }
181
182 if ( mDock )
183 {
184 mDock->setProperty( "dock_uuid", mUuid );
185 }
186}
187
188void QgsDockableWidgetHelper::setWidget( QWidget *widget )
189{
190 // Make sure the old mWidget is not stuck as a child of mDialog or mDock
191 if ( mWidget && mOwnerWindow )
192 {
193 mWidget->setParent( mOwnerWindow );
194 }
195 if ( mDialog )
196 {
197 mDialog->layout()->removeWidget( mWidget );
198 }
199 if ( mDock )
200 {
201 mDock->setWidget( nullptr );
202 }
203
204 mWidget = widget;
205 toggleDockMode( mIsDocked );
206}
207
208QgsDockWidget *QgsDockableWidgetHelper::dockWidget()
209{
210 return mDock.data();
211}
212
213QDialog *QgsDockableWidgetHelper::dialog()
214{
215 return mDialog.data();
216}
217
218void QgsDockableWidgetHelper::toggleDockMode( bool docked )
219{
220 // Make sure the old mWidget is not stuck as a child of mDialog or mDock
221 if ( mWidget && mOwnerWindow )
222 {
223 mWidget->setParent( mOwnerWindow );
224 }
225
226 // Remove both the dialog and the dock widget first
227 if ( mDock )
228 {
229 mDockGeometry = mDock->geometry();
230 mIsDockFloating = mDock->isFloating();
231 if ( mOwnerWindow )
232 mDockArea = mOwnerWindow->dockWidgetArea( mDock );
233
234 mDock->setWidget( nullptr );
235 if ( mOwnerWindow )
236 mOwnerWindow->removeDockWidget( mDock );
237 delete mDock;
238 mDock = nullptr;
239 }
240
241 if ( mDialog )
242 {
243 // going from window -> dock, so save current window geometry
244 if ( !mSettingKeyDockId.isEmpty() )
245 sSettingsDialogGeometry->setValue( mDialog->saveGeometry(), mSettingKeyDockId );
246
247 mDialogGeometry = mDialog->geometry();
248
249 if ( mWidget )
250 mDialog->layout()->removeWidget( mWidget );
251
252 delete mDialog;
253 mDialog = nullptr;
254 }
255
256 mIsDocked = docked;
257 if ( !mSettingKeyDockId.isEmpty() )
258 sSettingsIsDocked->setValue( mIsDocked, mSettingKeyDockId );
259
260 // If there is no widget set, do not create a dock or a dialog
261 if ( !mWidget )
262 return;
263
264 if ( docked )
265 {
266 // going from window -> dock
267 mDock = new QgsDockWidget( mOwnerWindow );
268 mDock->setWindowTitle( mWindowTitle );
269 mDock->setWidget( mWidget );
270 mDock->setObjectName( mObjectName );
271 mDock->setProperty( "dock_uuid", mUuid );
272 setupDockWidget();
273
274 if ( !mSettingKeyDockId.isEmpty() )
275 {
276 connect( mDock, &QgsDockWidget::dockLocationChanged, this, [this]( Qt::DockWidgetArea area ) {
277 sSettingsDockArea->setValue( area, mSettingKeyDockId );
278 } );
279 }
280
281 connect( mDock, &QgsDockWidget::closed, this, [this]() {
282 mDockGeometry = mDock->geometry();
283 mIsDockFloating = mDock->isFloating();
284 if ( mOwnerWindow )
285 mDockArea = mOwnerWindow->dockWidgetArea( mDock );
286 emit closed();
287 } );
288
289 if ( mOptions.testFlag( Option::PermanentWidget ) )
290 mDock->installEventFilter( this );
291
292 connect( mDock, &QgsDockWidget::visibilityChanged, this, &QgsDockableWidgetHelper::visibilityChanged );
293 mDock->setUserVisible( true );
294 emit visibilityChanged( true );
295 }
296 else
297 {
298 // going from dock -> window
299 // note -- we explicitly DO NOT set the parent for the dialog, as we want these treated as
300 // proper top level windows and have their own taskbar entries. See https://github.com/qgis/QGIS/issues/49286
301 if ( mOptions.testFlag( Option::PermanentWidget ) )
302 mDialog = new QgsNonRejectableDialog( nullptr, Qt::Window );
303 else
304 mDialog = new QDialog( nullptr, Qt::Window );
305 mDialog->setStyleSheet( sAppStylesheetFunction() );
306
307 mDialog->setWindowTitle( mWindowTitle );
308 mDialog->setObjectName( mObjectName );
309
310 if ( mOptions.testFlag( Option::PermanentWidget ) )
311 mDialog->installEventFilter( this );
312
313 QVBoxLayout *vl = new QVBoxLayout();
314 vl->setContentsMargins( 0, 0, 0, 0 );
315 vl->addWidget( mWidget );
316
317 if ( !mSettingKeyDockId.isEmpty() )
318 {
319 mDialog->restoreGeometry( sSettingsDialogGeometry->value( mSettingKeyDockId ).toByteArray() );
320 }
321 else
322 {
323 if ( !mDockGeometry.isEmpty() )
324 mDialog->setGeometry( mDockGeometry );
325 else if ( !mDialogGeometry.isEmpty() )
326 mDialog->setGeometry( mDialogGeometry );
327 }
328 mDialog->setLayout( vl );
329 mDialog->raise();
330 mDialog->show();
331
332 connect( mDialog, &QDialog::finished, this, [this]() {
333 mDialogGeometry = mDialog->geometry();
334 emit closed();
335 emit visibilityChanged( false );
336 } );
337
338 emit visibilityChanged( true );
339 }
340 emit dockModeToggled( docked );
341}
342
343void QgsDockableWidgetHelper::setUserVisible( bool visible )
344{
345 if ( mDialog )
346 {
347 if ( visible )
348 {
349 mDialog->show();
350 mDialog->raise();
351 mDialog->setWindowState( mDialog->windowState() & ~Qt::WindowMinimized );
352 mDialog->activateWindow();
353 }
354 else
355 {
356 mDialog->hide();
357 }
358 }
359 if ( mDock )
360 {
361 mDock->setUserVisible( visible );
362 }
363}
364
365void QgsDockableWidgetHelper::setWindowTitle( const QString &title )
366{
367 mWindowTitle = title;
368 if ( mDialog )
369 {
370 mDialog->setWindowTitle( title );
371 }
372 if ( mDock )
373 {
374 mDock->setWindowTitle( title );
375 }
376}
377
378void QgsDockableWidgetHelper::setDockObjectName( const QString &name )
379{
380 mObjectName = name;
381 if ( mDialog )
382 {
383 mDialog->setObjectName( name );
384 }
385 if ( mDock )
386 {
387 mDock->setObjectName( name );
388 }
389}
390
391QString QgsDockableWidgetHelper::dockObjectName() const { return mObjectName; }
392
393bool QgsDockableWidgetHelper::isUserVisible() const
394{
395 if ( mDialog )
396 {
397 return mDialog->isVisible();
398 }
399 if ( mDock )
400 {
401 return mDock->isUserVisible();
402 }
403 return false;
404}
405
406void QgsDockableWidgetHelper::setupDockWidget( const QStringList &tabSiblings )
407{
408 if ( !mDock )
409 return;
410
411 mDock->setFloating( mIsDockFloating );
412 // default dock geometry
413 if ( mDockGeometry.isEmpty() && mOwnerWindow )
414 {
415 const QFontMetrics fm( mOwnerWindow->font() );
416 const int initialDockSize = fm.horizontalAdvance( '0' ) * 75;
417 mDockGeometry = QRect( static_cast<int>( mOwnerWindow->rect().width() * 0.75 ), static_cast<int>( mOwnerWindow->rect().height() * 0.5 ), initialDockSize, initialDockSize );
418 }
419 if ( !tabSiblings.isEmpty() )
420 {
421 sAddTabifiedDockWidgetFunction( mDockArea, mDock, tabSiblings, false );
422 }
423 else if ( mOptions.testFlag( Option::RaiseTab ) )
424 {
425 sAddTabifiedDockWidgetFunction( mDockArea, mDock, mTabifyWith, true );
426 }
427 else if ( mOwnerWindow )
428 {
429 mOwnerWindow->addDockWidget( mDockArea, mDock );
430 }
431
432 // can only resize properly and set the dock geometry after pending events have been processed,
433 // so queue the geometry setting on the end of the event loop
434 QMetaObject::invokeMethod( mDock, [this] {
435 if (mIsDockFloating && sSettingsDockGeometry->exists( mSettingKeyDockId ) )
436 mDock->restoreGeometry( sSettingsDockGeometry->value( mSettingKeyDockId ).toByteArray() );
437 else if ( mIsDockFloating )
438 mDock->setGeometry( mDockGeometry ); }, Qt::QueuedConnection );
439}
440
441QToolButton *QgsDockableWidgetHelper::createDockUndockToolButton()
442{
443 QToolButton *toggleButton = new QToolButton;
444 toggleButton->setIcon( QgsApplication::getThemeIcon( u"mDockify.svg"_s ) );
445 toggleButton->setCheckable( true );
446 toggleButton->setChecked( mIsDocked );
447 toggleButton->setEnabled( true );
448
449 connect( toggleButton, &QToolButton::toggled, this, &QgsDockableWidgetHelper::toggleDockMode );
450 return toggleButton;
451}
452
453QAction *QgsDockableWidgetHelper::createDockUndockAction( const QString &title, QWidget *parent )
454{
455 QAction *toggleAction = new QAction( title, parent );
456 toggleAction->setIcon( QgsApplication::getThemeIcon( u"mDockify.svg"_s ) );
457 toggleAction->setCheckable( true );
458 toggleAction->setChecked( mIsDocked );
459 toggleAction->setEnabled( true );
460
461 connect( toggleAction, &QAction::toggled, this, &QgsDockableWidgetHelper::toggleDockMode );
462 return toggleAction;
463}
464
465bool QgsDockableWidgetHelper::eventFilter( QObject *watched, QEvent *event )
466{
467 if ( watched == mDialog )
468 {
469 if ( event->type() == QEvent::Close )
470 {
471 event->ignore();
472 mDialog->hide();
473 emit visibilityChanged( false );
474 return true;
475 }
476 }
477 else if ( watched == mDock )
478 {
479 if ( event->type() == QEvent::Close )
480 {
481 event->ignore();
482 mDock->hide();
483 emit visibilityChanged( false );
484 return true;
485 }
486 }
487 return QObject::eventFilter( watched, event );
488}
489
490//
491// QgsNonRejectableDialog
492//
493
494QgsNonRejectableDialog::QgsNonRejectableDialog( QWidget *parent, Qt::WindowFlags f )
495 : QDialog( parent, f )
496{
497}
498
499void QgsNonRejectableDialog::reject()
500{
501 // swallow rejection -- we don't want this dialog to be closable via escape key
502}
503
504
static QIcon getThemeIcon(const QString &name, const QColor &fillColor=QColor(), const QColor &strokeColor=QColor())
Helper to get a theme icon.
A QDockWidget subclass with more fine-grained control over how the widget is closed or opened.
void closed()
Emitted when dock widget is closed.
A boolean settings entry.
A template class for enum and flag settings entry.
A variant settings entry.