QGIS API Documentation  3.17.0-Master (c90b8c8084)
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 
18 #include "qgscollapsiblegroupbox.h"
19 #include "qgsapplication.h"
20 #include "qgslogger.h"
21 #include "qgssettings.h"
22 
23 #include <QToolButton>
24 #include <QMouseEvent>
25 #include <QPushButton>
26 #include <QStyleOptionGroupBox>
27 #include <QScrollArea>
28 
29 const QString COLLAPSE_HIDE_BORDER_FIX = QStringLiteral( " QgsCollapsibleGroupBoxBasic { border: none; }" );
30 
32  : QGroupBox( parent )
33 {
34  init();
35 }
36 
38  QWidget *parent )
39  : QGroupBox( title, parent )
40 {
41  init();
42 }
43 
45 {
46  // variables
47  mCollapsed = false;
48  mInitFlat = false;
49  mInitFlatChecked = false;
50  mScrollOnExpand = true;
51  mShown = false;
52  mParentScrollArea = nullptr;
53  mSyncParent = nullptr;
54  mAltDown = false;
55  mShiftDown = false;
56  mTitleClicked = false;
57 
58  // init icons
59  mCollapseIcon = QgsApplication::getThemeIcon( QStringLiteral( "/mIconCollapse.svg" ) );
60  mExpandIcon = QgsApplication::getThemeIcon( QStringLiteral( "/mIconExpand.svg" ) );
61 
62  // collapse button
64  mCollapseButton->setObjectName( QStringLiteral( "collapseButton" ) );
65  mCollapseButton->setAutoRaise( true );
66  mCollapseButton->setFixedSize( 16, 16 );
67  // TODO set size (as well as margins) depending on theme, in updateStyle()
68  mCollapseButton->setIconSize( QSize( 12, 12 ) );
69  mCollapseButton->setIcon( mCollapseIcon );
70  // FIXME: This appears to mess up parent-child relationships and causes double-frees of children when destroying in Qt5.10, needs further investigation
71  // See also https://github.com/qgis/QGIS/pull/6301
72 #if QT_VERSION < QT_VERSION_CHECK(5, 10, 0)
73  setFocusProxy( mCollapseButton );
74 #endif
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 
82 void QgsCollapsibleGroupBoxBasic::showEvent( QShowEvent *event )
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 &&
159  ( mAltDown || mShiftDown || !isCheckable() ) )
160  {
161  toggleCollapsed();
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,
197  QStyle::SC_GroupBoxLabel, this );
198 }
199 
201 {
202  mCollapseButton->setAltDown( false );
203  mCollapseButton->setShiftDown( false );
204  mAltDown = false;
205  mShiftDown = false;
206 }
207 
209 {
210  Q_UNUSED( chkd )
211  mCollapseButton->setEnabled( true ); // always keep enabled
212 }
213 
215 {
216  // expand/collapse when checkbox toggled by user click.
217  // don't do this on toggle signal, otherwise group boxes will default to collapsed
218  // in option dialog constructors, reducing discovery of options by new users and
219  // overriding user's auto-saved collapsed/expanded state for the group box
220  if ( chkd && isCollapsed() )
221  setCollapsed( false );
222  else if ( ! chkd && ! isCollapsed() )
223  setCollapsed( true );
224 }
225 
227 {
228  // verify if sender is this group box's collapse button
229  QgsGroupBoxCollapseButton *collBtn = qobject_cast<QgsGroupBoxCollapseButton *>( QObject::sender() );
230  bool senderCollBtn = ( collBtn && collBtn == mCollapseButton );
231 
234 
235  // find any sync group siblings and toggle them
236  if ( ( senderCollBtn || mTitleClicked )
237  && ( mAltDown || mShiftDown )
238  && !mSyncGroup.isEmpty() )
239  {
240  QgsDebugMsg( QStringLiteral( "Alt or Shift key down, syncing group" ) );
241  // get pointer to parent or grandparent widget
242  if ( auto *lParentWidget = parentWidget() )
243  {
244  mSyncParent = lParentWidget;
245  if ( mSyncParent->parentWidget() )
246  {
247  // don't use whole app for grandparent (common for dialogs that use main window for parent)
248  if ( mSyncParent->parentWidget()->objectName() != QLatin1String( "QgisApp" ) )
249  {
250  mSyncParent = mSyncParent->parentWidget();
251  }
252  }
253  }
254  else
255  {
256  mSyncParent = nullptr;
257  }
258 
259  if ( mSyncParent )
260  {
261  QgsDebugMsg( "found sync parent: " + mSyncParent->objectName() );
262 
263  bool thisCollapsed = mCollapsed; // get state of current box before its changed
264  const auto groupBoxes {mSyncParent->findChildren<QgsCollapsibleGroupBoxBasic *>()};
265  for ( QgsCollapsibleGroupBoxBasic *grpbox : groupBoxes )
266  {
267  if ( grpbox->syncGroup() == syncGroup() && grpbox->isEnabled() )
268  {
269  if ( mShiftDown && grpbox == this )
270  {
271  // expand current group box on shift-click
272  setCollapsed( false );
273  }
274  else
275  {
276  grpbox->setCollapsed( mShiftDown ? true : !thisCollapsed );
277  }
278  }
279  }
280 
281  clearModifiers();
282  return;
283  }
284  else
285  {
286  QgsDebugMsg( QStringLiteral( "did not find a sync parent" ) );
287  }
288  }
289 
290  // expand current group box on shift-click, even if no sync group
291  if ( mShiftDown )
292  {
293  setCollapsed( false );
294  }
295  else
296  {
298  }
299 
300  clearModifiers();
301 }
302 
303 void QgsCollapsibleGroupBoxBasic::setStyleSheet( const QString &style )
304 {
305 #if QT_VERSION < QT_VERSION_CHECK(5, 12, 4)
306  // Fix crash on old Qt versions, see #39693
307  QGroupBox::setStyleSheet( QString() );
308 #endif
309  QGroupBox::setStyleSheet( style );
310 }
311 
313 {
314  setUpdatesEnabled( false );
315 
316  QgsSettings settings;
317 
318  QStyleOptionGroupBox box;
319  initStyleOption( &box );
320  QRect rectFrame = style()->subControlRect( QStyle::CC_GroupBox, &box,
321  QStyle::SC_GroupBoxFrame, this );
322  QRect rectTitle = titleRect();
323 
324  // margin/offset defaults
325  int marginLeft = 20; // title margin for disclosure triangle
326  int marginRight = 5; // a little bit of space on the right, to match space on the left
327  int offsetLeft = 0; // offset for oxygen theme
328  int offsetStyle = QApplication::style()->objectName().contains( QLatin1String( "macintosh" ) ) ? 8 : 0;
329  int topBuffer = 1 + offsetStyle; // space between top of title or triangle and widget above
330  int offsetTop = topBuffer;
331  int offsetTopTri = topBuffer; // offset for triangle
332 
333  if ( mCollapseButton->height() < rectTitle.height() ) // triangle's height > title text's, offset triangle
334  {
335  offsetTopTri += ( rectTitle.height() - mCollapseButton->height() ) / 2;
336 // offsetTopTri += rectTitle.top();
337  }
338  else if ( rectTitle.height() < mCollapseButton->height() ) // title text's height < triangle's, offset title
339  {
340  offsetTop += ( mCollapseButton->height() - rectTitle.height() ) / 2;
341  }
342 
343  // calculate offset if frame overlaps triangle (oxygen theme)
344  // using an offset of 6 pixels from frame border
345  if ( QApplication::style()->objectName().compare( QLatin1String( "oxygen" ), Qt::CaseInsensitive ) == 0 )
346  {
347  QStyleOptionGroupBox box;
348  initStyleOption( &box );
349  QRect rectFrame = style()->subControlRect( QStyle::CC_GroupBox, &box,
350  QStyle::SC_GroupBoxFrame, this );
351  QRect rectCheckBox = style()->subControlRect( QStyle::CC_GroupBox, &box,
352  QStyle::SC_GroupBoxCheckBox, this );
353  if ( rectFrame.left() <= 0 )
354  offsetLeft = 6 + rectFrame.left();
355  if ( rectFrame.top() <= 0 )
356  {
357  if ( isCheckable() )
358  {
359  // if is checkable align with checkbox
360  offsetTop = ( rectCheckBox.height() / 2 ) -
361  ( mCollapseButton->height() / 2 ) + rectCheckBox.top();
362  offsetTopTri = offsetTop + 1;
363  }
364  else
365  {
366  offsetTop = 6 + rectFrame.top();
367  offsetTopTri = offsetTop;
368  }
369  }
370  }
371 
372  QgsDebugMsgLevel( QStringLiteral( "groupbox: %1 style: %2 offset: left=%3 top=%4 top2=%5" ).arg(
373  objectName(), QApplication::style()->objectName() ).arg( offsetLeft ).arg( offsetTop ).arg( offsetTopTri ), 5 );
374 
375  // customize style sheet for collapse/expand button and force left-aligned title
376  QString ss;
377  if ( QApplication::style()->objectName().contains( QLatin1String( "macintosh" ) ) )
378  {
379  ss += QLatin1String( "QgsCollapsibleGroupBoxBasic, QgsCollapsibleGroupBox {" );
380  ss += QStringLiteral( " margin-top: %1px;" ).arg( topBuffer + rectFrame.top() );
381  ss += '}';
382  }
383  ss += QLatin1String( "QgsCollapsibleGroupBoxBasic::title, QgsCollapsibleGroupBox::title {" );
384  ss += QLatin1String( " subcontrol-origin: margin;" );
385  ss += QLatin1String( " subcontrol-position: top left;" );
386  ss += QStringLiteral( " margin-left: %1px;" ).arg( marginLeft );
387  ss += QStringLiteral( " margin-right: %1px;" ).arg( marginRight );
388  ss += QStringLiteral( " left: %1px;" ).arg( offsetLeft );
389  ss += QStringLiteral( " top: %1px;" ).arg( offsetTop );
390  if ( QApplication::style()->objectName().contains( QLatin1String( "macintosh" ) ) )
391  {
392  ss += QLatin1String( " background-color: rgba(0,0,0,0)" );
393  }
394  ss += '}';
395  setStyleSheet( styleSheet() + ss );
396 
397  // clear toolbutton default background and border and apply offset
398  QString ssd;
399  ssd = QStringLiteral( "QgsCollapsibleGroupBoxBasic > QToolButton#%1, QgsCollapsibleGroupBox > QToolButton#%1 {" ).arg( mCollapseButton->objectName() );
400  ssd += QLatin1String( " background-color: rgba(255, 255, 255, 0); border: none;" );
401  ssd += QStringLiteral( "} QgsCollapsibleGroupBoxBasic > QToolButton#%1:focus, QgsCollapsibleGroupBox > QToolButton#%1:focus { border: 1px solid palette(highlight); }" ).arg( mCollapseButton->objectName() );
402  mCollapseButton->setStyleSheet( ssd );
403  if ( offsetLeft != 0 || offsetTopTri != 0 )
404  mCollapseButton->move( offsetLeft, offsetTopTri );
405  setUpdatesEnabled( true );
406 }
407 
409 {
410  bool changed = collapse != mCollapsed;
411  mCollapsed = collapse;
412 
413  if ( !isVisible() )
414  return;
415 
416  // for consistent look/spacing across platforms when collapsed
417  if ( ! mInitFlat ) // skip if initially set to flat in Designer
418  setFlat( collapse );
419 
420  // avoid flicker in X11
421  // NOTE: this causes app to crash when loading a project that hits a group box with
422  // 'collapse' set via dynamic property or in code (especially if auto-launching project)
423  // TODO: find another means of avoiding the X11 flicker
424 // QApplication::processEvents();
425 
426  // handle visual fixes for collapsing/expanding
428 
429  // set maximum height to hide contents - does this work in all envs?
430  // setMaximumHeight( collapse ? 25 : 16777215 );
431  setMaximumHeight( collapse ? titleRect().bottom() + 6 : 16777215 );
432  mCollapseButton->setIcon( collapse ? mExpandIcon : mCollapseIcon );
433 
434  // if expanding and is in a QScrollArea, scroll down to make entire widget visible
435  if ( mShown && mScrollOnExpand && !collapse && mParentScrollArea )
436  {
437  // process events so entire widget is shown
438  QApplication::processEvents();
439  mParentScrollArea->setUpdatesEnabled( false );
440  mParentScrollArea->ensureWidgetVisible( this );
441  //and then make sure the top of the widget is visible - otherwise tall group boxes
442  //scroll to their centres, which is disorienting for users
443  mParentScrollArea->ensureWidgetVisible( mCollapseButton, 0, 5 );
444  mParentScrollArea->setUpdatesEnabled( true );
445  }
446  // emit signal for connections using collapsed state
447  if ( changed )
449 }
450 
452 {
453  // handle child widgets so they don't paint while hidden
454  const char *hideKey = "CollGrpBxHide";
455 
456  QString ss = styleSheet();
457  if ( mCollapsed )
458  {
459  if ( !ss.contains( COLLAPSE_HIDE_BORDER_FIX ) )
460  {
462  setStyleSheet( ss );
463  }
464 
465  const auto constChildren = children();
466  for ( QObject *child : constChildren )
467  {
468  QWidget *w = qobject_cast<QWidget *>( child );
469  if ( w && w != mCollapseButton )
470  {
471  w->setProperty( hideKey, true );
472  w->hide();
473  }
474  }
475  }
476  else // on expand
477  {
478  if ( ss.contains( COLLAPSE_HIDE_BORDER_FIX ) )
479  {
480  ss.replace( COLLAPSE_HIDE_BORDER_FIX, QString() );
481  setStyleSheet( ss );
482  }
483 
484  const auto constChildren = children();
485  for ( QObject *child : constChildren )
486  {
487  QWidget *w = qobject_cast<QWidget *>( child );
488  if ( w && w != mCollapseButton )
489  {
490  if ( w->property( hideKey ).toBool() )
491  w->show();
492  }
493  }
494  }
495 }
496 
497 
498 // ----
499 
501  : QgsCollapsibleGroupBoxBasic( parent )
502  , mSettings( settings )
503 {
504  init();
505 }
506 
508  QWidget *parent, QgsSettings *settings )
509  : QgsCollapsibleGroupBoxBasic( title, parent )
510  , mSettings( settings )
511 {
512  init();
513 }
514 
516 {
517  saveState();
518  if ( mDelSettings ) // local settings obj to delete
519  delete mSettings;
520  mSettings = nullptr; // null the pointer (in case of outside settings obj)
521 }
522 
524 {
525  if ( mDelSettings ) // local settings obj to delete
526  delete mSettings;
527  mSettings = settings;
528  mDelSettings = false; // don't delete outside obj
529 }
530 
531 
533 {
534  // use pointer to app qsettings if no custom qsettings specified
535  // custom qsettings object may be from Python plugin
536  mDelSettings = false;
537  if ( !mSettings )
538  {
539  mSettings = new QgsSettings();
540  mDelSettings = true; // only delete obj created by class
541  }
542  // variables
543  mSaveCollapsedState = true;
544  // NOTE: only turn on mSaveCheckedState for groupboxes NOT used
545  // in multiple places or used as options for different parent objects
546  mSaveCheckedState = false;
547 
548  connect( this, &QObject::objectNameChanged, this, &QgsCollapsibleGroupBox::loadState );
549 }
550 
551 void QgsCollapsibleGroupBox::showEvent( QShowEvent *event )
552 {
553  // initialize widget on first show event only
554  if ( mShown )
555  {
556  event->accept();
557  return;
558  }
559 
560  // check if groupbox was set to flat in Designer or in code
561  if ( !mInitFlatChecked )
562  {
563  mInitFlat = isFlat();
564  mInitFlatChecked = true;
565  }
566 
567  loadState();
568 
570 }
571 
573 {
574  if ( objectName().isEmpty() || ( mSettingGroup.isEmpty() && window()->objectName().isEmpty() ) )
575  return QString(); // cannot get a valid key
576 
577  // save key for load/save state
578  // currently QgsCollapsibleGroupBox/window()/object
579  QString saveKey = '/' + objectName();
580  // QObject* parentWidget = parent();
581  // while ( parentWidget )
582  // {
583  // saveKey = "/" + parentWidget->objectName() + saveKey;
584  // parentWidget = parentWidget->parent();
585  // }
586  // if ( parent() )
587  // saveKey = "/" + parent()->objectName() + saveKey;
588  const QString setgrp = mSettingGroup.isEmpty() ? window()->objectName() : mSettingGroup;
589  saveKey = '/' + setgrp + saveKey;
590  saveKey = QStringLiteral( "QgsCollapsibleGroupBox" ) + saveKey;
591  return saveKey;
592 }
593 
595 {
596  if ( !mSettings )
597  return;
598 
599  if ( !isEnabled() || ( !mSaveCollapsedState && !mSaveCheckedState ) )
600  return;
601 
602  const QString key = saveKey();
603  if ( key.isEmpty() )
604  return;
605 
606  setUpdatesEnabled( false );
607 
608  if ( mSaveCheckedState )
609  {
610  QVariant val = mSettings->value( key + "/checked" );
611  if ( ! val.isNull() )
612  setChecked( val.toBool() );
613  }
614  if ( mSaveCollapsedState )
615  {
616  QVariant val = mSettings->value( key + "/collapsed" );
617  if ( ! val.isNull() )
618  setCollapsed( val.toBool() );
619  }
620 
621  setUpdatesEnabled( true );
622 }
623 
625 {
626  if ( !mSettings )
627  return;
628 
629  if ( !mShown || !isEnabled() || ( !mSaveCollapsedState && !mSaveCheckedState ) )
630  return;
631 
632  const QString key = saveKey();
633  if ( key.isEmpty() )
634  return;
635 
636  if ( mSaveCheckedState )
637  mSettings->setValue( key + QStringLiteral( "/checked" ), isChecked() );
638  if ( mSaveCollapsedState )
639  mSettings->setValue( key + QStringLiteral( "/collapsed" ), isCollapsed() );
640 }
641 
642 
644 {
645  mAltDown = ( event->modifiers() & ( Qt::AltModifier | Qt::ControlModifier ) );
646  mShiftDown = ( event->modifiers() & Qt::ShiftModifier );
647  QToolButton::mouseReleaseEvent( event );
648 }
This class is a composition of two QSettings instances:
Definition: qgssettings.h:61
#define QgsDebugMsg(str)
Definition: qgslogger.h:38
void collapsedStateChanged(bool collapsed)
Signal emitted when groupbox collapsed/expanded state is changed, and when first shown.
static QIcon getThemeIcon(const QString &name)
Helper to get a theme icon.
void saveState() const
Will save the collapsed and checked state.
void setCollapsed(bool collapse)
Collapse or uncollapse this groupbox.
void changeEvent(QEvent *event) override
void showEvent(QShowEvent *event) override
void setShiftDown(bool shiftdown)
bool isCollapsed() const
Returns the current collapsed state of this group box.
QgsGroupBoxCollapseButton * mCollapseButton
QString syncGroup() const
Named group which synchronizes collapsing action when triangle is clicked while holding alt modifier ...
void setSettings(QgsSettings *settings)
A groupbox that collapses/expands when toggled.
#define QgsDebugMsgLevel(str, level)
Definition: qgslogger.h:39
void setSyncGroup(const QString &grp)
Named group which synchronizes collapsing action when triangle is clicked while holding alt modifier ...
void mouseReleaseEvent(QMouseEvent *event) override
void mousePressEvent(QMouseEvent *event) override
void mouseReleaseEvent(QMouseEvent *event) override
QgsCollapsibleGroupBoxBasic(QWidget *parent=nullptr)
void showEvent(QShowEvent *event) override
void loadState()
Will load the collapsed and checked state.
QPointer< QgsSettings > mSettings
const QString COLLAPSE_HIDE_BORDER_FIX
QgsCollapsibleGroupBox(QWidget *parent=nullptr, QgsSettings *settings=nullptr)
void setStyleSheet(const QString &style)
Overridden to prepare base call and avoid crash due to specific QT versions.
void collapseExpandFixes()
Visual fixes for when group box is collapsed/expanded.