QGIS API Documentation  2.18.21-Las Palmas (9fba24a)
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 = "";
58  mAltDown = false;
59  mShiftDown = false;
60  mTitleClicked = false;
61 
62  // init icons
63  mCollapseIcon = QgsApplication::getThemeIcon( "/mIconCollapse.png" );
64  mExpandIcon = QgsApplication::getThemeIcon( "/mIconExpand.png" );
65 
66  // collapse button
68  mCollapseButton->setObjectName( "collapseButton" );
70  mCollapseButton->setFixedSize( 16, 16 );
71  // TODO set size (as well as margins) depending on theme, in updateStyle()
72  mCollapseButton->setIconSize( QSize( 12, 12 ) );
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 
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
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
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
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 {
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
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  {
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  {
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
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( "qgis/stylesheet/groupBoxCustom", QVariant( false ) ).toBool();
309 
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( "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() == "oxygen" )
338  {
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( "macintosh" ) )
370  {
371  ss += "QgsCollapsibleGroupBoxBasic, QgsCollapsibleGroupBox {";
372  ss += QString( " margin-top: %1px;" ).arg( topBuffer + ( usingQgsStyle ? rectTitle.height() + 5 : rectFrame.top() ) );
373  ss += '}';
374  }
375  ss += "QgsCollapsibleGroupBoxBasic::title, QgsCollapsibleGroupBox::title {";
376  ss += " subcontrol-origin: margin;";
377  ss += " subcontrol-position: top left;";
378  ss += QString( " margin-left: %1px;" ).arg( marginLeft );
379  ss += QString( " margin-right: %1px;" ).arg( marginRight );
380  ss += QString( " left: %1px;" ).arg( offsetLeft );
381  ss += QString( " top: %1px;" ).arg( offsetTop );
382  if ( QApplication::style()->objectName().contains( "macintosh" ) )
383  {
384  ss += " 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 = QString( "QgsCollapsibleGroupBoxBasic > QToolButton#%1, QgsCollapsibleGroupBox > QToolButton#%1 {" ).arg( mCollapseButton->objectName() );
392  ssd += " background-color: rgba(255, 255, 255, 0); border: none;";
393  ssd += QString( "} QgsCollapsibleGroupBoxBasic > QToolButton#%1:focus, QgsCollapsibleGroupBox > QToolButton#%1:focus { border: 1px solid palette(highlight); }" ).arg( mCollapseButton->objectName() );
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 );
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
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
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 
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 
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 = ""; // if not set, use window object name
525 }
526 
528 {
529  // initialise widget on first show event only
530  if ( mShown )
531  {
532  event->accept();
533  return;
534  }
535 
536  // check if groupbox was set to flat in Designer or in code
537  if ( !mInitFlatChecked )
538  {
539  mInitFlat = isFlat();
540  mInitFlatChecked = true;
541  }
542 
543  loadState();
544 
546 }
547 
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;
562  saveKey = '/' + setgrp + saveKey;
563  saveKey = "QgsCollapsibleGroupBox" + saveKey;
564  return saveKey;
565 }
566 
568 {
569  if ( !mSettings )
570  return;
571 
572  if ( !isEnabled() || ( !mSaveCollapsedState && !mSaveCheckedState ) )
573  return;
574 
575  setUpdatesEnabled( false );
576 
577  QString key = saveKey();
578  QVariant val;
579  if ( mSaveCheckedState )
580  {
581  val = mSettings->value( key + "/checked" );
582  if ( ! val.isNull() )
583  setChecked( val.toBool() );
584  }
585  if ( mSaveCollapsedState )
586  {
587  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 ( !isEnabled() || ( !mSaveCollapsedState && !mSaveCheckedState ) )
601  return;
602 
603  QString key = saveKey();
604 
605  if ( mSaveCheckedState )
606  mSettings->setValue( key + "/checked", isChecked() );
607  if ( mSaveCollapsedState )
608  mSettings->setValue( key + "/collapsed", isCollapsed() );
609 }
610 
QObject * child(const char *objName, const char *inheritsClass, bool recursiveSearch) const
void setStyleSheet(const QString &styleSheet)
Type type() const
QWidget * window() const
void clicked(bool checked)
#define QgsDebugMsg(str)
Definition: qgslogger.h:33
void setFocusPolicy(Qt::FocusPolicy policy)
QObject * sender() const
QString syncGroup
An optional group to be collapsed and uncollapsed in sync with this group box if the Alt-modifier is ...
QStyle * style() const
static QIcon getThemeIcon(const QString &theName)
Helper to get a theme icon.
const QObjectList & children() const
void collapsedStateChanged(bool collapsed)
Signal emitted when groupbox collapsed/expanded state is changed, and when first shown.
bool isVisible() const
int height() const
virtual QRect subControlRect(ComplexControl control, const QStyleOptionComplex *option, SubControl subControl, const QWidget *widget) const=0
void saveState() const
Will save the collapsed and checked state.
void setCollapsed(bool collapse)
Collapse or uncollapse this groupbox.
void changeEvent(QEvent *event) override
bool isFlat() const
void showEvent(QShowEvent *event) override
void setIcon(const QIcon &icon)
void setShiftDown(bool shiftdown)
QgsCollapsibleGroupBox(QWidget *parent=nullptr, QSettings *settings=nullptr)
QString tr(const char *sourceText, const char *disambiguation, int n)
bool isCollapsed() const
Returns the current collapsed state of this group box.
QgsGroupBoxCollapseButton * mCollapseButton
QList< T > findChildren(const QString &name) const
QString syncGroup() const
Named group which synchronizes collapsing action when triangle is clicked while holding alt modifier ...
void setEnabled(bool)
void processEvents(QFlags< QEventLoop::ProcessEventsFlag > flags)
void setChecked(bool checked)
QVariant property(const char *name) const
bool isNull() const
A groupbox that collapses/expands when toggled.
virtual void mouseReleaseEvent(QMouseEvent *event)
int top() const
void setUpdatesEnabled(bool enable)
void setIconSize(const QSize &size)
int left() const
Qt::MouseButton button() const
void setObjectName(const QString &name)
void setFocusProxy(QWidget *w)
bool isEmpty() const
void move(int x, int y)
void setSyncGroup(const QString &grp)
Named group which synchronizes collapsing action when triangle is clicked while holding alt modifier ...
void mouseReleaseEvent(QMouseEvent *event) override
bool contains(const QPoint &point, bool proper) const
void hide()
void setAutoRaise(bool enable)
void ensureWidgetVisible(QWidget *childWidget, int xmargin, int ymargin)
Qt::KeyboardModifiers modifiers() const
void setSettings(QSettings *settings)
void setFixedSize(const QSize &s)
void mousePressEvent(QMouseEvent *event) override
QVariant value(const QString &key, const QVariant &defaultValue) const
void toggled(bool on)
QgsCollapsibleGroupBoxBasic(QWidget *parent=nullptr)
QStyle * style()
virtual void changeEvent(QEvent *ev)
void setMaximumHeight(int maxh)
QString title() const
void showEvent(QShowEvent *event) override
bool isCheckable() const
QWidget * parentWidget() const
void initStyleOption(QStyleOptionGroupBox *option) const
void loadState()
Will load the collapsed and checked state.
bool toBool() const
bool setProperty(const char *name, const QVariant &value)
virtual void mousePressEvent(QMouseEvent *event)
void show()
const QPoint & pos() const
void setToolTip(const QString &)
bool connect(const QObject *sender, const char *signal, const QObject *receiver, const char *method, Qt::ConnectionType type)
QObject * parent() const
QString arg(qlonglong a, int fieldWidth, int base, const QChar &fillChar) const
virtual bool event(QEvent *e)
QPointer< QSettings > mSettings
void collapseExpandFixes()
Visual fixes for when group box is collapsed/expanded.