QGIS API Documentation  3.0.2-Girona (307d082)
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 
30  : QGroupBox( parent )
31 {
32  init();
33 }
34 
36  QWidget *parent )
37  : QGroupBox( title, parent )
38 {
39  init();
40 }
41 
43 {
44  // variables
45  mCollapsed = false;
46  mInitFlat = false;
47  mInitFlatChecked = false;
48  mScrollOnExpand = true;
49  mShown = false;
50  mParentScrollArea = nullptr;
51  mSyncParent = nullptr;
52  mAltDown = false;
53  mShiftDown = false;
54  mTitleClicked = false;
55 
56  // init icons
57  mCollapseIcon = QgsApplication::getThemeIcon( QStringLiteral( "/mIconCollapse.svg" ) );
58  mExpandIcon = QgsApplication::getThemeIcon( QStringLiteral( "/mIconExpand.svg" ) );
59 
60  // collapse button
62  mCollapseButton->setObjectName( QStringLiteral( "collapseButton" ) );
63  mCollapseButton->setAutoRaise( true );
64  mCollapseButton->setFixedSize( 16, 16 );
65  // TODO set size (as well as margins) depending on theme, in updateStyle()
66  mCollapseButton->setIconSize( QSize( 12, 12 ) );
67  mCollapseButton->setIcon( mCollapseIcon );
68  // FIXME: This appears to mess up parent-child relationships and causes double-frees of children when destroying in Qt5.10, needs further investigation
69  // See also https://github.com/qgis/QGIS/pull/6301
70 #if QT_VERSION < QT_VERSION_CHECK(5, 10, 0)
71  setFocusProxy( mCollapseButton );
72 #endif
73  setFocusPolicy( Qt::StrongFocus );
74 
75  connect( mCollapseButton, &QAbstractButton::clicked, this, &QgsCollapsibleGroupBoxBasic::toggleCollapsed );
76  connect( this, &QGroupBox::toggled, this, &QgsCollapsibleGroupBoxBasic::checkToggled );
77  connect( this, &QGroupBox::clicked, this, &QgsCollapsibleGroupBoxBasic::checkClicked );
78 }
79 
80 void QgsCollapsibleGroupBoxBasic::showEvent( QShowEvent *event )
81 {
82  // initialize widget on first show event only
83  if ( mShown )
84  {
85  event->accept();
86  return;
87  }
88 
89  // check if groupbox was set to flat in Designer or in code
90  if ( !mInitFlatChecked )
91  {
92  mInitFlat = isFlat();
93  mInitFlatChecked = true;
94  }
95 
96  // find parent QScrollArea - this might not work in complex layouts - should we look deeper?
97  if ( parent() && parent()->parent() )
98  mParentScrollArea = qobject_cast<QScrollArea *>( parent()->parent()->parent() );
99  else
100  mParentScrollArea = nullptr;
101  if ( mParentScrollArea )
102  {
103  QgsDebugMsgLevel( "found a QScrollArea parent: " + mParentScrollArea->objectName(), 5 );
104  }
105  else
106  {
107  QgsDebugMsgLevel( "did not find a QScrollArea parent", 5 );
108  }
109 
110  updateStyle();
111 
112  // expand if needed - any calls to setCollapsed() before only set mCollapsed, but have UI effect
113  if ( mCollapsed )
114  {
116  }
117  else
118  {
119  // emit signal for connections using collapsed state
121  }
122 
123  // verify triangle mirrors groupbox's enabled state
124  mCollapseButton->setEnabled( isEnabled() );
125 
126  // set mShown after first setCollapsed call or expanded groupboxes
127  // will scroll scroll areas when first shown
128  mShown = true;
129  event->accept();
130 }
131 
133 {
134  // avoid leaving checkbox in pressed state if alt- or shift-clicking
135  if ( event->modifiers() & ( Qt::AltModifier | Qt::ControlModifier | Qt::ShiftModifier )
136  && titleRect().contains( event->pos() )
137  && isCheckable() )
138  {
139  event->ignore();
140  return;
141  }
142 
143  // default behavior - pass to QGroupBox
144  QGroupBox::mousePressEvent( event );
145 }
146 
148 {
149  mAltDown = ( event->modifiers() & ( Qt::AltModifier | Qt::ControlModifier ) );
150  mShiftDown = ( event->modifiers() & Qt::ShiftModifier );
151  mTitleClicked = ( titleRect().contains( event->pos() ) );
152 
153  // sync group when title is alt-clicked
154  // collapse/expand when title is clicked and non-checkable
155  // expand current and collapse others on shift-click
156  if ( event->button() == Qt::LeftButton && mTitleClicked &&
157  ( mAltDown || mShiftDown || !isCheckable() ) )
158  {
159  toggleCollapsed();
160  return;
161  }
162 
163  // default behavior - pass to QGroupBox
164  QGroupBox::mouseReleaseEvent( event );
165 }
166 
168 {
169  // always re-enable mCollapseButton when groupbox was previously disabled
170  // e.g. resulting from a disabled parent of groupbox, or a signal/slot connection
171 
172  // default behavior - pass to QGroupBox
173  QGroupBox::changeEvent( event );
174 
175  if ( event->type() == QEvent::EnabledChange && isEnabled() )
176  mCollapseButton->setEnabled( true );
177 }
178 
180 {
181  mSyncGroup = grp;
182  QString tipTxt;
183  if ( !grp.isEmpty() )
184  {
185  tipTxt = tr( "Ctrl (or Alt)-click to toggle all" ) + '\n' + tr( "Shift-click to expand, then collapse others" );
186  }
187  mCollapseButton->setToolTip( tipTxt );
188 }
189 
191 {
192  QStyleOptionGroupBox box;
193  initStyleOption( &box );
194  return style()->subControlRect( QStyle::CC_GroupBox, &box,
195  QStyle::SC_GroupBoxLabel, this );
196 }
197 
199 {
200  mCollapseButton->setAltDown( false );
201  mCollapseButton->setShiftDown( false );
202  mAltDown = false;
203  mShiftDown = false;
204 }
205 
207 {
208  Q_UNUSED( chkd );
209  mCollapseButton->setEnabled( true ); // always keep enabled
210 }
211 
213 {
214  // expand/collapse when checkbox toggled by user click.
215  // don't do this on toggle signal, otherwise group boxes will default to collapsed
216  // in option dialog constructors, reducing discovery of options by new users and
217  // overriding user's auto-saved collapsed/expanded state for the group box
218  if ( chkd && isCollapsed() )
219  setCollapsed( false );
220  else if ( ! chkd && ! isCollapsed() )
221  setCollapsed( true );
222 }
223 
225 {
226  // verify if sender is this group box's collapse button
227  QgsGroupBoxCollapseButton *collBtn = qobject_cast<QgsGroupBoxCollapseButton *>( QObject::sender() );
228  bool senderCollBtn = ( collBtn && collBtn == mCollapseButton );
229 
232 
233  // find any sync group siblings and toggle them
234  if ( ( senderCollBtn || mTitleClicked )
235  && ( mAltDown || mShiftDown )
236  && !mSyncGroup.isEmpty() )
237  {
238  QgsDebugMsg( "Alt or Shift key down, syncing group" );
239  // get pointer to parent or grandparent widget
240  if ( parentWidget() )
241  {
242  mSyncParent = parentWidget();
243  if ( mSyncParent->parentWidget() )
244  {
245  // don't use whole app for grandparent (common for dialogs that use main window for parent)
246  if ( mSyncParent->parentWidget()->objectName() != QLatin1String( "QgisApp" ) )
247  {
248  mSyncParent = mSyncParent->parentWidget();
249  }
250  }
251  }
252  else
253  {
254  mSyncParent = nullptr;
255  }
256 
257  if ( mSyncParent )
258  {
259  QgsDebugMsg( "found sync parent: " + mSyncParent->objectName() );
260 
261  bool thisCollapsed = mCollapsed; // get state of current box before its changed
262  Q_FOREACH ( QgsCollapsibleGroupBoxBasic *grpbox, mSyncParent->findChildren<QgsCollapsibleGroupBoxBasic *>() )
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( "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 
301 {
302  setUpdatesEnabled( false );
303 
304  QgsSettings settings;
305  // NOTE: QGIS-Style groupbox styled in app stylesheet
306  bool usingQgsStyle = settings.value( QStringLiteral( "qgis/stylesheet/groupBoxCustom" ), QVariant( false ) ).toBool();
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" ) ) ? ( usingQgsStyle ? 1 : 8 ) : 0;
319  int topBuffer = ( usingQgsStyle ? 3 : 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().toLower() == QLatin1String( "oxygen" ) )
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( QString( "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 ( usingQgsStyle || QApplication::style()->objectName().contains( QLatin1String( "macintosh" ) ) )
368  {
369  ss += QLatin1String( "QgsCollapsibleGroupBoxBasic, QgsCollapsibleGroupBox {" );
370  ss += QStringLiteral( " margin-top: %1px;" ).arg( topBuffer + ( usingQgsStyle ? rectTitle.height() + 5 : 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  if ( mCollapsed )
447  {
448  Q_FOREACH ( QObject *child, children() )
449  {
450  QWidget *w = qobject_cast<QWidget *>( child );
451  if ( w && w != mCollapseButton )
452  {
453  w->setProperty( hideKey, true );
454  w->hide();
455  }
456  }
457  }
458  else // on expand
459  {
460  Q_FOREACH ( QObject *child, children() )
461  {
462  QWidget *w = qobject_cast<QWidget *>( child );
463  if ( w && w != mCollapseButton )
464  {
465  if ( w->property( hideKey ).toBool() )
466  w->show();
467  }
468  }
469  }
470 }
471 
472 
473 // ----
474 
476  : QgsCollapsibleGroupBoxBasic( parent )
477  , mSettings( settings )
478 {
479  init();
480 }
481 
483  QWidget *parent, QgsSettings *settings )
484  : QgsCollapsibleGroupBoxBasic( title, parent )
485  , mSettings( settings )
486 {
487  init();
488 }
489 
491 {
492  saveState();
493  if ( mDelSettings ) // local settings obj to delete
494  delete mSettings;
495  mSettings = nullptr; // null the pointer (in case of outside settings obj)
496 }
497 
499 {
500  if ( mDelSettings ) // local settings obj to delete
501  delete mSettings;
502  mSettings = settings;
503  mDelSettings = false; // don't delete outside obj
504 }
505 
506 
508 {
509  // use pointer to app qsettings if no custom qsettings specified
510  // custom qsettings object may be from Python plugin
511  mDelSettings = false;
512  if ( !mSettings )
513  {
514  mSettings = new QgsSettings();
515  mDelSettings = true; // only delete obj created by class
516  }
517  // variables
518  mSaveCollapsedState = true;
519  // NOTE: only turn on mSaveCheckedState for groupboxes NOT used
520  // in multiple places or used as options for different parent objects
521  mSaveCheckedState = false;
522 
523  connect( this, &QObject::objectNameChanged, this, &QgsCollapsibleGroupBox::loadState );
524 }
525 
526 void QgsCollapsibleGroupBox::showEvent( QShowEvent *event )
527 {
528  // initialize widget on first show event only
529  if ( mShown )
530  {
531  event->accept();
532  return;
533  }
534 
535  // check if groupbox was set to flat in Designer or in code
536  if ( !mInitFlatChecked )
537  {
538  mInitFlat = isFlat();
539  mInitFlatChecked = true;
540  }
541 
542  loadState();
543 
545 }
546 
548 {
549  if ( objectName().isEmpty() || ( mSettingGroup.isEmpty() && window()->objectName().isEmpty() ) )
550  return QString(); // cannot get a valid key
551 
552  // save key for load/save state
553  // currently QgsCollapsibleGroupBox/window()/object
554  QString saveKey = '/' + objectName();
555  // QObject* parentWidget = parent();
556  // while ( parentWidget )
557  // {
558  // saveKey = "/" + parentWidget->objectName() + saveKey;
559  // parentWidget = parentWidget->parent();
560  // }
561  // if ( parent() )
562  // saveKey = "/" + parent()->objectName() + saveKey;
563  const QString setgrp = mSettingGroup.isEmpty() ? window()->objectName() : mSettingGroup;
564  saveKey = '/' + setgrp + saveKey;
565  saveKey = QStringLiteral( "QgsCollapsibleGroupBox" ) + saveKey;
566  return saveKey;
567 }
568 
570 {
571  if ( !mSettings )
572  return;
573 
574  if ( !isEnabled() || ( !mSaveCollapsedState && !mSaveCheckedState ) )
575  return;
576 
577  const QString key = saveKey();
578  if ( key.isEmpty() )
579  return;
580 
581  setUpdatesEnabled( false );
582 
583  if ( mSaveCheckedState )
584  {
585  QVariant val = mSettings->value( key + "/checked" );
586  if ( ! val.isNull() )
587  setChecked( val.toBool() );
588  }
589  if ( mSaveCollapsedState )
590  {
591  QVariant val = mSettings->value( key + "/collapsed" );
592  if ( ! val.isNull() )
593  setCollapsed( val.toBool() );
594  }
595 
596  setUpdatesEnabled( true );
597 }
598 
600 {
601  if ( !mSettings )
602  return;
603 
604  if ( !mShown || !isEnabled() || ( !mSaveCollapsedState && !mSaveCheckedState ) )
605  return;
606 
607  const QString key = saveKey();
608  if ( key.isEmpty() )
609  return;
610 
611  if ( mSaveCheckedState )
612  mSettings->setValue( key + QStringLiteral( "/checked" ), isChecked() );
613  if ( mSaveCollapsedState )
614  mSettings->setValue( key + QStringLiteral( "/collapsed" ), isCollapsed() );
615 }
616 
This class is a composition of two QSettings instances:
Definition: qgssettings.h:57
#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 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
QgsCollapsibleGroupBoxBasic(QWidget *parent=nullptr)
void showEvent(QShowEvent *event) override
QVariant value(const QString &key, const QVariant &defaultValue=QVariant(), const Section section=NoSection) const
Returns the value for setting key.
void loadState()
Will load the collapsed and checked state.
QPointer< QgsSettings > mSettings
QgsCollapsibleGroupBox(QWidget *parent=nullptr, QgsSettings *settings=nullptr)
void collapseExpandFixes()
Visual fixes for when group box is collapsed/expanded.