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:
parent
c49135247a
commit
94b5faa9d8
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
||||
}
|
||||
|
||||
|
|
|
@ -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"
|
|
@ -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
|
|
@ -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"
|
|
@ -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
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
|
|
Loading…
Reference in New Issue