aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorMitch Curtis <mitch.curtis@theqtcompany.com>2015-07-16 12:00:55 +0200
committerMitch Curtis <mitch.curtis@theqtcompany.com>2015-08-06 11:43:48 +0000
commit293fc5e8f7df1b60a07d2e7e489e57059bb021bc (patch)
tree7ea3921797c2541c379cc91eb1f0c4f30d575fbc
parent1448ee0d1c02280aa33424f992b2f26d74615a43 (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.qml176
-rw-r--r--examples/quick/calendar/EventView.qml209
-rw-r--r--examples/quick/calendar/TumblerDelegate.qml54
-rw-r--r--examples/quick/calendar/calendar.pro4
-rw-r--r--examples/quick/calendar/calendar.qrc3
-rw-r--r--examples/quick/calendar/main.cpp178
-rw-r--r--examples/quick/calendar/main.qml139
-rw-r--r--src/extras/doc/images/qtquickextras2-tumbler-wrap.gifbin0 -> 28883 bytes
-rw-r--r--src/extras/qquicktumbler.cpp174
-rw-r--r--src/extras/qquicktumbler_p.h4
-rw-r--r--tests/auto/extras/data/tst_tumbler.qml204
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
new file mode 100644
index 00000000..c7624599
--- /dev/null
+++ b/src/extras/doc/images/qtquickextras2-tumbler-wrap.gif
Binary files differ
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();
}
}