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