QGIS API Documentation 3.37.0-Master (fdefdf9c27f)
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 QgsDebugMsgLevel( QStringLiteral( "Alt or Shift key down, syncing group" ), 2 );
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 QgsDebugMsgLevel( "found sync parent: " + mSyncParent->objectName(), 2 );
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 QgsDebugMsgLevel( QStringLiteral( "did not find a sync parent" ), 2 );
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
526{
527 // use pointer to app qsettings if no custom qsettings specified
528 // custom qsettings object may be from Python plugin
529 mDelSettings = false;
530 if ( !mSettings )
531 {
532 mSettings = new QgsSettings();
533 mDelSettings = true; // only delete obj created by class
534 }
535 // variables
536 mSaveCollapsedState = true;
537 // NOTE: only turn on mSaveCheckedState for groupboxes NOT used
538 // in multiple places or used as options for different parent objects
539 mSaveCheckedState = false;
540
541 connect( this, &QObject::objectNameChanged, this, &QgsCollapsibleGroupBox::loadState );
542
543 // save state immediately when collapsed state changes, so that other widgets created
544 // before this one is destroyed will correctly restore the new collapsed state
546}
547
548void QgsCollapsibleGroupBox::showEvent( QShowEvent *event )
549{
550 // initialize widget on first show event only
551 if ( mShown )
552 {
553 event->accept();
554 return;
555 }
556
557 // check if groupbox was set to flat in Designer or in code
558 if ( !mInitFlatChecked )
559 {
560 mInitFlat = isFlat();
561 mInitFlatChecked = true;
562 }
563
564 loadState();
565
567}
568
570{
571 if ( objectName().isEmpty() || ( mSettingGroup.isEmpty() && window()->objectName().isEmpty() ) )
572 return QString(); // cannot get a valid key
573
574 // save key for load/save state
575 // currently QgsCollapsibleGroupBox/window()/object
576 QString saveKey = '/' + objectName();
577 // QObject* parentWidget = parent();
578 // while ( parentWidget )
579 // {
580 // saveKey = "/" + parentWidget->objectName() + saveKey;
581 // parentWidget = parentWidget->parent();
582 // }
583 // if ( parent() )
584 // saveKey = "/" + parent()->objectName() + saveKey;
585 const QString setgrp = mSettingGroup.isEmpty() ? window()->objectName() : mSettingGroup;
586 saveKey = '/' + setgrp + saveKey;
587 saveKey = QStringLiteral( "QgsCollapsibleGroupBox" ) + saveKey;
588 return saveKey;
589}
590
592{
593 if ( !mSettings )
594 return;
595
596 if ( !isEnabled() || ( !mSaveCollapsedState && !mSaveCheckedState ) )
597 return;
598
599 const QString key = saveKey();
600 if ( key.isEmpty() )
601 return;
602
603 setUpdatesEnabled( false );
604
605 if ( mSaveCheckedState )
606 {
607 const QVariant val = mSettings->value( key + "/checked" );
608 if ( ! QgsVariantUtils::isNull( val ) )
609 setChecked( val.toBool() );
610 }
612 {
613 const QVariant val = mSettings->value( key + "/collapsed" );
614 if ( ! QgsVariantUtils::isNull( val ) )
615 setCollapsed( val.toBool() );
616 }
617
618 setUpdatesEnabled( true );
619}
620
622{
623 if ( !mSettings )
624 return;
625
626 if ( !mShown || !isEnabled() || ( !mSaveCollapsedState && !mSaveCheckedState ) )
627 return;
628
629 const QString key = saveKey();
630 if ( key.isEmpty() )
631 return;
632
633 if ( mSaveCheckedState )
634 mSettings->setValue( key + QStringLiteral( "/checked" ), isChecked() );
636 mSettings->setValue( key + QStringLiteral( "/collapsed" ), isCollapsed() );
637}
638
639
641{
642 mAltDown = ( event->modifiers() & ( Qt::AltModifier | Qt::ControlModifier ) );
643 mShiftDown = ( event->modifiers() & Qt::ShiftModifier );
644 QToolButton::mouseReleaseEvent( event );
645}
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:64
static bool isNull(const QVariant &variant, bool silenceNullWarnings=false)
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