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