QGIS API Documentation  2.7.0-Master
 All Classes Namespaces Files Functions Variables Typedefs Enumerations Enumerator Friends Macros Modules 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  setFocusProxy( mCollapseButton );
83  setFocusPolicy( Qt::StrongFocus );
84 
85  connect( mCollapseButton, SIGNAL( clicked() ), this, SLOT( toggleCollapsed() ) );
86  connect( this, SIGNAL( toggled( bool ) ), this, SLOT( checkToggled( bool ) ) );
87  connect( this, SIGNAL( clicked( bool ) ), this, SLOT( checkClicked( bool ) ) );
88 }
89 
90 void QgsCollapsibleGroupBoxBasic::showEvent( QShowEvent * event )
91 {
92  //QgsDebugMsg( "Entered" );
93  // initialise widget on first show event only
94  if ( mShown )
95  {
96  event->accept();
97  return;
98  }
99 
100  // check if groupbox was set to flat in Designer or in code
101  if ( !mInitFlatChecked )
102  {
103  mInitFlat = isFlat();
104  mInitFlatChecked = true;
105  }
106 
107  // find parent QScrollArea - this might not work in complex layouts - should we look deeper?
108  if ( parent() && parent()->parent() )
109  mParentScrollArea = dynamic_cast<QScrollArea*>( parent()->parent()->parent() );
110  else
111  mParentScrollArea = 0;
112  if ( mParentScrollArea )
113  {
114  QgsDebugMsg( "found a QScrollArea parent: " + mParentScrollArea->objectName() );
115  }
116  else
117  {
118  QgsDebugMsg( "did not find a QScrollArea parent" );
119  }
120 
121  updateStyle();
122 
123  // expand if needed - any calls to setCollapsed() before only set mCollapsed, but have UI effect
124  if ( mCollapsed )
125  {
127  }
128  else
129  {
130  // emit signal for connections using collapsed state
132  }
133 
134  // verify triangle mirrors groupbox's enabled state
135  mCollapseButton->setEnabled( isEnabled() );
136 
137  // set mShown after first setCollapsed call or expanded groupboxes
138  // will scroll scroll areas when first shown
139  mShown = true;
140  event->accept();
141 }
142 
144 {
145  // avoid leaving checkbox in pressed state if alt- or shift-clicking
146  if ( event->modifiers() & ( Qt::AltModifier | Qt::ControlModifier | Qt::ShiftModifier )
147  && titleRect().contains( event->pos() )
148  && isCheckable() )
149  {
150  event->ignore();
151  return;
152  }
153 
154  // default behaviour - pass to QGroupBox
155  QGroupBox::mousePressEvent( event );
156 }
157 
159 {
160  mAltDown = ( event->modifiers() & ( Qt::AltModifier | Qt::ControlModifier ) );
161  mShiftDown = ( event->modifiers() & Qt::ShiftModifier );
162  mTitleClicked = ( titleRect().contains( event->pos() ) );
163 
164  // sync group when title is alt-clicked
165  // collapse/expand when title is clicked and non-checkable
166  // expand current and collapse others on shift-click
167  if ( event->button() == Qt::LeftButton && mTitleClicked &&
168  ( mAltDown || mShiftDown || !isCheckable() ) )
169  {
170  toggleCollapsed();
171  return;
172  }
173 
174  // default behaviour - pass to QGroupBox
175  QGroupBox::mouseReleaseEvent( event );
176 }
177 
179 {
180  // always re-enable mCollapseButton when groupbox was previously disabled
181  // e.g. resulting from a disabled parent of groupbox, or a signal/slot connection
182 
183  // default behaviour - pass to QGroupBox
184  QGroupBox::changeEvent( event );
185 
186  if ( event->type() == QEvent::EnabledChange && isEnabled() )
187  mCollapseButton->setEnabled( true );
188 }
189 
191 {
192  mSyncGroup = grp;
193  QString tipTxt = QString( "" );
194  if ( !grp.isEmpty() )
195  {
196  tipTxt = tr( "Ctrl (or Alt)-click to toggle all" ) + "\n" + tr( "Shift-click to expand, then collapse others" );
197  }
198  mCollapseButton->setToolTip( tipTxt );
199 }
200 
202 {
203  QStyleOptionGroupBox box;
204  initStyleOption( &box );
205  return style()->subControlRect( QStyle::CC_GroupBox, &box,
206  QStyle::SC_GroupBoxLabel, this );
207 }
208 
210 {
211  mCollapseButton->setAltDown( false );
212  mCollapseButton->setShiftDown( false );
213  mAltDown = false;
214  mShiftDown = false;
215 }
216 
218 {
219  Q_UNUSED( chkd );
220  mCollapseButton->setEnabled( true ); // always keep enabled
221 }
222 
224 {
225  // expand/collapse when checkbox toggled by user click.
226  // don't do this on toggle signal, otherwise group boxes will default to collapsed
227  // in option dialog constructors, reducing discovery of options by new users and
228  // overriding user's auto-saved collapsed/expanded state for the group box
229  if ( chkd && isCollapsed() )
230  setCollapsed( false );
231  else if ( ! chkd && ! isCollapsed() )
232  setCollapsed( true );
233 }
234 
236 {
237  // verify if sender is this group box's collapse button
238  QgsGroupBoxCollapseButton *collBtn = qobject_cast<QgsGroupBoxCollapseButton*>( QObject::sender() );
239  bool senderCollBtn = ( collBtn && collBtn == mCollapseButton );
240 
243 
244  // find any sync group siblings and toggle them
245  if (( senderCollBtn || mTitleClicked )
246  && ( mAltDown || mShiftDown )
247  && !mSyncGroup.isEmpty() )
248  {
249  QgsDebugMsg( "Alt or Shift key down, syncing group" );
250  // get pointer to parent or grandparent widget
251  if ( parentWidget() )
252  {
253  mSyncParent = parentWidget();
254  if ( mSyncParent->parentWidget() )
255  {
256  // don't use whole app for grandparent (common for dialogs that use main window for parent)
257  if ( mSyncParent->parentWidget()->objectName() != QString( "QgisApp" ) )
258  {
259  mSyncParent = mSyncParent->parentWidget();
260  }
261  }
262  }
263  else
264  {
265  mSyncParent = 0;
266  }
267 
268  if ( mSyncParent )
269  {
270  QgsDebugMsg( "found sync parent: " + mSyncParent->objectName() );
271 
272  bool thisCollapsed = mCollapsed; // get state of current box before its changed
273  foreach ( QgsCollapsibleGroupBoxBasic *grpbox, mSyncParent->findChildren<QgsCollapsibleGroupBoxBasic*>() )
274  {
275  if ( grpbox->syncGroup() == syncGroup() && grpbox->isEnabled() )
276  {
277  if ( mShiftDown && grpbox == dynamic_cast<QgsCollapsibleGroupBoxBasic *>( this ) )
278  {
279  // expand current group box on shift-click
280  setCollapsed( false );
281  }
282  else
283  {
284  grpbox->setCollapsed( mShiftDown ? true : !thisCollapsed );
285  }
286  }
287  }
288 
289  clearModifiers();
290  return;
291  }
292  else
293  {
294  QgsDebugMsg( "did not find a sync parent" );
295  }
296  }
297 
298  // expand current group box on shift-click, even if no sync group
299  if ( mShiftDown )
300  {
301  setCollapsed( false );
302  }
303  else
304  {
306  }
307 
308  clearModifiers();
309 }
310 
312 {
313  setUpdatesEnabled( false );
314 
315  QSettings settings;
316  // NOTE: QGIS-Style groupbox styled in app stylesheet
317  bool usingQgsStyle = settings.value( "qgis/stylesheet/groupBoxCustom", QVariant( false ) ).toBool();
318 
319  QStyleOptionGroupBox box;
320  initStyleOption( &box );
321  QRect rectFrame = style()->subControlRect( QStyle::CC_GroupBox, &box,
322  QStyle::SC_GroupBoxFrame, this );
323  QRect rectTitle = titleRect();
324 
325  // margin/offset defaults
326  int marginLeft = 20; // title margin for disclosure triangle
327  int marginRight = 5; // a little bit of space on the right, to match space on the left
328  int offsetLeft = 0; // offset for oxygen theme
329  int offsetStyle = QApplication::style()->objectName().contains( "macintosh" ) ? ( usingQgsStyle ? 1 : 8 ) : 0;
330  int topBuffer = ( usingQgsStyle ? 3 : 1 ) + offsetStyle; // space between top of title or triangle and widget above
331  int offsetTop = topBuffer;
332  int offsetTopTri = topBuffer; // offset for triangle
333 
334  if ( mCollapseButton->height() < rectTitle.height() ) // triangle's height > title text's, offset triangle
335  {
336  offsetTopTri += ( rectTitle.height() - mCollapseButton->height() ) / 2;
337 // offsetTopTri += rectTitle.top();
338  }
339  else if ( rectTitle.height() < mCollapseButton->height() ) // title text's height < triangle's, offset title
340  {
341  offsetTop += ( mCollapseButton->height() - rectTitle.height() ) / 2;
342  }
343 
344  // calculate offset if frame overlaps triangle (oxygen theme)
345  // using an offset of 6 pixels from frame border
346  if ( QApplication::style()->objectName().toLower() == "oxygen" )
347  {
348  QStyleOptionGroupBox box;
349  initStyleOption( &box );
350  QRect rectFrame = style()->subControlRect( QStyle::CC_GroupBox, &box,
351  QStyle::SC_GroupBoxFrame, this );
352  QRect rectCheckBox = style()->subControlRect( QStyle::CC_GroupBox, &box,
353  QStyle::SC_GroupBoxCheckBox, this );
354  if ( rectFrame.left() <= 0 )
355  offsetLeft = 6 + rectFrame.left();
356  if ( rectFrame.top() <= 0 )
357  {
358  if ( isCheckable() )
359  {
360  // if is checkable align with checkbox
361  offsetTop = ( rectCheckBox.height() / 2 ) -
362  ( mCollapseButton->height() / 2 ) + rectCheckBox.top();
363  offsetTopTri = offsetTop + 1;
364  }
365  else
366  {
367  offsetTop = 6 + rectFrame.top();
368  offsetTopTri = offsetTop;
369  }
370  }
371  }
372 
373  QgsDebugMsg( QString( "groupbox: %1 style: %2 offset: left=%3 top=%4 top2=%5" ).arg(
374  objectName() ).arg( QApplication::style()->objectName() ).arg( offsetLeft ).arg( offsetTop ).arg( offsetTopTri ) );
375 
376  // customize style sheet for collapse/expand button and force left-aligned title
377  QString ss;
378  if ( usingQgsStyle || QApplication::style()->objectName().contains( "macintosh" ) )
379  {
380  ss += "QgsCollapsibleGroupBoxBasic, QgsCollapsibleGroupBox {";
381  ss += QString( " margin-top: %1px;" ).arg( topBuffer + ( usingQgsStyle ? rectTitle.height() + 5 : rectFrame.top() ) );
382  ss += "}";
383  }
384  ss += "QgsCollapsibleGroupBoxBasic::title, QgsCollapsibleGroupBox::title {";
385  ss += " subcontrol-origin: margin;";
386  ss += " subcontrol-position: top left;";
387  ss += QString( " margin-left: %1px;" ).arg( marginLeft );
388  ss += QString( " margin-right: %1px;" ).arg( marginRight );
389  ss += QString( " left: %1px;" ).arg( offsetLeft );
390  ss += QString( " top: %1px;" ).arg( offsetTop );
391  if ( QApplication::style()->objectName().contains( "macintosh" ) )
392  {
393  ss += " background-color: rgba(0,0,0,0)";
394  }
395  ss += "}";
396  setStyleSheet( ss );
397 
398  // clear toolbutton default background and border and apply offset
399  QString ssd;
400  ssd = QString( "QgsCollapsibleGroupBoxBasic > QToolButton#%1, QgsCollapsibleGroupBox > QToolButton#%1 {" ).arg( mCollapseButton->objectName() );
401  ssd += " background-color: rgba(255, 255, 255, 0); border: none;";
402  ssd += QString( "} QgsCollapsibleGroupBoxBasic > QToolButton#%1:focus, QgsCollapsibleGroupBox > QToolButton#%1:focus { border: 1px solid palette(highlight); }" ).arg( mCollapseButton->objectName() );
403  mCollapseButton->setStyleSheet( ssd );
404  if ( offsetLeft != 0 || offsetTopTri != 0 )
405  mCollapseButton->move( offsetLeft, offsetTopTri );
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->setUpdatesEnabled( false );
440  mParentScrollArea->ensureWidgetVisible( this );
441  //and then make sure the top of the widget is visible - otherwise tall group boxes
442  //scroll to their centres, which is disorienting for users
443  mParentScrollArea->ensureWidgetVisible( mCollapseButton, 0, 5 );
444  mParentScrollArea->setUpdatesEnabled( true );
445  }
446  // emit signal for connections using collapsed state
448 }
449 
451 {
452  // handle child widgets so they don't paint while hidden
453  const char* hideKey = "CollGrpBxHide";
454 
455  if ( mCollapsed )
456  {
457  Q_FOREACH ( QObject* child, children() )
458  {
459  QWidget* w = qobject_cast<QWidget*>( child );
460  if ( w && w != mCollapseButton )
461  {
462  w->setProperty( hideKey, true );
463  w->hide();
464  }
465  }
466  }
467  else // on expand
468  {
469  Q_FOREACH ( QObject* child, children() )
470  {
471  QWidget* w = qobject_cast<QWidget*>( child );
472  if ( w && w != mCollapseButton )
473  {
474  if ( w->property( hideKey ).toBool() )
475  w->show();
476  }
477  }
478  }
479 }
480 
481 
482 // ----
483 
484 QgsCollapsibleGroupBox::QgsCollapsibleGroupBox( QWidget *parent, QSettings* settings )
485  : QgsCollapsibleGroupBoxBasic( parent ), mSettings( settings )
486 {
487  init();
488 }
489 
491  QWidget *parent, QSettings* settings )
492  : QgsCollapsibleGroupBoxBasic( title, parent ), mSettings( settings )
493 {
494  init();
495 }
496 
498 {
499  //QgsDebugMsg( "Entered" );
500  saveState();
501  if ( mDelSettings ) // local settings obj to delete
502  delete mSettings;
503  mSettings = 0; // null the pointer (in case of outside settings obj)
504 }
505 
506 void QgsCollapsibleGroupBox::setSettings( QSettings* settings )
507 {
508  if ( mDelSettings ) // local settings obj to delete
509  delete mSettings;
510  mSettings = settings;
511  mDelSettings = false; // don't delete outside obj
512 }
513 
514 
516 {
517  //QgsDebugMsg( "Entered" );
518  // use pointer to app qsettings if no custom qsettings specified
519  // custom qsettings object may be from Python plugin
520  mDelSettings = false;
521  if ( !mSettings )
522  {
523  mSettings = new QSettings();
524  mDelSettings = true; // only delete obj created by class
525  }
526  // variables
527  mSaveCollapsedState = true;
528  // NOTE: only turn on mSaveCheckedState for groupboxes NOT used
529  // in multiple places or used as options for different parent objects
530  mSaveCheckedState = false;
531  mSettingGroup = ""; // if not set, use window object name
532 }
533 
534 void QgsCollapsibleGroupBox::showEvent( QShowEvent * event )
535 {
536  //QgsDebugMsg( "Entered" );
537  // initialise widget on first show event only
538  if ( mShown )
539  {
540  event->accept();
541  return;
542  }
543 
544  // check if groupbox was set to flat in Designer or in code
545  if ( !mInitFlatChecked )
546  {
547  mInitFlat = isFlat();
548  mInitFlatChecked = true;
549  }
550 
551  loadState();
552 
554 }
555 
557 {
558  // save key for load/save state
559  // currently QgsCollapsibleGroupBox/window()/object
560  QString saveKey = "/" + objectName();
561  // QObject* parentWidget = parent();
562  // while ( parentWidget != NULL )
563  // {
564  // saveKey = "/" + parentWidget->objectName() + saveKey;
565  // parentWidget = parentWidget->parent();
566  // }
567  // if ( parent() != NULL )
568  // saveKey = "/" + parent()->objectName() + saveKey;
569  QString setgrp = mSettingGroup.isEmpty() ? window()->objectName() : mSettingGroup;
570  saveKey = "/" + setgrp + saveKey;
571  saveKey = "QgsCollapsibleGroupBox" + saveKey;
572  return saveKey;
573 }
574 
576 {
577  //QgsDebugMsg( "Entered" );
578  if ( !mSettings )
579  return;
580 
581  if ( !isEnabled() || ( !mSaveCollapsedState && !mSaveCheckedState ) )
582  return;
583 
584  setUpdatesEnabled( false );
585 
586  QString key = saveKey();
587  QVariant val;
588  if ( mSaveCheckedState )
589  {
590  val = mSettings->value( key + "/checked" );
591  if ( ! val.isNull() )
592  setChecked( val.toBool() );
593  }
594  if ( mSaveCollapsedState )
595  {
596  val = mSettings->value( key + "/collapsed" );
597  if ( ! val.isNull() )
598  setCollapsed( val.toBool() );
599  }
600 
601  setUpdatesEnabled( true );
602 }
603 
605 {
606  //QgsDebugMsg( "Entered" );
607  if ( !mSettings )
608  return;
609 
610  if ( !isEnabled() || ( !mSaveCollapsedState && !mSaveCheckedState ) )
611  return;
612 
613  QString key = saveKey();
614 
615  if ( mSaveCheckedState )
616  mSettings->setValue( key + "/checked", isChecked() );
617  if ( mSaveCollapsedState )
618  mSettings->setValue( key + "/collapsed", isCollapsed() );
619 }
620 
QgsCollapsibleGroupBoxBasic(QWidget *parent=0)
void mouseReleaseEvent(QMouseEvent *event)
#define QgsDebugMsg(str)
Definition: qgslogger.h:33
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 ...