presentation/SearchTimelineModel.h
| Line | Branch | Exec | Source |
|---|---|---|---|
| 1 | #pragma once | ||
| 2 | |||
| 3 | #include "app/TimeHistogram.h" | ||
| 4 | #include <QAbstractListModel> | ||
| 5 | #include <QDate> | ||
| 6 | #include <QLocale> | ||
| 7 | #include <QMap> | ||
| 8 | #include <QPointer> | ||
| 9 | #include <QVariantList> | ||
| 10 | #include <QVector> | ||
| 11 | |||
| 12 | class MailSearchSession; | ||
| 13 | |||
| 14 | // Sprint 101 S101.5.2–S101.5.8, S101.5.11 (D-7, D-8, D-9, SD-04, SD-10, | ||
| 15 | // SD-11): the bars, axis labels and breadcrumb of a search tab's timeline, | ||
| 16 | // from the session's day counts through TimeHistogram. One row per bucket | ||
| 17 | // (empty buckets included, SD-04). Every interaction ends in one call on | ||
| 18 | // the session (setDateRange), so the date lives in the query alone (rule 8). | ||
| 19 | // | ||
| 20 | // Drill is instant (S101.5.4): when the query's date range narrows, the | ||
| 21 | // bars are re-bucketed from the day counts already known until the | ||
| 22 | // summary of the new query replaces them; `stale` is true meanwhile. | ||
| 23 | // | ||
| 24 | // Owner: MailSearchSessions (one, bound to the active tab's session); QML | ||
| 25 | // reads it as searchTabs.timeline and sets plotWidth. GUI thread only. | ||
| 26 | class SearchTimelineModel final : public QAbstractListModel { | ||
| 27 | 11167 | Q_OBJECT | |
| 28 | Q_PROPERTY(MailSearchSession *session READ session WRITE setSession NOTIFY sessionChanged) | ||
| 29 | Q_PROPERTY(qreal plotWidth READ plotWidth WRITE setPlotWidth NOTIFY timelineChanged) | ||
| 30 | Q_PROPERTY(QString unit READ unitName NOTIFY timelineChanged) | ||
| 31 | Q_PROPERTY(qint64 maxCount READ maxCount NOTIFY timelineChanged) | ||
| 32 | Q_PROPERTY(QVariantList levels READ levels NOTIFY timelineChanged) | ||
| 33 | Q_PROPERTY(QString rangeLabel READ rangeLabel NOTIFY timelineChanged) | ||
| 34 | Q_PROPERTY(bool canStep READ canStep NOTIFY timelineChanged) | ||
| 35 | Q_PROPERTY(bool canZoomOut READ canZoomOut NOTIFY timelineChanged) | ||
| 36 | Q_PROPERTY(bool stale READ stale NOTIFY timelineChanged) | ||
| 37 | Q_PROPERTY(bool hasData READ hasData NOTIFY timelineChanged) | ||
| 38 | Q_PROPERTY(int caretBucket READ caretBucket NOTIFY caretChanged) | ||
| 39 | Q_PROPERTY(int viewportFirst READ viewportFirst NOTIFY viewportChanged) | ||
| 40 | Q_PROPERTY(int viewportLast READ viewportLast NOTIFY viewportChanged) | ||
| 41 | |||
| 42 | public: | ||
| 43 | enum Role { FromRole = Qt::UserRole + 1, ToRole, CountRole, LabelRole, AccessibleRole, FractionRole, HourRole }; | ||
| 44 | |||
| 45 | explicit SearchTimelineModel(QObject *parent = nullptr); | ||
| 46 | |||
| 47 | int rowCount(const QModelIndex &parent = {}) const override; | ||
| 48 | QVariant data(const QModelIndex &index, int role) const override; | ||
| 49 | QHash<int, QByteArray> roleNames() const override; | ||
| 50 | |||
| 51 | ✗ | MailSearchSession *session() const { return m_session; } | |
| 52 | void setSession(MailSearchSession *session); | ||
| 53 | ✗ | qreal plotWidth() const { return m_plotWidth; } | |
| 54 | void setPlotWidth(qreal width); | ||
| 55 | QString unitName() const; | ||
| 56 | ✗ | qint64 maxCount() const { return m_maxCount; } | |
| 57 | QVariantList levels() const; | ||
| 58 | QString rangeLabel() const; | ||
| 59 | bool canStep() const; | ||
| 60 | 1 | bool canZoomOut() const { return m_constrained; } | |
| 61 | 594 | bool stale() const { return m_stale; } | |
| 62 | // There are dated hits (or a date range) to show; independent of the | ||
| 63 | // plot width, so a hidden band can be laid out before it has bars. | ||
| 64 | 1076 | bool hasData() const { return m_range.isValid(); } | |
| 65 | 652 | int caretBucket() const { return m_caret; } | |
| 66 | // S101.5.8: the buckets [first, last] of the rows visible in the list; | ||
| 67 | // -1 when the list shows every result or nothing is visible. | ||
| 68 | 214 | int viewportFirst() const { return m_viewportFirst; } | |
| 69 | 226 | int viewportLast() const { return m_viewportLast; } | |
| 70 | // The list's visible view rows, first <= last; (-1, -1) hides the band. | ||
| 71 | Q_INVOKABLE void setVisibleRows(int first, int last); | ||
| 72 | |||
| 73 | // Collision-free axis labels {bucket, label} for a slot width; a label | ||
| 74 | // is measured as characters × `charWidth` (the axis font's average). | ||
| 75 | Q_INVOKABLE QVariantList ticks(qreal bucketWidth, qreal charWidth) const; | ||
| 76 | // D-8: a click on bucket `index`. Hours do not filter: the view scrolls | ||
| 77 | // to the hour's first mail (hourRequested). | ||
| 78 | Q_INVOKABLE void drill(int index); | ||
| 79 | Q_INVOKABLE void brush(int first, int last); | ||
| 80 | Q_INVOKABLE void zoomOut(); | ||
| 81 | Q_INVOKABLE void zoomToLevel(int level); | ||
| 82 | // ‹ › : the previous/next period of the same length (−1 / +1). | ||
| 83 | Q_INVOKABLE void step(int direction); | ||
| 84 | // S101.5.9: a section of the grouped list ("March 2025", the unit of the | ||
| 85 | // bars): its number of results and the drill into it. | ||
| 86 | Q_INVOKABLE qint64 countForPeriod(const QString &label) const; | ||
| 87 | Q_INVOKABLE void drillPeriod(const QString &label); | ||
| 88 | // The read-out of a bucket in the info line: "142 results · March 2025". | ||
| 89 | Q_INVOKABLE QString tooltip(int index) const; | ||
| 90 | |||
| 91 | // Pure helpers, public for tests. | ||
| 92 | static TimeHistogram::Range steppedRange(const TimeHistogram::Range &range, int direction, | ||
| 93 | Qt::DayOfWeek weekStart); | ||
| 94 | |||
| 95 | signals: | ||
| 96 | void sessionChanged(); | ||
| 97 | void timelineChanged(); | ||
| 98 | void caretChanged(); | ||
| 99 | void viewportChanged(); | ||
| 100 | void hourRequested(int hour); | ||
| 101 | // A drill, brush, zoom or step set a range (D-30: ends the first-use hint). | ||
| 102 | void rangeApplied(); | ||
| 103 | |||
| 104 | private: | ||
| 105 | void rebuild(); | ||
| 106 | void updateCaret(); | ||
| 107 | void updateViewport(); | ||
| 108 | int bucketFor(const QDateTime &when) const; | ||
| 109 | void applyRange(const TimeHistogram::Range &range); | ||
| 110 | TimeHistogram::Range shownRange() const; | ||
| 111 | |||
| 112 | QPointer<MailSearchSession> m_session; | ||
| 113 | QMetaObject::Connection m_queryConnection; | ||
| 114 | QMetaObject::Connection m_summaryConnection; | ||
| 115 | QMetaObject::Connection m_selectionConnection; | ||
| 116 | QMetaObject::Connection m_rowsConnection; | ||
| 117 | qreal m_plotWidth = 0; | ||
| 118 | QLocale m_locale; | ||
| 119 | Qt::DayOfWeek m_weekStart = Qt::Monday; | ||
| 120 | QMap<QDate, qint64> m_dayCounts; // last summary (or server-updated) day counts | ||
| 121 | QVector<qint64> m_hourCounts; | ||
| 122 | QDate m_firstHit; | ||
| 123 | QDate m_lastHit; | ||
| 124 | TimeHistogram::Range m_range; // shown range | ||
| 125 | TimeHistogram::Unit m_unit = TimeHistogram::Unit::Year; | ||
| 126 | QList<TimeHistogram::Bucket> m_buckets; | ||
| 127 | qint64 m_maxCount = 0; | ||
| 128 | bool m_constrained = false; // the query has a date constraint | ||
| 129 | bool m_stale = false; | ||
| 130 | int m_caret = -1; | ||
| 131 | int m_visibleFirstRow = -1; | ||
| 132 | int m_visibleLastRow = -1; | ||
| 133 | int m_viewportFirst = -1; | ||
| 134 | int m_viewportLast = -1; | ||
| 135 | }; | ||
| 136 |