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