QGIS API Documentation  2.0.1-Dufour
 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
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
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  bool senderCollBtn = false;
237  QgsGroupBoxCollapseButton* collBtn = qobject_cast<QgsGroupBoxCollapseButton*>( QObject::sender() );
238  senderCollBtn = ( collBtn && collBtn == mCollapseButton );
239 
242 
243  // find any sync group siblings and toggle them
244  if (( senderCollBtn || mTitleClicked )
245  && ( mAltDown || mShiftDown )
246  && !mSyncGroup.isEmpty() )
247  {
248  QgsDebugMsg( "Alt or Shift key down, syncing group" );
249  // get pointer to parent or grandparent widget
250  if ( parentWidget() )
251  {
252  mSyncParent = parentWidget();
253  if ( mSyncParent->parentWidget() )
254  {
255  // don't use whole app for grandparent (common for dialogs that use main window for parent)
256  if ( mSyncParent->parentWidget()->objectName() != QString( "QgisApp" ) )
257  {
258  mSyncParent = mSyncParent->parentWidget();
259  }
260  }
261  }
262  else
263  {
264  mSyncParent = 0;
265  }
266 
267  if ( mSyncParent )
268  {
269  QgsDebugMsg( "found sync parent: " + mSyncParent->objectName() );
270 
271  bool thisCollapsed = mCollapsed; // get state of current box before its changed
272  foreach ( QgsCollapsibleGroupBoxBasic *grpbox, mSyncParent->findChildren<QgsCollapsibleGroupBoxBasic*>() )
273  {
274  if ( grpbox->syncGroup() == syncGroup() && grpbox->isEnabled() )
275  {
276  if ( mShiftDown && grpbox == dynamic_cast<QgsCollapsibleGroupBoxBasic *>( this ) )
277  {
278  // expand current group box on shift-click
279  setCollapsed( false );
280  }
281  else
282  {
283  grpbox->setCollapsed( mShiftDown ? true : !thisCollapsed );
284  }
285  }
286  }
287 
288  clearModifiers();
289  return;
290  }
291  else
292  {
293  QgsDebugMsg( "did not find a sync parent" );
294  }
295  }
296 
297  // expand current group box on shift-click, even if no sync group
298  if ( mShiftDown )
299  {
300  setCollapsed( false );
301  }
302  else
303  {
305  }
306 
307  clearModifiers();
308 }
309 
311 {
312  setUpdatesEnabled( false );
313 
314  QSettings settings;
315  // NOTE: QGIS-Style groupbox styled in app stylesheet
316  bool usingQgsStyle = settings.value( "qgis/stylesheet/groupBoxCustom", QVariant( false ) ).toBool();
317 
318  QStyleOptionGroupBox box;
319  initStyleOption( &box );
320  QRect rectFrame = style()->subControlRect( QStyle::CC_GroupBox, &box,
321  QStyle::SC_GroupBoxFrame, this );
322  QRect rectTitle = titleRect();
323 
324  // margin/offset defaults
325  int marginLeft = 20; // title margin for disclosure triangle
326  int marginRight = 5; // a little bit of space on the right, to match space on the left
327  int offsetLeft = 0; // offset for oxygen theme
328  int offsetStyle = QApplication::style()->objectName().contains( "macintosh" ) ? ( usingQgsStyle ? 1 : 8 ) : 0;
329  int topBuffer = ( usingQgsStyle ? 3 : 1 ) + offsetStyle; // space between top of title or triangle and widget above
330  int offsetTop = topBuffer;
331  int offsetTopTri = topBuffer; // offset for triangle
332 
333  if ( mCollapseButton->height() < rectTitle.height() ) // triangle's height > title text's, offset triangle
334  {
335  offsetTopTri += ( rectTitle.height() - mCollapseButton->height() ) / 2 ;
336 // offsetTopTri += rectTitle.top();
337  }
338  else if ( rectTitle.height() < mCollapseButton->height() ) // title text's height < triangle's, offset title
339  {
340  offsetTop += ( mCollapseButton->height() - rectTitle.height() ) / 2;
341  }
342 
343  // calculate offset if frame overlaps triangle (oxygen theme)
344  // using an offset of 6 pixels from frame border
345  if ( QApplication::style()->objectName().toLower() == "oxygen" )
346  {
347  QStyleOptionGroupBox box;
348  initStyleOption( &box );
349  QRect rectFrame = style()->subControlRect( QStyle::CC_GroupBox, &box,
350  QStyle::SC_GroupBoxFrame, this );
351  QRect rectCheckBox = style()->subControlRect( QStyle::CC_GroupBox, &box,
352  QStyle::SC_GroupBoxCheckBox, this );
353  if ( rectFrame.left() <= 0 )
354  offsetLeft = 6 + rectFrame.left();
355  if ( rectFrame.top() <= 0 )
356  {
357  if ( isCheckable() )
358  {
359  // if is checkable align with checkbox
360  offsetTop = ( rectCheckBox.height() / 2 ) -
361  ( mCollapseButton->height() / 2 ) + rectCheckBox.top();
362  offsetTopTri = offsetTop + 1;
363  }
364  else
365  {
366  offsetTop = 6 + rectFrame.top();
367  offsetTopTri = offsetTop;
368  }
369  }
370  }
371 
372  QgsDebugMsg( QString( "groupbox: %1 style: %2 offset: left=%3 top=%4 top2=%5" ).arg(
373  objectName() ).arg( QApplication::style()->objectName() ).arg( offsetLeft ).arg( offsetTop ).arg( offsetTopTri ) );
374 
375  // customize style sheet for collapse/expand button and force left-aligned title
376  QString ss;
377  if ( usingQgsStyle || QApplication::style()->objectName().contains( "macintosh" ) )
378  {
379  ss += "QgsCollapsibleGroupBoxBasic, QgsCollapsibleGroupBox {";
380  ss += QString( " margin-top: %1px;" ).arg( topBuffer + ( usingQgsStyle ? rectTitle.height() + 5 : rectFrame.top() ) );
381  ss += "}";
382  }
383  ss += "QgsCollapsibleGroupBoxBasic::title, QgsCollapsibleGroupBox::title {";
384  ss += " subcontrol-origin: margin;";
385  ss += " subcontrol-position: top left;";
386  ss += QString( " margin-left: %1px;" ).arg( marginLeft );
387  ss += QString( " margin-right: %1px;" ).arg( marginRight );
388  ss += QString( " left: %1px;" ).arg( offsetLeft );
389  ss += QString( " top: %1px;" ).arg( offsetTop );
390  if ( QApplication::style()->objectName().contains( "macintosh" ) )
391  {
392  ss += " background-color: rgba(0,0,0,0)";
393  }
394  ss += "}";
395  setStyleSheet( ss );
396 
397  // clear toolbutton default background and border and apply offset
398  QString ssd;
399  ssd = QString( "QgsCollapsibleGroupBoxBasic > QToolButton#%1, QgsCollapsibleGroupBox > QToolButton#%1 {" ).arg( mCollapseButton->objectName() );
400  ssd += " background-color: rgba(255, 255, 255, 0); border: none;";
401  ssd += "}";
402  mCollapseButton->setStyleSheet( ssd );
403  if ( offsetLeft != 0 || offsetTopTri != 0 )
404  mCollapseButton->move( offsetLeft, offsetTopTri );
405 
406  setUpdatesEnabled( true );
407 }
408 
410 {
411  mCollapsed = collapse;
412 
413  if ( !isVisible() )
414  return;
415 
416  // for consistent look/spacing across platforms when collapsed
417  if ( ! mInitFlat ) // skip if initially set to flat in Designer
418  setFlat( collapse );
419 
420  // avoid flicker in X11
421  // NOTE: this causes app to crash when loading a project that hits a group box with
422  // 'collapse' set via dynamic property or in code (especially if auto-launching project)
423  // TODO: find another means of avoiding the X11 flicker
424 // QApplication::processEvents();
425 
426  // handle visual fixes for collapsing/expanding
428 
429  // set maximum height to hide contents - does this work in all envs?
430  // setMaximumHeight( collapse ? 25 : 16777215 );
431  setMaximumHeight( collapse ? titleRect().bottom() + 6 : 16777215 );
432  mCollapseButton->setIcon( collapse ? mExpandIcon : mCollapseIcon );
433 
434  // if expanding and is in a QScrollArea, scroll down to make entire widget visible
435  if ( mShown && mScrollOnExpand && !collapse && mParentScrollArea )
436  {
437  // process events so entire widget is shown
438  QApplication::processEvents();
439  mParentScrollArea->ensureWidgetVisible( this );
440  }
441  // emit signal for connections using collapsed state
443 }
444 
446 {
447  if ( QApplication::style()->objectName().contains( "macintosh" ) )
448  {
449  // handle QPushButtons in form layouts that stay partly visible on collapse (Qt bug?)
450  // hide on collapse for fix, but only show buttons that were specifically hidden when expanding
451  // key hiding off of this group box's object name so it does not affect child group boxes
452  const QByteArray objKey = QString( "CollGrpBxHiddenButton_%1" ).arg( objectName() ).toUtf8();
453  const char* pbHideKey = objKey.constData();
454 
455  // handle child group box widgets that don't hide their frames on collapse of parent
456  const char* gbHideKey = "CollGrpBxHideGrpBx";
457 
458  if ( mCollapsed )
459  {
460  // first hide all child group boxes, regardless of whether they are collapsible
461  foreach ( QGroupBox* gbx, findChildren<QGroupBox *>() )
462  {
463  if ( gbx->isVisible() && !gbx->property( gbHideKey ).isValid() )
464  {
465  gbx->setProperty( gbHideKey, QVariant( true ) );
466  gbx->hide();
467  }
468  }
469 
470  // hide still visible push buttons belonging to this group box
471  foreach ( QPushButton* pBtn, findChildren<QPushButton *>() )
472  {
473  if ( pBtn->isVisible() && !pBtn->property( pbHideKey ).isValid() )
474  {
475  pBtn->setProperty( pbHideKey, QVariant( true ) );
476  pBtn->hide();
477  }
478  }
479  }
480  else // on expand
481  {
482  // first show push buttons belonging to this group box
483  foreach ( QPushButton* pBtn, findChildren<QPushButton *>() )
484  {
485  if ( pBtn->property( pbHideKey ).isValid() ) // don't have to check bool value
486  {
487  pBtn->setProperty( pbHideKey, QVariant() ); // remove property
488  pBtn->show();
489  }
490  }
491 
492  // show all hidden child group boxes
493  foreach ( QGroupBox* gbx, findChildren<QGroupBox *>() )
494  {
495  if ( gbx->property( gbHideKey ).isValid() ) // don't have to check bool value
496  {
497  gbx->setProperty( gbHideKey, QVariant() ); // remove property
498  gbx->show();
499  }
500  }
501  }
502  }
503 }
504 
505 
506 // ----
507 
508 QgsCollapsibleGroupBox::QgsCollapsibleGroupBox( QWidget *parent, QSettings* settings )
509  : QgsCollapsibleGroupBoxBasic( parent ), mSettings( settings )
510 {
511  init();
512 }
513 
515  QWidget *parent, QSettings* settings )
516  : QgsCollapsibleGroupBoxBasic( title, parent ), mSettings( settings )
517 {
518  init();
519 }
520 
522 {
523  //QgsDebugMsg( "Entered" );
524  saveState();
525  if ( mDelSettings ) // local settings obj to delete
526  delete mSettings;
527  mSettings = 0; // null the pointer (in case of outside settings obj)
528 }
529 
530 void QgsCollapsibleGroupBox::setSettings( QSettings* settings )
531 {
532  if ( mDelSettings ) // local settings obj to delete
533  delete mSettings;
534  mSettings = settings;
535  mDelSettings = false; // don't delete outside obj
536 }
537 
538 
540 {
541  //QgsDebugMsg( "Entered" );
542  // use pointer to app qsettings if no custom qsettings specified
543  // custom qsettings object may be from Python plugin
544  mDelSettings = false;
545  if ( !mSettings )
546  {
547  mSettings = new QSettings();
548  mDelSettings = true; // only delete obj created by class
549  }
550  // variables
551  mSaveCollapsedState = true;
552  // NOTE: only turn on mSaveCheckedState for groupboxes NOT used
553  // in multiple places or used as options for different parent objects
554  mSaveCheckedState = false;
555  mSettingGroup = ""; // if not set, use window object name
556 }
557 
558 void QgsCollapsibleGroupBox::showEvent( QShowEvent * event )
559 {
560  //QgsDebugMsg( "Entered" );
561  // initialise widget on first show event only
562  if ( mShown )
563  {
564  event->accept();
565  return;
566  }
567 
568  // check if groupbox was set to flat in Designer or in code
569  if ( !mInitFlatChecked )
570  {
571  mInitFlat = isFlat();
572  mInitFlatChecked = true;
573  }
574 
575  loadState();
576 
578 }
579 
581 {
582  // save key for load/save state
583  // currently QgsCollapsibleGroupBox/window()/object
584  QString saveKey = "/" + objectName();
585  // QObject* parentWidget = parent();
586  // while ( parentWidget != NULL )
587  // {
588  // saveKey = "/" + parentWidget->objectName() + saveKey;
589  // parentWidget = parentWidget->parent();
590  // }
591  // if ( parent() != NULL )
592  // saveKey = "/" + parent()->objectName() + saveKey;
593  QString setgrp = mSettingGroup.isEmpty() ? window()->objectName() : mSettingGroup;
594  saveKey = "/" + setgrp + saveKey;
595  saveKey = "QgsCollapsibleGroupBox" + saveKey;
596  return saveKey;
597 }
598 
600 {
601  //QgsDebugMsg( "Entered" );
602  if ( !mSettings )
603  return;
604 
605  if ( !isEnabled() || ( !mSaveCollapsedState && !mSaveCheckedState ) )
606  return;
607 
608  setUpdatesEnabled( false );
609 
610  QString key = saveKey();
611  QVariant val;
612  if ( mSaveCheckedState )
613  {
614  val = mSettings->value( key + "/checked" );
615  if ( ! val.isNull() )
616  setChecked( val.toBool() );
617  }
618  if ( mSaveCollapsedState )
619  {
620  val = mSettings->value( key + "/collapsed" );
621  if ( ! val.isNull() )
622  setCollapsed( val.toBool() );
623  }
624 
625  setUpdatesEnabled( true );
626 }
627 
629 {
630  //QgsDebugMsg( "Entered" );
631  if ( !mSettings )
632  return;
633 
634  if ( !isEnabled() || ( !mSaveCollapsedState && !mSaveCheckedState ) )
635  return;
636 
637  QString key = saveKey();
638 
639  if ( mSaveCheckedState )
640  mSettings->setValue( key + "/checked", isChecked() );
641  if ( mSaveCollapsedState )
642  mSettings->setValue( key + "/collapsed", isCollapsed() );
643 }
644