| /**************************************************************************** |
| ** |
| ** Copyright (C) 2016 The Qt Company Ltd. |
| ** Contact: https://www.qt.io/licensing/ |
| ** |
| ** This file is part of the Qt Quick Extras module of the Qt Toolkit. |
| ** |
| ** $QT_BEGIN_LICENSE:LGPL$ |
| ** Commercial License Usage |
| ** Licensees holding valid commercial Qt licenses may use this file in |
| ** accordance with the commercial license agreement provided with the |
| ** Software or, alternatively, in accordance with the terms contained in |
| ** a written agreement between you and The Qt Company. For licensing terms |
| ** and conditions see https://www.qt.io/terms-conditions. For further |
| ** information use the contact form at https://www.qt.io/contact-us. |
| ** |
| ** GNU Lesser General Public License Usage |
| ** Alternatively, this file may be used under the terms of the GNU Lesser |
| ** General Public License version 3 as published by the Free Software |
| ** Foundation and appearing in the file LICENSE.LGPL3 included in the |
| ** packaging of this file. Please review the following information to |
| ** ensure the GNU Lesser General Public License version 3 requirements |
| ** will be met: https://www.gnu.org/licenses/lgpl-3.0.html. |
| ** |
| ** GNU General Public License Usage |
| ** Alternatively, this file may be used under the terms of the GNU |
| ** General Public License version 2.0 or (at your option) the GNU General |
| ** Public license version 3 or any later version approved by the KDE Free |
| ** Qt Foundation. The licenses are as published by the Free Software |
| ** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 |
| ** included in the packaging of this file. Please review the following |
| ** information to ensure the GNU General Public License requirements will |
| ** be met: https://www.gnu.org/licenses/gpl-2.0.html and |
| ** https://www.gnu.org/licenses/gpl-3.0.html. |
| ** |
| ** $QT_END_LICENSE$ |
| ** |
| ****************************************************************************/ |
| |
| import QtQml 2.14 as Qml |
| import QtQuick 2.2 |
| import QtQuick.Controls 1.4 |
| import QtQuick.Controls.Styles 1.4 |
| import QtQuick.Controls.Private 1.0 |
| import QtQuick.Extras 1.4 |
| import QtQuick.Extras.Private 1.0 |
| import QtQuick.Layouts 1.0 |
| |
| /*! |
| \qmltype Tumbler |
| \inqmlmodule QtQuick.Extras |
| \since 5.5 |
| \ingroup extras |
| \ingroup extras-interactive |
| \brief A control that can have several spinnable wheels, each with items |
| that can be selected. |
| |
| \image tumbler.png A Tumbler |
| |
| \note Tumbler requires Qt 5.5.0 or later. |
| |
| The Tumbler control is used with one or more TumblerColumn items, which |
| define the content of each column: |
| |
| \code |
| Tumbler { |
| TumblerColumn { |
| model: 5 |
| } |
| TumblerColumn { |
| model: [0, 1, 2, 3, 4] |
| } |
| TumblerColumn { |
| model: ["A", "B", "C", "D", "E"] |
| } |
| } |
| \endcode |
| |
| You can also use a traditional model with roles: |
| |
| \code |
| Rectangle { |
| width: 220 |
| height: 350 |
| color: "#494d53" |
| |
| ListModel { |
| id: listModel |
| |
| ListElement { |
| foo: "A" |
| bar: "B" |
| baz: "C" |
| } |
| ListElement { |
| foo: "A" |
| bar: "B" |
| baz: "C" |
| } |
| ListElement { |
| foo: "A" |
| bar: "B" |
| baz: "C" |
| } |
| } |
| |
| Tumbler { |
| anchors.centerIn: parent |
| |
| TumblerColumn { |
| model: listModel |
| role: "foo" |
| } |
| TumblerColumn { |
| model: listModel |
| role: "bar" |
| } |
| TumblerColumn { |
| model: listModel |
| role: "baz" |
| } |
| } |
| } |
| \endcode |
| |
| \section1 Limitations |
| |
| For technical reasons, the model count must be equal to or greater than |
| \l {TumblerStyle::}{visibleItemCount} |
| plus one. The |
| \l {TumblerStyle::}{visibleItemCount} |
| must also be an odd number. |
| |
| You can create a custom appearance for a Tumbler by assigning a |
| \l {TumblerStyle}. To style |
| individual columns, use the \l {TumblerColumn::delegate}{delegate} and |
| \l {TumblerColumn::highlight}{highlight} properties of TumblerColumn. |
| */ |
| |
| Control { |
| id: tumbler |
| |
| /* |
| \qmlproperty Component Tumbler::style |
| |
| The style Component for this control. |
| */ |
| style: Settings.styleComponent(Settings.style, "TumblerStyle.qml", tumbler) |
| |
| ListModel { |
| id: columnModel |
| } |
| |
| /*! |
| \qmlproperty int Tumbler::columnCount |
| |
| The number of columns in the Tumbler. |
| */ |
| readonly property alias columnCount: columnModel.count |
| |
| /*! \internal */ |
| function __isValidColumnIndex(index) { |
| return index >= 0 && index < columnCount/* && columnRepeater.children.length === columnCount*/; |
| } |
| |
| /*! \internal */ |
| function __isValidColumnAndItemIndex(columnIndex, itemIndex) { |
| return __isValidColumnIndex(columnIndex) && itemIndex >= 0 && itemIndex < __viewAt(columnIndex).count; |
| } |
| |
| /*! |
| \qmlmethod int Tumbler::currentIndexAt(int columnIndex) |
| Returns the current index of the column at \a columnIndex, or \c null |
| if \a columnIndex is invalid. |
| */ |
| function currentIndexAt(columnIndex) { |
| if (!__isValidColumnIndex(columnIndex)) |
| return -1; |
| |
| return columnModel.get(columnIndex).columnObject.currentIndex; |
| } |
| |
| /*! |
| \qmlmethod void Tumbler::setCurrentIndexAt(int columnIndex, int itemIndex, int interval) |
| Sets the current index of the column at \a columnIndex to \a itemIndex. The animation |
| length can be set with \a interval, which defaults to \c 0. |
| |
| Does nothing if \a columnIndex or \a itemIndex are invalid. |
| */ |
| function setCurrentIndexAt(columnIndex, itemIndex, interval) { |
| if (!__isValidColumnAndItemIndex(columnIndex, itemIndex)) |
| return; |
| |
| var view = columnRepeater.itemAt(columnIndex).view; |
| if (view.currentIndex !== itemIndex) { |
| view.highlightMoveDuration = typeof interval !== 'undefined' ? interval : 0; |
| view.currentIndex = itemIndex; |
| view.highlightMoveDuration = Qt.binding(function(){ return __highlightMoveDuration; }); |
| } |
| } |
| |
| /*! |
| \qmlmethod TumblerColumn Tumbler::getColumn(int columnIndex) |
| Returns the column at \a columnIndex or \c null if the index is |
| invalid. |
| */ |
| function getColumn(columnIndex) { |
| if (!__isValidColumnIndex(columnIndex)) |
| return null; |
| |
| return columnModel.get(columnIndex).columnObject; |
| } |
| |
| /*! |
| \qmlmethod TumblerColumn Tumbler::addColumn(TumblerColumn column) |
| Adds a \a column and returns the added column. |
| |
| The \a column argument can be an instance of TumblerColumn, |
| or a \l Component. The component has to contain a TumblerColumn. |
| Otherwise \c null is returned. |
| */ |
| function addColumn(column) { |
| return insertColumn(columnCount, column); |
| } |
| |
| /*! |
| \qmlmethod TumblerColumn Tumbler::insertColumn(int index, TumblerColumn column) |
| Inserts a \a column at the given \a index and returns the inserted column. |
| |
| The \a column argument can be an instance of TumblerColumn, |
| or a \l Component. The component has to contain a TumblerColumn. |
| Otherwise, \c null is returned. |
| */ |
| function insertColumn(index, column) { |
| var object = column; |
| if (typeof column["createObject"] === "function") { |
| object = column.createObject(root); |
| } else if (object.__tumbler) { |
| console.warn("Tumbler::insertColumn(): you cannot add a column to multiple Tumblers") |
| return null; |
| } |
| if (index >= 0 && index <= columnCount && object.Accessible.role === Accessible.ColumnHeader) { |
| object.__tumbler = tumbler; |
| object.__index = index; |
| columnModel.insert(index, { columnObject: object }); |
| return object; |
| } |
| |
| if (object !== column) |
| object.destroy(); |
| console.warn("Tumbler::insertColumn(): invalid argument"); |
| return null; |
| } |
| |
| /* |
| Try making one selection bar by invisible highlight item hack, so that bars go across separators |
| */ |
| |
| Component.onCompleted: { |
| for (var i = 0; i < data.length; ++i) { |
| var column = data[i]; |
| if (column.Accessible.role === Accessible.ColumnHeader) |
| addColumn(column); |
| } |
| } |
| |
| /*! \internal */ |
| readonly property alias __columnRow: columnRow |
| /*! \internal */ |
| property int __highlightMoveDuration: 300 |
| |
| /*! \internal */ |
| function __viewAt(index) { |
| if (!__isValidColumnIndex(index)) |
| return null; |
| |
| return columnRepeater.itemAt(index).view; |
| } |
| |
| /*! \internal */ |
| readonly property alias __movementDelayTimer: movementDelayTimer |
| |
| // When the up/down arrow keys are held down on a PathView, |
| // the movement of the items is limited to the highlightMoveDuration, |
| // but there is no built-in guard against trying to move the items at |
| // the speed of the auto-repeat key presses. This results in sluggish |
| // movement, so we enforce a delay with a timer to avoid this. |
| Timer { |
| id: movementDelayTimer |
| interval: __highlightMoveDuration |
| } |
| |
| Loader { |
| id: backgroundLoader |
| sourceComponent: __style.background |
| anchors.fill: columnRow |
| } |
| |
| Loader { |
| id: frameLoader |
| sourceComponent: __style.frame |
| anchors.fill: columnRow |
| anchors.leftMargin: -__style.padding.left |
| anchors.rightMargin: -__style.padding.right |
| anchors.topMargin: -__style.padding.top |
| anchors.bottomMargin: -__style.padding.bottom |
| } |
| |
| Row { |
| id: columnRow |
| x: __style.padding.left |
| y: __style.padding.top |
| |
| Repeater { |
| id: columnRepeater |
| model: columnModel |
| delegate: Item { |
| id: columnItem |
| width: columnPathView.width + separatorDelegateLoader.width |
| height: columnPathView.height |
| |
| readonly property int __columnIndex: index |
| // For index-related functions and tests. |
| readonly property alias view: columnPathView |
| readonly property alias separator: separatorDelegateLoader.item |
| |
| PathView { |
| id: columnPathView |
| width: columnObject.width |
| height: tumbler.height - tumbler.__style.padding.top - tumbler.__style.padding.bottom |
| visible: columnObject.visible |
| clip: true |
| |
| Qml.Binding { |
| target: columnObject |
| property: "__currentIndex" |
| value: columnPathView.currentIndex |
| restoreMode: Binding.RestoreBinding |
| } |
| |
| // We add one here so that the delegate's don't just appear in the view instantly, |
| // but rather come from the top/bottom. To account for this adjustment elsewhere, |
| // we extend the path height by half an item's height at the top and bottom. |
| pathItemCount: tumbler.__style.visibleItemCount + 1 |
| preferredHighlightBegin: 0.5 |
| preferredHighlightEnd: 0.5 |
| highlightMoveDuration: tumbler.__highlightMoveDuration |
| highlight: Loader { |
| id: highlightLoader |
| objectName: "highlightLoader" |
| sourceComponent: columnObject.highlight ? columnObject.highlight : __style.highlight |
| width: columnPathView.width |
| |
| readonly property int __index: index |
| |
| property QtObject styleData: QtObject { |
| readonly property alias index: highlightLoader.__index |
| readonly property int column: columnItem.__columnIndex |
| readonly property bool activeFocus: columnPathView.activeFocus |
| } |
| } |
| dragMargin: width / 2 |
| |
| activeFocusOnTab: true |
| Keys.onDownPressed: { |
| if (!movementDelayTimer.running) { |
| columnPathView.incrementCurrentIndex(); |
| movementDelayTimer.start(); |
| } |
| } |
| Keys.onUpPressed: { |
| if (!movementDelayTimer.running) { |
| columnPathView.decrementCurrentIndex(); |
| movementDelayTimer.start(); |
| } |
| } |
| |
| path: Path { |
| startX: columnPathView.width / 2 |
| startY: -tumbler.__style.__delegateHeight / 2 |
| PathLine { |
| x: columnPathView.width / 2 |
| y: columnPathView.pathItemCount * tumbler.__style.__delegateHeight - tumbler.__style.__delegateHeight / 2 |
| } |
| } |
| |
| model: columnObject.model |
| |
| delegate: Item { |
| id: delegateRootItem |
| property var itemModel: model |
| |
| implicitWidth: itemDelegateLoader.width |
| implicitHeight: itemDelegateLoader.height |
| |
| Loader { |
| id: itemDelegateLoader |
| sourceComponent: columnObject.delegate ? columnObject.delegate : __style.delegate |
| width: columnObject.width |
| |
| onHeightChanged: tumbler.__style.__delegateHeight = height; |
| |
| property var model: itemModel |
| |
| readonly property var __modelData: modelData |
| readonly property int __columnDelegateIndex: index |
| property QtObject styleData: QtObject { |
| readonly property var modelData: itemDelegateLoader.__modelData |
| readonly property alias index: itemDelegateLoader.__columnDelegateIndex |
| readonly property int column: columnItem.__columnIndex |
| readonly property bool activeFocus: columnPathView.activeFocus |
| readonly property real displacement: { |
| var count = delegateRootItem.PathView.view.count; |
| var offset = delegateRootItem.PathView.view.offset; |
| |
| var d = count - index - offset; |
| var halfVisibleItems = Math.floor(tumbler.__style.visibleItemCount / 2) + 1; |
| if (d > halfVisibleItems) |
| d -= count; |
| else if (d < -halfVisibleItems) |
| d += count; |
| return d; |
| } |
| readonly property bool current: delegateRootItem.PathView.isCurrentItem |
| readonly property string role: columnObject.role |
| readonly property var value: (itemModel && itemModel.hasOwnProperty(role)) |
| ? itemModel[role] // Qml ListModel and QAbstractItemModel |
| : modelData && modelData.hasOwnProperty(role) |
| ? modelData[role] // QObjectList/QObject |
| : modelData != undefined ? modelData : "" // Models without role |
| } |
| } |
| } |
| } |
| |
| Loader { |
| anchors.fill: columnPathView |
| sourceComponent: columnObject.columnForeground ? columnObject.columnForeground : __style.columnForeground |
| |
| property QtObject styleData: QtObject { |
| readonly property int column: columnItem.__columnIndex |
| readonly property bool activeFocus: columnPathView.activeFocus |
| } |
| } |
| |
| Loader { |
| id: separatorDelegateLoader |
| objectName: "separatorDelegateLoader" |
| sourceComponent: __style.separator |
| // Don't need a separator after the last delegate. |
| active: __columnIndex < tumbler.columnCount - 1 |
| anchors.left: columnPathView.right |
| anchors.top: parent.top |
| anchors.bottom: parent.bottom |
| visible: columnObject.visible |
| |
| // Use the width of the first separator to help us |
| // determine the default separator width. |
| onWidthChanged: { |
| if (__columnIndex == 0) { |
| tumbler.__style.__separatorWidth = width; |
| } |
| } |
| |
| property QtObject styleData: QtObject { |
| readonly property int index: __columnIndex |
| } |
| } |
| } |
| } |
| } |
| |
| Loader { |
| id: foregroundLoader |
| sourceComponent: __style.foreground |
| anchors.fill: backgroundLoader |
| } |
| } |