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