QGIS API Documentation 3.41.0-Master (3440c17df1d)
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
40 QWidget *parent )
41 : QGroupBox( title, parent )
42{
43 init();
44}
45
46void QgsCollapsibleGroupBoxBasic::init()
47{
48 // variables
49 mCollapsed = false;
50 mInitFlat = false;
51 mInitFlatChecked = false;
52 mScrollOnExpand = true;
53 mShown = false;
54 mParentScrollArea = nullptr;
55 mSyncParent = nullptr;
56 mAltDown = false;
57 mShiftDown = false;
58 mTitleClicked = false;
59
60 // init icons
61 mCollapseIcon = QgsApplication::getThemeIcon( QStringLiteral( "/mIconCollapse.svg" ) );
62 mExpandIcon = QgsApplication::getThemeIcon( QStringLiteral( "/mIconExpand.svg" ) );
63
64 // collapse button
66 mCollapseButton->setObjectName( QStringLiteral( "collapseButton" ) );
67 mCollapseButton->setAutoRaise( true );
68 mCollapseButton->setFixedSize( 16, 16 );
69 // TODO set size (as well as margins) depending on theme, in updateStyle()
70 mCollapseButton->setIconSize( QSize( 12, 12 ) );
72 // FIXME: This appears to mess up parent-child relationships and causes double-frees of children when destroying in Qt5.10, needs further investigation
73 // See also https://github.com/qgis/QGIS/pull/6301
74 setFocusPolicy( Qt::StrongFocus );
75
76 connect( mCollapseButton, &QAbstractButton::clicked, this, &QgsCollapsibleGroupBoxBasic::toggleCollapsed );
77 connect( this, &QGroupBox::toggled, this, &QgsCollapsibleGroupBoxBasic::checkToggled );
78 connect( this, &QGroupBox::clicked, this, &QgsCollapsibleGroupBoxBasic::checkClicked );
79}
80
82{
83 // initialize widget on first show event only
84 if ( mShown )
85 {
86 event->accept();
87 return;
88 }
89
90 // check if groupbox was set to flat in Designer or in code
91 if ( !mInitFlatChecked )
92 {
93 mInitFlat = isFlat();
94 mInitFlatChecked = true;
95 }
96
97 // find parent QScrollArea - this might not work in complex layouts - should we look deeper?
98 if ( parent() && parent()->parent() )
99 mParentScrollArea = qobject_cast<QScrollArea *>( parent()->parent()->parent() );
100 else
101 mParentScrollArea = nullptr;
102 if ( mParentScrollArea )
103 {
104 QgsDebugMsgLevel( "found a QScrollArea parent: " + mParentScrollArea->objectName(), 5 );
105 }
106 else
107 {
108 QgsDebugMsgLevel( QStringLiteral( "did not find a QScrollArea parent" ), 5 );
109 }
110
111 updateStyle();
112
113 // expand if needed - any calls to setCollapsed() before only set mCollapsed, but have UI effect
114 if ( mCollapsed )
115 {
117 }
118 else
119 {
120 // emit signal for connections using collapsed state
122 }
123
124 // verify triangle mirrors groupbox's enabled state
125 mCollapseButton->setEnabled( isEnabled() );
126
127 // set mShown after first setCollapsed call or expanded groupboxes
128 // will scroll scroll areas when first shown
129 mShown = true;
130 event->accept();
131}
132
134{
135 // avoid leaving checkbox in pressed state if alt- or shift-clicking
136 if ( event->modifiers() & ( Qt::AltModifier | Qt::ControlModifier | Qt::ShiftModifier )
137 && titleRect().contains( event->pos() )
138 && isCheckable() )
139 {
140 event->ignore();
141 return;
142 }
143
144 // default behavior - pass to QGroupBox
145 QGroupBox::mousePressEvent( event );
146}
147
149{
150 mAltDown = ( event->modifiers() & ( Qt::AltModifier | Qt::ControlModifier ) );
151 mShiftDown = ( event->modifiers() & Qt::ShiftModifier );
152 mTitleClicked = ( titleRect().contains( event->pos() ) );
153
154 // sync group when title is alt-clicked
155 // collapse/expand when title is clicked and non-checkable
156 // expand current and collapse others on shift-click
157 if ( event->button() == Qt::LeftButton && mTitleClicked &&
158 ( 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,
196 QStyle::SC_GroupBoxLabel, this );
197}
198
200{
201 mCollapseButton->setAltDown( false );
203 mAltDown = false;
204 mShiftDown = false;
205}
206
208{
209 Q_UNUSED( chkd )
210 mCollapseButton->setEnabled( true ); // always keep enabled
211}
212
214{
215 // expand/collapse when checkbox toggled by user click.
216 // don't do this on toggle signal, otherwise group boxes will default to collapsed
217 // in option dialog constructors, reducing discovery of options by new users and
218 // overriding user's auto-saved collapsed/expanded state for the group box
219 if ( chkd && isCollapsed() )
220 setCollapsed( false );
221 else if ( ! chkd && ! isCollapsed() )
222 setCollapsed( true );
223}
224
226{
227 // verify if sender is this group box's collapse button
228 QgsGroupBoxCollapseButton *collBtn = qobject_cast<QgsGroupBoxCollapseButton *>( QObject::sender() );
229 const bool senderCollBtn = ( collBtn && collBtn == mCollapseButton );
230
233
234 // find any sync group siblings and toggle them
235 if ( ( senderCollBtn || mTitleClicked )
236 && ( mAltDown || mShiftDown )
237 && !mSyncGroup.isEmpty() )
238 {
239 QgsDebugMsgLevel( QStringLiteral( "Alt or Shift key down, syncing group" ), 2 );
240 // get pointer to parent or grandparent widget
241 if ( auto *lParentWidget = parentWidget() )
242 {
243 mSyncParent = lParentWidget;
244 if ( mSyncParent->parentWidget() )
245 {
246 // don't use whole app for grandparent (common for dialogs that use main window for parent)
247 if ( mSyncParent->parentWidget()->objectName() != QLatin1String( "QgisApp" ) )
248 {
249 mSyncParent = mSyncParent->parentWidget();
250 }
251 }
252 }
253 else
254 {
255 mSyncParent = nullptr;
256 }
257
258 if ( mSyncParent )
259 {
260 QgsDebugMsgLevel( "found sync parent: " + mSyncParent->objectName(), 2 );
261
262 const bool thisCollapsed = mCollapsed; // get state of current box before its changed
263 const auto groupBoxes {mSyncParent->findChildren<QgsCollapsibleGroupBoxBasic *>()};
264 for ( QgsCollapsibleGroupBoxBasic *grpbox : groupBoxes )
265 {
266 if ( grpbox->syncGroup() == syncGroup() && grpbox->isEnabled() )
267 {
268 if ( mShiftDown && grpbox == this )
269 {
270 // expand current group box on shift-click
271 setCollapsed( false );
272 }
273 else
274 {
275 grpbox->setCollapsed( mShiftDown ? true : !thisCollapsed );
276 }
277 }
278 }
279
281 return;
282 }
283 else
284 {
285 QgsDebugMsgLevel( QStringLiteral( "did not find a sync parent" ), 2 );
286 }
287 }
288
289 // expand current group box on shift-click, even if no sync group
290 if ( mShiftDown )
291 {
292 setCollapsed( false );
293 }
294 else
295 {
297 }
298
300}
301
303{
304 QGroupBox::setStyleSheet( style );
305}
306
308{
309 setUpdatesEnabled( false );
310
311 const QgsSettings settings;
312
313 QStyleOptionGroupBox box;
314 initStyleOption( &box );
315 const QRect rectFrame = style()->subControlRect( QStyle::CC_GroupBox, &box,
316 QStyle::SC_GroupBoxFrame, this );
317 const QRect rectTitle = titleRect();
318
319 // margin/offset defaults
320 const int marginLeft = 20; // title margin for disclosure triangle
321 const int marginRight = 5; // a little bit of space on the right, to match space on the left
322 int offsetLeft = 0; // offset for oxygen theme
323 const int offsetStyle = QApplication::style()->objectName().contains( QLatin1String( "macintosh" ) ) ? 8 : 0;
324 const int topBuffer = 1 + offsetStyle; // space between top of title or triangle and widget above
325 int offsetTop = topBuffer;
326 int offsetTopTri = topBuffer; // offset for triangle
327
328 if ( mCollapseButton->height() < rectTitle.height() ) // triangle's height > title text's, offset triangle
329 {
330 offsetTopTri += ( rectTitle.height() - mCollapseButton->height() ) / 2;
331// offsetTopTri += rectTitle.top();
332 }
333 else if ( rectTitle.height() < mCollapseButton->height() ) // title text's height < triangle's, offset title
334 {
335 offsetTop += ( mCollapseButton->height() - rectTitle.height() ) / 2;
336 }
337
338 // calculate offset if frame overlaps triangle (oxygen theme)
339 // using an offset of 6 pixels from frame border
340 if ( QApplication::style()->objectName().compare( QLatin1String( "oxygen" ), Qt::CaseInsensitive ) == 0 )
341 {
342 QStyleOptionGroupBox box;
343 initStyleOption( &box );
344 const QRect rectFrame = style()->subControlRect( QStyle::CC_GroupBox, &box,
345 QStyle::SC_GroupBoxFrame, this );
346 const QRect rectCheckBox = style()->subControlRect( QStyle::CC_GroupBox, &box,
347 QStyle::SC_GroupBoxCheckBox, this );
348 if ( rectFrame.left() <= 0 )
349 offsetLeft = 6 + rectFrame.left();
350 if ( rectFrame.top() <= 0 )
351 {
352 if ( isCheckable() )
353 {
354 // if is checkable align with checkbox
355 offsetTop = ( rectCheckBox.height() / 2 ) -
356 ( mCollapseButton->height() / 2 ) + rectCheckBox.top();
357 offsetTopTri = offsetTop + 1;
358 }
359 else
360 {
361 offsetTop = 6 + rectFrame.top();
362 offsetTopTri = offsetTop;
363 }
364 }
365 }
366
367 QgsDebugMsgLevel( QStringLiteral( "groupbox: %1 style: %2 offset: left=%3 top=%4 top2=%5" ).arg(
368 objectName(), QApplication::style()->objectName() ).arg( offsetLeft ).arg( offsetTop ).arg( offsetTopTri ), 5 );
369
370 // customize style sheet for collapse/expand button and force left-aligned title
371 QString ss;
372 if ( QApplication::style()->objectName().contains( QLatin1String( "macintosh" ) ) )
373 {
374 ss += QLatin1String( "QgsCollapsibleGroupBoxBasic, QgsCollapsibleGroupBox {" );
375 ss += QStringLiteral( " margin-top: %1px;" ).arg( topBuffer + rectFrame.top() );
376 ss += '}';
377 }
378 ss += QLatin1String( "QgsCollapsibleGroupBoxBasic::title, QgsCollapsibleGroupBox::title {" );
379 ss += QLatin1String( " subcontrol-origin: margin;" );
380 ss += QLatin1String( " subcontrol-position: top left;" );
381 ss += QStringLiteral( " margin-left: %1px;" ).arg( marginLeft );
382 ss += QStringLiteral( " margin-right: %1px;" ).arg( marginRight );
383 ss += QStringLiteral( " left: %1px;" ).arg( offsetLeft );
384 ss += QStringLiteral( " top: %1px;" ).arg( offsetTop );
385 if ( QApplication::style()->objectName().contains( QLatin1String( "macintosh" ) ) )
386 {
387 ss += QLatin1String( " background-color: rgba(0,0,0,0)" );
388 }
389 ss += '}';
390 setStyleSheet( styleSheet() + ss );
391
392 // clear toolbutton default background and border and apply offset
393 QString ssd;
394 ssd = QStringLiteral( "QgsCollapsibleGroupBoxBasic > QToolButton#%1, QgsCollapsibleGroupBox > QToolButton#%1 {" ).arg( mCollapseButton->objectName() );
395 ssd += QLatin1String( " background-color: rgba(255, 255, 255, 0); border: none;" );
396 ssd += QStringLiteral( "} QgsCollapsibleGroupBoxBasic > QToolButton#%1:focus, QgsCollapsibleGroupBox > QToolButton#%1:focus { border: 1px solid palette(highlight); }" ).arg( mCollapseButton->objectName() );
397 mCollapseButton->setStyleSheet( ssd );
398 if ( offsetLeft != 0 || offsetTopTri != 0 )
399 mCollapseButton->move( offsetLeft, offsetTopTri );
400 setUpdatesEnabled( true );
401}
402
404{
405 const bool changed = collapse != mCollapsed;
406 mCollapsed = collapse;
407
408 if ( !isVisible() )
409 return;
410
411 // for consistent look/spacing across platforms when collapsed
412 if ( ! mInitFlat ) // skip if initially set to flat in Designer
413 setFlat( collapse );
414
415 // avoid flicker in X11
416 // NOTE: this causes app to crash when loading a project that hits a group box with
417 // 'collapse' set via dynamic property or in code (especially if auto-launching project)
418 // TODO: find another means of avoiding the X11 flicker
419// QApplication::processEvents();
420
421 // handle visual fixes for collapsing/expanding
423
424 // set maximum height to hide contents - does this work in all envs?
425 // setMaximumHeight( collapse ? 25 : 16777215 );
426 setMaximumHeight( collapse ? titleRect().bottom() + 6 : 16777215 );
427 mCollapseButton->setIcon( collapse ? mExpandIcon : mCollapseIcon );
428
429 // if expanding and is in a QScrollArea, scroll down to make entire widget visible
430 if ( mShown && mScrollOnExpand && !collapse && mParentScrollArea )
431 {
432 // process events so entire widget is shown
433 QApplication::processEvents();
434 mParentScrollArea->setUpdatesEnabled( false );
435 mParentScrollArea->ensureWidgetVisible( this );
436 //and then make sure the top of the widget is visible - otherwise tall group boxes
437 //scroll to their centres, which is disorienting for users
438 mParentScrollArea->ensureWidgetVisible( mCollapseButton, 0, 5 );
439 mParentScrollArea->setUpdatesEnabled( true );
440 }
441 // emit signal for connections using collapsed state
442 if ( changed )
444}
445
447{
448 // handle child widgets so they don't paint while hidden
449 const char *hideKey = "CollGrpBxHide";
450
451 QString ss = styleSheet();
452 if ( mCollapsed )
453 {
454 if ( !ss.contains( COLLAPSE_HIDE_BORDER_FIX ) )
455 {
457 setStyleSheet( ss );
458 }
459
460 const auto constChildren = children();
461 for ( QObject *child : constChildren )
462 {
463 QWidget *w = qobject_cast<QWidget *>( child );
464 if ( w && w != mCollapseButton )
465 {
466 w->setProperty( hideKey, true );
467 w->hide();
468 }
469 }
470 }
471 else // on expand
472 {
473 if ( ss.contains( COLLAPSE_HIDE_BORDER_FIX ) )
474 {
475 ss.replace( COLLAPSE_HIDE_BORDER_FIX, QString() );
476 setStyleSheet( ss );
477 }
478
479 const auto constChildren = children();
480 for ( QObject *child : constChildren )
481 {
482 QWidget *w = qobject_cast<QWidget *>( child );
483 if ( w && w != mCollapseButton )
484 {
485 if ( w->property( hideKey ).toBool() )
486 w->show();
487 }
488 }
489 }
490}
491
492
493// ----
494
497 , mSettings( settings )
498{
499 init();
500}
501
503 QWidget *parent, QgsSettings *settings )
504 : QgsCollapsibleGroupBoxBasic( title, parent )
505 , mSettings( settings )
506{
507 init();
508}
509
511{
512 saveState();
513 if ( mDelSettings ) // local settings obj to delete
514 delete mSettings;
515 mSettings = nullptr; // null the pointer (in case of outside settings obj)
516}
517
519{
520 if ( mDelSettings ) // local settings obj to delete
521 delete mSettings;
522 mSettings = settings;
523 mDelSettings = false; // don't delete outside obj
524}
525
526void QgsCollapsibleGroupBox::init()
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 // save state immediately when collapsed state changes, so that other widgets created
545 // before this one is destroyed will correctly restore the new collapsed state
547}
548
549void QgsCollapsibleGroupBox::showEvent( QShowEvent *event )
550{
551 // initialize widget on first show event only
552 if ( mShown )
553 {
554 event->accept();
555 return;
556 }
557
558 // check if groupbox was set to flat in Designer or in code
559 if ( !mInitFlatChecked )
560 {
561 mInitFlat = isFlat();
562 mInitFlatChecked = true;
563 }
564
565 loadState();
566
568}
569
571{
572 if ( objectName().isEmpty() || ( mSettingGroup.isEmpty() && window()->objectName().isEmpty() ) )
573 return QString(); // cannot get a valid key
574
575 // save key for load/save state
576 // currently QgsCollapsibleGroupBox/window()/object
577 QString saveKey = '/' + objectName();
578 // QObject* parentWidget = parent();
579 // while ( parentWidget )
580 // {
581 // saveKey = "/" + parentWidget->objectName() + saveKey;
582 // parentWidget = parentWidget->parent();
583 // }
584 // if ( parent() )
585 // saveKey = "/" + parent()->objectName() + saveKey;
586 const QString setgrp = mSettingGroup.isEmpty() ? window()->objectName() : mSettingGroup;
587 saveKey = '/' + setgrp + saveKey;
588 saveKey = QStringLiteral( "QgsCollapsibleGroupBox" ) + saveKey;
589 return saveKey;
590}
591
593{
594 if ( !mSettings )
595 return;
596
597 if ( !isEnabled() || ( !mSaveCollapsedState && !mSaveCheckedState ) )
598 return;
599
600 const QString key = saveKey();
601 if ( key.isEmpty() )
602 return;
603
604 setUpdatesEnabled( false );
605
606 if ( mSaveCheckedState )
607 {
608 const QVariant val = mSettings->value( key + "/checked" );
609 if ( ! QgsVariantUtils::isNull( val ) )
610 setChecked( val.toBool() );
611 }
612 if ( mSaveCollapsedState )
613 {
614 const QVariant val = mSettings->value( key + "/collapsed" );
615 if ( ! QgsVariantUtils::isNull( val ) )
616 setCollapsed( val.toBool() );
617 }
618
619 setUpdatesEnabled( true );
620}
621
623{
624 if ( !mSettings )
625 return;
626
627 if ( !mShown || !isEnabled() || ( !mSaveCollapsedState && !mSaveCheckedState ) )
628 return;
629
630 const QString key = saveKey();
631 if ( key.isEmpty() )
632 return;
633
634 if ( mSaveCheckedState )
635 mSettings->setValue( key + QStringLiteral( "/checked" ), isChecked() );
636 if ( mSaveCollapsedState )
637 mSettings->setValue( key + QStringLiteral( "/collapsed" ), isCollapsed() );
638}
639
640
642{
643 mAltDown = ( event->modifiers() & ( Qt::AltModifier | Qt::ControlModifier ) );
644 mShiftDown = ( event->modifiers() & Qt::ShiftModifier );
645 QToolButton::mouseReleaseEvent( event );
646}
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