QGIS API Documentation 3.99.0-Master (2fe06baccd8)
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 <QStyleOptionGroupBox>
29#include <QToolButton>
30
31#include "moc_qgscollapsiblegroupbox.cpp"
32
33const QString COLLAPSE_HIDE_BORDER_FIX = QStringLiteral( " QgsCollapsibleGroupBoxBasic { border: none; }" );
34
36 : QGroupBox( parent )
37{
38 init();
39}
40
41QgsCollapsibleGroupBoxBasic::QgsCollapsibleGroupBoxBasic( const QString &title, QWidget *parent )
42 : QGroupBox( title, parent )
43{
44 init();
45}
46
47void QgsCollapsibleGroupBoxBasic::init()
48{
49 // variables
50 mCollapsed = false;
51 mInitFlat = false;
52 mInitFlatChecked = false;
53 mScrollOnExpand = true;
54 mShown = false;
55 mParentScrollArea = nullptr;
56 mSyncParent = nullptr;
57 mAltDown = false;
58 mShiftDown = false;
59 mTitleClicked = false;
60
61 // init icons
62 mCollapseIcon = QgsApplication::getThemeIcon( QStringLiteral( "/mIconCollapse.svg" ) );
63 mExpandIcon = QgsApplication::getThemeIcon( QStringLiteral( "/mIconExpand.svg" ) );
64
65 // collapse button
67 mCollapseButton->setObjectName( QStringLiteral( "collapseButton" ) );
68 mCollapseButton->setAutoRaise( true );
69 mCollapseButton->setFixedSize( 16, 16 );
70 // TODO set size (as well as margins) depending on theme, in updateStyle()
71 mCollapseButton->setIconSize( QSize( 12, 12 ) );
73 // FIXME: This appears to mess up parent-child relationships and causes double-frees of children when destroying in Qt5.10, needs further investigation
74 // See also https://github.com/qgis/QGIS/pull/6301
75 setFocusPolicy( Qt::StrongFocus );
76
77 connect( mCollapseButton, &QAbstractButton::clicked, this, &QgsCollapsibleGroupBoxBasic::toggleCollapsed );
78 connect( this, &QGroupBox::toggled, this, &QgsCollapsibleGroupBoxBasic::checkToggled );
79 connect( this, &QGroupBox::clicked, this, &QgsCollapsibleGroupBoxBasic::checkClicked );
80}
81
83{
84 // initialize widget on first show event only
85 if ( mShown )
86 {
87 event->accept();
88 return;
89 }
90
91 // check if groupbox was set to flat in Designer or in code
92 if ( !mInitFlatChecked )
93 {
94 mInitFlat = isFlat();
95 mInitFlatChecked = true;
96 }
97
98 // find parent QScrollArea - this might not work in complex layouts - should we look deeper?
99 if ( parent() && parent()->parent() )
100 mParentScrollArea = qobject_cast<QScrollArea *>( parent()->parent()->parent() );
101 else
102 mParentScrollArea = nullptr;
103 if ( mParentScrollArea )
104 {
105 QgsDebugMsgLevel( "found a QScrollArea parent: " + mParentScrollArea->objectName(), 5 );
106 }
107 else
108 {
109 QgsDebugMsgLevel( QStringLiteral( "did not find a QScrollArea parent" ), 5 );
110 }
111
112 updateStyle();
113
114 // expand if needed - any calls to setCollapsed() before only set mCollapsed, but have UI effect
115 if ( mCollapsed )
116 {
118 }
119 else
120 {
121 // emit signal for connections using collapsed state
123 }
124
125 // verify triangle mirrors groupbox's enabled state
126 mCollapseButton->setEnabled( isEnabled() );
127
128 // set mShown after first setCollapsed call or expanded groupboxes
129 // will scroll scroll areas when first shown
130 mShown = true;
131 event->accept();
132}
133
135{
136 // avoid leaving checkbox in pressed state if alt- or shift-clicking
137 if ( event->modifiers() & ( Qt::AltModifier | Qt::ControlModifier | Qt::ShiftModifier )
138 && titleRect().contains( event->pos() )
139 && isCheckable() )
140 {
141 event->ignore();
142 return;
143 }
144
145 // default behavior - pass to QGroupBox
146 QGroupBox::mousePressEvent( event );
147}
148
150{
151 mAltDown = ( event->modifiers() & ( Qt::AltModifier | Qt::ControlModifier ) );
152 mShiftDown = ( event->modifiers() & Qt::ShiftModifier );
153 mTitleClicked = ( titleRect().contains( event->pos() ) );
154
155 // sync group when title is alt-clicked
156 // collapse/expand when title is clicked and non-checkable
157 // expand current and collapse others on shift-click
158 if ( event->button() == Qt::LeftButton && mTitleClicked && ( mAltDown || mShiftDown || !isCheckable() ) )
159 {
161 return;
162 }
163
164 // default behavior - pass to QGroupBox
165 QGroupBox::mouseReleaseEvent( event );
166}
167
169{
170 // always re-enable mCollapseButton when groupbox was previously disabled
171 // e.g. resulting from a disabled parent of groupbox, or a signal/slot connection
172
173 // default behavior - pass to QGroupBox
174 QGroupBox::changeEvent( event );
175
176 if ( event->type() == QEvent::EnabledChange && isEnabled() )
177 mCollapseButton->setEnabled( true );
178}
179
181{
182 mSyncGroup = grp;
183 QString tipTxt;
184 if ( !grp.isEmpty() )
185 {
186 tipTxt = tr( "Ctrl (or Alt)-click to toggle all" ) + '\n' + tr( "Shift-click to expand, then collapse others" );
187 }
188 mCollapseButton->setToolTip( tipTxt );
189}
190
192{
193 QStyleOptionGroupBox box;
194 initStyleOption( &box );
195 return style()->subControlRect( QStyle::CC_GroupBox, &box, QStyle::SC_GroupBoxLabel, this );
196}
197
199{
200 mCollapseButton->setAltDown( false );
201 mCollapseButton->setShiftDown( 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
230 mAltDown = ( mAltDown || mCollapseButton->altDown() );
231 mShiftDown = ( mShiftDown || mCollapseButton->shiftDown() );
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, QStyle::SC_GroupBoxFrame, this );
315 const QRect rectTitle = titleRect();
316
317 // margin/offset defaults
318 const int marginLeft = 20; // title margin for disclosure triangle
319 const int marginRight = 5; // a little bit of space on the right, to match space on the left
320 int offsetLeft = 0; // offset for oxygen theme
321 const int offsetStyle = QApplication::style()->objectName().contains( QLatin1String( "macintosh" ) ) ? 8 : 0;
322 const int topBuffer = 1 + offsetStyle; // space between top of title or triangle and widget above
323 int offsetTop = topBuffer;
324 int offsetTopTri = topBuffer; // offset for triangle
325
326 if ( mCollapseButton->height() < rectTitle.height() ) // triangle's height > title text's, offset triangle
327 {
328 offsetTopTri += ( rectTitle.height() - mCollapseButton->height() ) / 2;
329 // offsetTopTri += rectTitle.top();
330 }
331 else if ( rectTitle.height() < mCollapseButton->height() ) // title text's height < triangle's, offset title
332 {
333 offsetTop += ( mCollapseButton->height() - rectTitle.height() ) / 2;
334 }
335
336 // calculate offset if frame overlaps triangle (oxygen theme)
337 // using an offset of 6 pixels from frame border
338 if ( QApplication::style()->objectName().compare( QLatin1String( "oxygen" ), Qt::CaseInsensitive ) == 0 )
339 {
340 QStyleOptionGroupBox box;
341 initStyleOption( &box );
342 const QRect rectFrame = style()->subControlRect( QStyle::CC_GroupBox, &box, QStyle::SC_GroupBoxFrame, this );
343 const QRect rectCheckBox = style()->subControlRect( QStyle::CC_GroupBox, &box, QStyle::SC_GroupBoxCheckBox, this );
344 if ( rectFrame.left() <= 0 )
345 offsetLeft = 6 + rectFrame.left();
346 if ( rectFrame.top() <= 0 )
347 {
348 if ( isCheckable() )
349 {
350 // if is checkable align with checkbox
351 offsetTop = ( rectCheckBox.height() / 2 ) - ( mCollapseButton->height() / 2 ) + rectCheckBox.top();
352 offsetTopTri = offsetTop + 1;
353 }
354 else
355 {
356 offsetTop = 6 + rectFrame.top();
357 offsetTopTri = offsetTop;
358 }
359 }
360 }
361
362 QgsDebugMsgLevel( QStringLiteral( "groupbox: %1 style: %2 offset: left=%3 top=%4 top2=%5" ).arg( objectName(), QApplication::style()->objectName() ).arg( offsetLeft ).arg( offsetTop ).arg( offsetTopTri ), 5 );
363
364 // customize style sheet for collapse/expand button and force left-aligned title
365 QString ss;
366 if ( QApplication::style()->objectName().contains( QLatin1String( "macintosh" ) ) )
367 {
368 ss += QLatin1String( "QgsCollapsibleGroupBoxBasic, QgsCollapsibleGroupBox {" );
369 ss += QStringLiteral( " margin-top: %1px;" ).arg( topBuffer + rectFrame.top() );
370 ss += '}';
371 }
372 ss += QLatin1String( "QgsCollapsibleGroupBoxBasic::title, QgsCollapsibleGroupBox::title {" );
373 ss += QLatin1String( " subcontrol-origin: margin;" );
374 ss += QLatin1String( " subcontrol-position: top left;" );
375 ss += QStringLiteral( " margin-left: %1px;" ).arg( marginLeft );
376 ss += QStringLiteral( " margin-right: %1px;" ).arg( marginRight );
377 ss += QStringLiteral( " left: %1px;" ).arg( offsetLeft );
378 ss += QStringLiteral( " top: %1px;" ).arg( offsetTop );
379 if ( QApplication::style()->objectName().contains( QLatin1String( "macintosh" ) ) )
380 {
381 ss += QLatin1String( " background-color: rgba(0,0,0,0)" );
382 }
383 ss += '}';
384 setStyleSheet( styleSheet() + ss );
385
386 // clear toolbutton default background and border and apply offset
387 QString ssd;
388 ssd = QStringLiteral( "QgsCollapsibleGroupBoxBasic > QToolButton#%1, QgsCollapsibleGroupBox > QToolButton#%1 {" ).arg( mCollapseButton->objectName() );
389 ssd += QLatin1String( " background-color: rgba(255, 255, 255, 0); border: none;" );
390 ssd += QStringLiteral( "} QgsCollapsibleGroupBoxBasic > QToolButton#%1:focus, QgsCollapsibleGroupBox > QToolButton#%1:focus { border: 1px solid palette(highlight); }" ).arg( mCollapseButton->objectName() );
391 mCollapseButton->setStyleSheet( ssd );
392 if ( offsetLeft != 0 || offsetTopTri != 0 )
393 mCollapseButton->move( offsetLeft, offsetTopTri );
394 setUpdatesEnabled( true );
395}
396
398{
399 const bool changed = collapse != mCollapsed;
400 mCollapsed = collapse;
401
402 if ( !isVisible() )
403 return;
404
405 // for consistent look/spacing across platforms when collapsed
406 if ( !mInitFlat ) // skip if initially set to flat in Designer
407 setFlat( collapse );
408
409 // avoid flicker in X11
410 // NOTE: this causes app to crash when loading a project that hits a group box with
411 // 'collapse' set via dynamic property or in code (especially if auto-launching project)
412 // TODO: find another means of avoiding the X11 flicker
413 // QApplication::processEvents();
414
415 // handle visual fixes for collapsing/expanding
417
418 // set maximum height to hide contents - does this work in all envs?
419 // setMaximumHeight( collapse ? 25 : 16777215 );
420 setMaximumHeight( collapse ? titleRect().bottom() + 6 : 16777215 );
421 mCollapseButton->setIcon( collapse ? mExpandIcon : mCollapseIcon );
422
423 // if expanding and is in a QScrollArea, scroll down to make entire widget visible
424 if ( mShown && mScrollOnExpand && !collapse && mParentScrollArea )
425 {
426 // process events so entire widget is shown
427 QApplication::processEvents();
428 mParentScrollArea->setUpdatesEnabled( false );
429 mParentScrollArea->ensureWidgetVisible( this );
430 //and then make sure the top of the widget is visible - otherwise tall group boxes
431 //scroll to their centres, which is disorienting for users
432 mParentScrollArea->ensureWidgetVisible( mCollapseButton, 0, 5 );
433 mParentScrollArea->setUpdatesEnabled( true );
434 }
435 // emit signal for connections using collapsed state
436 if ( changed )
438}
439
441{
442 // handle child widgets so they don't paint while hidden
443 const char *hideKey = "CollGrpBxHide";
444
445 QString ss = styleSheet();
446 if ( mCollapsed )
447 {
448 if ( !ss.contains( COLLAPSE_HIDE_BORDER_FIX ) )
449 {
451 setStyleSheet( ss );
452 }
453
454 const auto constChildren = children();
455 for ( QObject *child : constChildren )
456 {
457 QWidget *w = qobject_cast<QWidget *>( child );
458 // ignore already hidden widgets, so they won't become visible on expand
459 // see https://github.com/qgis/QGIS/issues/55443
460 if ( w && w != mCollapseButton && !w->isHidden() )
461 {
462 w->setProperty( hideKey, true );
463 w->hide();
464 }
465 }
466 }
467 else // on expand
468 {
469 if ( ss.contains( COLLAPSE_HIDE_BORDER_FIX ) )
470 {
471 ss.replace( COLLAPSE_HIDE_BORDER_FIX, QString() );
472 setStyleSheet( ss );
473 }
474
475 const auto constChildren = children();
476 for ( QObject *child : constChildren )
477 {
478 QWidget *w = qobject_cast<QWidget *>( child );
479 if ( w && w != mCollapseButton )
480 {
481 if ( w->property( hideKey ).toBool() )
482 w->show();
483 }
484 }
485 }
486}
487
488
489// ----
490
493 , mSettings( settings )
494{
495 init();
496}
497
498QgsCollapsibleGroupBox::QgsCollapsibleGroupBox( const QString &title, QWidget *parent, QgsSettings *settings )
499 : QgsCollapsibleGroupBoxBasic( title, parent )
500 , mSettings( settings )
501{
502 init();
503}
504
506{
507 saveState();
508 if ( mDelSettings ) // local settings obj to delete
509 delete mSettings;
510 mSettings = nullptr; // null the pointer (in case of outside settings obj)
511}
512
514{
515 if ( mDelSettings ) // local settings obj to delete
516 delete mSettings;
517 mSettings = settings;
518 mDelSettings = false; // don't delete outside obj
519}
520
521void QgsCollapsibleGroupBox::init()
522{
523 // use pointer to app qsettings if no custom qsettings specified
524 // custom qsettings object may be from Python plugin
525 mDelSettings = false;
526 if ( !mSettings )
527 {
528 mSettings = new QgsSettings();
529 mDelSettings = true; // only delete obj created by class
530 }
531 // variables
532 mSaveCollapsedState = true;
533 // NOTE: only turn on mSaveCheckedState for groupboxes NOT used
534 // in multiple places or used as options for different parent objects
535 mSaveCheckedState = false;
536
537 connect( this, &QObject::objectNameChanged, this, &QgsCollapsibleGroupBox::loadState );
538
539 // save state immediately when collapsed state changes, so that other widgets created
540 // before this one is destroyed will correctly restore the new collapsed state
542}
543
544void QgsCollapsibleGroupBox::showEvent( QShowEvent *event )
545{
546 // initialize widget on first show event only
547 if ( mShown )
548 {
549 event->accept();
550 return;
551 }
552
553 // check if groupbox was set to flat in Designer or in code
554 if ( !mInitFlatChecked )
555 {
556 mInitFlat = isFlat();
557 mInitFlatChecked = true;
558 }
559
560 loadState();
561
563}
564
566{
567 if ( objectName().isEmpty() || ( mSettingGroup.isEmpty() && window()->objectName().isEmpty() ) )
568 return QString(); // cannot get a valid key
569
570 // save key for load/save state
571 // currently QgsCollapsibleGroupBox/window()/object
572 QString saveKey = '/' + objectName();
573 // QObject* parentWidget = parent();
574 // while ( parentWidget )
575 // {
576 // saveKey = "/" + parentWidget->objectName() + saveKey;
577 // parentWidget = parentWidget->parent();
578 // }
579 // if ( parent() )
580 // saveKey = "/" + parent()->objectName() + saveKey;
581 const QString setgrp = mSettingGroup.isEmpty() ? window()->objectName() : mSettingGroup;
582 saveKey = '/' + setgrp + saveKey;
583 saveKey = QStringLiteral( "QgsCollapsibleGroupBox" ) + saveKey;
584 return saveKey;
585}
586
588{
589 if ( !mSettings )
590 return;
591
592 if ( !isEnabled() || ( !mSaveCollapsedState && !mSaveCheckedState ) )
593 return;
594
595 const QString key = saveKey();
596 if ( key.isEmpty() )
597 return;
598
599 setUpdatesEnabled( false );
600
601 if ( mSaveCheckedState )
602 {
603 const QVariant val = mSettings->value( key + "/checked" );
604 if ( !QgsVariantUtils::isNull( val ) )
605 setChecked( val.toBool() );
606 }
607 if ( mSaveCollapsedState )
608 {
609 const QVariant val = mSettings->value( key + "/collapsed" );
610 if ( !QgsVariantUtils::isNull( val ) )
611 setCollapsed( val.toBool() );
612 }
613
614 setUpdatesEnabled( true );
615}
616
618{
619 if ( !mSettings )
620 return;
621
622 if ( !mShown || !isEnabled() || ( !mSaveCollapsedState && !mSaveCheckedState ) )
623 return;
624
625 const QString key = saveKey();
626 if ( key.isEmpty() )
627 return;
628
629 if ( mSaveCheckedState )
630 mSettings->setValue( key + QStringLiteral( "/checked" ), isChecked() );
631 if ( mSaveCollapsedState )
632 mSettings->setValue( key + QStringLiteral( "/collapsed" ), isCollapsed() );
633}
634
635
637{
638 mAltDown = ( event->modifiers() & ( Qt::AltModifier | Qt::ControlModifier ) );
639 mShiftDown = ( event->modifiers() & Qt::ShiftModifier );
640 QToolButton::mouseReleaseEvent( event );
641}
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:65
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:61