QGIS API Documentation  2.99.0-Master (b058df7)
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  setFocusProxy( mCollapseButton );
69  setFocusPolicy( Qt::StrongFocus );
70 
71  connect( mCollapseButton, &QAbstractButton::clicked, this, &QgsCollapsibleGroupBoxBasic::toggleCollapsed );
72  connect( this, &QGroupBox::toggled, this, &QgsCollapsibleGroupBoxBasic::checkToggled );
73  connect( this, &QGroupBox::clicked, this, &QgsCollapsibleGroupBoxBasic::checkClicked );
74 }
75 
76 void QgsCollapsibleGroupBoxBasic::showEvent( QShowEvent *event )
77 {
78  // initialize widget on first show event only
79  if ( mShown )
80  {
81  event->accept();
82  return;
83  }
84 
85  // check if groupbox was set to flat in Designer or in code
86  if ( !mInitFlatChecked )
87  {
88  mInitFlat = isFlat();
89  mInitFlatChecked = true;
90  }
91 
92  // find parent QScrollArea - this might not work in complex layouts - should we look deeper?
93  if ( parent() && parent()->parent() )
94  mParentScrollArea = qobject_cast<QScrollArea *>( parent()->parent()->parent() );
95  else
96  mParentScrollArea = nullptr;
97  if ( mParentScrollArea )
98  {
99  QgsDebugMsgLevel( "found a QScrollArea parent: " + mParentScrollArea->objectName(), 5 );
100  }
101  else
102  {
103  QgsDebugMsgLevel( "did not find a QScrollArea parent", 5 );
104  }
105 
106  updateStyle();
107 
108  // expand if needed - any calls to setCollapsed() before only set mCollapsed, but have UI effect
109  if ( mCollapsed )
110  {
112  }
113  else
114  {
115  // emit signal for connections using collapsed state
117  }
118 
119  // verify triangle mirrors groupbox's enabled state
120  mCollapseButton->setEnabled( isEnabled() );
121 
122  // set mShown after first setCollapsed call or expanded groupboxes
123  // will scroll scroll areas when first shown
124  mShown = true;
125  event->accept();
126 }
127 
129 {
130  // avoid leaving checkbox in pressed state if alt- or shift-clicking
131  if ( event->modifiers() & ( Qt::AltModifier | Qt::ControlModifier | Qt::ShiftModifier )
132  && titleRect().contains( event->pos() )
133  && isCheckable() )
134  {
135  event->ignore();
136  return;
137  }
138 
139  // default behavior - pass to QGroupBox
140  QGroupBox::mousePressEvent( event );
141 }
142 
144 {
145  mAltDown = ( event->modifiers() & ( Qt::AltModifier | Qt::ControlModifier ) );
146  mShiftDown = ( event->modifiers() & Qt::ShiftModifier );
147  mTitleClicked = ( titleRect().contains( event->pos() ) );
148 
149  // sync group when title is alt-clicked
150  // collapse/expand when title is clicked and non-checkable
151  // expand current and collapse others on shift-click
152  if ( event->button() == Qt::LeftButton && mTitleClicked &&
153  ( mAltDown || mShiftDown || !isCheckable() ) )
154  {
155  toggleCollapsed();
156  return;
157  }
158 
159  // default behavior - pass to QGroupBox
160  QGroupBox::mouseReleaseEvent( event );
161 }
162 
164 {
165  // always re-enable mCollapseButton when groupbox was previously disabled
166  // e.g. resulting from a disabled parent of groupbox, or a signal/slot connection
167 
168  // default behavior - pass to QGroupBox
169  QGroupBox::changeEvent( event );
170 
171  if ( event->type() == QEvent::EnabledChange && isEnabled() )
172  mCollapseButton->setEnabled( true );
173 }
174 
176 {
177  mSyncGroup = grp;
178  QString tipTxt;
179  if ( !grp.isEmpty() )
180  {
181  tipTxt = tr( "Ctrl (or Alt)-click to toggle all" ) + '\n' + tr( "Shift-click to expand, then collapse others" );
182  }
183  mCollapseButton->setToolTip( tipTxt );
184 }
185 
187 {
188  QStyleOptionGroupBox box;
189  initStyleOption( &box );
190  return style()->subControlRect( QStyle::CC_GroupBox, &box,
191  QStyle::SC_GroupBoxLabel, this );
192 }
193 
195 {
196  mCollapseButton->setAltDown( false );
197  mCollapseButton->setShiftDown( false );
198  mAltDown = false;
199  mShiftDown = false;
200 }
201 
203 {
204  Q_UNUSED( chkd );
205  mCollapseButton->setEnabled( true ); // always keep enabled
206 }
207 
209 {
210  // expand/collapse when checkbox toggled by user click.
211  // don't do this on toggle signal, otherwise group boxes will default to collapsed
212  // in option dialog constructors, reducing discovery of options by new users and
213  // overriding user's auto-saved collapsed/expanded state for the group box
214  if ( chkd && isCollapsed() )
215  setCollapsed( false );
216  else if ( ! chkd && ! isCollapsed() )
217  setCollapsed( true );
218 }
219 
221 {
222  // verify if sender is this group box's collapse button
223  QgsGroupBoxCollapseButton *collBtn = qobject_cast<QgsGroupBoxCollapseButton *>( QObject::sender() );
224  bool senderCollBtn = ( collBtn && collBtn == mCollapseButton );
225 
228 
229  // find any sync group siblings and toggle them
230  if ( ( senderCollBtn || mTitleClicked )
231  && ( mAltDown || mShiftDown )
232  && !mSyncGroup.isEmpty() )
233  {
234  QgsDebugMsg( "Alt or Shift key down, syncing group" );
235  // get pointer to parent or grandparent widget
236  if ( parentWidget() )
237  {
238  mSyncParent = parentWidget();
239  if ( mSyncParent->parentWidget() )
240  {
241  // don't use whole app for grandparent (common for dialogs that use main window for parent)
242  if ( mSyncParent->parentWidget()->objectName() != QLatin1String( "QgisApp" ) )
243  {
244  mSyncParent = mSyncParent->parentWidget();
245  }
246  }
247  }
248  else
249  {
250  mSyncParent = nullptr;
251  }
252 
253  if ( mSyncParent )
254  {
255  QgsDebugMsg( "found sync parent: " + mSyncParent->objectName() );
256 
257  bool thisCollapsed = mCollapsed; // get state of current box before its changed
258  Q_FOREACH ( QgsCollapsibleGroupBoxBasic *grpbox, mSyncParent->findChildren<QgsCollapsibleGroupBoxBasic *>() )
259  {
260  if ( grpbox->syncGroup() == syncGroup() && grpbox->isEnabled() )
261  {
262  if ( mShiftDown && grpbox == this )
263  {
264  // expand current group box on shift-click
265  setCollapsed( false );
266  }
267  else
268  {
269  grpbox->setCollapsed( mShiftDown ? true : !thisCollapsed );
270  }
271  }
272  }
273 
274  clearModifiers();
275  return;
276  }
277  else
278  {
279  QgsDebugMsg( "did not find a sync parent" );
280  }
281  }
282 
283  // expand current group box on shift-click, even if no sync group
284  if ( mShiftDown )
285  {
286  setCollapsed( false );
287  }
288  else
289  {
291  }
292 
293  clearModifiers();
294 }
295 
297 {
298  setUpdatesEnabled( false );
299 
300  QgsSettings settings;
301  // NOTE: QGIS-Style groupbox styled in app stylesheet
302  bool usingQgsStyle = settings.value( QStringLiteral( "qgis/stylesheet/groupBoxCustom" ), QVariant( false ) ).toBool();
303 
304  QStyleOptionGroupBox box;
305  initStyleOption( &box );
306  QRect rectFrame = style()->subControlRect( QStyle::CC_GroupBox, &box,
307  QStyle::SC_GroupBoxFrame, this );
308  QRect rectTitle = titleRect();
309 
310  // margin/offset defaults
311  int marginLeft = 20; // title margin for disclosure triangle
312  int marginRight = 5; // a little bit of space on the right, to match space on the left
313  int offsetLeft = 0; // offset for oxygen theme
314  int offsetStyle = QApplication::style()->objectName().contains( QLatin1String( "macintosh" ) ) ? ( usingQgsStyle ? 1 : 8 ) : 0;
315  int topBuffer = ( usingQgsStyle ? 3 : 1 ) + offsetStyle; // space between top of title or triangle and widget above
316  int offsetTop = topBuffer;
317  int offsetTopTri = topBuffer; // offset for triangle
318 
319  if ( mCollapseButton->height() < rectTitle.height() ) // triangle's height > title text's, offset triangle
320  {
321  offsetTopTri += ( rectTitle.height() - mCollapseButton->height() ) / 2;
322 // offsetTopTri += rectTitle.top();
323  }
324  else if ( rectTitle.height() < mCollapseButton->height() ) // title text's height < triangle's, offset title
325  {
326  offsetTop += ( mCollapseButton->height() - rectTitle.height() ) / 2;
327  }
328 
329  // calculate offset if frame overlaps triangle (oxygen theme)
330  // using an offset of 6 pixels from frame border
331  if ( QApplication::style()->objectName().toLower() == QLatin1String( "oxygen" ) )
332  {
333  QStyleOptionGroupBox box;
334  initStyleOption( &box );
335  QRect rectFrame = style()->subControlRect( QStyle::CC_GroupBox, &box,
336  QStyle::SC_GroupBoxFrame, this );
337  QRect rectCheckBox = style()->subControlRect( QStyle::CC_GroupBox, &box,
338  QStyle::SC_GroupBoxCheckBox, this );
339  if ( rectFrame.left() <= 0 )
340  offsetLeft = 6 + rectFrame.left();
341  if ( rectFrame.top() <= 0 )
342  {
343  if ( isCheckable() )
344  {
345  // if is checkable align with checkbox
346  offsetTop = ( rectCheckBox.height() / 2 ) -
347  ( mCollapseButton->height() / 2 ) + rectCheckBox.top();
348  offsetTopTri = offsetTop + 1;
349  }
350  else
351  {
352  offsetTop = 6 + rectFrame.top();
353  offsetTopTri = offsetTop;
354  }
355  }
356  }
357 
358  QgsDebugMsgLevel( QString( "groupbox: %1 style: %2 offset: left=%3 top=%4 top2=%5" ).arg(
359  objectName(), QApplication::style()->objectName() ).arg( offsetLeft ).arg( offsetTop ).arg( offsetTopTri ), 5 );
360 
361  // customize style sheet for collapse/expand button and force left-aligned title
362  QString ss;
363  if ( usingQgsStyle || QApplication::style()->objectName().contains( QLatin1String( "macintosh" ) ) )
364  {
365  ss += QLatin1String( "QgsCollapsibleGroupBoxBasic, QgsCollapsibleGroupBox {" );
366  ss += QStringLiteral( " margin-top: %1px;" ).arg( topBuffer + ( usingQgsStyle ? rectTitle.height() + 5 : rectFrame.top() ) );
367  ss += '}';
368  }
369  ss += QLatin1String( "QgsCollapsibleGroupBoxBasic::title, QgsCollapsibleGroupBox::title {" );
370  ss += QLatin1String( " subcontrol-origin: margin;" );
371  ss += QLatin1String( " subcontrol-position: top left;" );
372  ss += QStringLiteral( " margin-left: %1px;" ).arg( marginLeft );
373  ss += QStringLiteral( " margin-right: %1px;" ).arg( marginRight );
374  ss += QStringLiteral( " left: %1px;" ).arg( offsetLeft );
375  ss += QStringLiteral( " top: %1px;" ).arg( offsetTop );
376  if ( QApplication::style()->objectName().contains( QLatin1String( "macintosh" ) ) )
377  {
378  ss += QLatin1String( " background-color: rgba(0,0,0,0)" );
379  }
380  ss += '}';
381  setStyleSheet( styleSheet() + ss );
382 
383  // clear toolbutton default background and border and apply offset
384  QString ssd;
385  ssd = QStringLiteral( "QgsCollapsibleGroupBoxBasic > QToolButton#%1, QgsCollapsibleGroupBox > QToolButton#%1 {" ).arg( mCollapseButton->objectName() );
386  ssd += QLatin1String( " background-color: rgba(255, 255, 255, 0); border: none;" );
387  ssd += QStringLiteral( "} QgsCollapsibleGroupBoxBasic > QToolButton#%1:focus, QgsCollapsibleGroupBox > QToolButton#%1:focus { border: 1px solid palette(highlight); }" ).arg( mCollapseButton->objectName() );
388  mCollapseButton->setStyleSheet( ssd );
389  if ( offsetLeft != 0 || offsetTopTri != 0 )
390  mCollapseButton->move( offsetLeft, offsetTopTri );
391  setUpdatesEnabled( true );
392 }
393 
395 {
396  bool changed = collapse != mCollapsed;
397  mCollapsed = collapse;
398 
399  if ( !isVisible() )
400  return;
401 
402  // for consistent look/spacing across platforms when collapsed
403  if ( ! mInitFlat ) // skip if initially set to flat in Designer
404  setFlat( collapse );
405 
406  // avoid flicker in X11
407  // NOTE: this causes app to crash when loading a project that hits a group box with
408  // 'collapse' set via dynamic property or in code (especially if auto-launching project)
409  // TODO: find another means of avoiding the X11 flicker
410 // QApplication::processEvents();
411 
412  // handle visual fixes for collapsing/expanding
414 
415  // set maximum height to hide contents - does this work in all envs?
416  // setMaximumHeight( collapse ? 25 : 16777215 );
417  setMaximumHeight( collapse ? titleRect().bottom() + 6 : 16777215 );
418  mCollapseButton->setIcon( collapse ? mExpandIcon : mCollapseIcon );
419 
420  // if expanding and is in a QScrollArea, scroll down to make entire widget visible
421  if ( mShown && mScrollOnExpand && !collapse && mParentScrollArea )
422  {
423  // process events so entire widget is shown
424  QApplication::processEvents();
425  mParentScrollArea->setUpdatesEnabled( false );
426  mParentScrollArea->ensureWidgetVisible( this );
427  //and then make sure the top of the widget is visible - otherwise tall group boxes
428  //scroll to their centres, which is disorienting for users
429  mParentScrollArea->ensureWidgetVisible( mCollapseButton, 0, 5 );
430  mParentScrollArea->setUpdatesEnabled( true );
431  }
432  // emit signal for connections using collapsed state
433  if ( changed )
435 }
436 
438 {
439  // handle child widgets so they don't paint while hidden
440  const char *hideKey = "CollGrpBxHide";
441 
442  if ( mCollapsed )
443  {
444  Q_FOREACH ( QObject *child, children() )
445  {
446  QWidget *w = qobject_cast<QWidget *>( child );
447  if ( w && w != mCollapseButton )
448  {
449  w->setProperty( hideKey, true );
450  w->hide();
451  }
452  }
453  }
454  else // on expand
455  {
456  Q_FOREACH ( QObject *child, children() )
457  {
458  QWidget *w = qobject_cast<QWidget *>( child );
459  if ( w && w != mCollapseButton )
460  {
461  if ( w->property( hideKey ).toBool() )
462  w->show();
463  }
464  }
465  }
466 }
467 
468 
469 // ----
470 
472  : QgsCollapsibleGroupBoxBasic( parent )
473  , mSettings( settings )
474 {
475  init();
476 }
477 
479  QWidget *parent, QgsSettings *settings )
480  : QgsCollapsibleGroupBoxBasic( title, parent )
481  , mSettings( settings )
482 {
483  init();
484 }
485 
487 {
488  saveState();
489  if ( mDelSettings ) // local settings obj to delete
490  delete mSettings;
491  mSettings = nullptr; // null the pointer (in case of outside settings obj)
492 }
493 
495 {
496  if ( mDelSettings ) // local settings obj to delete
497  delete mSettings;
498  mSettings = settings;
499  mDelSettings = false; // don't delete outside obj
500 }
501 
502 
504 {
505  // use pointer to app qsettings if no custom qsettings specified
506  // custom qsettings object may be from Python plugin
507  mDelSettings = false;
508  if ( !mSettings )
509  {
510  mSettings = new QgsSettings();
511  mDelSettings = true; // only delete obj created by class
512  }
513  // variables
514  mSaveCollapsedState = true;
515  // NOTE: only turn on mSaveCheckedState for groupboxes NOT used
516  // in multiple places or used as options for different parent objects
517  mSaveCheckedState = false;
518 
519  connect( this, &QObject::objectNameChanged, this, &QgsCollapsibleGroupBox::loadState );
520 }
521 
522 void QgsCollapsibleGroupBox::showEvent( QShowEvent *event )
523 {
524  // initialize widget on first show event only
525  if ( mShown )
526  {
527  event->accept();
528  return;
529  }
530 
531  // check if groupbox was set to flat in Designer or in code
532  if ( !mInitFlatChecked )
533  {
534  mInitFlat = isFlat();
535  mInitFlatChecked = true;
536  }
537 
538  loadState();
539 
541 }
542 
544 {
545  if ( objectName().isEmpty() || ( mSettingGroup.isEmpty() && window()->objectName().isEmpty() ) )
546  return QString(); // cannot get a valid key
547 
548  // save key for load/save state
549  // currently QgsCollapsibleGroupBox/window()/object
550  QString saveKey = '/' + objectName();
551  // QObject* parentWidget = parent();
552  // while ( parentWidget )
553  // {
554  // saveKey = "/" + parentWidget->objectName() + saveKey;
555  // parentWidget = parentWidget->parent();
556  // }
557  // if ( parent() )
558  // saveKey = "/" + parent()->objectName() + saveKey;
559  const QString setgrp = mSettingGroup.isEmpty() ? window()->objectName() : mSettingGroup;
560  saveKey = '/' + setgrp + saveKey;
561  saveKey = QStringLiteral( "QgsCollapsibleGroupBox" ) + saveKey;
562  return saveKey;
563 }
564 
566 {
567  if ( !mSettings )
568  return;
569 
570  if ( !isEnabled() || ( !mSaveCollapsedState && !mSaveCheckedState ) )
571  return;
572 
573  const QString key = saveKey();
574  if ( key.isEmpty() )
575  return;
576 
577  setUpdatesEnabled( false );
578 
579  if ( mSaveCheckedState )
580  {
581  QVariant val = mSettings->value( key + "/checked" );
582  if ( ! val.isNull() )
583  setChecked( val.toBool() );
584  }
585  if ( mSaveCollapsedState )
586  {
587  QVariant val = mSettings->value( key + "/collapsed" );
588  if ( ! val.isNull() )
589  setCollapsed( val.toBool() );
590  }
591 
592  setUpdatesEnabled( true );
593 }
594 
596 {
597  if ( !mSettings )
598  return;
599 
600  if ( !mShown || !isEnabled() || ( !mSaveCollapsedState && !mSaveCheckedState ) )
601  return;
602 
603  const QString key = saveKey();
604  if ( key.isEmpty() )
605  return;
606 
607  if ( mSaveCheckedState )
608  mSettings->setValue( key + QStringLiteral( "/checked" ), isChecked() );
609  if ( mSaveCollapsedState )
610  mSettings->setValue( key + QStringLiteral( "/collapsed" ), isCollapsed() );
611 }
612 
This class is a composition of two QSettings instances:
Definition: qgssettings.h:55
#define QgsDebugMsg(str)
Definition: qgslogger.h:37
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:38
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.