QGIS API Documentation  2.99.0-Master (6a61179)
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 
20 #include "qgsapplication.h"
21 #include "qgslogger.h"
22 
23 #include <QToolButton>
24 #include <QMouseEvent>
25 #include <QPushButton>
26 #include <QStyleOptionGroupBox>
27 #include <QSettings>
28 #include <QScrollArea>
29 
31  : QGroupBox( parent )
32 {
33  init();
34 }
35 
37  QWidget *parent )
38  : QGroupBox( title, parent )
39 {
40  init();
41 }
42 
44 {
45 }
46 
48 {
49  // variables
50  mCollapsed = false;
51  mInitFlat = false;
52  mInitFlatChecked = false;
53  mScrollOnExpand = true;
54  mShown = false;
55  mParentScrollArea = nullptr;
56  mSyncParent = nullptr;
57  mSyncGroup = QLatin1String( "" );
58  mAltDown = false;
59  mShiftDown = false;
60  mTitleClicked = false;
61 
62  // init icons
63  mCollapseIcon = QgsApplication::getThemeIcon( QStringLiteral( "/mIconCollapse.png" ) );
64  mExpandIcon = QgsApplication::getThemeIcon( QStringLiteral( "/mIconExpand.png" ) );
65 
66  // collapse button
68  mCollapseButton->setObjectName( QStringLiteral( "collapseButton" ) );
69  mCollapseButton->setAutoRaise( true );
70  mCollapseButton->setFixedSize( 16, 16 );
71  // TODO set size (as well as margins) depending on theme, in updateStyle()
72  mCollapseButton->setIconSize( QSize( 12, 12 ) );
73  mCollapseButton->setIcon( mCollapseIcon );
74  setFocusProxy( mCollapseButton );
75  setFocusPolicy( Qt::StrongFocus );
76 
77  connect( mCollapseButton, SIGNAL( clicked() ), this, SLOT( toggleCollapsed() ) );
78  connect( this, SIGNAL( toggled( bool ) ), this, SLOT( checkToggled( bool ) ) );
79  connect( this, SIGNAL( clicked( bool ) ), this, SLOT( checkClicked( bool ) ) );
80 }
81 
82 void QgsCollapsibleGroupBoxBasic::showEvent( QShowEvent * event )
83 {
84  // initialise 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 = dynamic_cast<QScrollArea*>( parent()->parent()->parent() );
101  else
102  mParentScrollArea = nullptr;
103  if ( mParentScrollArea )
104  {
105  QgsDebugMsg( "found a QScrollArea parent: " + mParentScrollArea->objectName() );
106  }
107  else
108  {
109  QgsDebugMsg( "did not find a QScrollArea parent" );
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 behaviour - 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 behaviour - 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 behaviour - 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( "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( "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  QSettings settings;
307  // NOTE: QGIS-Style groupbox styled in app stylesheet
308  bool usingQgsStyle = settings.value( QStringLiteral( "qgis/stylesheet/groupBoxCustom" ), QVariant( false ) ).toBool();
309 
310  QStyleOptionGroupBox box;
311  initStyleOption( &box );
312  QRect rectFrame = style()->subControlRect( QStyle::CC_GroupBox, &box,
313  QStyle::SC_GroupBoxFrame, this );
314  QRect rectTitle = titleRect();
315 
316  // margin/offset defaults
317  int marginLeft = 20; // title margin for disclosure triangle
318  int marginRight = 5; // a little bit of space on the right, to match space on the left
319  int offsetLeft = 0; // offset for oxygen theme
320  int offsetStyle = QApplication::style()->objectName().contains( QLatin1String( "macintosh" ) ) ? ( usingQgsStyle ? 1 : 8 ) : 0;
321  int topBuffer = ( usingQgsStyle ? 3 : 1 ) + offsetStyle; // space between top of title or triangle and widget above
322  int offsetTop = topBuffer;
323  int offsetTopTri = topBuffer; // offset for triangle
324 
325  if ( mCollapseButton->height() < rectTitle.height() ) // triangle's height > title text's, offset triangle
326  {
327  offsetTopTri += ( rectTitle.height() - mCollapseButton->height() ) / 2;
328 // offsetTopTri += rectTitle.top();
329  }
330  else if ( rectTitle.height() < mCollapseButton->height() ) // title text's height < triangle's, offset title
331  {
332  offsetTop += ( mCollapseButton->height() - rectTitle.height() ) / 2;
333  }
334 
335  // calculate offset if frame overlaps triangle (oxygen theme)
336  // using an offset of 6 pixels from frame border
337  if ( QApplication::style()->objectName().toLower() == QLatin1String( "oxygen" ) )
338  {
339  QStyleOptionGroupBox box;
340  initStyleOption( &box );
341  QRect rectFrame = style()->subControlRect( QStyle::CC_GroupBox, &box,
342  QStyle::SC_GroupBoxFrame, this );
343  QRect rectCheckBox = style()->subControlRect( QStyle::CC_GroupBox, &box,
344  QStyle::SC_GroupBoxCheckBox, this );
345  if ( rectFrame.left() <= 0 )
346  offsetLeft = 6 + rectFrame.left();
347  if ( rectFrame.top() <= 0 )
348  {
349  if ( isCheckable() )
350  {
351  // if is checkable align with checkbox
352  offsetTop = ( rectCheckBox.height() / 2 ) -
353  ( mCollapseButton->height() / 2 ) + rectCheckBox.top();
354  offsetTopTri = offsetTop + 1;
355  }
356  else
357  {
358  offsetTop = 6 + rectFrame.top();
359  offsetTopTri = offsetTop;
360  }
361  }
362  }
363 
364  QgsDebugMsg( QString( "groupbox: %1 style: %2 offset: left=%3 top=%4 top2=%5" ).arg(
365  objectName(), QApplication::style()->objectName() ).arg( offsetLeft ).arg( offsetTop ).arg( offsetTopTri ) );
366 
367  // customize style sheet for collapse/expand button and force left-aligned title
368  QString ss;
369  if ( usingQgsStyle || QApplication::style()->objectName().contains( QLatin1String( "macintosh" ) ) )
370  {
371  ss += QLatin1String( "QgsCollapsibleGroupBoxBasic, QgsCollapsibleGroupBox {" );
372  ss += QStringLiteral( " margin-top: %1px;" ).arg( topBuffer + ( usingQgsStyle ? rectTitle.height() + 5 : rectFrame.top() ) );
373  ss += '}';
374  }
375  ss += QLatin1String( "QgsCollapsibleGroupBoxBasic::title, QgsCollapsibleGroupBox::title {" );
376  ss += QLatin1String( " subcontrol-origin: margin;" );
377  ss += QLatin1String( " subcontrol-position: top left;" );
378  ss += QStringLiteral( " margin-left: %1px;" ).arg( marginLeft );
379  ss += QStringLiteral( " margin-right: %1px;" ).arg( marginRight );
380  ss += QStringLiteral( " left: %1px;" ).arg( offsetLeft );
381  ss += QStringLiteral( " top: %1px;" ).arg( offsetTop );
382  if ( QApplication::style()->objectName().contains( QLatin1String( "macintosh" ) ) )
383  {
384  ss += QLatin1String( " background-color: rgba(0,0,0,0)" );
385  }
386  ss += '}';
387  setStyleSheet( ss );
388 
389  // clear toolbutton default background and border and apply offset
390  QString ssd;
391  ssd = QStringLiteral( "QgsCollapsibleGroupBoxBasic > QToolButton#%1, QgsCollapsibleGroupBox > QToolButton#%1 {" ).arg( mCollapseButton->objectName() );
392  ssd += QLatin1String( " background-color: rgba(255, 255, 255, 0); border: none;" );
393  ssd += QStringLiteral( "} QgsCollapsibleGroupBoxBasic > QToolButton#%1:focus, QgsCollapsibleGroupBox > QToolButton#%1:focus { border: 1px solid palette(highlight); }" ).arg( mCollapseButton->objectName() );
394  mCollapseButton->setStyleSheet( ssd );
395  if ( offsetLeft != 0 || offsetTopTri != 0 )
396  mCollapseButton->move( offsetLeft, offsetTopTri );
397  setUpdatesEnabled( true );
398 }
399 
401 {
402  bool changed = collapse != mCollapsed;
403  mCollapsed = collapse;
404 
405  if ( !isVisible() )
406  return;
407 
408  // for consistent look/spacing across platforms when collapsed
409  if ( ! mInitFlat ) // skip if initially set to flat in Designer
410  setFlat( collapse );
411 
412  // avoid flicker in X11
413  // NOTE: this causes app to crash when loading a project that hits a group box with
414  // 'collapse' set via dynamic property or in code (especially if auto-launching project)
415  // TODO: find another means of avoiding the X11 flicker
416 // QApplication::processEvents();
417 
418  // handle visual fixes for collapsing/expanding
420 
421  // set maximum height to hide contents - does this work in all envs?
422  // setMaximumHeight( collapse ? 25 : 16777215 );
423  setMaximumHeight( collapse ? titleRect().bottom() + 6 : 16777215 );
424  mCollapseButton->setIcon( collapse ? mExpandIcon : mCollapseIcon );
425 
426  // if expanding and is in a QScrollArea, scroll down to make entire widget visible
427  if ( mShown && mScrollOnExpand && !collapse && mParentScrollArea )
428  {
429  // process events so entire widget is shown
430  QApplication::processEvents();
431  mParentScrollArea->setUpdatesEnabled( false );
432  mParentScrollArea->ensureWidgetVisible( this );
433  //and then make sure the top of the widget is visible - otherwise tall group boxes
434  //scroll to their centres, which is disorienting for users
435  mParentScrollArea->ensureWidgetVisible( mCollapseButton, 0, 5 );
436  mParentScrollArea->setUpdatesEnabled( true );
437  }
438  // emit signal for connections using collapsed state
439  if ( changed )
441 }
442 
444 {
445  // handle child widgets so they don't paint while hidden
446  const char* hideKey = "CollGrpBxHide";
447 
448  if ( mCollapsed )
449  {
450  Q_FOREACH ( QObject* child, children() )
451  {
452  QWidget* w = qobject_cast<QWidget*>( child );
453  if ( w && w != mCollapseButton )
454  {
455  w->setProperty( hideKey, true );
456  w->hide();
457  }
458  }
459  }
460  else // on expand
461  {
462  Q_FOREACH ( QObject* child, children() )
463  {
464  QWidget* w = qobject_cast<QWidget*>( child );
465  if ( w && w != mCollapseButton )
466  {
467  if ( w->property( hideKey ).toBool() )
468  w->show();
469  }
470  }
471  }
472 }
473 
474 
475 // ----
476 
477 QgsCollapsibleGroupBox::QgsCollapsibleGroupBox( QWidget *parent, QSettings* settings )
478  : QgsCollapsibleGroupBoxBasic( parent )
479  , mSettings( settings )
480 {
481  init();
482 }
483 
485  QWidget *parent, QSettings* settings )
486  : QgsCollapsibleGroupBoxBasic( title, parent )
487  , mSettings( settings )
488 {
489  init();
490 }
491 
493 {
494  saveState();
495  if ( mDelSettings ) // local settings obj to delete
496  delete mSettings;
497  mSettings = nullptr; // null the pointer (in case of outside settings obj)
498 }
499 
500 void QgsCollapsibleGroupBox::setSettings( QSettings* settings )
501 {
502  if ( mDelSettings ) // local settings obj to delete
503  delete mSettings;
504  mSettings = settings;
505  mDelSettings = false; // don't delete outside obj
506 }
507 
508 
510 {
511  // use pointer to app qsettings if no custom qsettings specified
512  // custom qsettings object may be from Python plugin
513  mDelSettings = false;
514  if ( !mSettings )
515  {
516  mSettings = new QSettings();
517  mDelSettings = true; // only delete obj created by class
518  }
519  // variables
520  mSaveCollapsedState = true;
521  // NOTE: only turn on mSaveCheckedState for groupboxes NOT used
522  // in multiple places or used as options for different parent objects
523  mSaveCheckedState = false;
524  mSettingGroup = QLatin1String( "" ); // if not set, use window object name
525 
526  connect( this, &QObject::objectNameChanged, this, &QgsCollapsibleGroupBox::loadState );
527 }
528 
529 void QgsCollapsibleGroupBox::showEvent( QShowEvent * event )
530 {
531  // initialise widget on first show event only
532  if ( mShown )
533  {
534  event->accept();
535  return;
536  }
537 
538  // check if groupbox was set to flat in Designer or in code
539  if ( !mInitFlatChecked )
540  {
541  mInitFlat = isFlat();
542  mInitFlatChecked = true;
543  }
544 
545  loadState();
546 
548 }
549 
551 {
552  if ( objectName().isEmpty() || ( mSettingGroup.isEmpty() && window()->objectName().isEmpty() ) )
553  return QString(); // cannot get a valid key
554 
555  // save key for load/save state
556  // currently QgsCollapsibleGroupBox/window()/object
557  QString saveKey = '/' + objectName();
558  // QObject* parentWidget = parent();
559  // while ( parentWidget )
560  // {
561  // saveKey = "/" + parentWidget->objectName() + saveKey;
562  // parentWidget = parentWidget->parent();
563  // }
564  // if ( parent() )
565  // saveKey = "/" + parent()->objectName() + saveKey;
566  const QString setgrp = mSettingGroup.isEmpty() ? window()->objectName() : mSettingGroup;
567  saveKey = '/' + setgrp + saveKey;
568  saveKey = QStringLiteral( "QgsCollapsibleGroupBox" ) + saveKey;
569  return saveKey;
570 }
571 
573 {
574  if ( !mSettings )
575  return;
576 
577  if ( !isEnabled() || ( !mSaveCollapsedState && !mSaveCheckedState ) )
578  return;
579 
580  const QString key = saveKey();
581  if ( key.isEmpty() )
582  return;
583 
584  setUpdatesEnabled( false );
585 
586  if ( mSaveCheckedState )
587  {
588  QVariant val = mSettings->value( key + "/checked" );
589  if ( ! val.isNull() )
590  setChecked( val.toBool() );
591  }
592  if ( mSaveCollapsedState )
593  {
594  QVariant val = mSettings->value( key + "/collapsed" );
595  if ( ! val.isNull() )
596  setCollapsed( val.toBool() );
597  }
598 
599  setUpdatesEnabled( true );
600 }
601 
603 {
604  if ( !mSettings )
605  return;
606 
607  if ( !mShown || !isEnabled() || ( !mSaveCollapsedState && !mSaveCheckedState ) )
608  return;
609 
610  const QString key = saveKey();
611  if ( key.isEmpty() )
612  return;
613 
614  if ( mSaveCheckedState )
615  mSettings->setValue( key + QStringLiteral( "/checked" ), isChecked() );
616  if ( mSaveCollapsedState )
617  mSettings->setValue( key + QStringLiteral( "/collapsed" ), isCollapsed() );
618 }
619 
#define QgsDebugMsg(str)
Definition: qgslogger.h:33
QString syncGroup
An optional group to be collapsed and uncollapsed in sync with this group box if the Alt-modifier is ...
static QIcon getThemeIcon(const QString &theName)
Helper to get a theme icon.
void collapsedStateChanged(bool collapsed)
Signal emitted when groupbox collapsed/expanded state is changed, and when first shown.
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)
QgsCollapsibleGroupBox(QWidget *parent=nullptr, QSettings *settings=nullptr)
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 ...
A groupbox that collapses/expands when toggled.
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 setSettings(QSettings *settings)
void mousePressEvent(QMouseEvent *event) override
QgsCollapsibleGroupBoxBasic(QWidget *parent=nullptr)
void showEvent(QShowEvent *event) override
void loadState()
Will load the collapsed and checked state.
QPointer< QSettings > mSettings
void collapseExpandFixes()
Visual fixes for when group box is collapsed/expanded.