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