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