1 /*=========================================================================
2 
3   Library:   CTK
4 
5   Copyright (c) Kitware Inc.
6 
7   Licensed under the Apache License, Version 2.0 (the "License");
8   you may not use this file except in compliance with the License.
9   You may obtain a copy of the License at
10 
11       http://www.apache.org/licenses/LICENSE-2.0.txt
12 
13   Unless required by applicable law or agreed to in writing, software
14   distributed under the License is distributed on an "AS IS" BASIS,
15   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
16   See the License for the specific language governing permissions and
17   limitations under the License.
18 
19 =========================================================================*/
20 
21 // Qt includes
22 #include <QApplication>
23 #include <QDebug>
24 #include <QChildEvent>
25 #include <QMouseEvent>
26 #include <QStylePainter>
27 #include <QStyleOptionGroupBox>
28 #include <QStyle>
29 
30 // CTK includes
31 #include "ctkCollapsibleGroupBox.h"
32 
33 #if QT_VERSION >= 0x040600
34 #include "ctkProxyStyle.h"
35 
36 //-----------------------------------------------------------------------------
37 class ctkCollapsibleGroupBoxStyle:public ctkProxyStyle
38 {
39 public:
40   typedef ctkProxyStyle Superclass;
ctkCollapsibleGroupBoxStyle(QStyle * style=0,QObject * parent=0)41   ctkCollapsibleGroupBoxStyle(QStyle* style = 0, QObject* parent =0)
42     : Superclass(style, parent)
43   {
44   }
drawPrimitive(PrimitiveElement pe,const QStyleOption * opt,QPainter * p,const QWidget * widget=0) const45   virtual void drawPrimitive(PrimitiveElement pe, const QStyleOption * opt, QPainter * p, const QWidget * widget = 0) const
46   {
47     if (pe == QStyle::PE_IndicatorCheckBox)
48       {
49       const ctkCollapsibleGroupBox* groupBox= qobject_cast<const ctkCollapsibleGroupBox*>(widget);
50       if (groupBox)
51         {
52         this->Superclass::drawPrimitive(groupBox->isChecked() ? QStyle::PE_IndicatorArrowDown : QStyle::PE_IndicatorArrowRight, opt, p, widget);
53         return;
54         }
55       }
56     this->Superclass::drawPrimitive(pe, opt, p, widget);
57   }
pixelMetric(PixelMetric metric,const QStyleOption * option,const QWidget * widget) const58   virtual int pixelMetric(PixelMetric metric, const QStyleOption * option, const QWidget * widget) const
59   {
60     if (metric == QStyle::PM_IndicatorHeight)
61       {
62       const ctkCollapsibleGroupBox* groupBox= qobject_cast<const ctkCollapsibleGroupBox*>(widget);
63       if (groupBox)
64         {
65         return groupBox->fontMetrics().height();
66         }
67       }
68     return this->Superclass::pixelMetric(metric, option, widget);
69   }
70 };
71 #endif
72 
73 //-----------------------------------------------------------------------------
74 class ctkCollapsibleGroupBoxPrivate
75 {
76   Q_DECLARE_PUBLIC(ctkCollapsibleGroupBox);
77 protected:
78   ctkCollapsibleGroupBox* const q_ptr;
79 public:
80   ctkCollapsibleGroupBoxPrivate(ctkCollapsibleGroupBox& object);
81   void init();
82   void setChildVisibility(QWidget* childWidget);
83 
84   /// Size of the widget for collapsing
85   QSize OldSize;
86   /// Maximum allowed height
87   int   MaxHeight;
88   int   CollapsedHeight;
89 
90   /// We change the visibility of the chidren in setChildrenVisibility
91   /// and we track when the visibility is changed to force it back to possibly
92   /// force the child to be hidden. To prevent infinite loop we need to know
93   /// who is changing children's visibility.
94   bool     ForcingVisibility;
95   /// Sometimes the creation of the widget is not done inside setVisible,
96   /// as we need to do special processing the first time the groupBox is
97   /// setVisible, we track its created state with the variable
98   bool     IsStateCreated;
99 
100 #if QT_VERSION >= 0x040600
101   /// Pointer to keep track of the proxy style
102   ctkCollapsibleGroupBoxStyle* GroupBoxStyle;
103 #endif
104 };
105 
106 //-----------------------------------------------------------------------------
ctkCollapsibleGroupBoxPrivate(ctkCollapsibleGroupBox & object)107 ctkCollapsibleGroupBoxPrivate::ctkCollapsibleGroupBoxPrivate(
108   ctkCollapsibleGroupBox& object)
109   :q_ptr(&object)
110 {
111   this->ForcingVisibility = false;
112   this->IsStateCreated = false;
113   this->MaxHeight = 0;
114   this->CollapsedHeight = 14;
115 #if QT_VERSION >= 0x040600
116   this->GroupBoxStyle = 0;
117 #endif
118 }
119 
120 //-----------------------------------------------------------------------------
init()121 void ctkCollapsibleGroupBoxPrivate::init()
122 {
123   Q_Q(ctkCollapsibleGroupBox);
124   q->setCheckable(true);
125   QObject::connect(q, SIGNAL(toggled(bool)), q, SLOT(expand(bool)));
126 
127   this->MaxHeight = q->maximumHeight();
128 #if QT_VERSION >= 0x040600
129   QWidget* parent = q->parentWidget();
130   QStyle* parentStyle = (parent) ? parent->style() : qApp->style();
131   this->GroupBoxStyle = new ctkCollapsibleGroupBoxStyle(parentStyle, qApp);
132   q->setStyle(this->GroupBoxStyle);
133   this->GroupBoxStyle->ensureBaseStyle();
134 #else
135   this->setStyleSheet(
136     "ctkCollapsibleGroupBox::indicator:checked{"
137     "image: url(:/Icons/expand-up.png);}"
138     "ctkCollapsibleGroupBox::indicator:unchecked{"
139     "image: url(:/Icons/expand-down.png);}");
140 #endif
141 }
142 //-----------------------------------------------------------------------------
setChildVisibility(QWidget * childWidget)143 void ctkCollapsibleGroupBoxPrivate::setChildVisibility(QWidget* childWidget)
144 {
145   Q_Q(ctkCollapsibleGroupBox);
146   // Don't hide children while the widget is not yet created (before show() is
147   // called). If we hide them (but don't set ExplicitShowHide), they would be
148   // shown anyway when they will be created (because ExplicitShowHide is not set).
149   // If we set ExplicitShowHide, then calling setVisible(false) on them would
150   // be a no (because they are already hidden and ExplicitShowHide is set).
151   // So we don't hide/show the children until the widget is created.
152   if (!q->testAttribute(Qt::WA_WState_Created))
153     {
154     return;
155     }
156   this->ForcingVisibility = true;
157 
158   bool visible= !q->collapsed();
159   // if the widget has been explicity hidden, then hide it.
160   if (childWidget->property("visibilityToParent").isValid()
161       && !childWidget->property("visibilityToParent").toBool())
162     {
163     visible = false;
164     }
165 
166   // Setting Qt::WA_WState_Visible to true during child construction can have
167   // undesirable side effects.
168   if (childWidget->testAttribute(Qt::WA_WState_Created) ||
169       !visible)
170     {
171     childWidget->setVisible(visible);
172     }
173 
174   // setVisible() has set the ExplicitShowHide flag, restore it as we don't want
175   // to make it like it was an explicit visible set because we want
176   // to allow any children to be explicitly hidden by the user.
177   if ((!childWidget->property("visibilityToParent").isValid() ||
178       childWidget->property("visibilityToParent").toBool()))
179     {
180     childWidget->setAttribute(Qt::WA_WState_ExplicitShowHide, false);
181     }
182   this->ForcingVisibility = false;
183 }
184 
185 //-----------------------------------------------------------------------------
ctkCollapsibleGroupBox(QWidget * _parent)186 ctkCollapsibleGroupBox::ctkCollapsibleGroupBox(QWidget* _parent)
187   :QGroupBox(_parent)
188   , d_ptr(new ctkCollapsibleGroupBoxPrivate(*this))
189 {
190   Q_D(ctkCollapsibleGroupBox);
191   d->init();
192 }
193 
194 //-----------------------------------------------------------------------------
ctkCollapsibleGroupBox(const QString & title,QWidget * _parent)195 ctkCollapsibleGroupBox::ctkCollapsibleGroupBox(const QString& title, QWidget* _parent)
196   :QGroupBox(title, _parent)
197   , d_ptr(new ctkCollapsibleGroupBoxPrivate(*this))
198 {
199   Q_D(ctkCollapsibleGroupBox);
200   d->init();
201 }
202 
203 //-----------------------------------------------------------------------------
~ctkCollapsibleGroupBox()204 ctkCollapsibleGroupBox::~ctkCollapsibleGroupBox()
205 {
206 
207 }
208 
209 //-----------------------------------------------------------------------------
setCollapsedHeight(int heightInPixels)210 void ctkCollapsibleGroupBox::setCollapsedHeight(int heightInPixels)
211 {
212   Q_D(ctkCollapsibleGroupBox);
213   d->CollapsedHeight = heightInPixels;
214 }
215 
216 //-----------------------------------------------------------------------------
collapsedHeight() const217 int ctkCollapsibleGroupBox::collapsedHeight()const
218 {
219   Q_D(const ctkCollapsibleGroupBox);
220   return d->CollapsedHeight;
221 }
222 
223 //-----------------------------------------------------------------------------
expand(bool _expand)224 void ctkCollapsibleGroupBox::expand(bool _expand)
225 {
226   Q_D(ctkCollapsibleGroupBox);
227   if (!_expand)
228     {
229     d->OldSize = this->size();
230     }
231 
232   // Update the visibility of all the children
233   // We can't use findChildren as it would return the grandchildren
234   foreach(QObject* childObject, this->children())
235     {
236     if (childObject->isWidgetType())
237       {
238       d->setChildVisibility(qobject_cast<QWidget*>(childObject));
239       }
240     }
241 
242   if (_expand)
243     {
244     this->setMaximumHeight(d->MaxHeight);
245     this->resize(d->OldSize);
246     }
247   else
248     {
249     d->MaxHeight = this->maximumHeight();
250     QStyleOptionGroupBox option;
251     this->initStyleOption(&option);
252     QRect labelRect = this->style()->subControlRect(
253       QStyle::CC_GroupBox, &option, QStyle::SC_GroupBoxLabel, this);
254     this->setMaximumHeight(labelRect.height() + d->CollapsedHeight);
255     }
256 }
257 
258 #if QT_VERSION < 0x040600
259 //-----------------------------------------------------------------------------
paintEvent(QPaintEvent * e)260 void ctkCollapsibleGroupBox::paintEvent(QPaintEvent* e)
261 {
262   this->QGroupBox::paintEvent(e);
263 
264   QStylePainter paint(this);
265   QStyleOptionGroupBox option;
266   initStyleOption(&option);
267   option.activeSubControls &= ~QStyle::SC_GroupBoxCheckBox;
268   paint.drawComplexControl(QStyle::CC_GroupBox, option);
269 
270 }
271 
272 //-----------------------------------------------------------------------------
mousePressEvent(QMouseEvent * event)273 void ctkCollapsibleGroupBox::mousePressEvent(QMouseEvent *event)
274 {
275     if (event->button() != Qt::LeftButton) {
276         event->ignore();
277         return;
278     }
279     // no animation
280 }
281 
282 //-----------------------------------------------------------------------------
mouseReleaseEvent(QMouseEvent * event)283 void ctkCollapsibleGroupBox::mouseReleaseEvent(QMouseEvent *event)
284 {
285     if (event->button() != Qt::LeftButton) {
286         event->ignore();
287         return;
288     }
289 
290     QStyleOptionGroupBox box;
291     initStyleOption(&box);
292     box.activeSubControls &= !QStyle::SC_GroupBoxCheckBox;
293     QStyle::SubControl released = style()->hitTestComplexControl(QStyle::CC_GroupBox, &box,
294                                                                  event->pos(), this);
295     bool toggle = this->isCheckable() && (released == QStyle::SC_GroupBoxLabel
296                                    || released == QStyle::SC_GroupBoxCheckBox);
297     if (toggle)
298       {
299       this->setChecked(!this->isChecked());
300       }
301 }
302 
303 #endif
304 
305 //-----------------------------------------------------------------------------
childEvent(QChildEvent * c)306 void ctkCollapsibleGroupBox::childEvent(QChildEvent* c)
307 {
308   Q_D(ctkCollapsibleGroupBox);
309   QObject* child = c->child();
310   if (c && c->type() == QEvent::ChildAdded &&
311       child && child->isWidgetType())
312     {
313     QWidget *childWidget = qobject_cast<QWidget*>(c->child());
314     // Handle the case where the child has already it's visibility set before
315     // being added to the widget
316     if (childWidget->testAttribute(Qt::WA_WState_ExplicitShowHide) &&
317         childWidget->testAttribute(Qt::WA_WState_Hidden))
318       {
319       // if the widget has explicitly set to hidden, then mark it as such
320       childWidget->setProperty("visibilityToParent", false);
321       }
322     // We want to catch all the child's Show/Hide events.
323     child->installEventFilter(this);
324     // If the child is added while ctkCollapsibleButton is collapsed, then we
325     // need to hide the child.
326     d->setChildVisibility(childWidget);
327     }
328   this->QGroupBox::childEvent(c);
329 }
330 
331 //-----------------------------------------------------------------------------
setVisible(bool show)332 void ctkCollapsibleGroupBox::setVisible(bool show)
333 {
334   Q_D(ctkCollapsibleGroupBox);
335   // calling QWidget::setVisible() on ctkCollapsibleGroupBox will eventually
336   // call QWidget::showChildren() or hideChildren() which will generate
337   // ShowToParent/HideToParent events but we want to ignore that case in
338   // eventFilter().
339   d->ForcingVisibility = true;
340   this->QGroupBox::setVisible(show);
341   d->ForcingVisibility = false;
342   // We have been ignoring setChildVisibility() while the collapsible button
343   // is not yet created, now that it is created, ensure that the children
344   // are correctly shown/hidden depending on their explicit visibility and
345   // the collapsed property of the button.
346   if (!d->IsStateCreated && this->testAttribute(Qt::WA_WState_Created))
347     {
348     d->IsStateCreated = true;
349     foreach(QObject* child, this->children())
350       {
351       QWidget* childWidget = qobject_cast<QWidget*>(child);
352       if (childWidget)
353         {
354         d->setChildVisibility(childWidget);
355         }
356       }
357     }
358 }
359 
360 //-----------------------------------------------------------------------------
eventFilter(QObject * child,QEvent * e)361 bool ctkCollapsibleGroupBox::eventFilter(QObject* child, QEvent* e)
362 {
363   Q_D(ctkCollapsibleGroupBox);
364   Q_ASSERT(child && e);
365   // Make sure the Show/QHide events are not generated by one of our
366   // ctkCollapsibleButton function.
367   if (d->ForcingVisibility)
368     {
369     return false;
370     }
371   // When we are here, it's because somewhere (not in ctkCollapsibleButton),
372   // someone explicitly called setVisible() on a child widget.
373   // If the collapsible button is collapsed/closed, then even if someone
374   // request the widget to be visible, we force it back to be hidden because
375   // they meant to be hidden to its parent, the collapsible button. However the
376   // child will later be shown when the button will be expanded/opened.
377   // On the other hand, if the user explicitly hide the child when the button
378   // is collapsed/closed, then we want to keep it hidden next time the
379   // collapsible button is expanded/opened.
380   if (e->type() == QEvent::ShowToParent)
381     {
382     child->setProperty("visibilityToParent", true);
383     Q_ASSERT(qobject_cast<QWidget*>(child));
384     // force the widget to be hidden if the button is collapsed.
385     d->setChildVisibility(qobject_cast<QWidget*>(child));
386     }
387   else if(e->type() == QEvent::HideToParent)
388     {
389     // we don't need to force the widget to be visible here.
390     child->setProperty("visibilityToParent", false);
391     }
392   return this->QGroupBox::eventFilter(child, e);
393 }
394