diff options
author | Mitch Curtis <mitch.curtis@theqtcompany.com> | 2015-07-16 12:00:55 +0200 |
---|---|---|
committer | Mitch Curtis <mitch.curtis@theqtcompany.com> | 2015-08-06 11:43:48 +0000 |
commit | 293fc5e8f7df1b60a07d2e7e489e57059bb021bc (patch) | |
tree | 7ea3921797c2541c379cc91eb1f0c4f30d575fbc | |
parent | 1448ee0d1c02280aa33424f992b2f26d74615a43 (diff) |
Add support for ListView to Tumbler.
This enables creation of non-wrapping Tumblers.
Change-Id: I0e21b860b84c456c0651923e87217cafc42c69b7
Reviewed-by: Mitch Curtis <mitch.curtis@theqtcompany.com>
-rw-r--r-- | examples/quick/calendar/DateTimePicker.qml | 176 | ||||
-rw-r--r-- | examples/quick/calendar/EventView.qml | 209 | ||||
-rw-r--r-- | examples/quick/calendar/TumblerDelegate.qml | 54 | ||||
-rw-r--r-- | examples/quick/calendar/calendar.pro | 4 | ||||
-rw-r--r-- | examples/quick/calendar/calendar.qrc | 3 | ||||
-rw-r--r-- | examples/quick/calendar/main.cpp | 178 | ||||
-rw-r--r-- | examples/quick/calendar/main.qml | 139 | ||||
-rw-r--r-- | src/extras/doc/images/qtquickextras2-tumbler-wrap.gif | bin | 0 -> 28883 bytes | |||
-rw-r--r-- | src/extras/qquicktumbler.cpp | 174 | ||||
-rw-r--r-- | src/extras/qquicktumbler_p.h | 4 | ||||
-rw-r--r-- | tests/auto/extras/data/tst_tumbler.qml | 204 |
11 files changed, 975 insertions, 170 deletions
diff --git a/examples/quick/calendar/DateTimePicker.qml b/examples/quick/calendar/DateTimePicker.qml new file mode 100644 index 00000000..88238048 --- /dev/null +++ b/examples/quick/calendar/DateTimePicker.qml @@ -0,0 +1,176 @@ +/**************************************************************************** +** +** Copyright (C) 2015 The Qt Company Ltd. +** Contact: http://www.qt.io/licensing/ +** +** This file is part of the examples of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:BSD$ +** You may use this file under the terms of the BSD license as follows: +** +** "Redistribution and use in source and binary forms, with or without +** modification, are permitted provided that the following conditions are +** met: +** * Redistributions of source code must retain the above copyright +** notice, this list of conditions and the following disclaimer. +** * Redistributions in binary form must reproduce the above copyright +** notice, this list of conditions and the following disclaimer in +** the documentation and/or other materials provided with the +** distribution. +** * Neither the name of The Qt Company Ltd nor the names of its +** contributors may be used to endorse or promote products derived +** from this software without specific prior written permission. +** +** +** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +import QtQuick 2.6 +import QtQuick.Calendar 2.0 +import QtQuick.Controls 2.0 +import QtQuick.Extras 2.0 + +Item { + id: dateTimePicker + enabled: dateToShow.getFullYear() >= fromYear || dateToShow.getFullYear() <= toYear + implicitWidth: row.implicitWidth + implicitHeight: row.implicitHeight + + readonly property var days: [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31] + + readonly property int fromYear: 2000 + readonly property int toYear: 2020 + + readonly property alias chosenDate: dateTimePicker.__date + property var __date: new Date( + fromYear + yearTumbler.currentIndex, + monthTumbler.currentIndex, + dayTumbler.currentIndex + 1, + hoursTumbler.currentIndex + (amPmTumbler.currentIndex == 0 ? 0 : 12), + minutesTumbler.currentIndex); + + property date dateToShow: new Date() + onDateToShowChanged: { + yearTumbler.currentIndex = dateToShow.getFullYear() - fromYear; + monthTumbler.currentIndex = dateToShow.getMonth(); + dayTumbler.currentIndex = dateToShow.getDate() - 1; + } + + FontMetrics { + id: fontMetrics + } + + Row { + id: row + spacing: 2 + + Frame { + padding: 0 + + Row { + Tumbler { + id: dayTumbler + + delegate: TumblerDelegate { + text: modelData + font.pixelSize: fontMetrics.font.pixelSize * (AbstractTumbler.tumbler.activeFocus ? 2 : 1.25) + } + + function updateModel() { + var previousIndex = dayTumbler.currentIndex; + var array = []; + var newDays = dateTimePicker.days[monthTumbler.currentIndex]; + for (var i = 0; i < newDays; ++i) { + array.push(i + 1); + } + dayTumbler.model = array; + dayTumbler.currentIndex = Math.min(newDays - 1, previousIndex); + } + + Component.onCompleted: updateModel() + } + Tumbler { + id: monthTumbler + model: ["Jan", "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"] + delegate: TumblerDelegate { + text: modelData + font.pixelSize: fontMetrics.font.pixelSize * (AbstractTumbler.tumbler.activeFocus ? 2 : 1.25) + } + onCurrentIndexChanged: dayTumbler.updateModel() + } + Tumbler { + id: yearTumbler + width: 80 + model: { + var years = []; + for (var i = fromYear; i <= toYear; ++i) { + years.push(i); + } + return years; + } + delegate: TumblerDelegate { + text: modelData + font.pixelSize: fontMetrics.font.pixelSize * (AbstractTumbler.tumbler.activeFocus ? 2 : 1.25) + } + } + } + } + + Frame { + padding: 0 + + Row { + Tumbler { + id: hoursTumbler + model: 12 + delegate: TumblerDelegate { + text: modelData.toString().length < 2 ? "0" + modelData : modelData + font.pixelSize: fontMetrics.font.pixelSize * (AbstractTumbler.tumbler.activeFocus ? 2 : 1.25) + } + } + + Tumbler { + id: minutesTumbler + model: 60 + delegate: TumblerDelegate { + text: modelData.toString().length < 2 ? "0" + modelData : modelData + font.pixelSize: fontMetrics.font.pixelSize * (AbstractTumbler.tumbler.activeFocus ? 2 : 1.25) + } + } + + Tumbler { + id: amPmTumbler + model: ["AM", "PM"] + delegate: TumblerDelegate { + font.pixelSize: fontMetrics.font.pixelSize * (AbstractTumbler.tumbler.activeFocus ? 2 : 1.25) + } + + contentItem: ListView { + anchors.fill: parent + model: amPmTumbler.model + delegate: amPmTumbler.delegate + + snapMode: ListView.SnapToItem + highlightRangeMode: ListView.StrictlyEnforceRange + preferredHighlightBegin: height / 2 - (height / 3 / 2) + preferredHighlightEnd: height / 2 + (height / 3 / 2) + clip: true + } + } + } + } + } +} diff --git a/examples/quick/calendar/EventView.qml b/examples/quick/calendar/EventView.qml new file mode 100644 index 00000000..3991e5eb --- /dev/null +++ b/examples/quick/calendar/EventView.qml @@ -0,0 +1,209 @@ +/**************************************************************************** +** +** Copyright (C) 2015 The Qt Company Ltd. +** Contact: http://www.qt.io/licensing/ +** +** This file is part of the examples of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:BSD$ +** You may use this file under the terms of the BSD license as follows: +** +** "Redistribution and use in source and binary forms, with or without +** modification, are permitted provided that the following conditions are +** met: +** * Redistributions of source code must retain the above copyright +** notice, this list of conditions and the following disclaimer. +** * Redistributions in binary form must reproduce the above copyright +** notice, this list of conditions and the following disclaimer in +** the documentation and/or other materials provided with the +** distribution. +** * Neither the name of The Qt Company Ltd nor the names of its +** contributors may be used to endorse or promote products derived +** from this software without specific prior written permission. +** +** +** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +import QtQuick 2.6 +import QtQuick.Controls 2.0 + +Rectangle { + border.color: Theme.frameColor + + property date selectedDate + property var locale + property var eventModel + + signal addEventClicked + + Component { + id: eventListHeader + + Row { + id: eventDateRow + width: parent.width + height: eventDayLabel.height + spacing: 10 + + Label { + id: eventDayLabel + text: selectedDate.getDate() + font.pointSize: 35 + } + + Column { + height: eventDayLabel.height + + Label { + readonly property var options: { weekday: "long" } + text: Qt.locale().standaloneDayName(selectedDate.getDay(), Locale.LongFormat) + font.pointSize: 18 + } + Label { + text: Qt.locale().standaloneMonthName(selectedDate.getMonth()) + + selectedDate.toLocaleDateString(Qt.locale(), " yyyy") + font.pointSize: 12 + } + } + } + } + + ListView { + id: eventsListView + spacing: 4 + clip: true + header: eventListHeader + anchors.fill: parent + anchors.margins: 10 + model: eventModel + + delegate: Rectangle { + width: eventsListView.width + height: eventItemColumn.height + anchors.horizontalCenter: parent.horizontalCenter + + Rectangle { + width: parent.width + height: 1 + color: "#eee" + } + + Column { + id: eventItemColumn + x: 4 + y: 4 + width: parent.width - 8 + height: timeRow.height + descriptionLabel.height + 8 + + Label { + id: descriptionLabel + width: parent.width + wrapMode: Text.Wrap + text: description + } + Row { + id: timeRow + width: parent.width + Label { + text: start.toLocaleTimeString(locale, Locale.ShortFormat) + color: "#aaa" + } + Label { + text: "-" + end.toLocaleTimeString(locale, Locale.ShortFormat) + visible: start.getTime() !== end.getTime() && start.getDate() === end.getDate() + color: "#aaa" + } + } + } + + MouseArea { + anchors.fill: parent + onPressAndHold: removeButton.opacity = 1 + onClicked: removeButton.opacity = 0 + } + + Button { + id: removeButton + opacity: 0 + + Behavior on opacity { + NumberAnimation { + duration: 150 + } + } + + anchors.right: parent.right + anchors.rightMargin: 12 + anchors.verticalCenter: parent.verticalCenter + + onClicked: eventModel.removeEvent(index) + + background: Rectangle { + implicitWidth: 32 + implicitHeight: 32 + + radius: width / 2 + color: Qt.tint(!addButton.enabled ? addButton.Theme.disabledColor : + addButton.activeFocus ? addButton.Theme.focusColor : "red", + addButton.pressed ? addButton.Theme.pressColor : "transparent") + } + } + + // Don't want the white icon to change opacity. + Rectangle { + anchors.centerIn: removeButton + width: 18 + height: 4 + radius: 1 + } + } + } + + Button { + id: addButton + + anchors.right: parent.right + anchors.bottom: parent.bottom + anchors.margins: 12 + + onClicked: addEventClicked() + + background: Rectangle { + implicitWidth: 32 + implicitHeight: 32 + + radius: width / 2 + color: Qt.tint(!addButton.enabled ? addButton.Theme.disabledColor : + addButton.activeFocus ? addButton.Theme.focusColor : addButton.Theme.accentColor, + addButton.pressed ? addButton.Theme.pressColor : "transparent") + } + + Rectangle { + anchors.centerIn: parent + width: 4 + height: 18 + radius: 1 + } + + Rectangle { + anchors.centerIn: parent + width: 18 + height: 4 + radius: 1 + } + } +} diff --git a/examples/quick/calendar/TumblerDelegate.qml b/examples/quick/calendar/TumblerDelegate.qml new file mode 100644 index 00000000..60d71f21 --- /dev/null +++ b/examples/quick/calendar/TumblerDelegate.qml @@ -0,0 +1,54 @@ +/**************************************************************************** +** +** Copyright (C) 2015 The Qt Company Ltd. +** Contact: http://www.qt.io/licensing/ +** +** This file is part of the examples of the Qt Toolkit. +** +** $QT_BEGIN_LICENSE:BSD$ +** You may use this file under the terms of the BSD license as follows: +** +** "Redistribution and use in source and binary forms, with or without +** modification, are permitted provided that the following conditions are +** met: +** * Redistributions of source code must retain the above copyright +** notice, this list of conditions and the following disclaimer. +** * Redistributions in binary form must reproduce the above copyright +** notice, this list of conditions and the following disclaimer in +** the documentation and/or other materials provided with the +** distribution. +** * Neither the name of The Qt Company Ltd nor the names of its +** contributors may be used to endorse or promote products derived +** from this software without specific prior written permission. +** +** +** THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS +** "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT +** LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR +** A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT +** OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, +** SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT +** LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, +** DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY +** THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT +** (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +** OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE." +** +** $QT_END_LICENSE$ +** +****************************************************************************/ + +import QtQuick 2.6 +import QtQuick.Extras 2.0 + +Text { + text: isNaN(modelData) ? modelData : modelData + 1 + color: "#666666" + opacity: 0.4 + Math.max(0, 1 - Math.abs(AbstractTumbler.displacement)) * 0.6 + horizontalAlignment: Text.AlignHCenter + verticalAlignment: Text.AlignVCenter + + Behavior on font.pixelSize { + NumberAnimation {} + } +} diff --git a/examples/quick/calendar/calendar.pro b/examples/quick/calendar/calendar.pro index f81d9e5a..105e7fc9 100644 --- a/examples/quick/calendar/calendar.pro +++ b/examples/quick/calendar/calendar.pro @@ -13,3 +13,7 @@ RESOURCES += \ target.path = $$[QT_INSTALL_EXAMPLES]/quickcalendar2/calendar INSTALLS += target + +DISTFILES += \ + DateTimePicker.qml \ + EventView.qml diff --git a/examples/quick/calendar/calendar.qrc b/examples/quick/calendar/calendar.qrc index 5f6483ac..d0f67b26 100644 --- a/examples/quick/calendar/calendar.qrc +++ b/examples/quick/calendar/calendar.qrc @@ -1,5 +1,8 @@ <RCC> <qresource prefix="/"> <file>main.qml</file> + <file>DateTimePicker.qml</file> + <file>TumblerDelegate.qml</file> + <file>EventView.qml</file> </qresource> </RCC> diff --git a/examples/quick/calendar/main.cpp b/examples/quick/calendar/main.cpp index ae41bdf0..1d739add 100644 --- a/examples/quick/calendar/main.cpp +++ b/examples/quick/calendar/main.cpp @@ -45,31 +45,84 @@ class Event : public QObject { Q_OBJECT - Q_PROPERTY(QString name MEMBER name NOTIFY nameChanged) + Q_PROPERTY(QString description MEMBER description NOTIFY descriptionChanged) Q_PROPERTY(QDateTime start MEMBER start NOTIFY startChanged) Q_PROPERTY(QDateTime end MEMBER end NOTIFY endChanged) public: - explicit Event(const QString &name, QObject *parent = 0) : QObject(parent), name(name) { } + explicit Event(const QString &description, QObject *parent = 0) : + QObject(parent), + description(description) + { + + } - QString name; + QString description; QDateTime start; QDateTime end; signals: - void nameChanged(); + void descriptionChanged(); void startChanged(); void endChanged(); }; -class SqlEventModel : public QSqlQueryModel +static void addEvent(const QString &description, const QDateTime &start, qint64 duration = 0) +{ + QSqlQuery query; + QDateTime end = start.addSecs(duration); + if (!query.exec(QStringLiteral("INSERT INTO Event (description, start, end) VALUES ('%1', %2, %3)") + .arg(description).arg(start.toMSecsSinceEpoch()).arg(end.toMSecsSinceEpoch()))) { + qWarning() << query.lastError(); + } +} + +// create an in-memory SQLITE database +static void createDatabase() +{ + QSqlDatabase db = QSqlDatabase::addDatabase("QSQLITE"); + db.setDatabaseName(":memory:"); + if (!db.open()) { + qFatal("Cannot open database"); + return; + } + + QSqlQuery query; + query.exec("CREATE TABLE IF NOT EXISTS Event (description TEXT, start BIGINT, end BIGINT)"); + + const QDate current = QDate::currentDate(); + addEvent("Job interview", QDateTime(current.addDays(-19), QTime(12, 0))); + addEvent("Grocery shopping", QDateTime(current.addDays(-14), QTime(18, 0))); + addEvent("Ice skating", QDateTime(current.addDays(-14), QTime(20, 0)), 5400); + addEvent("Dentist''s appointment", QDateTime(current.addDays(-8), QTime(14, 0)), 1800); + addEvent("Cross-country skiing", QDateTime(current.addDays(1), QTime(19, 30)), 3600); + addEvent("Conference", QDateTime(current.addDays(10), QTime(9, 0)), 432000); + addEvent("Hairdresser", QDateTime(current.addDays(19), QTime(13, 0))); + addEvent("Doctor''s appointment", QDateTime(current.addDays(21), QTime(16, 0))); + addEvent("Vacation", QDateTime(current.addDays(35), QTime(0, 0)), 604800); +} + +class SqlEventModel : public QSqlTableModel { Q_OBJECT Q_PROPERTY(QDate min READ min CONSTANT) Q_PROPERTY(QDate max READ max CONSTANT) + Q_PROPERTY(QDate date READ date WRITE setDate NOTIFY dateChanged FINAL) + Q_PROPERTY(int rowCount READ rowCount NOTIFY rowCountChanged) public: - SqlEventModel(QObject *parent = 0) : QSqlQueryModel(parent) { } + SqlEventModel(QObject *parent = 0) : + QSqlTableModel(parent, QSqlDatabase::database(":memory:")) + { + connect(this, SIGNAL(rowsInserted(QModelIndex,int,int)), this, SIGNAL(rowCountChanged())); + connect(this, SIGNAL(rowsRemoved(QModelIndex,int,int)), this, SIGNAL(rowCountChanged())); + + setTable("Event"); + setEditStrategy(QSqlTableModel::OnManualSubmit); + select(); + + setDate(QDate::currentDate()); + } QDate min() const { @@ -87,58 +140,83 @@ public: return QDate(); } - Q_INVOKABLE QList<QObject*> eventsForDate(const QDate &date) + QDate date() const + { + return mDate; + } + + void setDate(const QDate &date) { - qint64 from = QDateTime(date, QTime(0, 0)).toMSecsSinceEpoch(); - qint64 to = QDateTime(date, QTime(23, 59)).toMSecsSinceEpoch(); - - QSqlQuery query; - if (!query.exec(QStringLiteral("SELECT * FROM Event WHERE start <= %1 AND end >= %2").arg(to).arg(from))) - qFatal("Query failed"); - - QList<QObject*> events; - while (query.next()) { - Event *event = new Event(query.value("name").toString(), this); - event->start = QDateTime::fromMSecsSinceEpoch(query.value("start").toLongLong()); - event->end = QDateTime::fromMSecsSinceEpoch(query.value("end").toLongLong()); - events.append(event); + if (date != mDate) { + mDate = date; + + qint64 from = QDateTime(mDate, QTime(0, 0)).toMSecsSinceEpoch(); + qint64 to = QDateTime(mDate, QTime(23, 59)).toMSecsSinceEpoch(); + + setFilter(QStringLiteral("start <= %1 AND end >= %2").arg(to).arg(from)); + + emit dateChanged(); } - return events; } -}; -// create an in-memory SQLITE database -static bool addEvent(QSqlQuery* query, const QString &name, const QDateTime &start, qint64 duration = 0) -{ - QDateTime end = start.addSecs(duration); - return query->exec(QStringLiteral("insert into Event values('%1', %2, %3)").arg(name) - .arg(start.toMSecsSinceEpoch()) - .arg(end.toMSecsSinceEpoch())); -} + enum { + DescriptionRole = Qt::UserRole, + StartDateRole, + EndDateRole + }; -static void createDatabase() -{ - QSqlDatabase db = QSqlDatabase::addDatabase("QSQLITE"); - db.setDatabaseName(":memory:"); - if (!db.open()) { - qFatal("Cannot open database"); - return; + QHash<int,QByteArray> roleNames() const Q_DECL_OVERRIDE + { + QHash<int,QByteArray> names; + names[DescriptionRole] = "description"; + names[StartDateRole] = "start"; + names[EndDateRole] = "end"; + return names; } - QSqlQuery query; - query.exec("create table if not exists Event (name TEXT, start BIGINT, end BIGINT)"); + QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const Q_DECL_OVERRIDE + { + if (role < Qt::UserRole) + return QSqlTableModel::data(index, role); - const QDate current = QDate::currentDate(); - addEvent(&query, "Job interview", QDateTime(current.addDays(-19), QTime(12, 0))); - addEvent(&query, "Grocery shopping", QDateTime(current.addDays(-14), QTime(18, 0))); - addEvent(&query, "Ice skating", QDateTime(current.addDays(-14), QTime(20, 0)), 5400); - addEvent(&query, "Dentist's appointment", QDateTime(current.addDays(-8), QTime(14, 0)), 1800); - addEvent(&query, "Cross-country skiing", QDateTime(current.addDays(1), QTime(19, 30)), 3600); - addEvent(&query, "Conference", QDateTime(current.addDays(10), QTime(9, 0)), 432000); - addEvent(&query, "Hairdresser", QDateTime(current.addDays(19), QTime(13, 0))); - addEvent(&query, "Doctor's appointment", QDateTime(current.addDays(21), QTime(16, 0))); - addEvent(&query, "Vacation", QDateTime(current.addDays(35), QTime(0, 0)), 604800); -} + int columnIndex = role - DescriptionRole; + QModelIndex modelIndex = this->index(index.row(), columnIndex); + QVariant eventData = QSqlTableModel::data(modelIndex, Qt::DisplayRole); + if (role == DescriptionRole) + return eventData; + + return QDateTime::fromMSecsSinceEpoch(eventData.toLongLong()); + } + + Q_INVOKABLE void addEvent(const QString &description, const QDateTime &date) + { + const int row = rowCount(); + insertRows(row, 1); + setData(index(row, 0), description); + setData(index(row, 1), date.toMSecsSinceEpoch()); + setData(index(row, 2), date.toMSecsSinceEpoch()); + submitAll(); + } + + Q_INVOKABLE void removeEvent(int modelRow) + { + if (modelRow < 0 || modelRow >= rowCount()) { + qWarning() << "Invalid model row:" << modelRow; + return; + } + + removeRows(modelRow, 1); + submitAll(); + } + +signals: + void dateChanged(); + void rowCountChanged(); + +private: + // The date to show events for. + QDate mDate; +}; int main(int argc, char *argv[]) { diff --git a/examples/quick/calendar/main.qml b/examples/quick/calendar/main.qml index db7c2561..3b36272f 100644 --- a/examples/quick/calendar/main.qml +++ b/examples/quick/calendar/main.qml @@ -41,6 +41,7 @@ import QtQuick 2.6 import QtQuick.Controls 2.0 import QtQuick.Calendar 2.0 +import QtQuick.Layouts 1.0 import io.qt.examples.calendar 1.0 ApplicationWindow { @@ -55,12 +56,19 @@ ApplicationWindow { SqlEventModel { id: eventModel + date: calendar.selectedDate } - Flow { - id: row + StackView { + id: stackView anchors.fill: parent anchors.margins: 20 + + initialItem: flow + } + + Flow { + id: flow spacing: 10 layoutDirection: Qt.RightToLeft @@ -79,7 +87,7 @@ ApplicationWindow { } focus: true - currentIndex: -1 + currentIndex: model.indexOf(selectedDate.getFullYear(), selectedDate.getMonth() + 1) snapMode: ListView.SnapOneItem highlightMoveDuration: 250 highlightRangeMode: ListView.StrictlyEnforceRange @@ -139,8 +147,13 @@ ApplicationWindow { height: width radius: width / 2 opacity: 0.5 - color: pressed ? Theme.pressColor : "transparent" - border.color: eventModel.eventsForDate(model.date).length > 0 ? Theme.accentColor : "transparent" + color: pressed ? Theme.pressColor : "transparent"; + + SqlEventModel { + id: delegateEventModel + } + + border.color: delegateEventModel.rowCount > 0 ? Theme.accentColor : "transparent" } } } @@ -152,89 +165,53 @@ ApplicationWindow { } } - Component { - id: eventListHeader - - Row { - id: eventDateRow - width: parent.width - height: eventDayLabel.height - spacing: 10 - - Label { - id: eventDayLabel - text: calendar.selectedDate.getDate() - font.pointSize: 35 - } - - Column { - height: eventDayLabel.height + EventView { + width: (parent.width > parent.height ? (parent.width - parent.spacing) * 0.4 : parent.width) + height: (parent.height > parent.width ? (parent.height - parent.spacing) * 0.4 : parent.height) + selectedDate: calendar.selectedDate + eventModel: eventModel + locale: calendar.locale - Label { - readonly property var options: { weekday: "long" } - text: Qt.locale().standaloneDayName(calendar.selectedDate.getDay(), Locale.LongFormat) - font.pointSize: 18 - } - Label { - text: Qt.locale().standaloneMonthName(calendar.selectedDate.getMonth()) - + calendar.selectedDate.toLocaleDateString(Qt.locale(), " yyyy") - font.pointSize: 12 - } - } - } + onAddEventClicked: stackView.push(createEventComponent) } + } - Rectangle { - width: (parent.width > parent.height ? (parent.width - parent.spacing) * 0.4 : parent.width) - height: (parent.height > parent.width ? (parent.height - parent.spacing) * 0.4 : parent.height) - border.color: Theme.frameColor + Component { + id: createEventComponent - ListView { - id: eventsListView - spacing: 4 - clip: true - header: eventListHeader - anchors.fill: parent - anchors.margins: 10 - model: eventModel.eventsForDate(calendar.selectedDate) + ColumnLayout { + spacing: 10 + visible: AbstractStackView.index === stackView.currentIndex - delegate: Rectangle { - width: eventsListView.width - height: eventItemColumn.height - anchors.horizontalCenter: parent.horizontalCenter + DateTimePicker { + id: dateTimePicker + anchors.horizontalCenter: parent.horizontalCenter + dateToShow: calendar.selectedDate + } + Frame { + Layout.fillWidth: true - Rectangle { - width: parent.width - height: 1 - color: "#eee" - } + TextArea { + id: descriptionField + placeholder.text: "Description" + anchors.fill: parent + } + } + RowLayout { + Layout.fillWidth: true - Column { - id: eventItemColumn - x: 4 - y: 4 - width: parent.width - 8 - height: timeRow.height + nameLabel.height + 8 - - Label { - id: nameLabel - width: parent.width - wrapMode: Text.Wrap - text: modelData.name - } - Row { - id: timeRow - width: parent.width - Label { - text: modelData.start.toLocaleTimeString(calendar.locale, Locale.ShortFormat) - color: "#aaa" - } - Label { - text: "-" + new Date(modelData.end).toLocaleTimeString(calendar.locale, Locale.ShortFormat) - visible: modelData.start.getTime() !== modelData.end.getTime() && modelData.start.getDate() === modelData.end.getDate() - color: "#aaa" - } - } + Button { + text: "Cancel" + Layout.fillWidth: true + onClicked: stackView.pop() + } + Button { + text: "Create" + enabled: dateTimePicker.enabled + Layout.fillWidth: true + onClicked: { + eventModel.addEvent(descriptionField.text, dateTimePicker.chosenDate); + stackView.pop(); } } } diff --git a/src/extras/doc/images/qtquickextras2-tumbler-wrap.gif b/src/extras/doc/images/qtquickextras2-tumbler-wrap.gif Binary files differnew file mode 100644 index 00000000..c7624599 --- /dev/null +++ b/src/extras/doc/images/qtquickextras2-tumbler-wrap.gif diff --git a/src/extras/qquicktumbler.cpp b/src/extras/qquicktumbler.cpp index c5e32947..f278c9ca 100644 --- a/src/extras/qquicktumbler.cpp +++ b/src/extras/qquicktumbler.cpp @@ -49,7 +49,21 @@ QT_BEGIN_NAMESPACE \ingroup containers \brief A spinnable wheel of items that can be selected. - TODO + \code + Tumbler { + model: 5 + } + \endcode + + \section1 Non-wrapping Tumbler + + The default contentItem of Tumbler is a \l PathView, which wraps when it + reaches the top and bottom. To achieve a non-wrapping Tumbler, use ListView + as the contentItem: + + \snippet tst_tumbler.qml contentItem + + \image qtquickextras2-tumbler-wrap.gif */ class QQuickTumblerPrivate : public QQuickControlPrivate, public QQuickItemChangeListener @@ -63,6 +77,10 @@ public: { } + ~QQuickTumblerPrivate() + { + } + QVariant model; QQmlComponent *delegate; int visibleItemCount; @@ -84,11 +102,55 @@ static QList<QQuickItem *> contentItemChildItems(QQuickItem *contentItem) return flickable ? flickable->contentItem()->childItems() : contentItem->childItems(); } +namespace { + static inline qreal delegateHeight(const QQuickItem *contentItem, qreal topPadding, qreal bottomPadding, int visibleItemCount) + { + // TODO: can we/do we want to support spacing? + return (contentItem->height()/* - qMax(0, itemCount - 1) * spacing*/ - topPadding - bottomPadding) / visibleItemCount; + } + + enum ContentItemType { + UnsupportedContentItemType, + PathViewContentItem, + ListViewContentItem + }; + + static inline QQuickItem *actualContentItem(QQuickItem *rootContentItem, ContentItemType contentType) + { + if (contentType == PathViewContentItem) + return rootContentItem; + else if (contentType == ListViewContentItem) + return qobject_cast<QQuickFlickable*>(rootContentItem)->contentItem(); + + return Q_NULLPTR; + } + + static inline ContentItemType contentItemType(QQuickItem *rootContentItem) + { + if (rootContentItem->inherits("QQuickPathView")) + return PathViewContentItem; + else if (rootContentItem->inherits("QQuickListView")) + return ListViewContentItem; + + return UnsupportedContentItemType; + } + + static inline ContentItemType contentItemTypeFromDelegate(QQuickItem *delegateItem) + { + if (delegateItem->parentItem()->inherits("QQuickPathView")) { + return PathViewContentItem; + } else if (delegateItem->parentItem()->parentItem() + && delegateItem->parentItem()->parentItem()->inherits("QQuickListView")) { + return ListViewContentItem; + } + + return UnsupportedContentItemType; + } +} + void QQuickTumblerPrivate::updateItemHeights() { - // TODO: can we/do we want to support spacing? - const qreal itemHeight = (contentItem->height()/* - qMax(0, itemCount - 1) * spacing*/ - - topPadding - bottomPadding) / visibleItemCount; + const qreal itemHeight = delegateHeight(contentItem, topPadding, bottomPadding, visibleItemCount); foreach (QQuickItem *childItem, contentItemChildItems(contentItem)) childItem->setHeight(itemHeight); } @@ -114,6 +176,11 @@ void QQuickTumblerPrivate::itemChildRemoved(QQuickItem *, QQuickItem *) QQuickTumbler::QQuickTumbler(QQuickItem *parent) : QQuickControl(*(new QQuickTumblerPrivate), parent) { + setActiveFocusOnTab(true); +} + +QQuickTumbler::~QQuickTumbler() +{ } /*! @@ -199,7 +266,7 @@ void QQuickTumbler::setDelegate(QQmlComponent *delegate) \qmlproperty int QtQuickExtras2::Tumbler::visibleItemCount This property holds the number of items visible in the tumbler. It must be - an odd number. + an odd number, as the current item is always vertically centered. */ int QQuickTumbler::visibleItemCount() const { @@ -221,7 +288,7 @@ QQuickTumblerAttached *QQuickTumbler::qmlAttachedProperties(QObject *object) { QQuickItem *delegateItem = qobject_cast<QQuickItem *>(object); if (!delegateItem) { - qWarning() << "Attached properties of Tumbler must be accessed from within a delegate item"; + qWarning() << "Tumbler: attached properties of Tumbler must be accessed from within a delegate item"; return Q_NULLPTR; } @@ -264,17 +331,26 @@ void QQuickTumbler::contentItemChange(QQuickItem *newItem, QQuickItem *oldItem) disconnect(oldItem, SIGNAL(currentItemChanged()), this, SIGNAL(currentItemChanged())); disconnect(oldItem, SIGNAL(countChanged()), this, SIGNAL(countChanged())); - QQuickItemPrivate *oldItemPrivate = QQuickItemPrivate::get(oldItem); - oldItemPrivate->removeItemChangeListener(d, QQuickItemPrivate::Children); + ContentItemType oldContentItemType = contentItemType(oldItem); + QQuickItem *actualOldContentItem = actualContentItem(oldItem, oldContentItemType); + QQuickItemPrivate *actualContentItemPrivate = QQuickItemPrivate::get(actualOldContentItem); + actualContentItemPrivate->removeItemChangeListener(d, QQuickItemPrivate::Children); } if (newItem) { + ContentItemType contentType = contentItemType(newItem); + if (contentType == UnsupportedContentItemType) { + qWarning() << "Tumbler: contentItems other than PathView and ListView are not supported"; + return; + } + connect(newItem, SIGNAL(currentIndexChanged()), this, SIGNAL(currentIndexChanged())); connect(newItem, SIGNAL(currentItemChanged()), this, SIGNAL(currentItemChanged())); connect(newItem, SIGNAL(countChanged()), this, SIGNAL(countChanged())); - QQuickItemPrivate *newItemPrivate = QQuickItemPrivate::get(newItem); - newItemPrivate->addItemChangeListener(d, QQuickItemPrivate::Children); + QQuickItem *actualNewContentItem = actualContentItem(newItem, contentType); + QQuickItemPrivate *actualContentItemPrivate = QQuickItemPrivate::get(actualNewContentItem); + actualContentItemPrivate->addItemChangeListener(d, QQuickItemPrivate::Children); // If the previous currentIndex is -1, it means we had no contentItem previously. if (previousCurrentIndex != -1) { @@ -311,23 +387,28 @@ public: displacement(1) { if (!delegateItem->parentItem()) { - qWarning() << "Attached properties of Tumbler must be accessed from within a delegate item that has a parent"; + qWarning() << "Tumbler: attached properties must be accessed from within a delegate item that has a parent"; return; } QVariant indexContextProperty = qmlContext(delegateItem)->contextProperty(QStringLiteral("index")); if (!indexContextProperty.isValid()) { - qWarning() << "Attempting to access attached property on item without an \"index\" property"; + qWarning() << "Tumbler: attempting to access attached property on item without an \"index\" property"; return; } index = indexContextProperty.toInt(); - if (!delegateItem->parentItem()->inherits("QQuickPathView")) { - qWarning() << "contentItems other than PathView are not currently supported"; + const ContentItemType contentItemType = contentItemTypeFromDelegate(delegateItem); + if (contentItemType == UnsupportedContentItemType) return; - } - tumbler = qobject_cast<QQuickTumbler* >(delegateItem->parentItem()->parentItem()); + // ListView has an "additional" content item. + tumbler = qobject_cast<QQuickTumbler* >(contentItemType == PathViewContentItem + ? delegateItem->parentItem()->parentItem() : delegateItem->parentItem()->parentItem()->parentItem()); + Q_ASSERT(tumbler); + } + + ~QQuickTumblerAttachedPrivate() { } void itemGeometryChanged(QQuickItem *item, const QRectF &newGeometry, const QRectF &oldGeometry) Q_DECL_OVERRIDE; @@ -354,7 +435,7 @@ void QQuickTumblerAttachedPrivate::itemChildAdded(QQuickItem *, QQuickItem *) _q_calculateDisplacement(); } -void QQuickTumblerAttachedPrivate::itemChildRemoved(QQuickItem *, QQuickItem *child) +void QQuickTumblerAttachedPrivate::itemChildRemoved(QQuickItem *item, QQuickItem *child) { _q_calculateDisplacement(); @@ -363,7 +444,9 @@ void QQuickTumblerAttachedPrivate::itemChildRemoved(QQuickItem *, QQuickItem *ch // that our properties are attached to. If we don't remove the change // listener, the contentItem will attempt to notify a destroyed // listener, causing a crash. - QQuickItemPrivate *p = QQuickItemPrivate::get(tumbler->contentItem()); + + // item is the "actual content item" of Tumbler's contentItem, i.e. a PathView or ListView.contentItem + QQuickItemPrivate *p = QQuickItemPrivate::get(item); p->removeItemChangeListener(this, QQuickItemPrivate::Geometry | QQuickItemPrivate::Children); } } @@ -371,20 +454,35 @@ void QQuickTumblerAttachedPrivate::itemChildRemoved(QQuickItem *, QQuickItem *ch void QQuickTumblerAttachedPrivate::_q_calculateDisplacement() { const int previousDisplacement = displacement; + displacement = 0; - displacement = 1; + // This can happen in tests, so it may happen in normal usage too. + if (tumbler->count() == 0) + return; - // TODO: ListView has no offset property, need to try using contentY instead. - if (tumbler && tumbler->contentItem()->inherits("QQuickListView")) + ContentItemType contentType = contentItemType(tumbler->contentItem()); + if (contentType == UnsupportedContentItemType) return; - qreal offset = tumbler->contentItem()->property("offset").toReal(); - displacement = tumbler->count() - index - offset; - int halfVisibleItems = tumbler->visibleItemCount() / 2 + 1; - if (displacement > halfVisibleItems) - displacement -= tumbler->count(); - else if (displacement < -halfVisibleItems) - displacement += tumbler->count(); + qreal offset = 0; + + if (contentType == PathViewContentItem) { + offset = tumbler->contentItem()->property("offset").toReal(); + + displacement = tumbler->count() - index - offset; + int halfVisibleItems = tumbler->visibleItemCount() / 2 + 1; + if (displacement > halfVisibleItems) + displacement -= tumbler->count(); + else if (displacement < -halfVisibleItems) + displacement += tumbler->count(); + } else { + const qreal contentY = tumbler->contentItem()->property("contentY").toReal(); + const qreal delegateH = delegateHeight(tumbler->contentItem(), tumbler->topPadding(), tumbler->bottomPadding(), tumbler->visibleItemCount()); + const qreal preferredHighlightBegin = tumbler->contentItem()->property("preferredHighlightBegin").toReal(); + // Tumbler's displacement goes from negative at the top to positive towards the bottom, so we must switch this around. + const qreal reverseDisplacement = (contentY + preferredHighlightBegin) / delegateH; + displacement = reverseDisplacement - index; + } Q_Q(QQuickTumblerAttached); if (displacement != previousDisplacement) @@ -396,15 +494,27 @@ QQuickTumblerAttached::QQuickTumblerAttached(QQuickItem *delegateItem) : { Q_D(QQuickTumblerAttached); if (d->tumbler) { - // TODO: in case of listview, listen to contentItem of contentItem - QQuickItemPrivate *p = QQuickItemPrivate::get(d->tumbler->contentItem()); + QQuickItem *rootContentItem = d->tumbler->contentItem(); + const ContentItemType contentType = contentItemType(rootContentItem); + QQuickItemPrivate *p = QQuickItemPrivate::get(actualContentItem(rootContentItem, contentType)); p->addItemChangeListener(d, QQuickItemPrivate::Geometry | QQuickItemPrivate::Children); - // TODO: in case of ListView, listen to contentY changed - connect(d->tumbler->contentItem(), SIGNAL(offsetChanged()), this, SLOT(_q_calculateDisplacement())); + const char *contentItemSignal = contentType == PathViewContentItem + ? SIGNAL(offsetChanged()) : SIGNAL(contentYChanged()); + connect(d->tumbler->contentItem(), contentItemSignal, this, SLOT(_q_calculateDisplacement())); } } +QQuickTumblerAttached::~QQuickTumblerAttached() +{ +} + +QQuickTumbler *QQuickTumblerAttached::tumbler() const +{ + Q_D(const QQuickTumblerAttached); + return d->tumbler; +} + qreal QQuickTumblerAttached::displacement() const { Q_D(const QQuickTumblerAttached); diff --git a/src/extras/qquicktumbler_p.h b/src/extras/qquicktumbler_p.h index a2e493e5..4dc29ba9 100644 --- a/src/extras/qquicktumbler_p.h +++ b/src/extras/qquicktumbler_p.h @@ -70,6 +70,7 @@ class Q_QUICKEXTRAS_EXPORT QQuickTumbler : public QQuickControl public: explicit QQuickTumbler(QQuickItem *parent = Q_NULLPTR); + ~QQuickTumbler(); QVariant model() const; void setModel(const QVariant &model); @@ -114,11 +115,14 @@ class QQuickTumblerAttachedPrivate; class Q_QUICKEXTRAS_EXPORT QQuickTumblerAttached : public QObject { Q_OBJECT + Q_PROPERTY(QQuickTumbler *tumbler READ tumbler CONSTANT) Q_PROPERTY(qreal displacement READ displacement NOTIFY displacementChanged FINAL) public: explicit QQuickTumblerAttached(QQuickItem *delegateItem); + ~QQuickTumblerAttached(); + QQuickTumbler *tumbler() const; qreal displacement() const; Q_SIGNALS: diff --git a/tests/auto/extras/data/tst_tumbler.qml b/tests/auto/extras/data/tst_tumbler.qml index 0b0c30f6..24686646 100644 --- a/tests/auto/extras/data/tst_tumbler.qml +++ b/tests/auto/extras/data/tst_tumbler.qml @@ -51,10 +51,13 @@ TestCase { name: "Tumbler" property var tumbler: null + readonly property real defaultImplicitDelegateHeight: 200 / 3 + readonly property real defaultListViewTumblerOffset: -defaultImplicitDelegateHeight function init() { tumbler = Qt.createQmlObject("import QtQuick.Extras 2.0; Tumbler { }", testCase, ""); verify(tumbler, "Tumbler: failed to create an instance"); + compare(tumbler.contentItem.parent, tumbler); } function cleanup() { @@ -65,6 +68,10 @@ TestCase { return tumbler.leftPadding + tumbler.width / 2; } + function tumblerYCenter() { + return tumbler.topPadding + tumbler.height / 2; + } + // visualItemIndex is from 0 to the amount of visible items. function itemCenterPos(visualItemIndex) { var halfDelegateHeight = tumbler.contentItem.delegateHeight / 2; @@ -277,9 +284,15 @@ TestCase { property Component displacementDelegate: Text { objectName: "delegate" + index text: modelData + opacity: 0.2 + Math.max(0, 1 - Math.abs(AbstractTumbler.displacement)) * 0.8 horizontalAlignment: Text.AlignHCenter verticalAlignment: Text.AlignVCenter + Text { + text: parent.displacement.toFixed(2) + anchors.right: parent.right + } + property real displacement: AbstractTumbler.displacement } @@ -299,6 +312,183 @@ TestCase { // test displacement after adding and removing items } + Component { + id: listViewTumblerComponent + + Tumbler { + id: listViewTumbler + + //! [contentItem] + contentItem: ListView { + anchors.fill: parent + model: listViewTumbler.model + delegate: listViewTumbler.delegate + + snapMode: ListView.SnapToItem + highlightRangeMode: ListView.StrictlyEnforceRange + preferredHighlightBegin: height / 2 - (height / listViewTumbler.visibleItemCount / 2) + preferredHighlightEnd: height / 2 + (height / listViewTumbler.visibleItemCount / 2) + clip: true + } + //! [contentItem] + } + } + + function test_displacementListView_data() { + var offset = defaultListViewTumblerOffset; + + var data = [ + // At 0 contentY, the first item is current. + { contentY: offset, expectedDisplacements: [ + { index: 0, displacement: 0 }, + { index: 1, displacement: -1 }, + { index: 2, displacement: -2 } ] + }, + // When we start to move the first item down, the second item above it starts to become current. + { contentY: offset + defaultImplicitDelegateHeight * 0.25, expectedDisplacements: [ + { index: 0, displacement: 0.25 }, + { index: 1, displacement: -0.75 }, + { index: 2, displacement: -1.75 } ] + }, + { contentY: offset + defaultImplicitDelegateHeight * 0.5, expectedDisplacements: [ + { index: 0, displacement: 0.5 }, + { index: 1, displacement: -0.5 }, + { index: 2, displacement: -1.5 } ] + }, + { contentY: offset + defaultImplicitDelegateHeight * 0.75, expectedDisplacements: [ + { index: 0, displacement: 0.75 }, + { index: 1, displacement: -0.25 } ] + }, + { contentY: offset + defaultImplicitDelegateHeight * 3.5, expectedDisplacements: [ + { index: 3, displacement: 0.5 }, + { index: 4, displacement: -0.5 } ] + } + ]; + for (var i = 0; i < data.length; ++i) { + var row = data[i]; + row.tag = "contentY=" + row.contentY; + } + return data; + } + + function test_displacementListView(data) { + tumbler.destroy(); + // Sanity check that they're aren't any children at this stage. + tryCompare(testCase.children, "length", 0); + + tumbler = listViewTumblerComponent.createObject(testCase); + verify(tumbler); + + tumbler.delegate = displacementDelegate; + tumbler.model = 5; + compare(tumbler.count, 5); + // Ensure assumptions about the tumbler used in our data() function are correct. + compare(tumbler.contentItem.contentY, -defaultImplicitDelegateHeight); + var delegateCount = 0; + var listView = tumbler.contentItem; + var listViewContentItem = tumbler.contentItem.contentItem; + + // We use the mouse instead of setting contentY directly, otherwise the + // items snap back into place. This doesn't seem to be an issue for + // PathView for some reason. + // + // I tried lots of things to get this test to work with small changes + // in ListView's contentY ( to match the tests for a PathView-based Tumbler), but they didn't work: + // + // - Pressing once and then directly moving the mouse to the correct location + // - Pressing once and interpolating the mouse position to the correct location + // - Pressing once and doing some dragging up and down to trigger the + // overThreshold of QQuickFlickable + // + // Even after the last item above, QQuickFlickable wouldn't consider it a drag. + // It seems that overThreshold is set too late, and because the drag distance is quite small + // to begin with, nothing changes (the displacement was always very close to 0 in the end). + + // Ensure that we at least cover the distance required to reach the desired contentY. + var distanceToReachContentY = data.contentY - defaultListViewTumblerOffset; + var distance = Math.abs(distanceToReachContentY) + tumbler.height / 2; + // If distanceToReachContentY is 0, we're testing 0 displacement, so we don't need to do anything. + if (distanceToReachContentY != 0) { + mousePress(tumbler, tumblerXCenter(), tumblerYCenter()); + + var dragDirection = distanceToReachContentY > 0 ? -1 : 1; + for (var i = 0; i < distance && Math.floor(listView.contentY) !== Math.floor(data.contentY); ++i) { + mouseMove(tumbler, tumblerXCenter(), tumblerYCenter() + i * dragDirection, 10); + } + } + + for (var i = 0; i < data.expectedDisplacements.length; ++i) { + var delegate = findChild(listViewContentItem, "delegate" + data.expectedDisplacements[i].index); + verify(delegate); + compare(delegate.height, defaultImplicitDelegateHeight); + // Due to the way we must perform this test, we can't expect high precision. + var expectedDisplacement = data.expectedDisplacements[i].displacement; + fuzzyCompare(delegate.displacement, expectedDisplacement, 0.1, + "Delegate of ListView-based Tumbler at index " + data.expectedDisplacements[i].index + + " has displacement of " + delegate.displacement + " when it should be " + expectedDisplacement); + } + + if (distanceToReachContentY != 0) + mouseRelease(tumbler, tumblerXCenter(), itemCenterPos(1) + (data.contentY - defaultListViewTumblerOffset), Qt.LeftButton); + } + + function test_listViewFlickAboveBounds_data() { + // Tests that flicking above the bounds when already at the top of the + // tumbler doesn't result in an incorrect displacement. + var data = []; + // Less than two items doesn't make sense. The default visibleItemCount + // is 3, so we test a bit more than double that. + for (var i = 2; i <= 7; ++i) { + data.push({ tag: i + " items", model: i }); + } + return data; + } + + function test_listViewFlickAboveBounds(data) { + tumbler.destroy(); + + tumbler = listViewTumblerComponent.createObject(testCase); + verify(tumbler); + + tumbler.delegate = displacementDelegate; + tumbler.model = data.model; + + mousePress(tumbler, tumblerXCenter(), tumblerYCenter()); + + // Ensure it's stationary. + var listView = tumbler.contentItem; + compare(listView.contentY, defaultListViewTumblerOffset); + + // We could just move up until the contentY changed, but this is safer. + var distance = tumbler.height; + var changed = false; + + for (var i = 0; i < distance && !changed; ++i) { + mouseMove(tumbler, tumblerXCenter(), tumblerYCenter() + i, 10); + + // Don't test until the contentY has actually changed. + if (Math.abs(listView.contentY) - listView.preferredHighlightBegin > 0.01) { + + for (var delegateIndex = 0; delegateIndex < Math.min(tumbler.count, tumbler.visibleItemCount); ++delegateIndex) { + var delegate = findChild(listView.contentItem, "delegate" + delegateIndex); + verify(delegate); + + verify(delegate.displacement <= -delegateIndex, "Delegate at index " + delegateIndex + " has a displacement of " + + delegate.displacement + " when it should be less than or equal to " + -delegateIndex); + verify(delegate.displacement > -delegateIndex - 0.1, "Delegate at index 0 has a displacement of " + + delegate.displacement + " when it should be greater than ~ " + -delegateIndex - 0.1); + } + + changed = true; + } + } + + // Sanity check that something was actually tested. + verify(changed); + + mouseRelease(tumbler, tumblerXCenter(), tumbler.topPadding); + } + property Component objectNameDelegate: Text { objectName: "delegate" + index text: modelData @@ -347,7 +537,7 @@ TestCase { property real displacement: AbstractTumbler.displacement } - property Component listViewComponent: ListView {} + property Component gridViewComponent: GridView {} property Component simpleDisplacementDelegate: Text { property real displacement: AbstractTumbler.displacement property int index: -1 @@ -361,17 +551,17 @@ TestCase { // // Cause displacement to be changed. The warning isn't triggered if we don't do this. // tumbler.contentItem.offset += 1; - ignoreWarning("Attached properties of Tumbler must be accessed from within a delegate item that has a parent"); + ignoreWarning("Tumbler: attached properties must be accessed from within a delegate item that has a parent"); noParentDelegateComponent.createObject(null); - ignoreWarning("Attempting to access attached property on item without an \"index\" property"); + ignoreWarning("Tumbler: attempting to access attached property on item without an \"index\" property"); var object = noParentDelegateComponent.createObject(testCase); object.destroy(); - var listView = listViewComponent.createObject(testCase); - ignoreWarning("contentItems other than PathView are not currently supported"); - object = simpleDisplacementDelegate.createObject(listView); + // Should not be any warnings from this, as ListView, for example, doesn't produce warnings for the same code. + var gridView = gridViewComponent.createObject(testCase); + object = simpleDisplacementDelegate.createObject(gridView); object.destroy(); - listView.destroy(); + gridView.destroy(); } } |