QGIS API Documentation  2.2.0-Valmiera
 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  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 
405  setUpdatesEnabled( true );
406 }
407 
409 {
410  mCollapsed = collapse;
411 
412  if ( !isVisible() )
413  return;
414 
415  // for consistent look/spacing across platforms when collapsed
416  if ( ! mInitFlat ) // skip if initially set to flat in Designer
417  setFlat( collapse );
418 
419  // avoid flicker in X11
420  // NOTE: this causes app to crash when loading a project that hits a group box with
421  // 'collapse' set via dynamic property or in code (especially if auto-launching project)
422  // TODO: find another means of avoiding the X11 flicker
423 // QApplication::processEvents();
424 
425  // handle visual fixes for collapsing/expanding
427 
428  // set maximum height to hide contents - does this work in all envs?
429  // setMaximumHeight( collapse ? 25 : 16777215 );
430  setMaximumHeight( collapse ? titleRect().bottom() + 6 : 16777215 );
431  mCollapseButton->setIcon( collapse ? mExpandIcon : mCollapseIcon );
432 
433  // if expanding and is in a QScrollArea, scroll down to make entire widget visible
434  if ( mShown && mScrollOnExpand && !collapse && mParentScrollArea )
435  {
436  // process events so entire widget is shown
437  QApplication::processEvents();
438  mParentScrollArea->ensureWidgetVisible( this );
439  }
440  // emit signal for connections using collapsed state
442 }
443 
445 {
446  if ( QApplication::style()->objectName().contains( "macintosh" ) )
447  {
448  // handle QPushButtons in form layouts that stay partly visible on collapse (Qt bug?)
449  // hide on collapse for fix, but only show buttons that were specifically hidden when expanding
450  // key hiding off of this group box's object name so it does not affect child group boxes
451  const QByteArray objKey = QString( "CollGrpBxHiddenButton_%1" ).arg( objectName() ).toUtf8();
452  const char* pbHideKey = objKey.constData();
453 
454  // handle child group box widgets that don't hide their frames on collapse of parent
455  const char* gbHideKey = "CollGrpBxHideGrpBx";
456 
457  if ( mCollapsed )
458  {
459  // first hide all child group boxes, regardless of whether they are collapsible
460  foreach ( QGroupBox* gbx, findChildren<QGroupBox *>() )
461  {
462  if ( gbx->isVisible() && !gbx->property( gbHideKey ).isValid() )
463  {
464  gbx->setProperty( gbHideKey, QVariant( true ) );
465  gbx->hide();
466  }
467  }
468 
469  // hide still visible push buttons belonging to this group box
470  foreach ( QPushButton* pBtn, findChildren<QPushButton *>() )
471  {
472  if ( pBtn->isVisible() && !pBtn->property( pbHideKey ).isValid() )
473  {
474  pBtn->setProperty( pbHideKey, QVariant( true ) );
475  pBtn->hide();
476  }
477  }
478  }
479  else // on expand
480  {
481  // first show push buttons belonging to this group box
482  foreach ( QPushButton* pBtn, findChildren<QPushButton *>() )
483  {
484  if ( pBtn->property( pbHideKey ).isValid() ) // don't have to check bool value
485  {
486  pBtn->setProperty( pbHideKey, QVariant() ); // remove property
487  pBtn->show();
488  }
489  }
490 
491  // show all hidden child group boxes
492  foreach ( QGroupBox* gbx, findChildren<QGroupBox *>() )
493  {
494  if ( gbx->property( gbHideKey ).isValid() ) // don't have to check bool value
495  {
496  gbx->setProperty( gbHideKey, QVariant() ); // remove property
497  gbx->show();
498  }
499  }
500  }
501  }
502 }
503 
504 
505 // ----
506 
507 QgsCollapsibleGroupBox::QgsCollapsibleGroupBox( QWidget *parent, QSettings* settings )
508  : QgsCollapsibleGroupBoxBasic( parent ), mSettings( settings )
509 {
510  init();
511 }
512 
514  QWidget *parent, QSettings* settings )
515  : QgsCollapsibleGroupBoxBasic( title, parent ), mSettings( settings )
516 {
517  init();
518 }
519 
521 {
522  //QgsDebugMsg( "Entered" );
523  saveState();
524  if ( mDelSettings ) // local settings obj to delete
525  delete mSettings;
526  mSettings = 0; // null the pointer (in case of outside settings obj)
527 }
528 
529 void QgsCollapsibleGroupBox::setSettings( QSettings* settings )
530 {
531  if ( mDelSettings ) // local settings obj to delete
532  delete mSettings;
533  mSettings = settings;
534  mDelSettings = false; // don't delete outside obj
535 }
536 
537 
539 {
540  //QgsDebugMsg( "Entered" );
541  // use pointer to app qsettings if no custom qsettings specified
542  // custom qsettings object may be from Python plugin
543  mDelSettings = false;
544  if ( !mSettings )
545  {
546  mSettings = new QSettings();
547  mDelSettings = true; // only delete obj created by class
548  }
549  // variables
550  mSaveCollapsedState = true;
551  // NOTE: only turn on mSaveCheckedState for groupboxes NOT used
552  // in multiple places or used as options for different parent objects
553  mSaveCheckedState = false;
554  mSettingGroup = ""; // if not set, use window object name
555 }
556 
557 void QgsCollapsibleGroupBox::showEvent( QShowEvent * event )
558 {
559  //QgsDebugMsg( "Entered" );
560  // initialise widget on first show event only
561  if ( mShown )
562  {
563  event->accept();
564  return;
565  }
566 
567  // check if groupbox was set to flat in Designer or in code
568  if ( !mInitFlatChecked )
569  {
570  mInitFlat = isFlat();
571  mInitFlatChecked = true;
572  }
573 
574  loadState();
575 
577 }
578 
580 {
581  // save key for load/save state
582  // currently QgsCollapsibleGroupBox/window()/object
583  QString saveKey = "/" + objectName();
584  // QObject* parentWidget = parent();
585  // while ( parentWidget != NULL )
586  // {
587  // saveKey = "/" + parentWidget->objectName() + saveKey;
588  // parentWidget = parentWidget->parent();
589  // }
590  // if ( parent() != NULL )
591  // saveKey = "/" + parent()->objectName() + saveKey;
592  QString setgrp = mSettingGroup.isEmpty() ? window()->objectName() : mSettingGroup;
593  saveKey = "/" + setgrp + saveKey;
594  saveKey = "QgsCollapsibleGroupBox" + saveKey;
595  return saveKey;
596 }
597 
599 {
600  //QgsDebugMsg( "Entered" );
601  if ( !mSettings )
602  return;
603 
604  if ( !isEnabled() || ( !mSaveCollapsedState && !mSaveCheckedState ) )
605  return;
606 
607  setUpdatesEnabled( false );
608 
609  QString key = saveKey();
610  QVariant val;
611  if ( mSaveCheckedState )
612  {
613  val = mSettings->value( key + "/checked" );
614  if ( ! val.isNull() )
615  setChecked( val.toBool() );
616  }
617  if ( mSaveCollapsedState )
618  {
619  val = mSettings->value( key + "/collapsed" );
620  if ( ! val.isNull() )
621  setCollapsed( val.toBool() );
622  }
623 
624  setUpdatesEnabled( true );
625 }
626 
628 {
629  //QgsDebugMsg( "Entered" );
630  if ( !mSettings )
631  return;
632 
633  if ( !isEnabled() || ( !mSaveCollapsedState && !mSaveCheckedState ) )
634  return;
635 
636  QString key = saveKey();
637 
638  if ( mSaveCheckedState )
639  mSettings->setValue( key + "/checked", isChecked() );
640  if ( mSaveCollapsedState )
641  mSettings->setValue( key + "/collapsed", isCollapsed() );
642 }
643