From 00255f67a0563fb84e4c33682c4dd5f6c0641c6a Mon Sep 17 00:00:00 2001 From: jordi fita mas Date: Tue, 7 Jan 2025 12:53:27 +0100 Subject: [PATCH] Begin TimelineView control MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This is supposed to be like a kind of horizontal ListView, however the elements need to be placed according to its starting date, and must be as long as the number of days of the stay. As far as i know, i can not do that with a Qt-provided ListView because it has a strict one-to-one mapping between QModelIndex’s row and visual element’s row (i.e., the first item is always the first row, the second item the second row, etc.), while i to have “holes” in the rows for item that are not continous in time. Unfortunately, Qt Quick does not provide a C++ API, meaning that i can not derive from QQuickListView or, rather, QQuickItemView, without using the private API. And that API is private for a reason; i do not want to see myself redoing the control because they have changed who knows what. Thus, had to base this control on QQuickItem. I am also pretty sure i will not be able to use QAbstractItemModel for this control, as, again, that model assumes that everything is continuous: lists have rows next to each other, tables columns next to each other, and trees are “just” nested tables. But i am not certain yet. Meanwhile, what i really need to do is to show a delegate for each “filled in” day, and that’s is what this controls does for now. Since i can not inherit from QQuickFlickable—not that it would be a great idea here, since i need a list of these views anyway—, i have to know the viewport’s position and width in order to have only the minimum number of items visible. I do like QQmlDelgateModel (that i can not reuse without private API), and reuse the delegates when possible. --- src/CMakeLists.txt | 1 + src/ReservationsPage.qml | 66 +++++++++- src/timelineview.cpp | 260 +++++++++++++++++++++++++++++++++++++++ src/timelineview.h | 77 ++++++++++++ 4 files changed, 401 insertions(+), 3 deletions(-) create mode 100644 src/timelineview.cpp create mode 100644 src/timelineview.h diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index bca704a..04c0481 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -12,6 +12,7 @@ qt_add_qml_module(${PROJECT_NAME} calendarlistmodel.cpp calendarlistmodel.h database.cpp database.h mnemonicattached.cpp mnemonicattached.h + timelineview.cpp timelineview.h QML_FILES ErrorNotification.qml Expander.qml diff --git a/src/ReservationsPage.qml b/src/ReservationsPage.qml index 5a3bb61..006d802 100644 --- a/src/ReservationsPage.qml +++ b/src/ReservationsPage.qml @@ -2,6 +2,7 @@ pragma ComponentBehavior: Bound import QtQuick import QtQuick.Controls import QtQuick.Layouts +import Camper Page { title: qsTr("Reservations") @@ -17,17 +18,76 @@ Page { } ListView { - anchors.fill: parent + id: lodgingList - delegate: Text { + ScrollBar.vertical: verticalScroll + anchors.bottom: parent.bottom + anchors.left: parent.left + anchors.top: parent.top + headerPositioning: ListView.OverlayHeader + model: calendarList + width: 100 + + delegate: Label { required property string name text: name } - model: CalendarListModel { + header: Pane { + z: 2 + + Label { + text: qsTr("Lodging") + } } } + ListView { + id: timelineList + + anchors.bottom: parent.bottom + anchors.left: lodgingList.right + anchors.right: parent.right + anchors.top: parent.top + clip: true + contentWidth: 2184 + flickableDirection: Flickable.AutoFlickDirection + headerPositioning: ListView.OverlayHeader + model: calendarList + + ScrollBar.horizontal: ScrollBar { + } + ScrollBar.vertical: ScrollBar { + id: verticalScroll + + } + delegate: TimelineView { + fromDate: "2024-11-01" + height: 16 + toDate: "2025-01-31" + viewportWidth: ListView.view.width + viewportX: ListView.view.contentX + + delegate: Rectangle { + border.color: "black" + border.width: 1 + color: "blue" + } + } + header: Pane { + z: 2 + + Label { + text: qsTr("Calendar") + } + } + } + + CalendarListModel { + id: calendarList + + } + MnemonicAction { id: logoutAction diff --git a/src/timelineview.cpp b/src/timelineview.cpp new file mode 100644 index 0000000..b43b08e --- /dev/null +++ b/src/timelineview.cpp @@ -0,0 +1,260 @@ +#include "timelineview.h" +#include +#include + +struct TimelineViewItem +{ + TimelineViewItem(qint64 day, qint64 len, QQuickItem &qitem, TimelineView &view) + : item(&qitem) + , day(day) + { + item->setParentItem(&view); + item->setPosition(QPointF(day * view.dayWidth(), 0.0)); + item->setSize(QSizeF(len * view.dayWidth(), view.height())); + item->setVisible(true); + } + + ~TimelineViewItem() + { + item->setVisible(false); + item->setParent(nullptr); + } + + qreal left() const { return item->x(); } + qreal right() const { return item->x() + item->width(); } + + QQuickItem *item; + const qint64 day; +}; + +TimelineView::TimelineView(QQuickItem *parent) + : QQuickItem(parent) + , m_dayWidth(24.0) + , m_delegate(nullptr) + , m_fromDate() + , m_items() + , m_reusableItems() + , m_toDate() + , m_viewportX(0) + , m_prevViewportX(0) + , m_viewportWidth(0) + , m_prevViewportWidth(0) +{} + +TimelineView::~TimelineView() +{ + clear(); + drainItems(); +} + +qreal TimelineView::dayWidth() const +{ + return m_dayWidth; +} + +void TimelineView::setDayWidth(qreal width) +{ + if (width == m_dayWidth) { + return; + } + m_dayWidth = width; + emit dayWidthChanged(m_dayWidth); + updateImplicitWidth(); + clear(); + populate(); +} + +QQmlComponent *TimelineView::delegate() const +{ + return m_delegate; +} + +void TimelineView::setDelegate(QQmlComponent *delegate) +{ + if (delegate == m_delegate) { + return; + } + m_delegate = delegate; + emit delegateChanged(m_delegate); + clear(); + drainItems(); + populate(); + update(); +} + +QDate TimelineView::fromDate() const +{ + return m_fromDate; +} + +void TimelineView::setFromDate(QDate date) +{ + if (date == m_fromDate) { + return; + } + m_fromDate = date; + emit fromDateChanged(m_fromDate); + updateImplicitWidth(); + populate(); +} + +QDate TimelineView::toDate() const +{ + return m_toDate; +} + +void TimelineView::setToDate(QDate date) +{ + if (date == m_toDate) { + return; + } + m_toDate = date; + emit toDateChanged(m_toDate); + updateImplicitWidth(); + populate(); +} + +qreal TimelineView::viewportX() const +{ + return m_viewportX; +} + +void TimelineView::setViewportX(qreal x) +{ + if (x == m_viewportX) { + return; + } + m_viewportX = x; + emit viewportXChanged(m_viewportX); + populate(); +} + +qreal TimelineView::viewportWidth() const +{ + return m_viewportWidth; +} + +void TimelineView::setViewportWidth(qreal width) +{ + if (width == m_viewportWidth) { + return; + } + m_viewportWidth = width; + emit viewportWidthChanged(m_viewportWidth); + populate(); +} + +void TimelineView::componentComplete() +{ + QQuickItem::componentComplete(); + populate(); +} + +TimelineViewItem *TimelineView::createItem(qint64 day, qint64 len) +{ + QQuickItem *item = m_reusableItems.isEmpty() ? qobject_cast(m_delegate->create( + m_delegate->creationContext())) + : m_reusableItems.takeLast(); + if (!item) { + qmlWarning(m_delegate) << TimelineView::tr("Delegate must be of Item type"); + return nullptr; + } + auto *viewItem = new TimelineViewItem(day, len, *item, *this); + return viewItem; +} + +void TimelineView::releaseItem(TimelineViewItem *item) +{ + if (!item) { + return; + } + qsizetype index = m_items.indexOf(item); + if (index < 0) { + return; + } + QQuickItem *qitem = item->item; + if (!m_reusableItems.contains(qitem)) { + m_reusableItems.append(qitem); + } + m_items.removeAt(index); + delete item; +} + +void TimelineView::drainItems() +{ + qDeleteAll(m_reusableItems); + m_reusableItems.clear(); +} + +void TimelineView::clear() +{ + while (!m_items.isEmpty()) { + releaseItem(m_items.last()); + } +} + +void TimelineView::populate() +{ + if (!isComponentComplete() || !m_delegate) { + return; + } + if (m_viewportX > m_prevViewportX) { + // Delete from the left + while (!m_items.isEmpty()) { + TimelineViewItem *item = m_items.first(); + if (item->right() >= m_viewportX) { + break; + } + releaseItem(item); + } + } 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) { + TimelineViewItem *viewItem = createItem(day, len); + if (!viewItem) { + break; + } + m_items.prepend(viewItem); + } + } + int currentRight = m_viewportX + m_viewportWidth; + int prevRight = m_prevViewportX + m_prevViewportWidth; + if (currentRight < prevRight) { + // Delete from the right + while (!m_items.isEmpty()) { + TimelineViewItem *item = m_items.last(); + if (item->left() < currentRight) { + break; + } + releaseItem(item); + } + } 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) { + TimelineViewItem *viewItem = createItem(day, len); + if (!viewItem) { + break; + } + m_items.append(viewItem); + } + } + m_prevViewportX = m_viewportX; + m_prevViewportWidth = m_viewportWidth; +} + +void TimelineView::updateImplicitWidth() +{ + setImplicitWidth(m_dayWidth * m_fromDate.daysTo(m_toDate)); +} + +#include "moc_timelineview.cpp" diff --git a/src/timelineview.h b/src/timelineview.h new file mode 100644 index 0000000..01e39c2 --- /dev/null +++ b/src/timelineview.h @@ -0,0 +1,77 @@ +#ifndef TIMELINEVIEW_H +#define TIMELINEVIEW_H + +#include +#include +#include + +struct TimelineViewItem; +class TimelineView : public QQuickItem +{ + Q_OBJECT + QML_ELEMENT + Q_PROPERTY(qreal dayWidth READ dayWidth WRITE setDayWidth NOTIFY dayWidthChanged) + 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(qreal viewportX READ viewportX WRITE setViewportX NOTIFY viewportXChanged) + Q_PROPERTY( + qreal viewportWidth READ viewportWidth WRITE setViewportWidth NOTIFY viewportWidthChanged) + +public: + TimelineView(QQuickItem *parent = nullptr); + ~TimelineView() override; + + qreal dayWidth() const; + void setDayWidth(qreal width); + + QQmlComponent *delegate() const; + void setDelegate(QQmlComponent *delegate); + + QDate fromDate() const; + void setFromDate(QDate date); + + QDate toDate() const; + void setToDate(QDate date); + + qreal viewportX() const; + void setViewportX(qreal x); + + qreal viewportWidth() const; + void setViewportWidth(qreal width); + +signals: + void dayWidthChanged(qreal width); + void delegateChanged(QQmlComponent *delegate); + void fromDateChanged(QDate date); + void toDateChanged(QDate date); + void viewportXChanged(qreal x); + void viewportWidthChanged(qreal width); + +protected: + void componentComplete() override; + +private: + Q_DISABLE_COPY_MOVE(TimelineView) + + TimelineViewItem *createItem(qint64 day, qint64 len); + void releaseItem(TimelineViewItem *item); + void drainItems(); + + void clear(); + void populate(); + void updateImplicitWidth(); + + qreal m_dayWidth; + QQmlComponent *m_delegate; + QDate m_fromDate; + QList m_items; + QList m_reusableItems; + QDate m_toDate; + qreal m_viewportX; + qreal m_prevViewportX; + qreal m_viewportWidth; + qreal m_prevViewportWidth; +}; + +#endif // TIMELINEVIEW_H