QGIS API Documentation 3.29.0-Master (ade4f0cf0f)
qgscollapsiblegroupbox.cpp
Go to the documentation of this file.
1/***************************************************************************
2 qgscollapsiblegroupbox.cpp
3 -------------------
4 begin : August 2012
5 copyright : (C) 2012 by Etienne Tourigny
6 email : etourigny dot dev at gmail dot com
7 ***************************************************************************/
8
9/***************************************************************************
10 * *
11 * This program is free software; you can redistribute it and/or modify *
12 * it under the terms of the GNU General Public License as published by *
13 * the Free Software Foundation; either version 2 of the License, or *
14 * (at your option) any later version. *
15 * *
16 ***************************************************************************/
17
19#include "qgsapplication.h"
20#include "qgslogger.h"
21#include "qgssettings.h"
22#include "qgsvariantutils.h"
23
24#include <QToolButton>
25#include <QMouseEvent>
26#include <QPushButton>
27#include <QStyleOptionGroupBox>
28#include <QScrollArea>
29
30const QString COLLAPSE_HIDE_BORDER_FIX = QStringLiteral( " QgsCollapsibleGroupBoxBasic { border: none; }" );
31
33 : QGroupBox( parent )
34{
35 init();
36}
37
39 QWidget *parent )
40 : QGroupBox( title, parent )
41{
42 init();
43}
44
46{
47 // variables
48 mCollapsed = false;
49 mInitFlat = false;
50 mInitFlatChecked = false;
51 mScrollOnExpand = true;
52 mShown = false;
53 mParentScrollArea = nullptr;
54 mSyncParent = nullptr;
55 mAltDown = false;
56 mShiftDown = false;
57 mTitleClicked = false;
58
59 // init icons
60 mCollapseIcon = QgsApplication::getThemeIcon( QStringLiteral( "/mIconCollapse.svg" ) );
61 mExpandIcon = QgsApplication::getThemeIcon( QStringLiteral( "/mIconExpand.svg" ) );
62
63 // collapse button
65 mCollapseButton->setObjectName( QStringLiteral( "collapseButton" ) );
66 mCollapseButton->setAutoRaise( true );
67 mCollapseButton->setFixedSize( 16, 16 );
68 // TODO set size (as well as margins) depending on theme, in updateStyle()
69 mCollapseButton->setIconSize( QSize( 12, 12 ) );
71 // FIXME: This appears to mess up parent-child relationships and causes double-frees of children when destroying in Qt5.10, needs further investigation
72 // See also https://github.com/qgis/QGIS/pull/6301
73 setFocusPolicy( Qt::StrongFocus );
74
75 connect( mCollapseButton, &QAbstractButton::clicked, this, &QgsCollapsibleGroupBoxBasic::toggleCollapsed );
76 connect( this, &QGroupBox::toggled, this, &QgsCollapsibleGroupBoxBasic::checkToggled );
77 connect( this, &QGroupBox::clicked, this, &QgsCollapsibleGroupBoxBasic::checkClicked );
78}
79
81{
82 // initialize widget on first show event only
83 if ( mShown )
84 {
85 event->accept();
86 return;
87 }
88
89 // check if groupbox was set to flat in Designer or in code
90 if ( !mInitFlatChecked )
91 {
92 mInitFlat = isFlat();
93 mInitFlatChecked = true;
94 }
95
96 // find parent QScrollArea - this might not work in complex layouts - should we look deeper?
97 if ( parent() && parent()->parent() )
98 mParentScrollArea = qobject_cast<QScrollArea *>( parent()->parent()->parent() );
99 else
100 mParentScrollArea = nullptr;
101 if ( mParentScrollArea )
102 {
103 QgsDebugMsgLevel( "found a QScrollArea parent: " + mParentScrollArea->objectName(), 5 );
104 }
105 else
106 {
107 QgsDebugMsgLevel( QStringLiteral( "did not find a QScrollArea parent" ), 5 );
108 }
109
110 updateStyle();
111
112 // expand if needed - any calls to setCollapsed() before only set mCollapsed, but have UI effect
113 if ( mCollapsed )
114 {
116 }
117 else
118 {
119 // emit signal for connections using collapsed state
121 }
122
123 // verify triangle mirrors groupbox's enabled state
124 mCollapseButton->setEnabled( isEnabled() );
125
126 // set mShown after first setCollapsed call or expanded groupboxes
127 // will scroll scroll areas when first shown
128 mShown = true;
129 event->accept();
130}
131
133{
134 // avoid leaving checkbox in pressed state if alt- or shift-clicking
135 if ( event->modifiers() & ( Qt::AltModifier | Qt::ControlModifier | Qt::ShiftModifier )
136 && titleRect().contains( event->pos() )
137 && isCheckable() )
138 {
139 event->ignore();
140 return;
141 }
142
143 // default behavior - pass to QGroupBox
144 QGroupBox::mousePressEvent( event );
145}
146
148{
149 mAltDown = ( event->modifiers() & ( Qt::AltModifier | Qt::ControlModifier ) );
150 mShiftDown = ( event->modifiers() & Qt::ShiftModifier );
151 mTitleClicked = ( titleRect().contains( event->pos() ) );
152
153 // sync group when title is alt-clicked
154 // collapse/expand when title is clicked and non-checkable
155 // expand current and collapse others on shift-click
156 if ( event->button() == Qt::LeftButton && mTitleClicked &&
157 ( mAltDown || mShiftDown || !isCheckable() ) )
158 {
160 return;
161 }
162
163 // default behavior - pass to QGroupBox
164 QGroupBox::mouseReleaseEvent( event );
165}
166
168{
169 // always re-enable mCollapseButton when groupbox was previously disabled
170 // e.g. resulting from a disabled parent of groupbox, or a signal/slot connection
171
172 // default behavior - pass to QGroupBox
173 QGroupBox::changeEvent( event );
174
175 if ( event->type() == QEvent::EnabledChange && isEnabled() )
176 mCollapseButton->setEnabled( true );
177}
178
180{
181 mSyncGroup = grp;
182 QString tipTxt;
183 if ( !grp.isEmpty() )
184 {
185 tipTxt = tr( "Ctrl (or Alt)-click to toggle all" ) + '\n' + tr( "Shift-click to expand, then collapse others" );
186 }
187 mCollapseButton->setToolTip( tipTxt );
188}
189
191{
192 QStyleOptionGroupBox box;
193 initStyleOption( &box );
194 return style()->subControlRect( QStyle::CC_GroupBox, &box,
195 QStyle::SC_GroupBoxLabel, this );
196}
197
199{
200 mCollapseButton->setAltDown( false );
202 mAltDown = false;
203 mShiftDown = false;
204}
205
207{
208 Q_UNUSED( chkd )
209 mCollapseButton->setEnabled( true ); // always keep enabled
210}
211
213{
214 // expand/collapse when checkbox toggled by user click.
215 // don't do this on toggle signal, otherwise group boxes will default to collapsed
216 // in option dialog constructors, reducing discovery of options by new users and
217 // overriding user's auto-saved collapsed/expanded state for the group box
218 if ( chkd && isCollapsed() )
219 setCollapsed( false );
220 else if ( ! chkd && ! isCollapsed() )
221 setCollapsed( true );
222}
223
225{
226 // verify if sender is this group box's collapse button
227 QgsGroupBoxCollapseButton *collBtn = qobject_cast<QgsGroupBoxCollapseButton *>( QObject::sender() );
228 const bool senderCollBtn = ( collBtn && collBtn == mCollapseButton );
229
232
233 // find any sync group siblings and toggle them
234 if ( ( senderCollBtn || mTitleClicked )
235 && ( mAltDown || mShiftDown )
236 && !mSyncGroup.isEmpty() )
237 {
238 QgsDebugMsg( QStringLiteral( "Alt or Shift key down, syncing group" ) );
239 // get pointer to parent or grandparent widget
240 if ( auto *lParentWidget = parentWidget() )
241 {
242 mSyncParent = lParentWidget;
243 if ( mSyncParent->parentWidget() )
244 {
245 // don't use whole app for grandparent (common for dialogs that use main window for parent)
246 if ( mSyncParent->parentWidget()->objectName() != QLatin1String( "QgisApp" ) )
247 {
248 mSyncParent = mSyncParent->parentWidget();
249 }
250 }
251 }
252 else
253 {
254 mSyncParent = nullptr;
255 }
256
257 if ( mSyncParent )
258 {
259 QgsDebugMsg( "found sync parent: " + mSyncParent->objectName() );
260
261 const bool thisCollapsed = mCollapsed; // get state of current box before its changed
262 const auto groupBoxes {mSyncParent->findChildren<QgsCollapsibleGroupBoxBasic *>()};
263 for ( QgsCollapsibleGroupBoxBasic *grpbox : groupBoxes )
264 {
265 if ( grpbox->syncGroup() == syncGroup() && grpbox->isEnabled() )
266 {
267 if ( mShiftDown && grpbox == this )
268 {
269 // expand current group box on shift-click
270 setCollapsed( false );
271 }
272 else
273 {
274 grpbox->setCollapsed( mShiftDown ? true : !thisCollapsed );
275 }
276 }
277 }
278
280 return;
281 }
282 else
283 {
284 QgsDebugMsg( QStringLiteral( "did not find a sync parent" ) );
285 }
286 }
287
288 // expand current group box on shift-click, even if no sync group
289 if ( mShiftDown )
290 {
291 setCollapsed( false );
292 }
293 else
294 {
296 }
297
299}
300
302{
303 QGroupBox::setStyleSheet( style );
304}
305
307{
308 setUpdatesEnabled( false );
309
310 const QgsSettings settings;
311
312 QStyleOptionGroupBox box;
313 initStyleOption( &box );
314 const QRect rectFrame = style()->subControlRect( QStyle::CC_GroupBox, &box,
315 QStyle::SC_GroupBoxFrame, this );
316 const QRect rectTitle = titleRect();
317
318 // margin/offset defaults
319 const int marginLeft = 20; // title margin for disclosure triangle
320 const int marginRight = 5; // a little bit of space on the right, to match space on the left
321 int offsetLeft = 0; // offset for oxygen theme
322 const int offsetStyle = QApplication::style()->objectName().contains( QLatin1String( "macintosh" ) ) ? 8 : 0;
323 const int topBuffer = 1 + offsetStyle; // space between top of title or triangle and widget above
324 int offsetTop = topBuffer;
325 int offsetTopTri = topBuffer; // offset for triangle
326
327 if ( mCollapseButton->height() < rectTitle.height() ) // triangle's height > title text's, offset triangle
328 {
329 offsetTopTri += ( rectTitle.height() - mCollapseButton->height() ) / 2;
330// offsetTopTri += rectTitle.top();
331 }
332 else if ( rectTitle.height() < mCollapseButton->height() ) // title text's height < triangle's, offset title
333 {
334 offsetTop += ( mCollapseButton->height() - rectTitle.height() ) / 2;
335 }
336
337 // calculate offset if frame overlaps triangle (oxygen theme)
338 // using an offset of 6 pixels from frame border
339 if ( QApplication::style()->objectName().compare( QLatin1String( "oxygen" ), Qt::CaseInsensitive ) == 0 )
340 {
341 QStyleOptionGroupBox box;
342 initStyleOption( &box );
343 const QRect rectFrame = style()->subControlRect( QStyle::CC_GroupBox, &box,
344 QStyle::SC_GroupBoxFrame, this );
345 const QRect rectCheckBox = style()->subControlRect( QStyle::CC_GroupBox, &box,
346 QStyle::SC_GroupBoxCheckBox, this );
347 if ( rectFrame.left() <= 0 )
348 offsetLeft = 6 + rectFrame.left();
349 if ( rectFrame.top() <= 0 )
350 {
351 if ( isCheckable() )
352 {
353 // if is checkable align with checkbox
354 offsetTop = ( rectCheckBox.height() / 2 ) -
355 ( mCollapseButton->height() / 2 ) + rectCheckBox.top();
356 offsetTopTri = offsetTop + 1;
357 }
358 else
359 {
360 offsetTop = 6 + rectFrame.top();
361 offsetTopTri = offsetTop;
362 }
363 }
364 }
365
366 QgsDebugMsgLevel( QStringLiteral( "groupbox: %1 style: %2 offset: left=%3 top=%4 top2=%5" ).arg(
367 objectName(), QApplication::style()->objectName() ).arg( offsetLeft ).arg( offsetTop ).arg( offsetTopTri ), 5 );
368
369 // customize style sheet for collapse/expand button and force left-aligned title
370 QString ss;
371 if ( QApplication::style()->objectName().contains( QLatin1String( "macintosh" ) ) )
372 {
373 ss += QLatin1String( "QgsCollapsibleGroupBoxBasic, QgsCollapsibleGroupBox {" );
374 ss += QStringLiteral( " margin-top: %1px;" ).arg( topBuffer + rectFrame.top() );
375 ss += '}';
376 }
377 ss += QLatin1String( "QgsCollapsibleGroupBoxBasic::title, QgsCollapsibleGroupBox::title {" );
378 ss += QLatin1String( " subcontrol-origin: margin;" );
379 ss += QLatin1String( " subcontrol-position: top left;" );
380 ss += QStringLiteral( " margin-left: %1px;" ).arg( marginLeft );
381 ss += QStringLiteral( " margin-right: %1px;" ).arg( marginRight );
382 ss += QStringLiteral( " left: %1px;" ).arg( offsetLeft );
383 ss += QStringLiteral( " top: %1px;" ).arg( offsetTop );
384 if ( QApplication::style()->objectName().contains( QLatin1String( "macintosh" ) ) )
385 {
386 ss += QLatin1String( " background-color: rgba(0,0,0,0)" );
387 }
388 ss += '}';
389 setStyleSheet( styleSheet() + ss );
390
391 // clear toolbutton default background and border and apply offset
392 QString ssd;
393 ssd = QStringLiteral( "QgsCollapsibleGroupBoxBasic > QToolButton#%1, QgsCollapsibleGroupBox > QToolButton#%1 {" ).arg( mCollapseButton->objectName() );
394 ssd += QLatin1String( " background-color: rgba(255, 255, 255, 0); border: none;" );
395 ssd += QStringLiteral( "} QgsCollapsibleGroupBoxBasic > QToolButton#%1:focus, QgsCollapsibleGroupBox > QToolButton#%1:focus { border: 1px solid palette(highlight); }" ).arg( mCollapseButton->objectName() );
396 mCollapseButton->setStyleSheet( ssd );
397 if ( offsetLeft != 0 || offsetTopTri != 0 )
398 mCollapseButton->move( offsetLeft, offsetTopTri );
399 setUpdatesEnabled( true );
400}
401
403{
404 const bool changed = collapse != mCollapsed;
405 mCollapsed = collapse;
406
407 if ( !isVisible() )
408 return;
409
410 // for consistent look/spacing across platforms when collapsed
411 if ( ! mInitFlat ) // skip if initially set to flat in Designer
412 setFlat( collapse );
413
414 // avoid flicker in X11
415 // NOTE: this causes app to crash when loading a project that hits a group box with
416 // 'collapse' set via dynamic property or in code (especially if auto-launching project)
417 // TODO: find another means of avoiding the X11 flicker
418// QApplication::processEvents();
419
420 // handle visual fixes for collapsing/expanding
422
423 // set maximum height to hide contents - does this work in all envs?
424 // setMaximumHeight( collapse ? 25 : 16777215 );
425 setMaximumHeight( collapse ? titleRect().bottom() + 6 : 16777215 );
426 mCollapseButton->setIcon( collapse ? mExpandIcon : mCollapseIcon );
427
428 // if expanding and is in a QScrollArea, scroll down to make entire widget visible
429 if ( mShown && mScrollOnExpand && !collapse && mParentScrollArea )
430 {
431 // process events so entire widget is shown
432 QApplication::processEvents();
433 mParentScrollArea->setUpdatesEnabled( false );
434 mParentScrollArea->ensureWidgetVisible( this );
435 //and then make sure the top of the widget is visible - otherwise tall group boxes
436 //scroll to their centres, which is disorienting for users
437 mParentScrollArea->ensureWidgetVisible( mCollapseButton, 0, 5 );
438 mParentScrollArea->setUpdatesEnabled( true );
439 }
440 // emit signal for connections using collapsed state
441 if ( changed )
443}
444
446{
447 // handle child widgets so they don't paint while hidden
448 const char *hideKey = "CollGrpBxHide";
449
450 QString ss = styleSheet();
451 if ( mCollapsed )
452 {
453 if ( !ss.contains( COLLAPSE_HIDE_BORDER_FIX ) )
454 {
456 setStyleSheet( ss );
457 }
458
459 const auto constChildren = children();
460 for ( QObject *child : constChildren )
461 {
462 QWidget *w = qobject_cast<QWidget *>( child );
463 if ( w && w != mCollapseButton )
464 {
465 w->setProperty( hideKey, true );
466 w->hide();
467 }
468 }
469 }
470 else // on expand
471 {
472 if ( ss.contains( COLLAPSE_HIDE_BORDER_FIX ) )
473 {
474 ss.replace( COLLAPSE_HIDE_BORDER_FIX, QString() );
475 setStyleSheet( ss );
476 }
477
478 const auto constChildren = children();
479 for ( QObject *child : constChildren )
480 {
481 QWidget *w = qobject_cast<QWidget *>( child );
482 if ( w && w != mCollapseButton )
483 {
484 if ( w->property( hideKey ).toBool() )
485 w->show();
486 }
487 }
488 }
489}
490
491
492// ----
493
496 , mSettings( settings )
497{
498 init();
499}
500
502 QWidget *parent, QgsSettings *settings )
503 : QgsCollapsibleGroupBoxBasic( title, parent )
504 , mSettings( settings )
505{
506 init();
507}
508
510{
511 saveState();
512 if ( mDelSettings ) // local settings obj to delete
513 delete mSettings;
514 mSettings = nullptr; // null the pointer (in case of outside settings obj)
515}
516
518{
519 if ( mDelSettings ) // local settings obj to delete
520 delete mSettings;
521 mSettings = settings;
522 mDelSettings = false; // don't delete outside obj
523}
524
525
527{
528 // use pointer to app qsettings if no custom qsettings specified
529 // custom qsettings object may be from Python plugin
530 mDelSettings = false;
531 if ( !mSettings )
532 {
533 mSettings = new QgsSettings();
534 mDelSettings = true; // only delete obj created by class
535 }
536 // variables
537 mSaveCollapsedState = true;
538 // NOTE: only turn on mSaveCheckedState for groupboxes NOT used
539 // in multiple places or used as options for different parent objects
540 mSaveCheckedState = false;
541
542 connect( this, &QObject::objectNameChanged, this, &QgsCollapsibleGroupBox::loadState );
543}
544
545void QgsCollapsibleGroupBox::showEvent( QShowEvent *event )
546{
547 // initialize widget on first show event only
548 if ( mShown )
549 {
550 event->accept();
551 return;
552 }
553
554 // check if groupbox was set to flat in Designer or in code
555 if ( !mInitFlatChecked )
556 {
557 mInitFlat = isFlat();
558 mInitFlatChecked = true;
559 }
560
561 loadState();
562
564}
565
567{
568 if ( objectName().isEmpty() || ( mSettingGroup.isEmpty() && window()->objectName().isEmpty() ) )
569 return QString(); // cannot get a valid key
570
571 // save key for load/save state
572 // currently QgsCollapsibleGroupBox/window()/object
573 QString saveKey = '/' + objectName();
574 // QObject* parentWidget = parent();
575 // while ( parentWidget )
576 // {
577 // saveKey = "/" + parentWidget->objectName() + saveKey;
578 // parentWidget = parentWidget->parent();
579 // }
580 // if ( parent() )
581 // saveKey = "/" + parent()->objectName() + saveKey;
582 const QString setgrp = mSettingGroup.isEmpty() ? window()->objectName() : mSettingGroup;
583 saveKey = '/' + setgrp + saveKey;
584 saveKey = QStringLiteral( "QgsCollapsibleGroupBox" ) + saveKey;
585 return saveKey;
586}
587
589{
590 if ( !mSettings )
591 return;
592
593 if ( !isEnabled() || ( !mSaveCollapsedState && !mSaveCheckedState ) )
594 return;
595
596 const QString key = saveKey();
597 if ( key.isEmpty() )
598 return;
599
600 setUpdatesEnabled( false );
601
602 if ( mSaveCheckedState )
603 {
604 const QVariant val = mSettings->value( key + "/checked" );
605 if ( ! QgsVariantUtils::isNull( val ) )
606 setChecked( val.toBool() );
607 }
609 {
610 const QVariant val = mSettings->value( key + "/collapsed" );
611 if ( ! QgsVariantUtils::isNull( val ) )
612 setCollapsed( val.toBool() );
613 }
614
615 setUpdatesEnabled( true );
616}
617
619{
620 if ( !mSettings )
621 return;
622
623 if ( !mShown || !isEnabled() || ( !mSaveCollapsedState && !mSaveCheckedState ) )
624 return;
625
626 const QString key = saveKey();
627 if ( key.isEmpty() )
628 return;
629
630 if ( mSaveCheckedState )
631 mSettings->setValue( key + QStringLiteral( "/checked" ), isChecked() );
633 mSettings->setValue( key + QStringLiteral( "/collapsed" ), isCollapsed() );
634}
635
636
638{
639 mAltDown = ( event->modifiers() & ( Qt::AltModifier | Qt::ControlModifier ) );
640 mShiftDown = ( event->modifiers() & Qt::ShiftModifier );
641 QToolButton::mouseReleaseEvent( event );
642}
static QIcon getThemeIcon(const QString &name, const QColor &fillColor=QColor(), const QColor &strokeColor=QColor())
Helper to get a theme icon.
A groupbox that collapses/expands when toggled.
QString syncGroup
An optional group to be collapsed and uncollapsed in sync with this group box if the Alt-modifier is ...
void changeEvent(QEvent *event) override
void collapseExpandFixes()
Visual fixes for when group box is collapsed/expanded.
QgsCollapsibleGroupBoxBasic(QWidget *parent=nullptr)
void setSyncGroup(const QString &grp)
Named group which synchronizes collapsing action when triangle is clicked while holding alt modifier ...
bool isCollapsed() const
Returns the current collapsed state of this group box.
void showEvent(QShowEvent *event) override
void collapsedStateChanged(bool collapsed)
Signal emitted when groupbox collapsed/expanded state is changed, and when first shown.
QgsGroupBoxCollapseButton * mCollapseButton
void setStyleSheet(const QString &style)
Overridden to prepare base call and avoid crash due to specific QT versions.
void setCollapsed(bool collapse)
Collapse or uncollapse this groupbox.
void mousePressEvent(QMouseEvent *event) override
void mouseReleaseEvent(QMouseEvent *event) override
void saveState() const
Will save the collapsed and checked state.
QPointer< QgsSettings > mSettings
QgsCollapsibleGroupBox(QWidget *parent=nullptr, QgsSettings *settings=nullptr)
void showEvent(QShowEvent *event) override
void loadState()
Will load the collapsed and checked state.
void setSettings(QgsSettings *settings)
void setShiftDown(bool shiftdown)
void mouseReleaseEvent(QMouseEvent *event) override
This class is a composition of two QSettings instances:
Definition: qgssettings.h:62
static bool isNull(const QVariant &variant)
Returns true if the specified variant should be considered a NULL value.
const QString COLLAPSE_HIDE_BORDER_FIX
#define QgsDebugMsgLevel(str, level)
Definition: qgslogger.h:39
#define QgsDebugMsg(str)
Definition: qgslogger.h:38