QGIS API Documentation  3.4.15-Madeira (e83d02e274)
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  Q_FOREACH ( QgsCollapsibleGroupBoxBasic *grpbox, mSyncParent->findChildren<QgsCollapsibleGroupBoxBasic *>() )
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 
280  clearModifiers();
281  return;
282  }
283  else
284  {
285  QgsDebugMsg( QStringLiteral( "did not find a sync parent" ) );
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 
299  clearModifiers();
300 }
301 
303 {
304  setUpdatesEnabled( false );
305 
306  QgsSettings settings;
307 
308  QStyleOptionGroupBox box;
309  initStyleOption( &box );
310  QRect rectFrame = style()->subControlRect( QStyle::CC_GroupBox, &box,
311  QStyle::SC_GroupBoxFrame, this );
312  QRect rectTitle = titleRect();
313 
314  // margin/offset defaults
315  int marginLeft = 20; // title margin for disclosure triangle
316  int marginRight = 5; // a little bit of space on the right, to match space on the left
317  int offsetLeft = 0; // offset for oxygen theme
318  int offsetStyle = QApplication::style()->objectName().contains( QLatin1String( "macintosh" ) ) ? 8 : 0;
319  int topBuffer = 1 + offsetStyle; // space between top of title or triangle and widget above
320  int offsetTop = topBuffer;
321  int offsetTopTri = topBuffer; // offset for triangle
322 
323  if ( mCollapseButton->height() < rectTitle.height() ) // triangle's height > title text's, offset triangle
324  {
325  offsetTopTri += ( rectTitle.height() - mCollapseButton->height() ) / 2;
326 // offsetTopTri += rectTitle.top();
327  }
328  else if ( rectTitle.height() < mCollapseButton->height() ) // title text's height < triangle's, offset title
329  {
330  offsetTop += ( mCollapseButton->height() - rectTitle.height() ) / 2;
331  }
332 
333  // calculate offset if frame overlaps triangle (oxygen theme)
334  // using an offset of 6 pixels from frame border
335  if ( QApplication::style()->objectName().compare( QLatin1String( "oxygen" ), Qt::CaseInsensitive ) == 0 )
336  {
337  QStyleOptionGroupBox box;
338  initStyleOption( &box );
339  QRect rectFrame = style()->subControlRect( QStyle::CC_GroupBox, &box,
340  QStyle::SC_GroupBoxFrame, this );
341  QRect rectCheckBox = style()->subControlRect( QStyle::CC_GroupBox, &box,
342  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 ) -
351  ( mCollapseButton->height() / 2 ) + rectCheckBox.top();
352  offsetTopTri = offsetTop + 1;
353  }
354  else
355  {
356  offsetTop = 6 + rectFrame.top();
357  offsetTopTri = offsetTop;
358  }
359  }
360  }
361 
362  QgsDebugMsgLevel( QStringLiteral( "groupbox: %1 style: %2 offset: left=%3 top=%4 top2=%5" ).arg(
363  objectName(), QApplication::style()->objectName() ).arg( offsetLeft ).arg( offsetTop ).arg( offsetTopTri ), 5 );
364 
365  // customize style sheet for collapse/expand button and force left-aligned title
366  QString ss;
367  if ( QApplication::style()->objectName().contains( QLatin1String( "macintosh" ) ) )
368  {
369  ss += QLatin1String( "QgsCollapsibleGroupBoxBasic, QgsCollapsibleGroupBox {" );
370  ss += QStringLiteral( " margin-top: %1px;" ).arg( topBuffer + rectFrame.top() );
371  ss += '}';
372  }
373  ss += QLatin1String( "QgsCollapsibleGroupBoxBasic::title, QgsCollapsibleGroupBox::title {" );
374  ss += QLatin1String( " subcontrol-origin: margin;" );
375  ss += QLatin1String( " subcontrol-position: top left;" );
376  ss += QStringLiteral( " margin-left: %1px;" ).arg( marginLeft );
377  ss += QStringLiteral( " margin-right: %1px;" ).arg( marginRight );
378  ss += QStringLiteral( " left: %1px;" ).arg( offsetLeft );
379  ss += QStringLiteral( " top: %1px;" ).arg( offsetTop );
380  if ( QApplication::style()->objectName().contains( QLatin1String( "macintosh" ) ) )
381  {
382  ss += QLatin1String( " background-color: rgba(0,0,0,0)" );
383  }
384  ss += '}';
385  setStyleSheet( styleSheet() + ss );
386 
387  // clear toolbutton default background and border and apply offset
388  QString ssd;
389  ssd = QStringLiteral( "QgsCollapsibleGroupBoxBasic > QToolButton#%1, QgsCollapsibleGroupBox > QToolButton#%1 {" ).arg( mCollapseButton->objectName() );
390  ssd += QLatin1String( " background-color: rgba(255, 255, 255, 0); border: none;" );
391  ssd += QStringLiteral( "} QgsCollapsibleGroupBoxBasic > QToolButton#%1:focus, QgsCollapsibleGroupBox > QToolButton#%1:focus { border: 1px solid palette(highlight); }" ).arg( mCollapseButton->objectName() );
392  mCollapseButton->setStyleSheet( ssd );
393  if ( offsetLeft != 0 || offsetTopTri != 0 )
394  mCollapseButton->move( offsetLeft, offsetTopTri );
395  setUpdatesEnabled( true );
396 }
397 
399 {
400  bool changed = collapse != mCollapsed;
401  mCollapsed = collapse;
402 
403  if ( !isVisible() )
404  return;
405 
406  // for consistent look/spacing across platforms when collapsed
407  if ( ! mInitFlat ) // skip if initially set to flat in Designer
408  setFlat( collapse );
409 
410  // avoid flicker in X11
411  // NOTE: this causes app to crash when loading a project that hits a group box with
412  // 'collapse' set via dynamic property or in code (especially if auto-launching project)
413  // TODO: find another means of avoiding the X11 flicker
414 // QApplication::processEvents();
415 
416  // handle visual fixes for collapsing/expanding
418 
419  // set maximum height to hide contents - does this work in all envs?
420  // setMaximumHeight( collapse ? 25 : 16777215 );
421  setMaximumHeight( collapse ? titleRect().bottom() + 6 : 16777215 );
422  mCollapseButton->setIcon( collapse ? mExpandIcon : mCollapseIcon );
423 
424  // if expanding and is in a QScrollArea, scroll down to make entire widget visible
425  if ( mShown && mScrollOnExpand && !collapse && mParentScrollArea )
426  {
427  // process events so entire widget is shown
428  QApplication::processEvents();
429  mParentScrollArea->setUpdatesEnabled( false );
430  mParentScrollArea->ensureWidgetVisible( this );
431  //and then make sure the top of the widget is visible - otherwise tall group boxes
432  //scroll to their centres, which is disorienting for users
433  mParentScrollArea->ensureWidgetVisible( mCollapseButton, 0, 5 );
434  mParentScrollArea->setUpdatesEnabled( true );
435  }
436  // emit signal for connections using collapsed state
437  if ( changed )
439 }
440 
442 {
443  // handle child widgets so they don't paint while hidden
444  const char *hideKey = "CollGrpBxHide";
445 
446  QString ss = styleSheet();
447  if ( mCollapsed )
448  {
449  if ( !ss.contains( COLLAPSE_HIDE_BORDER_FIX ) )
450  {
452  setStyleSheet( ss );
453  }
454 
455  Q_FOREACH ( QObject *child, children() )
456  {
457  QWidget *w = qobject_cast<QWidget *>( child );
458  if ( w && w != mCollapseButton )
459  {
460  w->setProperty( hideKey, true );
461  w->hide();
462  }
463  }
464  }
465  else // on expand
466  {
467  if ( ss.contains( COLLAPSE_HIDE_BORDER_FIX ) )
468  {
469  ss.replace( COLLAPSE_HIDE_BORDER_FIX, QString() );
470  setStyleSheet( ss );
471  }
472 
473  Q_FOREACH ( QObject *child, children() )
474  {
475  QWidget *w = qobject_cast<QWidget *>( child );
476  if ( w && w != mCollapseButton )
477  {
478  if ( w->property( hideKey ).toBool() )
479  w->show();
480  }
481  }
482  }
483 }
484 
485 
486 // ----
487 
489  : QgsCollapsibleGroupBoxBasic( parent )
490  , mSettings( settings )
491 {
492  init();
493 }
494 
496  QWidget *parent, QgsSettings *settings )
497  : QgsCollapsibleGroupBoxBasic( title, parent )
498  , mSettings( settings )
499 {
500  init();
501 }
502 
504 {
505  saveState();
506  if ( mDelSettings ) // local settings obj to delete
507  delete mSettings;
508  mSettings = nullptr; // null the pointer (in case of outside settings obj)
509 }
510 
512 {
513  if ( mDelSettings ) // local settings obj to delete
514  delete mSettings;
515  mSettings = settings;
516  mDelSettings = false; // don't delete outside obj
517 }
518 
519 
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 
539 void QgsCollapsibleGroupBox::showEvent( QShowEvent *event )
540 {
541  // initialize widget on first show event only
542  if ( mShown )
543  {
544  event->accept();
545  return;
546  }
547 
548  // check if groupbox was set to flat in Designer or in code
549  if ( !mInitFlatChecked )
550  {
551  mInitFlat = isFlat();
552  mInitFlatChecked = true;
553  }
554 
555  loadState();
556 
558 }
559 
561 {
562  if ( objectName().isEmpty() || ( mSettingGroup.isEmpty() && window()->objectName().isEmpty() ) )
563  return QString(); // cannot get a valid key
564 
565  // save key for load/save state
566  // currently QgsCollapsibleGroupBox/window()/object
567  QString saveKey = '/' + objectName();
568  // QObject* parentWidget = parent();
569  // while ( parentWidget )
570  // {
571  // saveKey = "/" + parentWidget->objectName() + saveKey;
572  // parentWidget = parentWidget->parent();
573  // }
574  // if ( parent() )
575  // saveKey = "/" + parent()->objectName() + saveKey;
576  const QString setgrp = mSettingGroup.isEmpty() ? window()->objectName() : mSettingGroup;
577  saveKey = '/' + setgrp + saveKey;
578  saveKey = QStringLiteral( "QgsCollapsibleGroupBox" ) + saveKey;
579  return saveKey;
580 }
581 
583 {
584  if ( !mSettings )
585  return;
586 
587  if ( !isEnabled() || ( !mSaveCollapsedState && !mSaveCheckedState ) )
588  return;
589 
590  const QString key = saveKey();
591  if ( key.isEmpty() )
592  return;
593 
594  setUpdatesEnabled( false );
595 
596  if ( mSaveCheckedState )
597  {
598  QVariant val = mSettings->value( key + "/checked" );
599  if ( ! val.isNull() )
600  setChecked( val.toBool() );
601  }
602  if ( mSaveCollapsedState )
603  {
604  QVariant val = mSettings->value( key + "/collapsed" );
605  if ( ! val.isNull() )
606  setCollapsed( val.toBool() );
607  }
608 
609  setUpdatesEnabled( true );
610 }
611 
613 {
614  if ( !mSettings )
615  return;
616 
617  if ( !mShown || !isEnabled() || ( !mSaveCollapsedState && !mSaveCheckedState ) )
618  return;
619 
620  const QString key = saveKey();
621  if ( key.isEmpty() )
622  return;
623 
624  if ( mSaveCheckedState )
625  mSettings->setValue( key + QStringLiteral( "/checked" ), isChecked() );
626  if ( mSaveCollapsedState )
627  mSettings->setValue( key + QStringLiteral( "/collapsed" ), isCollapsed() );
628 }
629 
630 
632 {
633  mAltDown = ( event->modifiers() & ( Qt::AltModifier | Qt::ControlModifier ) );
634  mShiftDown = ( event->modifiers() & Qt::ShiftModifier );
635  QToolButton::mouseReleaseEvent( event );
636 }
This class is a composition of two QSettings instances:
Definition: qgssettings.h:58
#define QgsDebugMsg(str)
Definition: qgslogger.h:38
QString syncGroup
An optional group to be collapsed and uncollapsed in sync with this group box if the Alt-modifier is ...
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 setCollapsed(bool collapse)
Collapse or uncollapse this groupbox.
void changeEvent(QEvent *event) override
void showEvent(QShowEvent *event) override
void setShiftDown(bool shiftdown)
QgsGroupBoxCollapseButton * mCollapseButton
void setSettings(QgsSettings *settings)
bool isCollapsed() const
Returns the current collapsed state of this group box.
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 saveState() const
Will save the collapsed and checked state.
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.
QString syncGroup() const
Named group which synchronizes collapsing action when triangle is clicked while holding alt modifier ...