ctkTreeComboBox.cpp 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407
  1. /*=========================================================================
  2. Library: CTK
  3. Copyright (c) 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 <QDesktopWidget>
  17. #include <QEvent>
  18. #include <QHeaderView>
  19. #include <QKeyEvent>
  20. #include <QLayout>
  21. #include <QScrollBar>
  22. #include <QInputContext>
  23. #include <QMouseEvent>
  24. #include <QModelIndex>
  25. #include <QStack>
  26. #include <QTreeView>
  27. #include <QDebug>
  28. // CTK includes
  29. #include "ctkTreeComboBox.h"
  30. // -------------------------------------------------------------------------
  31. class ctkTreeComboBoxPrivate
  32. {
  33. Q_DECLARE_PUBLIC(ctkTreeComboBox);
  34. protected:
  35. ctkTreeComboBox* const q_ptr;
  36. public:
  37. ctkTreeComboBoxPrivate(ctkTreeComboBox& object);
  38. int computeWidthHint()const;
  39. bool SkipNextHide;
  40. bool RootSet;
  41. bool SendCurrentItem;
  42. QPersistentModelIndex Root;
  43. };
  44. // -------------------------------------------------------------------------
  45. ctkTreeComboBoxPrivate::ctkTreeComboBoxPrivate(ctkTreeComboBox& object)
  46. :q_ptr(&object)
  47. {
  48. this->SkipNextHide = false;
  49. this->RootSet = false;
  50. this->SendCurrentItem = false;
  51. }
  52. // -------------------------------------------------------------------------
  53. int ctkTreeComboBoxPrivate::computeWidthHint()const
  54. {
  55. Q_Q(const ctkTreeComboBox);
  56. return q->view()->sizeHintForColumn(q->modelColumn());
  57. }
  58. // -------------------------------------------------------------------------
  59. ctkTreeComboBox::ctkTreeComboBox(QWidget* _parent):Superclass(_parent)
  60. , d_ptr(new ctkTreeComboBoxPrivate(*this))
  61. {
  62. QTreeView* treeView = new QTreeView(this);
  63. treeView->setHeaderHidden(true);
  64. this->setView(treeView);
  65. // we install the filter AFTER the QComboBox installed it.
  66. // so that our eventFilter will be called first
  67. this->view()->viewport()->installEventFilter(this);
  68. connect(treeView, SIGNAL(collapsed(const QModelIndex&)),
  69. this, SLOT(resizePopup()));
  70. connect(treeView, SIGNAL(expanded(const QModelIndex&)),
  71. this, SLOT(resizePopup()));
  72. }
  73. // -------------------------------------------------------------------------
  74. ctkTreeComboBox::~ctkTreeComboBox()
  75. {
  76. }
  77. // -------------------------------------------------------------------------
  78. bool ctkTreeComboBox::eventFilter(QObject* object, QEvent* _event)
  79. {
  80. Q_D(ctkTreeComboBox);
  81. Q_UNUSED(object);
  82. bool res = false;
  83. d->SendCurrentItem = false;
  84. switch (_event->type())
  85. {
  86. default:
  87. break;
  88. case QEvent::ShortcutOverride:
  89. switch (static_cast<QKeyEvent*>(_event)->key())
  90. {
  91. case Qt::Key_Enter:
  92. case Qt::Key_Return:
  93. case Qt::Key_Select:
  94. d->SendCurrentItem = true;
  95. break;
  96. default:
  97. break;
  98. }
  99. break;
  100. case QEvent::MouseButtonRelease:
  101. QMouseEvent* mouseEvent = dynamic_cast<QMouseEvent*>(_event);
  102. QModelIndex index = this->view()->indexAt(mouseEvent->pos());
  103. // do we click the branch (+ or -) or the item itself ?
  104. if (this->view()->model()->hasChildren(index) &&
  105. (index.flags() & Qt::ItemIsSelectable) &&
  106. !this->view()->visualRect(index).contains(mouseEvent->pos()))
  107. {//qDebug() << "Set skip on";
  108. // if the branch is clicked, then we don't want to close the
  109. // popup. (we don't want to select the item, just expand it.)
  110. // of course, all that doesn't apply with unselectable items, as
  111. // they won't close the popup.
  112. d->SkipNextHide = true;
  113. }
  114. // we want to get rid of an odd behavior.
  115. // If the user highlight a selectable item and then
  116. // click on the branch of an unselectable item while keeping the
  117. // previous selection. The popup would be normally closed in that
  118. // case. We don't want that.
  119. if ( this->view()->model()->hasChildren(index) &&
  120. !(index.flags() & Qt::ItemIsSelectable) &&
  121. !this->view()->visualRect(index).contains(mouseEvent->pos()))
  122. {//qDebug() << "eat";
  123. // eat the event, don't go to the QComboBox event filters.
  124. res = true;
  125. }
  126. d->SendCurrentItem = this->view()->rect().contains(mouseEvent->pos()) &&
  127. this->view()->currentIndex().isValid() &&
  128. (this->view()->currentIndex().flags() & Qt::ItemIsEnabled) &&
  129. (this->view()->currentIndex().flags() & Qt::ItemIsSelectable);
  130. break;
  131. }
  132. return res;
  133. }
  134. // -------------------------------------------------------------------------
  135. void ctkTreeComboBox::hidePopup()
  136. {
  137. Q_D(ctkTreeComboBox);
  138. if (d->SkipNextHide)
  139. {// don't hide the popup if the selected item is a parent.
  140. d->SkipNextHide = false;
  141. //this->setCurrentIndex(-1);
  142. //qDebug() << "skip";
  143. //this->QComboBox::showPopup();
  144. }
  145. else
  146. {
  147. //QModelIndex _currentIndex = this->view()->currentIndex();
  148. //qDebug() << "ctkTreeComboBox::hidePopup() " << _currentIndex << " " << _currentIndex.row();
  149. //qDebug() << "before: " << this->currentIndex() << this->view()->currentIndex();
  150. this->QComboBox::hidePopup();
  151. //qDebug() << "after: " << this->currentIndex() << this->view()->currentIndex();
  152. //this->setRootModelIndex(_currentIndex.parent());
  153. //this->setCurrentIndex(_currentIndex.row());
  154. if (d->SendCurrentItem)
  155. {
  156. d->SendCurrentItem = false;
  157. QKeyEvent event(QEvent::ShortcutOverride, Qt::Key_Enter, Qt::NoModifier);
  158. QApplication::sendEvent(this->view(), &event);
  159. }
  160. //qDebug() << "after2: " << this->currentIndex() << this->view()->currentIndex();
  161. }
  162. }
  163. // -------------------------------------------------------------------------
  164. void ctkTreeComboBox::paintEvent(QPaintEvent *p)
  165. {
  166. //qDebug() << __FUNCTION__ << " " << this->currentText() << " " << this->currentIndex() ;
  167. //qDebug() << this->itemText(0) << this->itemText(1);
  168. this->QComboBox::paintEvent(p);
  169. }
  170. // -------------------------------------------------------------------------
  171. QTreeView* ctkTreeComboBox::treeView()const
  172. {
  173. return qobject_cast<QTreeView*>(this->view());
  174. }
  175. // -------------------------------------------------------------------------
  176. void ctkTreeComboBox::resizePopup()
  177. {
  178. // copied from QComboBox.cpp
  179. Q_D(ctkTreeComboBox);
  180. QStyle * const style = this->style();
  181. QWidget* container = qobject_cast<QWidget*>(this->view()->parent());
  182. QStyleOptionComboBox opt;
  183. this->initStyleOption(&opt);
  184. QRect listRect(style->subControlRect(QStyle::CC_ComboBox, &opt,
  185. QStyle::SC_ComboBoxListBoxPopup, this));
  186. QRect screen = QApplication::desktop()->availableGeometry(
  187. QApplication::desktop()->screenNumber(this));
  188. QPoint below = this->mapToGlobal(listRect.bottomLeft());
  189. int belowHeight = screen.bottom() - below.y();
  190. QPoint above = this->mapToGlobal(listRect.topLeft());
  191. int aboveHeight = above.y() - screen.y();
  192. bool boundToScreen = !this->window()->testAttribute(Qt::WA_DontShowOnScreen);
  193. const bool usePopup = style->styleHint(QStyle::SH_ComboBox_Popup, &opt, this);
  194. {
  195. int listHeight = 0;
  196. int count = 0;
  197. QStack<QModelIndex> toCheck;
  198. toCheck.push(this->view()->rootIndex());
  199. #ifndef QT_NO_TREEVIEW
  200. QTreeView *treeView = qobject_cast<QTreeView*>(this->view());
  201. if (treeView && treeView->header() && !treeView->header()->isHidden())
  202. listHeight += treeView->header()->height();
  203. #endif
  204. while (!toCheck.isEmpty())
  205. {
  206. QModelIndex parent = toCheck.pop();
  207. for (int i = 0; i < this->model()->rowCount(parent); ++i)
  208. {
  209. QModelIndex idx = this->model()->index(i, this->modelColumn(), parent);
  210. if (!idx.isValid())
  211. {
  212. continue;
  213. }
  214. listHeight += this->view()->visualRect(idx).height(); /* + container->spacing() */;
  215. #ifndef QT_NO_TREEVIEW
  216. if (this->model()->hasChildren(idx) && treeView && treeView->isExpanded(idx))
  217. {
  218. toCheck.push(idx);
  219. }
  220. #endif
  221. ++count;
  222. if (!usePopup && count > this->maxVisibleItems())
  223. {
  224. toCheck.clear();
  225. break;
  226. }
  227. }
  228. }
  229. listRect.setHeight(listHeight);
  230. }
  231. {
  232. // add the spacing for the grid on the top and the bottom;
  233. int heightMargin = 0;//2*container->spacing();
  234. // add the frame of the container
  235. int marginTop, marginBottom;
  236. container->getContentsMargins(0, &marginTop, 0, &marginBottom);
  237. heightMargin += marginTop + marginBottom;
  238. //add the frame of the view
  239. this->view()->getContentsMargins(0, &marginTop, 0, &marginBottom);
  240. //marginTop += static_cast<QAbstractScrollAreaPrivate *>(QObjectPrivate::get(this->view()))->top;
  241. //marginBottom += static_cast<QAbstractScrollAreaPrivate *>(QObjectPrivate::get(this->view()))->bottom;
  242. heightMargin += marginTop + marginBottom;
  243. listRect.setHeight(listRect.height() + heightMargin);
  244. }
  245. // Add space for margin at top and bottom if the style wants it.
  246. if (usePopup)
  247. {
  248. listRect.setHeight(listRect.height() + style->pixelMetric(QStyle::PM_MenuVMargin, &opt, this) * 2);
  249. }
  250. // Make sure the popup is wide enough to display its contents.
  251. if (usePopup)
  252. {
  253. const int diff = d->computeWidthHint() - this->width();
  254. if (diff > 0)
  255. {
  256. listRect.setWidth(listRect.width() + diff);
  257. }
  258. }
  259. //we need to activate the layout to make sure the min/maximum size are set when the widget was not yet show
  260. container->layout()->activate();
  261. //takes account of the minimum/maximum size of the container
  262. listRect.setSize( listRect.size().expandedTo(container->minimumSize())
  263. .boundedTo(container->maximumSize()));
  264. // make sure the widget fits on screen
  265. if (boundToScreen)
  266. {
  267. if (listRect.width() > screen.width() )
  268. {
  269. listRect.setWidth(screen.width());
  270. }
  271. if (this->mapToGlobal(listRect.bottomRight()).x() > screen.right())
  272. {
  273. below.setX(screen.x() + screen.width() - listRect.width());
  274. above.setX(screen.x() + screen.width() - listRect.width());
  275. }
  276. if (this->mapToGlobal(listRect.topLeft()).x() < screen.x() )
  277. {
  278. below.setX(screen.x());
  279. above.setX(screen.x());
  280. }
  281. }
  282. if (usePopup)
  283. {
  284. // Position horizontally.
  285. listRect.moveLeft(above.x());
  286. #ifndef Q_WS_S60
  287. // Position vertically so the curently selected item lines up
  288. // with the combo box.
  289. const QRect currentItemRect = this->view()->visualRect(this->view()->currentIndex());
  290. const int offset = listRect.top() - currentItemRect.top();
  291. listRect.moveTop(above.y() + offset - listRect.top());
  292. #endif
  293. // Clamp the listRect height and vertical position so we don't expand outside the
  294. // available screen geometry.This may override the vertical position, but it is more
  295. // important to show as much as possible of the popup.
  296. const int height = !boundToScreen ? listRect.height() : qMin(listRect.height(), screen.height());
  297. #ifdef Q_WS_S60
  298. //popup needs to be stretched with screen minimum dimension
  299. listRect.setHeight(qMin(screen.height(), screen.width()));
  300. #else
  301. listRect.setHeight(height);
  302. #endif
  303. if (boundToScreen)
  304. {
  305. if (listRect.top() < screen.top())
  306. {
  307. listRect.moveTop(screen.top());
  308. }
  309. if (listRect.bottom() > screen.bottom())
  310. {
  311. listRect.moveBottom(screen.bottom());
  312. }
  313. }
  314. #ifdef Q_WS_S60
  315. if (screen.width() < screen.height())
  316. {
  317. // in portait, menu should be positioned above softkeys
  318. listRect.moveBottom(screen.bottom());
  319. }
  320. else
  321. {
  322. TRect staConTopRect = TRect();
  323. AknLayoutUtils::LayoutMetricsRect(AknLayoutUtils::EStaconTop, staConTopRect);
  324. listRect.setWidth(listRect.height());
  325. //by default popup is centered on screen in landscape
  326. listRect.moveCenter(screen.center());
  327. if (staConTopRect.IsEmpty())
  328. {
  329. // landscape without stacon, menu should be at the right
  330. (opt.direction == Qt::LeftToRight) ? listRect.setRight(screen.right()) :
  331. listRect.setLeft(screen.left());
  332. }
  333. }
  334. #endif
  335. }
  336. else if (!boundToScreen || listRect.height() <= belowHeight)
  337. {
  338. listRect.moveTopLeft(below);
  339. }
  340. else if (listRect.height() <= aboveHeight)
  341. {
  342. listRect.moveBottomLeft(above);
  343. }
  344. else if (belowHeight >= aboveHeight)
  345. {
  346. listRect.setHeight(belowHeight);
  347. listRect.moveTopLeft(below);
  348. }
  349. else
  350. {
  351. listRect.setHeight(aboveHeight);
  352. listRect.moveBottomLeft(above);
  353. }
  354. #ifndef QT_NO_IM
  355. if (QInputContext *qic = this->inputContext())
  356. {
  357. qic->reset();
  358. }
  359. #endif
  360. QScrollBar *sb = this->view()->horizontalScrollBar();
  361. Qt::ScrollBarPolicy policy = this->view()->horizontalScrollBarPolicy();
  362. bool needHorizontalScrollBar =
  363. (policy == Qt::ScrollBarAsNeeded || policy == Qt::ScrollBarAlwaysOn)
  364. && sb->minimum() < sb->maximum();
  365. if (needHorizontalScrollBar)
  366. {
  367. listRect.adjust(0, 0, 0, sb->height());
  368. }
  369. container->setGeometry(listRect);
  370. }