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