From 94b5faa9d8d440450dd60a21e25c75169a13c9cf Mon Sep 17 00:00:00 2001 From: jordi fita mas Date: Thu, 9 Jan 2025 20:08:40 +0100 Subject: [PATCH] Add TimelineModel and use it to feed each TimelineView MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit I have my doubts whether this model should be QAbstractListModel-derived or not. At first i thought i could not be, because i need to be able to get a “view” (i.e., a [begin, end) pair of iterators) based on the segment of TimelineView that becomes exposed due to scroll. However, QAbstractItemModel::match returns a QModelIndexList, and that could also be used for the same purpose, except that there is no QDateRange…. Until i have more needs for this model, the current coupling between TimelineModel and TimelineView is OK, as i do not expect TimelineView to be used for anything else. I also renamed CalendarModel to TimelineListModel, since this is the important part—the timeline—, and there is no “calendar” thing anywhere in the application—at least, not what traditionally is understood as a calendar, that is. --- src/CMakeLists.txt | 3 +- src/ReservationsPage.qml | 11 ++- src/timelinelistmodel.cpp | 194 ++++++++++++++++++++++++++++++++++++++ src/timelinelistmodel.h | 43 +++++++++ src/timelinemodel.cpp | 77 +++++++++++++++ src/timelinemodel.h | 48 ++++++++++ src/timelineview.cpp | 54 +++++++---- src/timelineview.h | 9 +- 8 files changed, 417 insertions(+), 22 deletions(-) create mode 100644 src/timelinelistmodel.cpp create mode 100644 src/timelinelistmodel.h create mode 100644 src/timelinemodel.cpp create mode 100644 src/timelinemodel.h diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index edab1bd..f69465f 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -9,10 +9,11 @@ qt_add_qml_module(${PROJECT_NAME} QtCore QtQuick SOURCES - calendarlistmodel.cpp calendarlistmodel.h database.cpp database.h mnemonicattached.cpp mnemonicattached.h timelinedaymodel.cpp timelinedaymodel.h + timelinelistmodel.cpp timelinelistmodel.h + timelinemodel.cpp timelinemodel.h timelineview.cpp timelineview.h QML_FILES ErrorNotification.qml diff --git a/src/ReservationsPage.qml b/src/ReservationsPage.qml index 0e8f0a6..c2903a0 100644 --- a/src/ReservationsPage.qml +++ b/src/ReservationsPage.qml @@ -31,7 +31,7 @@ Page { anchors.left: parent.left anchors.top: parent.top headerPositioning: ListView.OverlayHeader - model: calendarList + model: timelineListModel width: 100 delegate: Label { @@ -59,7 +59,7 @@ Page { contentWidth: headerItem.implicitWidth flickableDirection: Flickable.AutoFlickDirection headerPositioning: ListView.OverlayHeader - model: calendarList + model: timelineListModel ScrollBar.horizontal: ScrollBar { } @@ -68,9 +68,12 @@ Page { } delegate: TimelineView { + required property TimelineModel timeline + dayWidth: page.dayWidth fromDate: page.fromDate height: 16 + model: timeline toDate: page.toDate viewportWidth: ListView.view.width viewportX: ListView.view.contentX @@ -94,8 +97,8 @@ Page { } } - CalendarListModel { - id: calendarList + TimelineListModel { + id: timelineListModel } diff --git a/src/timelinelistmodel.cpp b/src/timelinelistmodel.cpp new file mode 100644 index 0000000..3acdff9 --- /dev/null +++ b/src/timelinelistmodel.cpp @@ -0,0 +1,194 @@ +#include "timelinelistmodel.h" +#include +#include +#include +#include "database.h" +#include "timelinemodel.h" + +struct TimelineListModel::Item +{ + Item(int id, const QString name, QObject *parent = nullptr) + : id(id) + , name(name) + , timeline(new TimelineModel(parent)) + {} + + ~Item() { timeline->deleteLater(); } + + int id; + QString name; + TimelineModel *timeline; +}; + +TimelineListModel::TimelineListModel(QObject *parent) + : QAbstractListModel{parent} + , m_items{} +{ + fetch(); +} + +TimelineListModel::~TimelineListModel() +{ + qDeleteAll(m_items); +} + +QVariant TimelineListModel::headerData(int section, Qt::Orientation orientation, int role) const +{ + if (role != Qt::DisplayRole) { + return {}; + } + return section; +} + +int TimelineListModel::rowCount(const QModelIndex &parent) const +{ + if (parent.isValid()) { + return 0; + } + + return m_items.count(); +} + +QHash TimelineListModel::roleNames() const +{ + QHash roles = QAbstractItemModel::roleNames(); + roles[NameRole] = "name"; + roles[TimelineRole] = "timeline"; + return roles; +} + +QModelIndex TimelineListModel::findIndexById(int id) +{ + for (qsizetype row = 0; row < m_items.count(); ++row) { + if (m_items.at(row)->id == id) { + return index(row); + } + } + return {}; +} + +void TimelineListModel::fetch() +{ + struct Lodging + { + int id; + QString label; + }; + + struct Results + { + QList lodgings; + QHash> bookings; + }; + + Database::query([]() { + Results results; + { + QSqlQuery query("select campsite_id, label from campsite order by label, campsite_id"); + while (query.next()) { + Lodging item{query.value(0).toInt(), query.value(1).toString()}; + results.lodgings.append(item); + } + } + { + QSqlQuery query(" select " + " campsite_id " + " , booking_id " + " , lower(booking_campsite.stay) " + " , upper(booking_campsite.stay) - lower(booking_campsite.stay)" + " , holder_name " + " , booking_status " + " , true" + " , true" + " from booking_campsite " + " join booking using (booking_id) " + " order by lower(booking_campsite.stay), booking_id " + ""); + while (query.next()) { + int lodgingId = query.value(0).toInt(); + auto *item = new TimelineModel::Item{ + query.value(1).toInt(), + query.value(2).toDate(), + query.value(3).toInt(), + query.value(4).toString(), + query.value(5).toString(), + query.value(6).toBool(), + query.value(7).toBool(), + }; + results.bookings[lodgingId].append(item); + } + } + return results; + }).then(this, [this](const Results &results) { + qsizetype itemsRow = 0; + qsizetype lodgingsRow = 0; + + for (; itemsRow < m_items.count() && lodgingsRow < results.lodgings.count(); + ++itemsRow, ++lodgingsRow) { + Item *item = m_items.at(itemsRow); + const Lodging &lodging = results.lodgings.at(lodgingsRow); + + if (lodging.id == item->id) { + if (lodging.label != item->name) { + item->name = lodging.label; + QModelIndex itemIndex = index(itemsRow); + emit dataChanged(itemIndex, itemIndex, {Qt::DisplayRole, NameRole}); + }; + continue; + } + + if (QModelIndex itemIndex = findIndexById(lodging.id); itemIndex.isValid()) { + beginMoveRows({}, itemIndex.row(), itemIndex.row(), {}, itemsRow); + m_items.move(itemIndex.row(), itemsRow); + endMoveRows(); + // Move one step backwards, and check change of label in next loop. + --itemsRow; + --lodgingsRow; + } else { + beginInsertRows({}, itemsRow, itemsRow); + m_items.insert(itemsRow, new Item(lodging.id, lodging.label, this)); + endInsertRows(); + } + } + if (itemsRow < m_items.count()) { + beginRemoveRows({}, itemsRow, m_items.count() - 1); + m_items.remove(itemsRow, m_items.count() - itemsRow); + endRemoveRows(); + } + if (lodgingsRow < results.lodgings.count()) { + beginInsertRows({}, + m_items.count(), + m_items.count() + results.lodgings.count() - lodgingsRow - 1); + for (; lodgingsRow < results.lodgings.count(); ++lodgingsRow) { + const Lodging &lodging = results.lodgings.at(lodgingsRow); + m_items.append(new Item(lodging.id, lodging.label, this)); + } + endInsertRows(); + } + for (Item *item : m_items) { + item->timeline->update(results.bookings.value(item->id)); + } + }); +} + +QVariant TimelineListModel::data(const QModelIndex &index, int role) const +{ + if (!checkIndex(index, + QAbstractItemModel::CheckIndexOption::IndexIsValid + | QAbstractItemModel::CheckIndexOption::ParentIsInvalid)) { + return {}; + } + + Item *item = m_items.at(index.row()); + switch (role) { + case Qt::DisplayRole: + case NameRole: + return item->name; + case TimelineRole: + return QVariant::fromValue(item->timeline); + } + + return {}; +} + +#include "moc_timelinelistmodel.cpp" diff --git a/src/timelinelistmodel.h b/src/timelinelistmodel.h new file mode 100644 index 0000000..441b96d --- /dev/null +++ b/src/timelinelistmodel.h @@ -0,0 +1,43 @@ +#ifndef TIMELINELISTMODEL_H +#define TIMELINELISTMODEL_H + +#include +#include +#include + +class TimelineListModel : public QAbstractListModel +{ + Q_OBJECT + QML_ELEMENT + +public: + enum Roles { + NameRole = Qt::UserRole, + TimelineRole, + }; + + explicit TimelineListModel(QObject *parent = nullptr); + ~TimelineListModel() override; + + QVariant headerData(int section, + Qt::Orientation orientation, + int role = Qt::DisplayRole) const override; + + QVariant data(const QModelIndex &index, int role = Qt::DisplayRole) const override; + + int rowCount(const QModelIndex &parent = QModelIndex()) const override; + + QHash roleNames() const override; + +private: + Q_DISABLE_COPY_MOVE(TimelineListModel) + + struct Item; + + QModelIndex findIndexById(int id); + void fetch(); + + QList m_items; +}; + +#endif // TIMELINELISTMODEL_H diff --git a/src/timelinemodel.cpp b/src/timelinemodel.cpp new file mode 100644 index 0000000..2566fa5 --- /dev/null +++ b/src/timelinemodel.cpp @@ -0,0 +1,77 @@ +#include "timelinemodel.h" + +TimelineModel::TimelineModel(QObject *parent) + : QObject{parent} + , m_items() +{} + +TimelineModel::~TimelineModel() +{ + clear(); +} + +void TimelineModel::update(const QList &items) +{ + clear(); + m_items.assign(items.begin(), items.end()); +} + +std::pair TimelineModel::indexesOf(QDate from, QDate to) const +{ + qsizetype begin = searchIndex(from); + if (begin == -1) { + return std::make_pair(begin, begin); + } + + while (begin > 0) { + --begin; + const Item *item = m_items.at(begin); + if (m_items.at(begin)->departure() < from) { + ++begin; + break; + } + } + + for (qsizetype end = begin + 1; end < m_items.count(); end++) { + if (m_items.at(end)->arrival >= to) { + return std::make_pair(begin, end); + } + } + return std::make_pair(begin, m_items.count()); +} + +const TimelineModel::Item *TimelineModel::at(qsizetype index) const +{ + return m_items.at(index); +} + +void TimelineModel::clear() +{ + qDeleteAll(m_items); + m_items.clear(); +} + +qsizetype TimelineModel::searchIndex(QDate date) const +{ + if (m_items.isEmpty()) { + return -1; + } + + if (m_items.last()->arrival < date) { + return m_items.size(); + } + + qsizetype begin = 0; + qsizetype end = m_items.size() - 1; + qsizetype middle; + for (middle = end / 2; begin <= end; middle = (begin + end) / 2) { + if (m_items.at(middle)->arrival < date) { + begin = middle + 1; + } else { + end = middle - 1; + } + } + return middle + (m_items.at(middle)->arrival < date ? 1 : 0); +} + +#include "moc_timelinemodel.cpp" diff --git a/src/timelinemodel.h b/src/timelinemodel.h new file mode 100644 index 0000000..b21148b --- /dev/null +++ b/src/timelinemodel.h @@ -0,0 +1,48 @@ +#ifndef TIMELINEMODEL_H +#define TIMELINEMODEL_H + +#include +#include +#include +#include +#include + +class TimelineModel : public QObject +{ + Q_OBJECT + QML_ELEMENT + QML_UNCREATABLE("") + +public: + struct Item + { + int id; + QDate arrival; + int nights; + QString holder; + QString status; + bool hasBegin; + bool hasEnd; + QDate departure() const { return arrival.addDays(nights); } + }; + + explicit TimelineModel(QObject *parent = nullptr); + ~TimelineModel() override; + + void update(const QList &items); + + std::pair indexesOf(QDate from, QDate to) const; + const Item *at(qsizetype index) const; + +signals: + +private: + Q_DISABLE_COPY_MOVE(TimelineModel) + + void clear(); + qsizetype searchIndex(QDate date) const; + + QList m_items; +}; + +#endif // TIMELINEMODEL_H diff --git a/src/timelineview.cpp b/src/timelineview.cpp index f0ef8a5..9df9052 100644 --- a/src/timelineview.cpp +++ b/src/timelineview.cpp @@ -31,10 +31,11 @@ TimelineView::TimelineView(QQuickItem *parent) : QQuickItem(parent) , m_dayWidth(24.0) , m_delegate(nullptr) + , m_toDate() + , m_model(nullptr) , m_fromDate() , m_items() , m_reusableItems() - , m_toDate() , m_viewportX(0) , m_prevViewportX(0) , m_viewportWidth(0) @@ -114,6 +115,22 @@ void TimelineView::setToDate(QDate date) populate(); } +TimelineModel *TimelineView::model() const +{ + return m_model; +} + +void TimelineView::setModel(TimelineModel *model) +{ + if (model == m_model) { + return; + } + m_model = model; + emit modelChanged(m_model); + clear(); + populate(); +} + qreal TimelineView::viewportX() const { return m_viewportX; @@ -195,7 +212,7 @@ void TimelineView::clear() void TimelineView::populate() { - if (!isComponentComplete() || !m_delegate) { + if (!isComponentComplete() || !m_delegate || !m_model) { return; } if (m_viewportX > m_prevViewportX) { @@ -209,13 +226,16 @@ void TimelineView::populate() } } else if (m_viewportX < m_prevViewportX) { // Insert from the left - for (qint64 day = m_items.isEmpty() ? qCeil(m_prevViewportX / m_dayWidth) - 1 - : m_items.first()->day - 1, - len = 1, - lastDay = qMax(-1, qFloor((m_viewportX - (len * m_dayWidth)) / m_dayWidth)); - day > lastDay; - day -= len) { - TimelineView::Item *viewItem = createItem(day, len); + auto [begin, end] = m_model->indexesOf(m_fromDate.addDays(qFloor(m_viewportX / m_dayWidth)), + m_fromDate.addDays( + qCeil(m_prevViewportX / m_dayWidth))); + for (qsizetype i = end - 1; i >= begin; --i) { + const TimelineModel::Item *item = m_model->at(i); + qint64 day = m_fromDate.daysTo(item->arrival); + if (!m_items.isEmpty() && m_items.first()->day <= day) { + continue; + } + TimelineView::Item *viewItem = createItem(day, item->nights); if (!viewItem) { break; } @@ -235,13 +255,15 @@ void TimelineView::populate() } } else if (currentRight > prevRight) { // Insert from the right - for (qint64 day = m_items.isEmpty() ? qCeil(prevRight / m_dayWidth) - 1 - : m_items.last()->day + 1, - len = 1, - lastDay = qFloor((currentRight + (len * m_dayWidth)) / m_dayWidth); - day < lastDay; - day += len) { - TimelineView::Item *viewItem = createItem(day, len); + auto [begin, end] = m_model->indexesOf(m_fromDate.addDays(qFloor(prevRight / m_dayWidth)), + m_fromDate.addDays(qCeil(currentRight / m_dayWidth))); + for (qsizetype i = begin; i < end; ++i) { + const TimelineModel::Item *item = m_model->at(i); + qint64 day = m_fromDate.daysTo(item->arrival); + if (!m_items.isEmpty() && m_items.last()->day >= day) { + continue; + } + TimelineView::Item *viewItem = createItem(day, item->nights); if (!viewItem) { break; } diff --git a/src/timelineview.h b/src/timelineview.h index a385a55..c462cd5 100644 --- a/src/timelineview.h +++ b/src/timelineview.h @@ -4,6 +4,7 @@ #include #include #include +#include "timelinemodel.h" class TimelineView : public QQuickItem { @@ -13,6 +14,7 @@ class TimelineView : public QQuickItem Q_PROPERTY(QQmlComponent *delegate READ delegate WRITE setDelegate NOTIFY delegateChanged) Q_PROPERTY(QDate fromDate READ fromDate WRITE setFromDate NOTIFY fromDateChanged) Q_PROPERTY(QDate toDate READ toDate WRITE setToDate NOTIFY toDateChanged) + Q_PROPERTY(TimelineModel *model READ model WRITE setModel NOTIFY modelChanged) Q_PROPERTY(qreal viewportX READ viewportX WRITE setViewportX NOTIFY viewportXChanged) Q_PROPERTY( qreal viewportWidth READ viewportWidth WRITE setViewportWidth NOTIFY viewportWidthChanged) @@ -33,6 +35,9 @@ public: QDate toDate() const; void setToDate(QDate date); + TimelineModel *model() const; + void setModel(TimelineModel *model); + qreal viewportX() const; void setViewportX(qreal x); @@ -44,6 +49,7 @@ signals: void delegateChanged(QQmlComponent *delegate); void fromDateChanged(QDate date); void toDateChanged(QDate date); + void modelChanged(TimelineModel *model); void viewportXChanged(qreal x); void viewportWidthChanged(qreal width); @@ -66,9 +72,10 @@ private: qreal m_dayWidth; QQmlComponent *m_delegate; QDate m_fromDate; + QDate m_toDate; + TimelineModel *m_model; QList m_items; QList m_reusableItems; - QDate m_toDate; qreal m_viewportX; qreal m_prevViewportX; qreal m_viewportWidth;