Add TimelineModel and use it to feed each TimelineView

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.
This commit is contained in:
jordi fita mas 2025-01-09 20:08:40 +01:00
parent c49135247a
commit 94b5faa9d8
8 changed files with 417 additions and 22 deletions

View File

@ -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

View File

@ -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
}

194
src/timelinelistmodel.cpp Normal file
View File

@ -0,0 +1,194 @@
#include "timelinelistmodel.h"
#include <QDebug>
#include <QSqlQuery>
#include <QVariant>
#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<int, QByteArray> TimelineListModel::roleNames() const
{
QHash<int, QByteArray> 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<Lodging> lodgings;
QHash<int, QList<TimelineModel::Item *>> 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"

43
src/timelinelistmodel.h Normal file
View File

@ -0,0 +1,43 @@
#ifndef TIMELINELISTMODEL_H
#define TIMELINELISTMODEL_H
#include <QAbstractListModel>
#include <QList>
#include <QtQmlIntegration>
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<int, QByteArray> roleNames() const override;
private:
Q_DISABLE_COPY_MOVE(TimelineListModel)
struct Item;
QModelIndex findIndexById(int id);
void fetch();
QList<Item *> m_items;
};
#endif // TIMELINELISTMODEL_H

77
src/timelinemodel.cpp Normal file
View File

@ -0,0 +1,77 @@
#include "timelinemodel.h"
TimelineModel::TimelineModel(QObject *parent)
: QObject{parent}
, m_items()
{}
TimelineModel::~TimelineModel()
{
clear();
}
void TimelineModel::update(const QList<Item *> &items)
{
clear();
m_items.assign(items.begin(), items.end());
}
std::pair<qsizetype, qsizetype> 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"

48
src/timelinemodel.h Normal file
View File

@ -0,0 +1,48 @@
#ifndef TIMELINEMODEL_H
#define TIMELINEMODEL_H
#include <QDate>
#include <QList>
#include <QObject>
#include <QtQmlIntegration>
#include <utility>
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<Item *> &items);
std::pair<qsizetype, qsizetype> 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<Item *> m_items;
};
#endif // TIMELINEMODEL_H

View File

@ -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;
}

View File

@ -4,6 +4,7 @@
#include <QList>
#include <QQmlComponent>
#include <QQuickItem>
#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<Item *> m_items;
QList<QQuickItem *> m_reusableItems;
QDate m_toDate;
qreal m_viewportX;
qreal m_prevViewportX;
qreal m_viewportWidth;