ctkCollapsibleButton.cpp 19 KB

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