QGIS API Documentation  3.27.0-Master (0e23467727)
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  setFocusPolicy( Qt::StrongFocus );
73 
74  connect( mCollapseButton, &QAbstractButton::clicked, this, &QgsCollapsibleGroupBoxBasic::toggleCollapsed );
75  connect( this, &QGroupBox::toggled, this, &QgsCollapsibleGroupBoxBasic::checkToggled );
76  connect( this, &QGroupBox::clicked, this, &QgsCollapsibleGroupBoxBasic::checkClicked );
77 }
78 
79 void QgsCollapsibleGroupBoxBasic::showEvent( QShowEvent *event )
80 {
81  // initialize widget on first show event only
82  if ( mShown )
83  {
84  event->accept();
85  return;
86  }
87 
88  // check if groupbox was set to flat in Designer or in code
89  if ( !mInitFlatChecked )
90  {
91  mInitFlat = isFlat();
92  mInitFlatChecked = true;
93  }
94 
95  // find parent QScrollArea - this might not work in complex layouts - should we look deeper?
96  if ( parent() && parent()->parent() )
97  mParentScrollArea = qobject_cast<QScrollArea *>( parent()->parent()->parent() );
98  else
99  mParentScrollArea = nullptr;
100  if ( mParentScrollArea )
101  {
102  QgsDebugMsgLevel( "found a QScrollArea parent: " + mParentScrollArea->objectName(), 5 );
103  }
104  else
105  {
106  QgsDebugMsgLevel( QStringLiteral( "did not find a QScrollArea parent" ), 5 );
107  }
108 
109  updateStyle();
110 
111  // expand if needed - any calls to setCollapsed() before only set mCollapsed, but have UI effect
112  if ( mCollapsed )
113  {
115  }
116  else
117  {
118  // emit signal for connections using collapsed state
120  }
121 
122  // verify triangle mirrors groupbox's enabled state
123  mCollapseButton->setEnabled( isEnabled() );
124 
125  // set mShown after first setCollapsed call or expanded groupboxes
126  // will scroll scroll areas when first shown
127  mShown = true;
128  event->accept();
129 }
130 
132 {
133  // avoid leaving checkbox in pressed state if alt- or shift-clicking
134  if ( event->modifiers() & ( Qt::AltModifier | Qt::ControlModifier | Qt::ShiftModifier )
135  && titleRect().contains( event->pos() )
136  && isCheckable() )
137  {
138  event->ignore();
139  return;
140  }
141 
142  // default behavior - pass to QGroupBox
143  QGroupBox::mousePressEvent( event );
144 }
145 
147 {
148  mAltDown = ( event->modifiers() & ( Qt::AltModifier | Qt::ControlModifier ) );
149  mShiftDown = ( event->modifiers() & Qt::ShiftModifier );
150  mTitleClicked = ( titleRect().contains( event->pos() ) );
151 
152  // sync group when title is alt-clicked
153  // collapse/expand when title is clicked and non-checkable
154  // expand current and collapse others on shift-click
155  if ( event->button() == Qt::LeftButton && mTitleClicked &&
156  ( mAltDown || mShiftDown || !isCheckable() ) )
157  {
158  toggleCollapsed();
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,
194  QStyle::SC_GroupBoxLabel, this );
195 }
196 
198 {
199  mCollapseButton->setAltDown( false );
200  mCollapseButton->setShiftDown( false );
201  mAltDown = false;
202  mShiftDown = false;
203 }
204 
206 {
207  Q_UNUSED( chkd )
208  mCollapseButton->setEnabled( true ); // always keep enabled
209 }
210 
212 {
213  // expand/collapse when checkbox toggled by user click.
214  // don't do this on toggle signal, otherwise group boxes will default to collapsed
215  // in option dialog constructors, reducing discovery of options by new users and
216  // overriding user's auto-saved collapsed/expanded state for the group box
217  if ( chkd && isCollapsed() )
218  setCollapsed( false );
219  else if ( ! chkd && ! isCollapsed() )
220  setCollapsed( true );
221 }
222 
224 {
225  // verify if sender is this group box's collapse button
226  QgsGroupBoxCollapseButton *collBtn = qobject_cast<QgsGroupBoxCollapseButton *>( QObject::sender() );
227  const bool senderCollBtn = ( collBtn && collBtn == mCollapseButton );
228 
231 
232  // find any sync group siblings and toggle them
233  if ( ( senderCollBtn || mTitleClicked )
234  && ( mAltDown || mShiftDown )
235  && !mSyncGroup.isEmpty() )
236  {
237  QgsDebugMsg( QStringLiteral( "Alt or Shift key down, syncing group" ) );
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() != QLatin1String( "QgisApp" ) )
246  {
247  mSyncParent = mSyncParent->parentWidget();
248  }
249  }
250  }
251  else
252  {
253  mSyncParent = nullptr;
254  }
255 
256  if ( mSyncParent )
257  {
258  QgsDebugMsg( "found sync parent: " + mSyncParent->objectName() );
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 
278  clearModifiers();
279  return;
280  }
281  else
282  {
283  QgsDebugMsg( QStringLiteral( "did not find a sync parent" ) );
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 
297  clearModifiers();
298 }
299 
300 void QgsCollapsibleGroupBoxBasic::setStyleSheet( const QString &style )
301 {
302 #if QT_VERSION < QT_VERSION_CHECK(5, 12, 4)
303  // Fix crash on old Qt versions, see #39693
304  QGroupBox::setStyleSheet( QString() );
305 #endif
306  QGroupBox::setStyleSheet( style );
307 }
308 
310 {
311  setUpdatesEnabled( false );
312 
313  const QgsSettings settings;
314 
315  QStyleOptionGroupBox box;
316  initStyleOption( &box );
317  const QRect rectFrame = style()->subControlRect( QStyle::CC_GroupBox, &box,
318  QStyle::SC_GroupBoxFrame, this );
319  const QRect rectTitle = titleRect();
320 
321  // margin/offset defaults
322  const int marginLeft = 20; // title margin for disclosure triangle
323  const int marginRight = 5; // a little bit of space on the right, to match space on the left
324  int offsetLeft = 0; // offset for oxygen theme
325  const int offsetStyle = QApplication::style()->objectName().contains( QLatin1String( "macintosh" ) ) ? 8 : 0;
326  const int topBuffer = 1 + offsetStyle; // space between top of title or triangle and widget above
327  int offsetTop = topBuffer;
328  int offsetTopTri = topBuffer; // offset for triangle
329 
330  if ( mCollapseButton->height() < rectTitle.height() ) // triangle's height > title text's, offset triangle
331  {
332  offsetTopTri += ( rectTitle.height() - mCollapseButton->height() ) / 2;
333 // offsetTopTri += rectTitle.top();
334  }
335  else if ( rectTitle.height() < mCollapseButton->height() ) // title text's height < triangle's, offset title
336  {
337  offsetTop += ( mCollapseButton->height() - rectTitle.height() ) / 2;
338  }
339 
340  // calculate offset if frame overlaps triangle (oxygen theme)
341  // using an offset of 6 pixels from frame border
342  if ( QApplication::style()->objectName().compare( QLatin1String( "oxygen" ), Qt::CaseInsensitive ) == 0 )
343  {
344  QStyleOptionGroupBox box;
345  initStyleOption( &box );
346  const QRect rectFrame = style()->subControlRect( QStyle::CC_GroupBox, &box,
347  QStyle::SC_GroupBoxFrame, this );
348  const QRect rectCheckBox = style()->subControlRect( QStyle::CC_GroupBox, &box,
349  QStyle::SC_GroupBoxCheckBox, this );
350  if ( rectFrame.left() <= 0 )
351  offsetLeft = 6 + rectFrame.left();
352  if ( rectFrame.top() <= 0 )
353  {
354  if ( isCheckable() )
355  {
356  // if is checkable align with checkbox
357  offsetTop = ( rectCheckBox.height() / 2 ) -
358  ( mCollapseButton->height() / 2 ) + rectCheckBox.top();
359  offsetTopTri = offsetTop + 1;
360  }
361  else
362  {
363  offsetTop = 6 + rectFrame.top();
364  offsetTopTri = offsetTop;
365  }
366  }
367  }
368 
369  QgsDebugMsgLevel( QStringLiteral( "groupbox: %1 style: %2 offset: left=%3 top=%4 top2=%5" ).arg(
370  objectName(), QApplication::style()->objectName() ).arg( offsetLeft ).arg( offsetTop ).arg( offsetTopTri ), 5 );
371 
372  // customize style sheet for collapse/expand button and force left-aligned title
373  QString ss;
374  if ( QApplication::style()->objectName().contains( QLatin1String( "macintosh" ) ) )
375  {
376  ss += QLatin1String( "QgsCollapsibleGroupBoxBasic, QgsCollapsibleGroupBox {" );
377  ss += QStringLiteral( " margin-top: %1px;" ).arg( topBuffer + rectFrame.top() );
378  ss += '}';
379  }
380  ss += QLatin1String( "QgsCollapsibleGroupBoxBasic::title, QgsCollapsibleGroupBox::title {" );
381  ss += QLatin1String( " subcontrol-origin: margin;" );
382  ss += QLatin1String( " subcontrol-position: top left;" );
383  ss += QStringLiteral( " margin-left: %1px;" ).arg( marginLeft );
384  ss += QStringLiteral( " margin-right: %1px;" ).arg( marginRight );
385  ss += QStringLiteral( " left: %1px;" ).arg( offsetLeft );
386  ss += QStringLiteral( " top: %1px;" ).arg( offsetTop );
387  if ( QApplication::style()->objectName().contains( QLatin1String( "macintosh" ) ) )
388  {
389  ss += QLatin1String( " background-color: rgba(0,0,0,0)" );
390  }
391  ss += '}';
392  setStyleSheet( styleSheet() + ss );
393 
394  // clear toolbutton default background and border and apply offset
395  QString ssd;
396  ssd = QStringLiteral( "QgsCollapsibleGroupBoxBasic > QToolButton#%1, QgsCollapsibleGroupBox > QToolButton#%1 {" ).arg( mCollapseButton->objectName() );
397  ssd += QLatin1String( " background-color: rgba(255, 255, 255, 0); border: none;" );
398  ssd += QStringLiteral( "} QgsCollapsibleGroupBoxBasic > QToolButton#%1:focus, QgsCollapsibleGroupBox > QToolButton#%1:focus { border: 1px solid palette(highlight); }" ).arg( mCollapseButton->objectName() );
399  mCollapseButton->setStyleSheet( ssd );
400  if ( offsetLeft != 0 || offsetTopTri != 0 )
401  mCollapseButton->move( offsetLeft, offsetTopTri );
402  setUpdatesEnabled( true );
403 }
404 
406 {
407  const bool changed = collapse != mCollapsed;
408  mCollapsed = collapse;
409 
410  if ( !isVisible() )
411  return;
412 
413  // for consistent look/spacing across platforms when collapsed
414  if ( ! mInitFlat ) // skip if initially set to flat in Designer
415  setFlat( collapse );
416 
417  // avoid flicker in X11
418  // NOTE: this causes app to crash when loading a project that hits a group box with
419  // 'collapse' set via dynamic property or in code (especially if auto-launching project)
420  // TODO: find another means of avoiding the X11 flicker
421 // QApplication::processEvents();
422 
423  // handle visual fixes for collapsing/expanding
425 
426  // set maximum height to hide contents - does this work in all envs?
427  // setMaximumHeight( collapse ? 25 : 16777215 );
428  setMaximumHeight( collapse ? titleRect().bottom() + 6 : 16777215 );
429  mCollapseButton->setIcon( collapse ? mExpandIcon : mCollapseIcon );
430 
431  // if expanding and is in a QScrollArea, scroll down to make entire widget visible
432  if ( mShown && mScrollOnExpand && !collapse && mParentScrollArea )
433  {
434  // process events so entire widget is shown
435  QApplication::processEvents();
436  mParentScrollArea->setUpdatesEnabled( false );
437  mParentScrollArea->ensureWidgetVisible( this );
438  //and then make sure the top of the widget is visible - otherwise tall group boxes
439  //scroll to their centres, which is disorienting for users
440  mParentScrollArea->ensureWidgetVisible( mCollapseButton, 0, 5 );
441  mParentScrollArea->setUpdatesEnabled( true );
442  }
443  // emit signal for connections using collapsed state
444  if ( changed )
446 }
447 
449 {
450  // handle child widgets so they don't paint while hidden
451  const char *hideKey = "CollGrpBxHide";
452 
453  QString ss = styleSheet();
454  if ( mCollapsed )
455  {
456  if ( !ss.contains( COLLAPSE_HIDE_BORDER_FIX ) )
457  {
459  setStyleSheet( ss );
460  }
461 
462  const auto constChildren = children();
463  for ( QObject *child : constChildren )
464  {
465  QWidget *w = qobject_cast<QWidget *>( child );
466  if ( w && w != mCollapseButton )
467  {
468  w->setProperty( hideKey, true );
469  w->hide();
470  }
471  }
472  }
473  else // on expand
474  {
475  if ( ss.contains( COLLAPSE_HIDE_BORDER_FIX ) )
476  {
477  ss.replace( COLLAPSE_HIDE_BORDER_FIX, QString() );
478  setStyleSheet( ss );
479  }
480 
481  const auto constChildren = children();
482  for ( QObject *child : constChildren )
483  {
484  QWidget *w = qobject_cast<QWidget *>( child );
485  if ( w && w != mCollapseButton )
486  {
487  if ( w->property( hideKey ).toBool() )
488  w->show();
489  }
490  }
491  }
492 }
493 
494 
495 // ----
496 
498  : QgsCollapsibleGroupBoxBasic( parent )
499  , mSettings( settings )
500 {
501  init();
502 }
503 
505  QWidget *parent, QgsSettings *settings )
506  : QgsCollapsibleGroupBoxBasic( title, parent )
507  , mSettings( settings )
508 {
509  init();
510 }
511 
513 {
514  saveState();
515  if ( mDelSettings ) // local settings obj to delete
516  delete mSettings;
517  mSettings = nullptr; // null the pointer (in case of outside settings obj)
518 }
519 
521 {
522  if ( mDelSettings ) // local settings obj to delete
523  delete mSettings;
524  mSettings = settings;
525  mDelSettings = false; // don't delete outside obj
526 }
527 
528 
530 {
531  // use pointer to app qsettings if no custom qsettings specified
532  // custom qsettings object may be from Python plugin
533  mDelSettings = false;
534  if ( !mSettings )
535  {
536  mSettings = new QgsSettings();
537  mDelSettings = true; // only delete obj created by class
538  }
539  // variables
540  mSaveCollapsedState = true;
541  // NOTE: only turn on mSaveCheckedState for groupboxes NOT used
542  // in multiple places or used as options for different parent objects
543  mSaveCheckedState = false;
544 
545  connect( this, &QObject::objectNameChanged, this, &QgsCollapsibleGroupBox::loadState );
546 }
547 
548 void QgsCollapsibleGroupBox::showEvent( QShowEvent *event )
549 {
550  // initialize widget on first show event only
551  if ( mShown )
552  {
553  event->accept();
554  return;
555  }
556 
557  // check if groupbox was set to flat in Designer or in code
558  if ( !mInitFlatChecked )
559  {
560  mInitFlat = isFlat();
561  mInitFlatChecked = true;
562  }
563 
564  loadState();
565 
567 }
568 
570 {
571  if ( objectName().isEmpty() || ( mSettingGroup.isEmpty() && window()->objectName().isEmpty() ) )
572  return QString(); // cannot get a valid key
573 
574  // save key for load/save state
575  // currently QgsCollapsibleGroupBox/window()/object
576  QString saveKey = '/' + objectName();
577  // QObject* parentWidget = parent();
578  // while ( parentWidget )
579  // {
580  // saveKey = "/" + parentWidget->objectName() + saveKey;
581  // parentWidget = parentWidget->parent();
582  // }
583  // if ( parent() )
584  // saveKey = "/" + parent()->objectName() + saveKey;
585  const QString setgrp = mSettingGroup.isEmpty() ? window()->objectName() : mSettingGroup;
586  saveKey = '/' + setgrp + saveKey;
587  saveKey = QStringLiteral( "QgsCollapsibleGroupBox" ) + saveKey;
588  return saveKey;
589 }
590 
592 {
593  if ( !mSettings )
594  return;
595 
596  if ( !isEnabled() || ( !mSaveCollapsedState && !mSaveCheckedState ) )
597  return;
598 
599  const QString key = saveKey();
600  if ( key.isEmpty() )
601  return;
602 
603  setUpdatesEnabled( false );
604 
605  if ( mSaveCheckedState )
606  {
607  const QVariant val = mSettings->value( key + "/checked" );
608  if ( ! val.isNull() )
609  setChecked( val.toBool() );
610  }
611  if ( mSaveCollapsedState )
612  {
613  const QVariant val = mSettings->value( key + "/collapsed" );
614  if ( ! val.isNull() )
615  setCollapsed( val.toBool() );
616  }
617 
618  setUpdatesEnabled( true );
619 }
620 
622 {
623  if ( !mSettings )
624  return;
625 
626  if ( !mShown || !isEnabled() || ( !mSaveCollapsedState && !mSaveCheckedState ) )
627  return;
628 
629  const QString key = saveKey();
630  if ( key.isEmpty() )
631  return;
632 
633  if ( mSaveCheckedState )
634  mSettings->setValue( key + QStringLiteral( "/checked" ), isChecked() );
635  if ( mSaveCollapsedState )
636  mSettings->setValue( key + QStringLiteral( "/collapsed" ), isCollapsed() );
637 }
638 
639 
641 {
642  mAltDown = ( event->modifiers() & ( Qt::AltModifier | Qt::ControlModifier ) );
643  mShiftDown = ( event->modifiers() & Qt::ShiftModifier );
644  QToolButton::mouseReleaseEvent( event );
645 }
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.
QPointer< QgsSettings > mSettings
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:62
const QString COLLAPSE_HIDE_BORDER_FIX
#define QgsDebugMsgLevel(str, level)
Definition: qgslogger.h:39
#define QgsDebugMsg(str)
Definition: qgslogger.h:38