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