ctkCollapsibleButton.cpp 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616
  1. /*=========================================================================
  2. Library: CTK
  3. Copyright (c) 2010 Kitware Inc.
  4. Licensed under the Apache License, Version 2.0 (the "License");
  5. you may not use this file except in compliance with the License.
  6. You may obtain a copy of the License at
  7. http://www.commontk.org/LICENSE
  8. Unless required by applicable law or agreed to in writing, software
  9. distributed under the License is distributed on an "AS IS" BASIS,
  10. WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  11. See the License for the specific language governing permissions and
  12. limitations under the License.
  13. =========================================================================*/
  14. // Qt includes
  15. #include <QApplication>
  16. #include <QCleanlooksStyle>
  17. #include <QDebug>
  18. #include <QLayout>
  19. #include <QMouseEvent>
  20. #include <QPainter>
  21. #include <QPushButton>
  22. #include <QStyle>
  23. #include <QStyleOptionButton>
  24. #include <QStyleOptionFrameV3>
  25. // CTK includes
  26. #include "ctkCollapsibleButton.h"
  27. //-----------------------------------------------------------------------------
  28. class ctkCollapsibleButtonPrivate : public ctkPrivate<ctkCollapsibleButton>
  29. {
  30. public:
  31. CTK_DECLARE_PUBLIC(ctkCollapsibleButton);
  32. void init();
  33. bool Collapsed;
  34. // Contents frame
  35. QFrame::Shape ContentsFrameShape;
  36. QFrame::Shadow ContentsFrameShadow;
  37. int ContentsLineWidth;
  38. int ContentsMidLineWidth;
  39. int CollapsedHeight;
  40. bool ExclusiveMouseOver;
  41. bool LookOffWhenChecked;
  42. int MaximumHeight; // use carefully
  43. // Tuning of the button look&feel
  44. Qt::Alignment TextAlignment;
  45. Qt::Alignment IndicatorAlignment;
  46. };
  47. //-----------------------------------------------------------------------------
  48. void ctkCollapsibleButtonPrivate::init()
  49. {
  50. CTK_P(ctkCollapsibleButton);
  51. p->setCheckable(true);
  52. // checked and Collapsed are synchronized: checked != Collapsed
  53. p->setChecked(true);
  54. this->Collapsed = false;
  55. this->ContentsFrameShape = QFrame::NoFrame;
  56. this->ContentsFrameShadow = QFrame::Plain;
  57. this->ContentsLineWidth = 1;
  58. this->ContentsMidLineWidth = 0;
  59. this->CollapsedHeight = 10;
  60. this->ExclusiveMouseOver = false;
  61. this->LookOffWhenChecked = true; // set as a prop ?
  62. this->MaximumHeight = p->maximumHeight();
  63. this->TextAlignment = Qt::AlignLeft | Qt::AlignVCenter;
  64. this->IndicatorAlignment = Qt::AlignLeft | Qt::AlignVCenter;
  65. p->setSizePolicy(QSizePolicy(QSizePolicy::Minimum,
  66. QSizePolicy::Preferred,
  67. QSizePolicy::DefaultType));
  68. p->setContentsMargins(0, p->buttonSizeHint().height(),0 , 0);
  69. QObject::connect(p, SIGNAL(toggled(bool)),
  70. p, SLOT(onToggled(bool)));
  71. }
  72. //-----------------------------------------------------------------------------
  73. void ctkCollapsibleButton::initStyleOption(QStyleOptionButton* option)const
  74. {
  75. CTK_D(const ctkCollapsibleButton);
  76. if (option == 0)
  77. {
  78. return;
  79. }
  80. option->initFrom(this);
  81. if (this->isDown() )
  82. {
  83. option->state |= QStyle::State_Sunken;
  84. }
  85. if (this->isChecked() && !d->LookOffWhenChecked)
  86. {
  87. option->state |= QStyle::State_On;
  88. }
  89. if (!this->isDown())
  90. {
  91. option->state |= QStyle::State_Raised;
  92. }
  93. option->text = this->text();
  94. option->icon = this->icon();
  95. option->iconSize = QSize(this->style()->pixelMetric(QStyle::PM_IndicatorWidth, option, this),
  96. this->style()->pixelMetric(QStyle::PM_IndicatorHeight, option, this));
  97. int buttonHeight = this->buttonSizeHint().height();//qMax(this->fontMetrics().height(), option->iconSize.height());
  98. option->rect.setHeight(buttonHeight);
  99. }
  100. //-----------------------------------------------------------------------------
  101. ctkCollapsibleButton::ctkCollapsibleButton(QWidget* _parent)
  102. :QAbstractButton(_parent)
  103. {
  104. CTK_INIT_PRIVATE(ctkCollapsibleButton);
  105. ctk_d()->init();
  106. }
  107. //-----------------------------------------------------------------------------
  108. ctkCollapsibleButton::ctkCollapsibleButton(const QString& title, QWidget* _parent)
  109. :QAbstractButton(_parent)
  110. {
  111. CTK_INIT_PRIVATE(ctkCollapsibleButton);
  112. ctk_d()->init();
  113. this->setText(title);
  114. }
  115. //-----------------------------------------------------------------------------
  116. ctkCollapsibleButton::~ctkCollapsibleButton()
  117. {
  118. }
  119. //-----------------------------------------------------------------------------
  120. void ctkCollapsibleButton::setCollapsed(bool c)
  121. {
  122. if (!this->isCheckable())
  123. {
  124. // not sure if one should handle this case...
  125. this->collapse(c);
  126. return;
  127. }
  128. this->setChecked(!c);
  129. }
  130. //-----------------------------------------------------------------------------
  131. bool ctkCollapsibleButton::collapsed()const
  132. {
  133. return ctk_d()->Collapsed;
  134. }
  135. //-----------------------------------------------------------------------------
  136. void ctkCollapsibleButton::setCollapsedHeight(int h)
  137. {
  138. ctk_d()->CollapsedHeight = h;
  139. this->updateGeometry();
  140. }
  141. //-----------------------------------------------------------------------------
  142. int ctkCollapsibleButton::collapsedHeight()const
  143. {
  144. return ctk_d()->CollapsedHeight;
  145. }
  146. //-----------------------------------------------------------------------------
  147. void ctkCollapsibleButton::onToggled(bool checked)
  148. {
  149. if (this->isCheckable())
  150. {
  151. this->collapse(!checked);
  152. }
  153. }
  154. //-----------------------------------------------------------------------------
  155. void ctkCollapsibleButton::collapse(bool c)
  156. {
  157. CTK_D(ctkCollapsibleButton);
  158. if (c == d->Collapsed)
  159. {
  160. return;
  161. }
  162. d->Collapsed = c;
  163. // we do that here as setVisible calls will correctly refresh the widget
  164. if (c)
  165. {
  166. d->MaximumHeight = this->maximumHeight();
  167. this->setMaximumHeight(this->sizeHint().height());
  168. //this->updateGeometry();
  169. }
  170. else
  171. {
  172. // restore maximumheight
  173. this->setMaximumHeight(d->MaximumHeight);
  174. this->updateGeometry();
  175. }
  176. QObjectList childList = this->children();
  177. for (int i = 0; i < childList.size(); ++i)
  178. {
  179. QObject *o = childList.at(i);
  180. if (!o->isWidgetType())
  181. {
  182. continue;
  183. }
  184. QWidget *w = static_cast<QWidget *>(o);
  185. w->setVisible(!c);
  186. }
  187. // this might be too many updates...
  188. QWidget* _parent = this->parentWidget();
  189. if (!d->Collapsed && (!_parent || !_parent->layout()))
  190. {
  191. this->resize(this->sizeHint());
  192. }
  193. else
  194. {
  195. this->updateGeometry();
  196. }
  197. //this->update(QRect(QPoint(0,0), this->sizeHint()));
  198. //this->repaint(QRect(QPoint(0,0), this->sizeHint()));
  199. emit contentsCollapsed(c);
  200. }
  201. //-----------------------------------------------------------------------------
  202. QFrame::Shape ctkCollapsibleButton::contentsFrameShape() const
  203. {
  204. return ctk_d()->ContentsFrameShape;
  205. }
  206. //-----------------------------------------------------------------------------
  207. void ctkCollapsibleButton::setContentsFrameShape(QFrame::Shape s)
  208. {
  209. ctk_d()->ContentsFrameShape = s;
  210. }
  211. //-----------------------------------------------------------------------------
  212. QFrame::Shadow ctkCollapsibleButton::contentsFrameShadow() const
  213. {
  214. return ctk_d()->ContentsFrameShadow;
  215. }
  216. //-----------------------------------------------------------------------------
  217. void ctkCollapsibleButton::setContentsFrameShadow(QFrame::Shadow s)
  218. {
  219. ctk_d()->ContentsFrameShadow = s;
  220. }
  221. //-----------------------------------------------------------------------------
  222. int ctkCollapsibleButton:: contentsLineWidth() const
  223. {
  224. return ctk_d()->ContentsLineWidth;
  225. }
  226. //-----------------------------------------------------------------------------
  227. void ctkCollapsibleButton::setContentsLineWidth(int w)
  228. {
  229. ctk_d()->ContentsLineWidth = w;
  230. }
  231. //-----------------------------------------------------------------------------
  232. int ctkCollapsibleButton::contentsMidLineWidth() const
  233. {
  234. return ctk_d()->ContentsMidLineWidth;
  235. }
  236. //-----------------------------------------------------------------------------
  237. void ctkCollapsibleButton::setContentsMidLineWidth(int w)
  238. {
  239. ctk_d()->ContentsMidLineWidth = w;
  240. }
  241. //-----------------------------------------------------------------------------
  242. void ctkCollapsibleButton::setButtonTextAlignment(Qt::Alignment textAlignment)
  243. {
  244. CTK_D(ctkCollapsibleButton);
  245. d->TextAlignment = textAlignment;
  246. this->update();
  247. }
  248. //-----------------------------------------------------------------------------
  249. Qt::Alignment ctkCollapsibleButton::buttonTextAlignment()const
  250. {
  251. CTK_D(const ctkCollapsibleButton);
  252. return d->TextAlignment;
  253. }
  254. //-----------------------------------------------------------------------------
  255. void ctkCollapsibleButton::setIndicatorAlignment(Qt::Alignment indicatorAlignment)
  256. {
  257. CTK_D(ctkCollapsibleButton);
  258. d->IndicatorAlignment = indicatorAlignment;
  259. this->update();
  260. }
  261. //-----------------------------------------------------------------------------
  262. Qt::Alignment ctkCollapsibleButton::indicatorAlignment()const
  263. {
  264. CTK_D(const ctkCollapsibleButton);
  265. return d->IndicatorAlignment;
  266. }
  267. //-----------------------------------------------------------------------------
  268. QSize ctkCollapsibleButton::buttonSizeHint()const
  269. {
  270. int w = 0, h = 0;
  271. QStyleOptionButton opt;
  272. opt.initFrom(this);
  273. // indicator
  274. QSize indicatorSize = QSize(style()->pixelMetric(QStyle::PM_IndicatorWidth, &opt, this),
  275. style()->pixelMetric(QStyle::PM_IndicatorHeight, &opt, this));
  276. int indicatorSpacing = style()->pixelMetric(QStyle::PM_CheckBoxLabelSpacing, &opt, this);
  277. int ih = indicatorSize.height();
  278. int iw = indicatorSize.width() + indicatorSpacing;
  279. w += iw;
  280. h = qMax(h, ih);
  281. // text
  282. QString string(this->text());
  283. bool empty = string.isEmpty();
  284. if (empty)
  285. {
  286. string = QString::fromLatin1("XXXX");
  287. }
  288. QFontMetrics fm = this->fontMetrics();
  289. QSize sz = fm.size(Qt::TextShowMnemonic, string);
  290. if(!empty || !w)
  291. {
  292. w += sz.width();
  293. }
  294. h = qMax(h, sz.height());
  295. //opt.rect.setSize(QSize(w, h)); // PM_MenuButtonIndicator depends on the height
  296. QSize buttonSize = (style()->sizeFromContents(QStyle::CT_PushButton, &opt, QSize(w, h), this).
  297. expandedTo(QApplication::globalStrut()));
  298. return buttonSize;
  299. }
  300. //-----------------------------------------------------------------------------
  301. QSize ctkCollapsibleButton::minimumSizeHint()const
  302. {
  303. CTK_D(const ctkCollapsibleButton);
  304. QSize buttonSize = this->buttonSizeHint();
  305. if (d->Collapsed)
  306. {
  307. return buttonSize + QSize(0,d->CollapsedHeight);
  308. }
  309. // open
  310. if (this->layout() == 0)
  311. {// no layout, means the button is empty ?
  312. return buttonSize;
  313. }
  314. QSize s = this->QAbstractButton::minimumSizeHint();
  315. return s.expandedTo(buttonSize);
  316. }
  317. //-----------------------------------------------------------------------------
  318. QSize ctkCollapsibleButton::sizeHint()const
  319. {
  320. CTK_D(const ctkCollapsibleButton);
  321. QSize buttonSize = this->buttonSizeHint();
  322. if (d->Collapsed)
  323. {
  324. return buttonSize + QSize(0,d->CollapsedHeight);
  325. }
  326. // open
  327. // QAbstractButton works well only if a layout is set
  328. QSize s = this->QAbstractButton::sizeHint();
  329. return s.expandedTo(buttonSize + QSize(0, d->CollapsedHeight));
  330. }
  331. //-----------------------------------------------------------------------------
  332. void ctkCollapsibleButton::paintEvent(QPaintEvent * _event)
  333. {
  334. CTK_D(ctkCollapsibleButton);
  335. QPainter p(this);
  336. // Draw Button
  337. QStyleOptionButton opt;
  338. this->initStyleOption(&opt);
  339. // We don't want to have the highlight effect on the button when mouse is
  340. // over a child. We want the highlight effect only when the mouse is just
  341. // over itself.
  342. // same as this->underMouse()
  343. bool exclusiveMouseOver = false;
  344. if (opt.state & QStyle::State_MouseOver)
  345. {
  346. QRect buttonRect = opt.rect;
  347. QList<QWidget*> _children = this->findChildren<QWidget*>();
  348. QList<QWidget*>::ConstIterator it;
  349. for (it = _children.constBegin(); it != _children.constEnd(); ++it )
  350. {
  351. if ((*it)->underMouse())
  352. {
  353. // the mouse has been moved from the collapsible button to one
  354. // of its children. The paint event rect is the child rect, this
  355. // is why we have to request another paint event to redraw the
  356. // button to remove the highlight effect.
  357. if (!_event->rect().contains(buttonRect))
  358. {// repaint the button rect.
  359. this->update(buttonRect);
  360. }
  361. opt.state &= ~QStyle::State_MouseOver;
  362. exclusiveMouseOver = true;
  363. break;
  364. }
  365. }
  366. if (d->ExclusiveMouseOver && !exclusiveMouseOver)
  367. {
  368. // the mouse is over the widget, but not over the children. As it
  369. // has been de-highlighted in the past, we should refresh the button
  370. // rect to re-highlight the button.
  371. if (!_event->rect().contains(buttonRect))
  372. {// repaint the button rect.
  373. this->update(buttonRect);
  374. }
  375. }
  376. }
  377. d->ExclusiveMouseOver = exclusiveMouseOver;
  378. QSize indicatorSize = QSize(style()->pixelMetric(QStyle::PM_IndicatorWidth, &opt, this),
  379. style()->pixelMetric(QStyle::PM_IndicatorHeight, &opt, this));
  380. opt.iconSize = indicatorSize;
  381. style()->drawControl(QStyle::CE_PushButtonBevel, &opt, &p, this);
  382. // TBD is PE_PanelButtonCommand better ?
  383. //style()->drawPrimitive(QStyle::PE_PanelButtonCommand, &opt, &p, this);
  384. int buttonHeight = opt.rect.height();
  385. uint tf = d->TextAlignment;
  386. if (this->style()->styleHint(QStyle::SH_UnderlineShortcut, &opt, this))
  387. {
  388. tf |= Qt::TextShowMnemonic;
  389. }
  390. else
  391. {
  392. tf |= Qt::TextHideMnemonic;
  393. }
  394. int textWidth = opt.fontMetrics.boundingRect(opt.rect, tf, opt.text).width();
  395. int indicatorSpacing = this->style()->pixelMetric(QStyle::PM_CheckBoxLabelSpacing, &opt, this);
  396. int buttonMargin = this->style()->pixelMetric(QStyle::PM_ButtonMargin, &opt, this);
  397. // Draw Indicator
  398. QStyleOption indicatorOpt;
  399. indicatorOpt.init(this);
  400. if (d->IndicatorAlignment & Qt::AlignLeft)
  401. {
  402. indicatorOpt.rect = QRect((buttonHeight - indicatorSize.width()) / 2,
  403. (buttonHeight - indicatorSize.height()) / 2,
  404. indicatorSize.width(), indicatorSize.height());
  405. }
  406. else if (d->IndicatorAlignment & Qt::AlignHCenter)
  407. {
  408. int w = indicatorSize.width();
  409. if (!opt.text.isEmpty() && (d->TextAlignment & Qt::AlignHCenter))
  410. {
  411. w += textWidth + indicatorSpacing;
  412. }
  413. indicatorOpt.rect = QRect(opt.rect.x()+ opt.rect.width() /2 - w / 2,
  414. (buttonHeight - indicatorSize.height()) / 2,
  415. indicatorSize.width(), indicatorSize.height());
  416. if (d->TextAlignment & Qt::AlignLeft &&
  417. indicatorOpt.rect.left() < opt.rect.x() + buttonMargin + textWidth)
  418. {
  419. indicatorOpt.rect.moveLeft(opt.rect.x() + buttonMargin + textWidth);
  420. }
  421. else if (d->TextAlignment & Qt::AlignRight &&
  422. indicatorOpt.rect.right() > opt.rect.right() - buttonMargin - textWidth)
  423. {
  424. indicatorOpt.rect.moveRight(opt.rect.right() - buttonMargin - textWidth);
  425. }
  426. }
  427. else if (d->IndicatorAlignment & Qt::AlignRight)
  428. {
  429. indicatorOpt.rect = QRect(opt.rect.width() - (buttonHeight - indicatorSize.width()) / 2
  430. - indicatorSize.width(),
  431. (buttonHeight - indicatorSize.height()) / 2,
  432. indicatorSize.width(), indicatorSize.height());
  433. }
  434. if (d->Collapsed)
  435. {
  436. style()->drawPrimitive(QStyle::PE_IndicatorArrowDown, &indicatorOpt, &p, this);
  437. }
  438. else
  439. {
  440. style()->drawPrimitive(QStyle::PE_IndicatorArrowUp, &indicatorOpt, &p, this);
  441. }
  442. // Draw Text
  443. if (d->TextAlignment & Qt::AlignLeft)
  444. {
  445. if (d->IndicatorAlignment & Qt::AlignLeft)
  446. {
  447. opt.rect.setLeft(indicatorOpt.rect.right() + indicatorSpacing);
  448. }
  449. else
  450. {
  451. opt.rect.setLeft(opt.rect.x() + buttonMargin);
  452. }
  453. }
  454. else if (d->TextAlignment & Qt::AlignHCenter)
  455. {
  456. if (d->IndicatorAlignment & Qt::AlignHCenter)
  457. {
  458. opt.rect.setLeft(indicatorOpt.rect.right() + indicatorSpacing);
  459. }
  460. else
  461. {
  462. opt.rect.setLeft(opt.rect.x() + opt.rect.width() / 2 - textWidth / 2);
  463. if (d->IndicatorAlignment & Qt::AlignLeft)
  464. {
  465. opt.rect.setLeft( qMin(indicatorOpt.rect.left() + indicatorSpacing, opt.rect.left()) );
  466. }
  467. }
  468. }
  469. else if (d->TextAlignment & Qt::AlignRight)
  470. {
  471. if (d->IndicatorAlignment & Qt::AlignRight)
  472. {
  473. opt.rect.setLeft(indicatorOpt.rect.left() - indicatorSpacing - textWidth);
  474. }
  475. else
  476. {
  477. opt.rect.setLeft(opt.rect.right() - buttonMargin - textWidth);
  478. }
  479. }
  480. // all the computations have been made infering the text would be left oriented
  481. tf &= ~Qt::AlignHCenter & ~Qt::AlignRight;
  482. tf |= Qt::AlignLeft;
  483. style()->drawItemText(&p, opt.rect, tf, opt.palette, (opt.state & QStyle::State_Enabled),
  484. opt.text, QPalette::ButtonText);
  485. // Draw Frame around contents
  486. QStyleOptionFrameV3 f;
  487. f.init(this);
  488. // HACK: on some styles, the frame doesn't exactly touch the button.
  489. // this is because the button has some kind of extra border.
  490. if (qobject_cast<QCleanlooksStyle*>(this->style()) != 0)
  491. {
  492. f.rect.setTop(buttonHeight - 1);
  493. }
  494. else
  495. {
  496. f.rect.setTop(buttonHeight);
  497. }
  498. f.frameShape = d->ContentsFrameShape;
  499. switch (d->ContentsFrameShadow)
  500. {
  501. case QFrame::Sunken:
  502. f.state |= QStyle::State_Sunken;
  503. break;
  504. case QFrame::Raised:
  505. f.state |= QStyle::State_Raised;
  506. break;
  507. default:
  508. case QFrame::Plain:
  509. break;
  510. }
  511. f.lineWidth = d->ContentsLineWidth;
  512. f.midLineWidth = d->ContentsMidLineWidth;
  513. style()->drawControl(QStyle::CE_ShapedFrame, &f, &p, this);
  514. }
  515. //-----------------------------------------------------------------------------
  516. bool ctkCollapsibleButton::hitButton(const QPoint & _pos)const
  517. {
  518. QStyleOptionButton opt;
  519. this->initStyleOption(&opt);
  520. return opt.rect.contains(_pos);
  521. }
  522. //-----------------------------------------------------------------------------
  523. void ctkCollapsibleButton::childEvent(QChildEvent* c)
  524. {
  525. if(c && c->type() == QEvent::ChildAdded)
  526. {
  527. if (c->child() && c->child()->isWidgetType())
  528. {
  529. QWidget *w = static_cast<QWidget*>(c->child());
  530. w->setVisible(!ctk_d()->Collapsed);
  531. }
  532. }
  533. QWidget::childEvent(c);
  534. }
  535. //-----------------------------------------------------------------------------
  536. bool ctkCollapsibleButton::event(QEvent *event)
  537. {
  538. if (event->type() == QEvent::StyleChange
  539. || event->type() == QEvent::FontChange
  540. #ifdef Q_WS_MAC
  541. || event->type() == QEvent::MacSizeChange
  542. #endif
  543. )
  544. {
  545. this->setContentsMargins(0, this->buttonSizeHint().height(),0 , 0);
  546. if (this->collapsed())
  547. {
  548. this->setMaximumHeight(this->sizeHint().height());
  549. }
  550. }
  551. return QAbstractButton::event(event);
  552. }