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