QGIS API Documentation  3.8.0-Zanzibar (11aff65)
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 ( parentWidget() )
243  {
244  mSyncParent = parentWidget();
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 
304 {
305  setUpdatesEnabled( false );
306 
307  QgsSettings settings;
308 
309  QStyleOptionGroupBox box;
310  initStyleOption( &box );
311  QRect rectFrame = style()->subControlRect( QStyle::CC_GroupBox, &box,
312  QStyle::SC_GroupBoxFrame, this );
313  QRect rectTitle = titleRect();
314 
315  // margin/offset defaults
316  int marginLeft = 20; // title margin for disclosure triangle
317  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  int offsetStyle = QApplication::style()->objectName().contains( QLatin1String( "macintosh" ) ) ? 8 : 0;
320  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  QRect rectFrame = style()->subControlRect( QStyle::CC_GroupBox, &box,
341  QStyle::SC_GroupBoxFrame, this );
342  QRect rectCheckBox = style()->subControlRect( QStyle::CC_GroupBox, &box,
343  QStyle::SC_GroupBoxCheckBox, this );
344  if ( rectFrame.left() <= 0 )
345  offsetLeft = 6 + rectFrame.left();
346  if ( rectFrame.top() <= 0 )
347  {
348  if ( isCheckable() )
349  {
350  // if is checkable align with checkbox
351  offsetTop = ( rectCheckBox.height() / 2 ) -
352  ( mCollapseButton->height() / 2 ) + rectCheckBox.top();
353  offsetTopTri = offsetTop + 1;
354  }
355  else
356  {
357  offsetTop = 6 + rectFrame.top();
358  offsetTopTri = offsetTop;
359  }
360  }
361  }
362 
363  QgsDebugMsgLevel( QStringLiteral( "groupbox: %1 style: %2 offset: left=%3 top=%4 top2=%5" ).arg(
364  objectName(), QApplication::style()->objectName() ).arg( offsetLeft ).arg( offsetTop ).arg( offsetTopTri ), 5 );
365 
366  // customize style sheet for collapse/expand button and force left-aligned title
367  QString ss;
368  if ( QApplication::style()->objectName().contains( QLatin1String( "macintosh" ) ) )
369  {
370  ss += QLatin1String( "QgsCollapsibleGroupBoxBasic, QgsCollapsibleGroupBox {" );
371  ss += QStringLiteral( " margin-top: %1px;" ).arg( topBuffer + rectFrame.top() );
372  ss += '}';
373  }
374  ss += QLatin1String( "QgsCollapsibleGroupBoxBasic::title, QgsCollapsibleGroupBox::title {" );
375  ss += QLatin1String( " subcontrol-origin: margin;" );
376  ss += QLatin1String( " subcontrol-position: top left;" );
377  ss += QStringLiteral( " margin-left: %1px;" ).arg( marginLeft );
378  ss += QStringLiteral( " margin-right: %1px;" ).arg( marginRight );
379  ss += QStringLiteral( " left: %1px;" ).arg( offsetLeft );
380  ss += QStringLiteral( " top: %1px;" ).arg( offsetTop );
381  if ( QApplication::style()->objectName().contains( QLatin1String( "macintosh" ) ) )
382  {
383  ss += QLatin1String( " background-color: rgba(0,0,0,0)" );
384  }
385  ss += '}';
386  setStyleSheet( styleSheet() + ss );
387 
388  // clear toolbutton default background and border and apply offset
389  QString ssd;
390  ssd = QStringLiteral( "QgsCollapsibleGroupBoxBasic > QToolButton#%1, QgsCollapsibleGroupBox > QToolButton#%1 {" ).arg( mCollapseButton->objectName() );
391  ssd += QLatin1String( " background-color: rgba(255, 255, 255, 0); border: none;" );
392  ssd += QStringLiteral( "} QgsCollapsibleGroupBoxBasic > QToolButton#%1:focus, QgsCollapsibleGroupBox > QToolButton#%1:focus { border: 1px solid palette(highlight); }" ).arg( mCollapseButton->objectName() );
393  mCollapseButton->setStyleSheet( ssd );
394  if ( offsetLeft != 0 || offsetTopTri != 0 )
395  mCollapseButton->move( offsetLeft, offsetTopTri );
396  setUpdatesEnabled( true );
397 }
398 
400 {
401  bool changed = collapse != mCollapsed;
402  mCollapsed = collapse;
403 
404  if ( !isVisible() )
405  return;
406 
407  // for consistent look/spacing across platforms when collapsed
408  if ( ! mInitFlat ) // skip if initially set to flat in Designer
409  setFlat( collapse );
410 
411  // avoid flicker in X11
412  // NOTE: this causes app to crash when loading a project that hits a group box with
413  // 'collapse' set via dynamic property or in code (especially if auto-launching project)
414  // TODO: find another means of avoiding the X11 flicker
415 // QApplication::processEvents();
416 
417  // handle visual fixes for collapsing/expanding
419 
420  // set maximum height to hide contents - does this work in all envs?
421  // setMaximumHeight( collapse ? 25 : 16777215 );
422  setMaximumHeight( collapse ? titleRect().bottom() + 6 : 16777215 );
423  mCollapseButton->setIcon( collapse ? mExpandIcon : mCollapseIcon );
424 
425  // if expanding and is in a QScrollArea, scroll down to make entire widget visible
426  if ( mShown && mScrollOnExpand && !collapse && mParentScrollArea )
427  {
428  // process events so entire widget is shown
429  QApplication::processEvents();
430  mParentScrollArea->setUpdatesEnabled( false );
431  mParentScrollArea->ensureWidgetVisible( this );
432  //and then make sure the top of the widget is visible - otherwise tall group boxes
433  //scroll to their centres, which is disorienting for users
434  mParentScrollArea->ensureWidgetVisible( mCollapseButton, 0, 5 );
435  mParentScrollArea->setUpdatesEnabled( true );
436  }
437  // emit signal for connections using collapsed state
438  if ( changed )
440 }
441 
443 {
444  // handle child widgets so they don't paint while hidden
445  const char *hideKey = "CollGrpBxHide";
446 
447  QString ss = styleSheet();
448  if ( mCollapsed )
449  {
450  if ( !ss.contains( COLLAPSE_HIDE_BORDER_FIX ) )
451  {
453  setStyleSheet( ss );
454  }
455 
456  const auto constChildren = children();
457  for ( QObject *child : constChildren )
458  {
459  QWidget *w = qobject_cast<QWidget *>( child );
460  if ( w && w != mCollapseButton )
461  {
462  w->setProperty( hideKey, true );
463  w->hide();
464  }
465  }
466  }
467  else // on expand
468  {
469  if ( ss.contains( COLLAPSE_HIDE_BORDER_FIX ) )
470  {
471  ss.replace( COLLAPSE_HIDE_BORDER_FIX, QString() );
472  setStyleSheet( ss );
473  }
474 
475  const auto constChildren = children();
476  for ( QObject *child : constChildren )
477  {
478  QWidget *w = qobject_cast<QWidget *>( child );
479  if ( w && w != mCollapseButton )
480  {
481  if ( w->property( hideKey ).toBool() )
482  w->show();
483  }
484  }
485  }
486 }
487 
488 
489 // ----
490 
492  : QgsCollapsibleGroupBoxBasic( parent )
493  , mSettings( settings )
494 {
495  init();
496 }
497 
499  QWidget *parent, QgsSettings *settings )
500  : QgsCollapsibleGroupBoxBasic( title, parent )
501  , mSettings( settings )
502 {
503  init();
504 }
505 
507 {
508  saveState();
509  if ( mDelSettings ) // local settings obj to delete
510  delete mSettings;
511  mSettings = nullptr; // null the pointer (in case of outside settings obj)
512 }
513 
515 {
516  if ( mDelSettings ) // local settings obj to delete
517  delete mSettings;
518  mSettings = settings;
519  mDelSettings = false; // don't delete outside obj
520 }
521 
522 
524 {
525  // use pointer to app qsettings if no custom qsettings specified
526  // custom qsettings object may be from Python plugin
527  mDelSettings = false;
528  if ( !mSettings )
529  {
530  mSettings = new QgsSettings();
531  mDelSettings = true; // only delete obj created by class
532  }
533  // variables
534  mSaveCollapsedState = true;
535  // NOTE: only turn on mSaveCheckedState for groupboxes NOT used
536  // in multiple places or used as options for different parent objects
537  mSaveCheckedState = false;
538 
539  connect( this, &QObject::objectNameChanged, this, &QgsCollapsibleGroupBox::loadState );
540 }
541 
542 void QgsCollapsibleGroupBox::showEvent( QShowEvent *event )
543 {
544  // initialize widget on first show event only
545  if ( mShown )
546  {
547  event->accept();
548  return;
549  }
550 
551  // check if groupbox was set to flat in Designer or in code
552  if ( !mInitFlatChecked )
553  {
554  mInitFlat = isFlat();
555  mInitFlatChecked = true;
556  }
557 
558  loadState();
559 
561 }
562 
564 {
565  if ( objectName().isEmpty() || ( mSettingGroup.isEmpty() && window()->objectName().isEmpty() ) )
566  return QString(); // cannot get a valid key
567 
568  // save key for load/save state
569  // currently QgsCollapsibleGroupBox/window()/object
570  QString saveKey = '/' + objectName();
571  // QObject* parentWidget = parent();
572  // while ( parentWidget )
573  // {
574  // saveKey = "/" + parentWidget->objectName() + saveKey;
575  // parentWidget = parentWidget->parent();
576  // }
577  // if ( parent() )
578  // saveKey = "/" + parent()->objectName() + saveKey;
579  const QString setgrp = mSettingGroup.isEmpty() ? window()->objectName() : mSettingGroup;
580  saveKey = '/' + setgrp + saveKey;
581  saveKey = QStringLiteral( "QgsCollapsibleGroupBox" ) + saveKey;
582  return saveKey;
583 }
584 
586 {
587  if ( !mSettings )
588  return;
589 
590  if ( !isEnabled() || ( !mSaveCollapsedState && !mSaveCheckedState ) )
591  return;
592 
593  const QString key = saveKey();
594  if ( key.isEmpty() )
595  return;
596 
597  setUpdatesEnabled( false );
598 
599  if ( mSaveCheckedState )
600  {
601  QVariant val = mSettings->value( key + "/checked" );
602  if ( ! val.isNull() )
603  setChecked( val.toBool() );
604  }
605  if ( mSaveCollapsedState )
606  {
607  QVariant val = mSettings->value( key + "/collapsed" );
608  if ( ! val.isNull() )
609  setCollapsed( val.toBool() );
610  }
611 
612  setUpdatesEnabled( true );
613 }
614 
616 {
617  if ( !mSettings )
618  return;
619 
620  if ( !mShown || !isEnabled() || ( !mSaveCollapsedState && !mSaveCheckedState ) )
621  return;
622 
623  const QString key = saveKey();
624  if ( key.isEmpty() )
625  return;
626 
627  if ( mSaveCheckedState )
628  mSettings->setValue( key + QStringLiteral( "/checked" ), isChecked() );
629  if ( mSaveCollapsedState )
630  mSettings->setValue( key + QStringLiteral( "/collapsed" ), isCollapsed() );
631 }
632 
633 
635 {
636  mAltDown = ( event->modifiers() & ( Qt::AltModifier | Qt::ControlModifier ) );
637  mShiftDown = ( event->modifiers() & Qt::ShiftModifier );
638  QToolButton::mouseReleaseEvent( event );
639 }
This class is a composition of two QSettings instances:
Definition: qgssettings.h:58
#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 collapseExpandFixes()
Visual fixes for when group box is collapsed/expanded.