QGIS API Documentation 3.41.0-Master (cea29feecf2)
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#include "moc_qgscollapsiblegroupbox.cpp"
20#include "qgsapplication.h"
21#include "qgslogger.h"
22#include "qgssettings.h"
23#include "qgsvariantutils.h"
24
25#include <QToolButton>
26#include <QMouseEvent>
27#include <QPushButton>
28#include <QStyleOptionGroupBox>
29#include <QScrollArea>
30
31const QString COLLAPSE_HIDE_BORDER_FIX = QStringLiteral( " QgsCollapsibleGroupBoxBasic { border: none; }" );
32
34 : QGroupBox( parent )
35{
36 init();
37}
38
39QgsCollapsibleGroupBoxBasic::QgsCollapsibleGroupBoxBasic( const QString &title, QWidget *parent )
40 : QGroupBox( title, parent )
41{
42 init();
43}
44
45void QgsCollapsibleGroupBoxBasic::init()
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 && ( mAltDown || mShiftDown || !isCheckable() ) )
157 {
159 return;
160 }
161
162 // default behavior - pass to QGroupBox
163 QGroupBox::mouseReleaseEvent( event );
164}
165
167{
168 // always re-enable mCollapseButton when groupbox was previously disabled
169 // e.g. resulting from a disabled parent of groupbox, or a signal/slot connection
170
171 // default behavior - pass to QGroupBox
172 QGroupBox::changeEvent( event );
173
174 if ( event->type() == QEvent::EnabledChange && isEnabled() )
175 mCollapseButton->setEnabled( true );
176}
177
179{
180 mSyncGroup = grp;
181 QString tipTxt;
182 if ( !grp.isEmpty() )
183 {
184 tipTxt = tr( "Ctrl (or Alt)-click to toggle all" ) + '\n' + tr( "Shift-click to expand, then collapse others" );
185 }
186 mCollapseButton->setToolTip( tipTxt );
187}
188
190{
191 QStyleOptionGroupBox box;
192 initStyleOption( &box );
193 return style()->subControlRect( QStyle::CC_GroupBox, &box, QStyle::SC_GroupBoxLabel, this );
194}
195
197{
198 mCollapseButton->setAltDown( false );
200 mAltDown = false;
201 mShiftDown = false;
202}
203
205{
206 Q_UNUSED( chkd )
207 mCollapseButton->setEnabled( true ); // always keep enabled
208}
209
211{
212 // expand/collapse when checkbox toggled by user click.
213 // don't do this on toggle signal, otherwise group boxes will default to collapsed
214 // in option dialog constructors, reducing discovery of options by new users and
215 // overriding user's auto-saved collapsed/expanded state for the group box
216 if ( chkd && isCollapsed() )
217 setCollapsed( false );
218 else if ( !chkd && !isCollapsed() )
219 setCollapsed( true );
220}
221
223{
224 // verify if sender is this group box's collapse button
225 QgsGroupBoxCollapseButton *collBtn = qobject_cast<QgsGroupBoxCollapseButton *>( QObject::sender() );
226 const bool senderCollBtn = ( collBtn && collBtn == mCollapseButton );
227
230
231 // find any sync group siblings and toggle them
232 if ( ( senderCollBtn || mTitleClicked )
233 && ( mAltDown || mShiftDown )
234 && !mSyncGroup.isEmpty() )
235 {
236 QgsDebugMsgLevel( QStringLiteral( "Alt or Shift key down, syncing group" ), 2 );
237 // get pointer to parent or grandparent widget
238 if ( auto *lParentWidget = parentWidget() )
239 {
240 mSyncParent = lParentWidget;
241 if ( mSyncParent->parentWidget() )
242 {
243 // don't use whole app for grandparent (common for dialogs that use main window for parent)
244 if ( mSyncParent->parentWidget()->objectName() != QLatin1String( "QgisApp" ) )
245 {
246 mSyncParent = mSyncParent->parentWidget();
247 }
248 }
249 }
250 else
251 {
252 mSyncParent = nullptr;
253 }
254
255 if ( mSyncParent )
256 {
257 QgsDebugMsgLevel( "found sync parent: " + mSyncParent->objectName(), 2 );
258
259 const bool thisCollapsed = mCollapsed; // get state of current box before its changed
260 const auto groupBoxes { mSyncParent->findChildren<QgsCollapsibleGroupBoxBasic *>() };
261 for ( QgsCollapsibleGroupBoxBasic *grpbox : groupBoxes )
262 {
263 if ( grpbox->syncGroup() == syncGroup() && grpbox->isEnabled() )
264 {
265 if ( mShiftDown && grpbox == this )
266 {
267 // expand current group box on shift-click
268 setCollapsed( false );
269 }
270 else
271 {
272 grpbox->setCollapsed( mShiftDown ? true : !thisCollapsed );
273 }
274 }
275 }
276
278 return;
279 }
280 else
281 {
282 QgsDebugMsgLevel( QStringLiteral( "did not find a sync parent" ), 2 );
283 }
284 }
285
286 // expand current group box on shift-click, even if no sync group
287 if ( mShiftDown )
288 {
289 setCollapsed( false );
290 }
291 else
292 {
294 }
295
297}
298
300{
301 QGroupBox::setStyleSheet( style );
302}
303
305{
306 setUpdatesEnabled( false );
307
308 const QgsSettings settings;
309
310 QStyleOptionGroupBox box;
311 initStyleOption( &box );
312 const QRect rectFrame = style()->subControlRect( QStyle::CC_GroupBox, &box, QStyle::SC_GroupBoxFrame, this );
313 const QRect rectTitle = titleRect();
314
315 // margin/offset defaults
316 const int marginLeft = 20; // title margin for disclosure triangle
317 const int marginRight = 5; // a little bit of space on the right, to match space on the left
318 int offsetLeft = 0; // offset for oxygen theme
319 const int offsetStyle = QApplication::style()->objectName().contains( QLatin1String( "macintosh" ) ) ? 8 : 0;
320 const int topBuffer = 1 + offsetStyle; // space between top of title or triangle and widget above
321 int offsetTop = topBuffer;
322 int offsetTopTri = topBuffer; // offset for triangle
323
324 if ( mCollapseButton->height() < rectTitle.height() ) // triangle's height > title text's, offset triangle
325 {
326 offsetTopTri += ( rectTitle.height() - mCollapseButton->height() ) / 2;
327 // offsetTopTri += rectTitle.top();
328 }
329 else if ( rectTitle.height() < mCollapseButton->height() ) // title text's height < triangle's, offset title
330 {
331 offsetTop += ( mCollapseButton->height() - rectTitle.height() ) / 2;
332 }
333
334 // calculate offset if frame overlaps triangle (oxygen theme)
335 // using an offset of 6 pixels from frame border
336 if ( QApplication::style()->objectName().compare( QLatin1String( "oxygen" ), Qt::CaseInsensitive ) == 0 )
337 {
338 QStyleOptionGroupBox box;
339 initStyleOption( &box );
340 const QRect rectFrame = style()->subControlRect( QStyle::CC_GroupBox, &box, QStyle::SC_GroupBoxFrame, this );
341 const QRect rectCheckBox = style()->subControlRect( QStyle::CC_GroupBox, &box, QStyle::SC_GroupBoxCheckBox, this );
342 if ( rectFrame.left() <= 0 )
343 offsetLeft = 6 + rectFrame.left();
344 if ( rectFrame.top() <= 0 )
345 {
346 if ( isCheckable() )
347 {
348 // if is checkable align with checkbox
349 offsetTop = ( rectCheckBox.height() / 2 ) - ( mCollapseButton->height() / 2 ) + rectCheckBox.top();
350 offsetTopTri = offsetTop + 1;
351 }
352 else
353 {
354 offsetTop = 6 + rectFrame.top();
355 offsetTopTri = offsetTop;
356 }
357 }
358 }
359
360 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 );
361
362 // customize style sheet for collapse/expand button and force left-aligned title
363 QString ss;
364 if ( QApplication::style()->objectName().contains( QLatin1String( "macintosh" ) ) )
365 {
366 ss += QLatin1String( "QgsCollapsibleGroupBoxBasic, QgsCollapsibleGroupBox {" );
367 ss += QStringLiteral( " margin-top: %1px;" ).arg( topBuffer + rectFrame.top() );
368 ss += '}';
369 }
370 ss += QLatin1String( "QgsCollapsibleGroupBoxBasic::title, QgsCollapsibleGroupBox::title {" );
371 ss += QLatin1String( " subcontrol-origin: margin;" );
372 ss += QLatin1String( " subcontrol-position: top left;" );
373 ss += QStringLiteral( " margin-left: %1px;" ).arg( marginLeft );
374 ss += QStringLiteral( " margin-right: %1px;" ).arg( marginRight );
375 ss += QStringLiteral( " left: %1px;" ).arg( offsetLeft );
376 ss += QStringLiteral( " top: %1px;" ).arg( offsetTop );
377 if ( QApplication::style()->objectName().contains( QLatin1String( "macintosh" ) ) )
378 {
379 ss += QLatin1String( " background-color: rgba(0,0,0,0)" );
380 }
381 ss += '}';
382 setStyleSheet( styleSheet() + ss );
383
384 // clear toolbutton default background and border and apply offset
385 QString ssd;
386 ssd = QStringLiteral( "QgsCollapsibleGroupBoxBasic > QToolButton#%1, QgsCollapsibleGroupBox > QToolButton#%1 {" ).arg( mCollapseButton->objectName() );
387 ssd += QLatin1String( " background-color: rgba(255, 255, 255, 0); border: none;" );
388 ssd += QStringLiteral( "} QgsCollapsibleGroupBoxBasic > QToolButton#%1:focus, QgsCollapsibleGroupBox > QToolButton#%1:focus { border: 1px solid palette(highlight); }" ).arg( mCollapseButton->objectName() );
389 mCollapseButton->setStyleSheet( ssd );
390 if ( offsetLeft != 0 || offsetTopTri != 0 )
391 mCollapseButton->move( offsetLeft, offsetTopTri );
392 setUpdatesEnabled( true );
393}
394
396{
397 const bool changed = collapse != mCollapsed;
398 mCollapsed = collapse;
399
400 if ( !isVisible() )
401 return;
402
403 // for consistent look/spacing across platforms when collapsed
404 if ( !mInitFlat ) // skip if initially set to flat in Designer
405 setFlat( collapse );
406
407 // avoid flicker in X11
408 // NOTE: this causes app to crash when loading a project that hits a group box with
409 // 'collapse' set via dynamic property or in code (especially if auto-launching project)
410 // TODO: find another means of avoiding the X11 flicker
411 // QApplication::processEvents();
412
413 // handle visual fixes for collapsing/expanding
415
416 // set maximum height to hide contents - does this work in all envs?
417 // setMaximumHeight( collapse ? 25 : 16777215 );
418 setMaximumHeight( collapse ? titleRect().bottom() + 6 : 16777215 );
419 mCollapseButton->setIcon( collapse ? mExpandIcon : mCollapseIcon );
420
421 // if expanding and is in a QScrollArea, scroll down to make entire widget visible
422 if ( mShown && mScrollOnExpand && !collapse && mParentScrollArea )
423 {
424 // process events so entire widget is shown
425 QApplication::processEvents();
426 mParentScrollArea->setUpdatesEnabled( false );
427 mParentScrollArea->ensureWidgetVisible( this );
428 //and then make sure the top of the widget is visible - otherwise tall group boxes
429 //scroll to their centres, which is disorienting for users
430 mParentScrollArea->ensureWidgetVisible( mCollapseButton, 0, 5 );
431 mParentScrollArea->setUpdatesEnabled( true );
432 }
433 // emit signal for connections using collapsed state
434 if ( changed )
436}
437
439{
440 // handle child widgets so they don't paint while hidden
441 const char *hideKey = "CollGrpBxHide";
442
443 QString ss = styleSheet();
444 if ( mCollapsed )
445 {
446 if ( !ss.contains( COLLAPSE_HIDE_BORDER_FIX ) )
447 {
449 setStyleSheet( ss );
450 }
451
452 const auto constChildren = children();
453 for ( QObject *child : constChildren )
454 {
455 QWidget *w = qobject_cast<QWidget *>( child );
456 if ( w && w != mCollapseButton )
457 {
458 w->setProperty( hideKey, true );
459 w->hide();
460 }
461 }
462 }
463 else // on expand
464 {
465 if ( ss.contains( COLLAPSE_HIDE_BORDER_FIX ) )
466 {
467 ss.replace( COLLAPSE_HIDE_BORDER_FIX, QString() );
468 setStyleSheet( ss );
469 }
470
471 const auto constChildren = children();
472 for ( QObject *child : constChildren )
473 {
474 QWidget *w = qobject_cast<QWidget *>( child );
475 if ( w && w != mCollapseButton )
476 {
477 if ( w->property( hideKey ).toBool() )
478 w->show();
479 }
480 }
481 }
482}
483
484
485// ----
486
489 , mSettings( settings )
490{
491 init();
492}
493
494QgsCollapsibleGroupBox::QgsCollapsibleGroupBox( const QString &title, QWidget *parent, QgsSettings *settings )
495 : QgsCollapsibleGroupBoxBasic( title, parent )
496 , mSettings( settings )
497{
498 init();
499}
500
502{
503 saveState();
504 if ( mDelSettings ) // local settings obj to delete
505 delete mSettings;
506 mSettings = nullptr; // null the pointer (in case of outside settings obj)
507}
508
510{
511 if ( mDelSettings ) // local settings obj to delete
512 delete mSettings;
513 mSettings = settings;
514 mDelSettings = false; // don't delete outside obj
515}
516
517void QgsCollapsibleGroupBox::init()
518{
519 // use pointer to app qsettings if no custom qsettings specified
520 // custom qsettings object may be from Python plugin
521 mDelSettings = false;
522 if ( !mSettings )
523 {
524 mSettings = new QgsSettings();
525 mDelSettings = true; // only delete obj created by class
526 }
527 // variables
528 mSaveCollapsedState = true;
529 // NOTE: only turn on mSaveCheckedState for groupboxes NOT used
530 // in multiple places or used as options for different parent objects
531 mSaveCheckedState = false;
532
533 connect( this, &QObject::objectNameChanged, this, &QgsCollapsibleGroupBox::loadState );
534
535 // save state immediately when collapsed state changes, so that other widgets created
536 // before this one is destroyed will correctly restore the new collapsed state
538}
539
540void QgsCollapsibleGroupBox::showEvent( QShowEvent *event )
541{
542 // initialize widget on first show event only
543 if ( mShown )
544 {
545 event->accept();
546 return;
547 }
548
549 // check if groupbox was set to flat in Designer or in code
550 if ( !mInitFlatChecked )
551 {
552 mInitFlat = isFlat();
553 mInitFlatChecked = true;
554 }
555
556 loadState();
557
559}
560
562{
563 if ( objectName().isEmpty() || ( mSettingGroup.isEmpty() && window()->objectName().isEmpty() ) )
564 return QString(); // cannot get a valid key
565
566 // save key for load/save state
567 // currently QgsCollapsibleGroupBox/window()/object
568 QString saveKey = '/' + objectName();
569 // QObject* parentWidget = parent();
570 // while ( parentWidget )
571 // {
572 // saveKey = "/" + parentWidget->objectName() + saveKey;
573 // parentWidget = parentWidget->parent();
574 // }
575 // if ( parent() )
576 // saveKey = "/" + parent()->objectName() + saveKey;
577 const QString setgrp = mSettingGroup.isEmpty() ? window()->objectName() : mSettingGroup;
578 saveKey = '/' + setgrp + saveKey;
579 saveKey = QStringLiteral( "QgsCollapsibleGroupBox" ) + saveKey;
580 return saveKey;
581}
582
584{
585 if ( !mSettings )
586 return;
587
588 if ( !isEnabled() || ( !mSaveCollapsedState && !mSaveCheckedState ) )
589 return;
590
591 const QString key = saveKey();
592 if ( key.isEmpty() )
593 return;
594
595 setUpdatesEnabled( false );
596
597 if ( mSaveCheckedState )
598 {
599 const QVariant val = mSettings->value( key + "/checked" );
600 if ( !QgsVariantUtils::isNull( val ) )
601 setChecked( val.toBool() );
602 }
603 if ( mSaveCollapsedState )
604 {
605 const QVariant val = mSettings->value( key + "/collapsed" );
606 if ( !QgsVariantUtils::isNull( val ) )
607 setCollapsed( val.toBool() );
608 }
609
610 setUpdatesEnabled( true );
611}
612
614{
615 if ( !mSettings )
616 return;
617
618 if ( !mShown || !isEnabled() || ( !mSaveCollapsedState && !mSaveCheckedState ) )
619 return;
620
621 const QString key = saveKey();
622 if ( key.isEmpty() )
623 return;
624
625 if ( mSaveCheckedState )
626 mSettings->setValue( key + QStringLiteral( "/checked" ), isChecked() );
627 if ( mSaveCollapsedState )
628 mSettings->setValue( key + QStringLiteral( "/collapsed" ), isCollapsed() );
629}
630
631
633{
634 mAltDown = ( event->modifiers() & ( Qt::AltModifier | Qt::ControlModifier ) );
635 mShiftDown = ( event->modifiers() & Qt::ShiftModifier );
636 QToolButton::mouseReleaseEvent( event );
637}
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.
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