QGIS API Documentation 4.1.0-Master (d6fb7a379fb)
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
39std::function<QString()> QgsDockableWidgetHelper::sAppStylesheetFunction = [] { return QString(); };
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( sAppStylesheetFunction() );
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:6776
@ ForceDocked
Force the widget to be docked.
Definition qgis.h:6778
@ ForceDialog
Force the widget to be shown in a dialog.
Definition qgis.h:6779
@ RestorePreviousState
Restore the previous state of this dock.
Definition qgis.h:6777
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.
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).