Tumbler.qml 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479
  1. /****************************************************************************
  2. **
  3. ** Copyright (C) 2016 The Qt Company Ltd.
  4. ** Contact: https://www.qt.io/licensing/
  5. **
  6. ** This file is part of the Qt Quick Extras module of the Qt Toolkit.
  7. **
  8. ** $QT_BEGIN_LICENSE:LGPL$
  9. ** Commercial License Usage
  10. ** Licensees holding valid commercial Qt licenses may use this file in
  11. ** accordance with the commercial license agreement provided with the
  12. ** Software or, alternatively, in accordance with the terms contained in
  13. ** a written agreement between you and The Qt Company. For licensing terms
  14. ** and conditions see https://www.qt.io/terms-conditions. For further
  15. ** information use the contact form at https://www.qt.io/contact-us.
  16. **
  17. ** GNU Lesser General Public License Usage
  18. ** Alternatively, this file may be used under the terms of the GNU Lesser
  19. ** General Public License version 3 as published by the Free Software
  20. ** Foundation and appearing in the file LICENSE.LGPL3 included in the
  21. ** packaging of this file. Please review the following information to
  22. ** ensure the GNU Lesser General Public License version 3 requirements
  23. ** will be met: https://www.gnu.org/licenses/lgpl-3.0.html.
  24. **
  25. ** GNU General Public License Usage
  26. ** Alternatively, this file may be used under the terms of the GNU
  27. ** General Public License version 2.0 or (at your option) the GNU General
  28. ** Public license version 3 or any later version approved by the KDE Free
  29. ** Qt Foundation. The licenses are as published by the Free Software
  30. ** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3
  31. ** included in the packaging of this file. Please review the following
  32. ** information to ensure the GNU General Public License requirements will
  33. ** be met: https://www.gnu.org/licenses/gpl-2.0.html and
  34. ** https://www.gnu.org/licenses/gpl-3.0.html.
  35. **
  36. ** $QT_END_LICENSE$
  37. **
  38. ****************************************************************************/
  39. import QtQml 2.14 as Qml
  40. import QtQuick 2.2
  41. import QtQuick.Controls 1.4
  42. import QtQuick.Controls.Styles 1.4
  43. import QtQuick.Controls.Private 1.0
  44. import QtQuick.Extras 1.4
  45. import QtQuick.Extras.Private 1.0
  46. import QtQuick.Layouts 1.0
  47. /*!
  48. \qmltype Tumbler
  49. \inqmlmodule QtQuick.Extras
  50. \since 5.5
  51. \ingroup extras
  52. \ingroup extras-interactive
  53. \brief A control that can have several spinnable wheels, each with items
  54. that can be selected.
  55. \image tumbler.png A Tumbler
  56. \note Tumbler requires Qt 5.5.0 or later.
  57. The Tumbler control is used with one or more TumblerColumn items, which
  58. define the content of each column:
  59. \code
  60. Tumbler {
  61. TumblerColumn {
  62. model: 5
  63. }
  64. TumblerColumn {
  65. model: [0, 1, 2, 3, 4]
  66. }
  67. TumblerColumn {
  68. model: ["A", "B", "C", "D", "E"]
  69. }
  70. }
  71. \endcode
  72. You can also use a traditional model with roles:
  73. \code
  74. Rectangle {
  75. width: 220
  76. height: 350
  77. color: "#494d53"
  78. ListModel {
  79. id: listModel
  80. ListElement {
  81. foo: "A"
  82. bar: "B"
  83. baz: "C"
  84. }
  85. ListElement {
  86. foo: "A"
  87. bar: "B"
  88. baz: "C"
  89. }
  90. ListElement {
  91. foo: "A"
  92. bar: "B"
  93. baz: "C"
  94. }
  95. }
  96. Tumbler {
  97. anchors.centerIn: parent
  98. TumblerColumn {
  99. model: listModel
  100. role: "foo"
  101. }
  102. TumblerColumn {
  103. model: listModel
  104. role: "bar"
  105. }
  106. TumblerColumn {
  107. model: listModel
  108. role: "baz"
  109. }
  110. }
  111. }
  112. \endcode
  113. \section1 Limitations
  114. For technical reasons, the model count must be equal to or greater than
  115. \l {TumblerStyle::}{visibleItemCount}
  116. plus one. The
  117. \l {TumblerStyle::}{visibleItemCount}
  118. must also be an odd number.
  119. You can create a custom appearance for a Tumbler by assigning a
  120. \l {TumblerStyle}. To style
  121. individual columns, use the \l {TumblerColumn::delegate}{delegate} and
  122. \l {TumblerColumn::highlight}{highlight} properties of TumblerColumn.
  123. */
  124. Control {
  125. id: tumbler
  126. /*
  127. \qmlproperty Component Tumbler::style
  128. The style Component for this control.
  129. */
  130. style: Settings.styleComponent(Settings.style, "TumblerStyle.qml", tumbler)
  131. ListModel {
  132. id: columnModel
  133. }
  134. /*!
  135. \qmlproperty int Tumbler::columnCount
  136. The number of columns in the Tumbler.
  137. */
  138. readonly property alias columnCount: columnModel.count
  139. /*! \internal */
  140. function __isValidColumnIndex(index) {
  141. return index >= 0 && index < columnCount/* && columnRepeater.children.length === columnCount*/;
  142. }
  143. /*! \internal */
  144. function __isValidColumnAndItemIndex(columnIndex, itemIndex) {
  145. return __isValidColumnIndex(columnIndex) && itemIndex >= 0 && itemIndex < __viewAt(columnIndex).count;
  146. }
  147. /*!
  148. \qmlmethod int Tumbler::currentIndexAt(int columnIndex)
  149. Returns the current index of the column at \a columnIndex, or \c null
  150. if \a columnIndex is invalid.
  151. */
  152. function currentIndexAt(columnIndex) {
  153. if (!__isValidColumnIndex(columnIndex))
  154. return -1;
  155. return columnModel.get(columnIndex).columnObject.currentIndex;
  156. }
  157. /*!
  158. \qmlmethod void Tumbler::setCurrentIndexAt(int columnIndex, int itemIndex, int interval)
  159. Sets the current index of the column at \a columnIndex to \a itemIndex. The animation
  160. length can be set with \a interval, which defaults to \c 0.
  161. Does nothing if \a columnIndex or \a itemIndex are invalid.
  162. */
  163. function setCurrentIndexAt(columnIndex, itemIndex, interval) {
  164. if (!__isValidColumnAndItemIndex(columnIndex, itemIndex))
  165. return;
  166. var view = columnRepeater.itemAt(columnIndex).view;
  167. if (view.currentIndex !== itemIndex) {
  168. view.highlightMoveDuration = typeof interval !== 'undefined' ? interval : 0;
  169. view.currentIndex = itemIndex;
  170. view.highlightMoveDuration = Qt.binding(function(){ return __highlightMoveDuration; });
  171. }
  172. }
  173. /*!
  174. \qmlmethod TumblerColumn Tumbler::getColumn(int columnIndex)
  175. Returns the column at \a columnIndex or \c null if the index is
  176. invalid.
  177. */
  178. function getColumn(columnIndex) {
  179. if (!__isValidColumnIndex(columnIndex))
  180. return null;
  181. return columnModel.get(columnIndex).columnObject;
  182. }
  183. /*!
  184. \qmlmethod TumblerColumn Tumbler::addColumn(TumblerColumn column)
  185. Adds a \a column and returns the added column.
  186. The \a column argument can be an instance of TumblerColumn,
  187. or a \l Component. The component has to contain a TumblerColumn.
  188. Otherwise \c null is returned.
  189. */
  190. function addColumn(column) {
  191. return insertColumn(columnCount, column);
  192. }
  193. /*!
  194. \qmlmethod TumblerColumn Tumbler::insertColumn(int index, TumblerColumn column)
  195. Inserts a \a column at the given \a index and returns the inserted column.
  196. The \a column argument can be an instance of TumblerColumn,
  197. or a \l Component. The component has to contain a TumblerColumn.
  198. Otherwise, \c null is returned.
  199. */
  200. function insertColumn(index, column) {
  201. var object = column;
  202. if (typeof column["createObject"] === "function") {
  203. object = column.createObject(root);
  204. } else if (object.__tumbler) {
  205. console.warn("Tumbler::insertColumn(): you cannot add a column to multiple Tumblers")
  206. return null;
  207. }
  208. if (index >= 0 && index <= columnCount && object.accessibleRole === Accessible.ColumnHeader) {
  209. object.__tumbler = tumbler;
  210. object.__index = index;
  211. columnModel.insert(index, { columnObject: object });
  212. return object;
  213. }
  214. if (object !== column)
  215. object.destroy();
  216. console.warn("Tumbler::insertColumn(): invalid argument");
  217. return null;
  218. }
  219. /*
  220. Try making one selection bar by invisible highlight item hack, so that bars go across separators
  221. */
  222. Component.onCompleted: {
  223. for (var i = 0; i < data.length; ++i) {
  224. var column = data[i];
  225. if (column.accessibleRole === Accessible.ColumnHeader)
  226. addColumn(column);
  227. }
  228. }
  229. /*! \internal */
  230. readonly property alias __columnRow: columnRow
  231. /*! \internal */
  232. property int __highlightMoveDuration: 300
  233. /*! \internal */
  234. function __viewAt(index) {
  235. if (!__isValidColumnIndex(index))
  236. return null;
  237. return columnRepeater.itemAt(index).view;
  238. }
  239. /*! \internal */
  240. readonly property alias __movementDelayTimer: movementDelayTimer
  241. // When the up/down arrow keys are held down on a PathView,
  242. // the movement of the items is limited to the highlightMoveDuration,
  243. // but there is no built-in guard against trying to move the items at
  244. // the speed of the auto-repeat key presses. This results in sluggish
  245. // movement, so we enforce a delay with a timer to avoid this.
  246. Timer {
  247. id: movementDelayTimer
  248. interval: __highlightMoveDuration
  249. }
  250. Loader {
  251. id: backgroundLoader
  252. sourceComponent: __style.background
  253. anchors.fill: columnRow
  254. }
  255. Loader {
  256. id: frameLoader
  257. sourceComponent: __style.frame
  258. anchors.fill: columnRow
  259. anchors.leftMargin: -__style.padding.left
  260. anchors.rightMargin: -__style.padding.right
  261. anchors.topMargin: -__style.padding.top
  262. anchors.bottomMargin: -__style.padding.bottom
  263. }
  264. Row {
  265. id: columnRow
  266. x: __style.padding.left
  267. y: __style.padding.top
  268. Repeater {
  269. id: columnRepeater
  270. model: columnModel
  271. delegate: Item {
  272. id: columnItem
  273. width: columnPathView.width + separatorDelegateLoader.width
  274. height: columnPathView.height
  275. readonly property int __columnIndex: index
  276. // For index-related functions and tests.
  277. readonly property alias view: columnPathView
  278. readonly property alias separator: separatorDelegateLoader.item
  279. PathView {
  280. id: columnPathView
  281. width: columnObject.width
  282. height: tumbler.height - tumbler.__style.padding.top - tumbler.__style.padding.bottom
  283. visible: columnObject.visible
  284. clip: true
  285. Qml.Binding {
  286. target: columnObject
  287. property: "__currentIndex"
  288. value: columnPathView.currentIndex
  289. restoreMode: Binding.RestoreBinding
  290. }
  291. // We add one here so that the delegate's don't just appear in the view instantly,
  292. // but rather come from the top/bottom. To account for this adjustment elsewhere,
  293. // we extend the path height by half an item's height at the top and bottom.
  294. pathItemCount: tumbler.__style.visibleItemCount + 1
  295. preferredHighlightBegin: 0.5
  296. preferredHighlightEnd: 0.5
  297. highlightMoveDuration: tumbler.__highlightMoveDuration
  298. highlight: Loader {
  299. id: highlightLoader
  300. objectName: "highlightLoader"
  301. sourceComponent: columnObject.highlight ? columnObject.highlight : __style.highlight
  302. width: columnPathView.width
  303. readonly property int __index: index
  304. property QtObject styleData: QtObject {
  305. readonly property alias index: highlightLoader.__index
  306. readonly property int column: columnItem.__columnIndex
  307. readonly property bool activeFocus: columnPathView.activeFocus
  308. }
  309. }
  310. dragMargin: width / 2
  311. activeFocusOnTab: true
  312. Keys.onDownPressed: {
  313. if (!movementDelayTimer.running) {
  314. columnPathView.incrementCurrentIndex();
  315. movementDelayTimer.start();
  316. }
  317. }
  318. Keys.onUpPressed: {
  319. if (!movementDelayTimer.running) {
  320. columnPathView.decrementCurrentIndex();
  321. movementDelayTimer.start();
  322. }
  323. }
  324. path: Path {
  325. startX: columnPathView.width / 2
  326. startY: -tumbler.__style.__delegateHeight / 2
  327. PathLine {
  328. x: columnPathView.width / 2
  329. y: columnPathView.pathItemCount * tumbler.__style.__delegateHeight - tumbler.__style.__delegateHeight / 2
  330. }
  331. }
  332. model: columnObject.model
  333. delegate: Item {
  334. id: delegateRootItem
  335. property var itemModel: model
  336. implicitWidth: itemDelegateLoader.width
  337. implicitHeight: itemDelegateLoader.height
  338. Loader {
  339. id: itemDelegateLoader
  340. sourceComponent: columnObject.delegate ? columnObject.delegate : __style.delegate
  341. width: columnObject.width
  342. onHeightChanged: tumbler.__style.__delegateHeight = height;
  343. property var model: itemModel
  344. readonly property var __modelData: modelData
  345. readonly property int __columnDelegateIndex: index
  346. property QtObject styleData: QtObject {
  347. readonly property var modelData: itemDelegateLoader.__modelData
  348. readonly property alias index: itemDelegateLoader.__columnDelegateIndex
  349. readonly property int column: columnItem.__columnIndex
  350. readonly property bool activeFocus: columnPathView.activeFocus
  351. readonly property real displacement: {
  352. var count = delegateRootItem.PathView.view.count;
  353. var offset = delegateRootItem.PathView.view.offset;
  354. var d = count - index - offset;
  355. var halfVisibleItems = Math.floor(tumbler.__style.visibleItemCount / 2) + 1;
  356. if (d > halfVisibleItems)
  357. d -= count;
  358. else if (d < -halfVisibleItems)
  359. d += count;
  360. return d;
  361. }
  362. readonly property bool current: delegateRootItem.PathView.isCurrentItem
  363. readonly property string role: columnObject.role
  364. readonly property var value: (itemModel && itemModel.hasOwnProperty(role))
  365. ? itemModel[role] // Qml ListModel and QAbstractItemModel
  366. : modelData && modelData.hasOwnProperty(role)
  367. ? modelData[role] // QObjectList/QObject
  368. : modelData != undefined ? modelData : "" // Models without role
  369. }
  370. }
  371. }
  372. }
  373. Loader {
  374. anchors.fill: columnPathView
  375. sourceComponent: columnObject.columnForeground ? columnObject.columnForeground : __style.columnForeground
  376. property QtObject styleData: QtObject {
  377. readonly property int column: columnItem.__columnIndex
  378. readonly property bool activeFocus: columnPathView.activeFocus
  379. }
  380. }
  381. Loader {
  382. id: separatorDelegateLoader
  383. objectName: "separatorDelegateLoader"
  384. sourceComponent: __style.separator
  385. // Don't need a separator after the last delegate.
  386. active: __columnIndex < tumbler.columnCount - 1
  387. anchors.left: columnPathView.right
  388. anchors.top: parent.top
  389. anchors.bottom: parent.bottom
  390. visible: columnObject.visible
  391. // Use the width of the first separator to help us
  392. // determine the default separator width.
  393. onWidthChanged: {
  394. if (__columnIndex == 0) {
  395. tumbler.__style.__separatorWidth = width;
  396. }
  397. }
  398. property QtObject styleData: QtObject {
  399. readonly property int index: __columnIndex
  400. }
  401. }
  402. }
  403. }
  404. }
  405. Loader {
  406. id: foregroundLoader
  407. sourceComponent: __style.foreground
  408. anchors.fill: backgroundLoader
  409. }
  410. }