MailJDTourDownloadDocsQualityTestsCoverage

Which source lines and branches the tests execute. Lines 91.5 %, branches 75.0 % (minimum 85 % and 72 %). main @ 399ffe3d · 21 October 2026 · pipeline 7238.

GCC Code Coverage Report


Directory: src/
Coverage: low: ≥ 0% medium: ≥ 75.0% high: ≥ 90.0%
Coverage Exec / Excl / Total
Lines: 97.7% 210 / 0 / 215
Functions: 100.0% 35 / 0 / 35
Branches: 81.1% 340 / 0 / 419

presentation/SearchTimelineModel.cpp
Line Branch Exec Source
1 #include "SearchTimelineModel.h"
2
3 #include "MailSearchSession.h"
4 #include "SearchLog.h"
5 #include "SearchTermModel.h"
6 #include <QCoreApplication>
7 #include <algorithm>
8
9 using namespace TimeHistogram;
10
11 namespace {
12
13 8902 QString tr(const char *text) { return QCoreApplication::translate("SearchTimelineModel", text); }
14
15 // "1 result" / "142 results" in the locale's digits.
16 8902 QString results(qint64 count, const QLocale &locale) {
17
10/14
✓ Branch 2 → 3 taken 1512 times.
✓ Branch 2 → 4 taken 7390 times.
✓ Branch 3 → 8 taken 1512 times.
✓ Branch 4 → 5 taken 7390 times.
✓ Branch 6 → 7 taken 7390 times.
✓ Branch 7 → 8 taken 7390 times.
✓ Branch 9 → 10 taken 7390 times.
✓ Branch 9 → 11 taken 1512 times.
✓ Branch 11 → 12 taken 7390 times.
✓ Branch 11 → 13 taken 1512 times.
✗ Branch 15 → 16 not taken.
✗ Branch 15 → 17 not taken.
✗ Branch 20 → 21 not taken.
✗ Branch 20 → 22 not taken.
17804 return count == 1 ? tr("1 result") : tr("%1 results").arg(locale.toString(count));
18 }
19
20
2/2
✓ Branch 3 → 4 taken 954 times.
✓ Branch 3 → 5 taken 4534 times.
5488 QString iso(const QDate &date) { return date.isValid() ? date.toString(Qt::ISODate) : QString(); }
21
22 7 bool wholeYear(const Range &range) {
23
8/8
✓ Branch 2 → 3 taken 7 times.
✓ Branch 3 → 4 taken 7 times.
✓ Branch 5 → 6 taken 4 times.
✓ Branch 5 → 11 taken 3 times.
✓ Branch 6 → 7 taken 4 times.
✓ Branch 7 → 8 taken 4 times.
✓ Branch 9 → 10 taken 3 times.
✓ Branch 9 → 11 taken 1 time.
7 return range.first == QDate(range.first.year(), 1, 1) && range.last == QDate(range.first.year(), 12, 31);
24 }
25
26 4 bool wholeMonth(const Range &range) {
27
6/7
✓ Branch 2 → 3 taken 4 times.
✓ Branch 3 → 4 taken 2 times.
✓ Branch 3 → 9 taken 2 times.
✓ Branch 4 → 5 taken 2 times.
✓ Branch 5 → 6 taken 2 times.
✓ Branch 7 → 8 taken 2 times.
✗ Branch 7 → 9 not taken.
4 return range.first.day() == 1 && range.last == range.first.addMonths(1).addDays(-1);
28 }
29
30 } // namespace
31
32 800 SearchTimelineModel::SearchTimelineModel(QObject *parent)
33
9/9
✓ Branch 4 → 5 taken 800 times.
✓ Branch 5 → 6 taken 800 times.
✓ Branch 6 → 7 taken 800 times.
✓ Branch 7 → 8 taken 800 times.
✓ Branch 8 → 9 taken 800 times.
✓ Branch 9 → 10 taken 800 times.
✓ Branch 12 → 13 taken 800 times.
✓ Branch 13 → 14 taken 800 times.
✓ Branch 14 → 15 taken 800 times.
800 : QAbstractListModel(parent), m_weekStart(m_locale.firstDayOfWeek()) {}
34
35 31509 int SearchTimelineModel::rowCount(const QModelIndex &parent) const {
36
1/2
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 5 taken 31509 times.
31509 return parent.isValid() ? 0 : int(m_buckets.size());
37 }
38
39 26755 QVariant SearchTimelineModel::data(const QModelIndex &index, int role) const {
40
3/6
✓ Branch 3 → 4 taken 26755 times.
✗ Branch 3 → 7 not taken.
✗ Branch 6 → 7 not taken.
✓ Branch 6 → 8 taken 26755 times.
✗ Branch 9 → 10 not taken.
✓ Branch 9 → 11 taken 26755 times.
26755 if (!index.isValid() || index.row() >= m_buckets.size()) return {};
41 26755 const Bucket &bucket = m_buckets.at(index.row());
42
5/8
✓ Branch 13 → 14 taken 2 times.
✗ Branch 13 → 18 not taken.
✓ Branch 13 → 22 taken 9021 times.
✓ Branch 13 → 23 taken 450 times.
✓ Branch 13 → 27 taken 8866 times.
✓ Branch 13 → 45 taken 8416 times.
✗ Branch 13 → 49 not taken.
✗ Branch 13 → 50 not taken.
26755 switch (role) {
43
1/1
✓ Branch 14 → 15 taken 2 times.
2 case FromRole: return iso(bucket.range.first);
44 ✗ case ToRole: return iso(bucket.range.last);
45 9021 case CountRole: return bucket.count;
46
1/1
✓ Branch 23 → 24 taken 450 times.
450 case LabelRole: return bucketLabel(bucket, m_unit, m_locale);
47 8866 case AccessibleRole:
48 //: A timeline bar for screen readers: "March 2025, 142 results, 3 of 12".
49
4/4
✓ Branch 27 → 28 taken 8866 times.
✓ Branch 28 → 29 taken 8866 times.
✓ Branch 29 → 30 taken 8866 times.
✓ Branch 30 → 31 taken 8866 times.
17732 return tr("%1, %2, %3 of %4").arg(bucketLabel(bucket, m_unit, m_locale), results(bucket.count, m_locale))
50
2/2
✓ Branch 33 → 34 taken 8866 times.
✓ Branch 36 → 37 taken 8866 times.
26598 .arg(index.row() + 1).arg(m_buckets.size());
51
2/2
✓ Branch 45 → 46 taken 8380 times.
✓ Branch 45 → 47 taken 36 times.
8416 case FractionRole: return m_maxCount > 0 ? qreal(bucket.count) / qreal(m_maxCount) : 0.0;
52 ✗ case HourRole: return bucket.hour;
53 ✗ default: return {};
54 }
55 }
56
57 662 QHash<int, QByteArray> SearchTimelineModel::roleNames() const {
58 ✗ return {{FromRole, "from"}, {ToRole, "to"}, {CountRole, "count"}, {LabelRole, "label"},
59
2/4
✓ Branch 11 → 12 taken 4634 times.
✓ Branch 11 → 13 taken 662 times.
✗ Branch 16 → 17 not taken.
✗ Branch 16 → 18 not taken.
5296 {AccessibleRole, "accessibleText"}, {FractionRole, "fraction"}, {HourRole, "hour"}};
60 662 }
61
62 145 void SearchTimelineModel::setSession(MailSearchSession *session) {
63
2/2
✓ Branch 3 → 4 taken 13 times.
✓ Branch 3 → 5 taken 132 times.
145 if (m_session == session) return;
64 132 disconnect(m_queryConnection);
65 132 disconnect(m_summaryConnection);
66 132 disconnect(m_selectionConnection);
67 132 disconnect(m_rowsConnection);
68 132 m_session = session;
69 132 m_visibleFirstRow = m_visibleLastRow = -1;
70 132 m_dayCounts.clear();
71 132 m_hourCounts.clear();
72
1/1
✓ Branch 12 → 13 taken 132 times.
132 m_firstHit = m_lastHit = QDate();
73
2/2
✓ Branch 14 → 15 taken 107 times.
✓ Branch 14 → 61 taken 25 times.
132 if (m_session) {
74
1/1
✓ Branch 16 → 17 taken 107 times.
214 m_queryConnection = connect(m_session, &MailSearchSession::queryChanged, this, [this] {
75 126 m_stale = true; // the previous day counts stand in until the new summary (S101.5.4)
76 126 rebuild();
77 107 });
78
1/1
✓ Branch 20 → 21 taken 107 times.
214 m_summaryConnection = connect(m_session, &MailSearchSession::summaryChanged, this, [this] {
79
1/1
✓ Branch 3 → 4 taken 555 times.
555 const QString state = m_session->summaryState();
80
5/6
✓ Branch 6 → 7 taken 129 times.
✓ Branch 6 → 11 taken 426 times.
✓ Branch 9 → 10 taken 129 times.
✗ Branch 9 → 11 not taken.
✓ Branch 12 → 13 taken 129 times.
✓ Branch 12 → 14 taken 426 times.
555 if (state != QLatin1String("ready") && state != QLatin1String("limited")) return;
81 426 const MailSearchSummary &summary = m_session->summary();
82 426 m_dayCounts = summary.dayCounts;
83 426 m_hourCounts = summary.hourCounts;
84
5/5
✓ Branch 18 → 19 taken 426 times.
✓ Branch 19 → 20 taken 25 times.
✓ Branch 19 → 21 taken 401 times.
✓ Branch 20 → 23 taken 25 times.
✓ Branch 21 → 22 taken 401 times.
426 m_firstHit = m_dayCounts.isEmpty() ? QDate() : m_dayCounts.firstKey();
85
5/5
✓ Branch 23 → 24 taken 426 times.
✓ Branch 24 → 25 taken 25 times.
✓ Branch 24 → 26 taken 401 times.
✓ Branch 25 → 28 taken 25 times.
✓ Branch 26 → 27 taken 401 times.
426 m_lastHit = m_dayCounts.isEmpty() ? QDate() : m_dayCounts.lastKey();
86 426 m_stale = false;
87
1/1
✓ Branch 28 → 29 taken 426 times.
426 rebuild();
88
2/2
✓ Branch 31 → 32 taken 426 times.
✓ Branch 31 → 34 taken 129 times.
662 });
89
1/1
✓ Branch 24 → 25 taken 107 times.
214 m_selectionConnection = connect(m_session, &MailSearchSession::selectionChanged, this,
90 214 &SearchTimelineModel::updateCaret);
91
1/1
✓ Branch 28 → 29 taken 107 times.
107 m_rowsConnection = connect(m_session, &MailSearchSession::rowsChanged, this, &SearchTimelineModel::updateViewport);
92
1/1
✓ Branch 32 → 33 taken 107 times.
107 const QString state = m_session->summaryState();
93
5/6
✓ Branch 35 → 36 taken 93 times.
✓ Branch 35 → 39 taken 14 times.
✗ Branch 38 → 39 not taken.
✓ Branch 38 → 40 taken 93 times.
✓ Branch 41 → 42 taken 14 times.
✓ Branch 41 → 59 taken 93 times.
107 if (state == QLatin1String("ready") || state == QLatin1String("limited")) {
94 14 m_dayCounts = m_session->summary().dayCounts;
95 14 m_hourCounts = m_session->summary().hourCounts;
96
3/5
✓ Branch 48 → 49 taken 14 times.
✗ Branch 49 → 50 not taken.
✓ Branch 49 → 51 taken 14 times.
✗ Branch 50 → 53 not taken.
✓ Branch 51 → 52 taken 14 times.
14 m_firstHit = m_dayCounts.isEmpty() ? QDate() : m_dayCounts.firstKey();
97
3/5
✓ Branch 53 → 54 taken 14 times.
✗ Branch 54 → 55 not taken.
✓ Branch 54 → 56 taken 14 times.
✗ Branch 55 → 58 not taken.
✓ Branch 56 → 57 taken 14 times.
14 m_lastHit = m_dayCounts.isEmpty() ? QDate() : m_dayCounts.lastKey();
98 }
99 107 }
100 132 m_stale = false;
101 132 emit sessionChanged();
102 132 rebuild();
103 }
104
105 115 void SearchTimelineModel::setPlotWidth(qreal width) {
106
1/2
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 5 taken 115 times.
115 if (qFuzzyCompare(m_plotWidth + 1, width + 1)) return;
107 115 m_plotWidth = width;
108 115 rebuild();
109 }
110
111 6141 QString SearchTimelineModel::unitName() const {
112
5/6
✓ Branch 2 → 3 taken 336 times.
✓ Branch 2 → 8 taken 4632 times.
✓ Branch 2 → 13 taken 75 times.
✓ Branch 2 → 18 taken 1096 times.
✓ Branch 2 → 23 taken 2 times.
✗ Branch 2 → 28 not taken.
6141 switch (m_unit) {
113 672 case Unit::Year: return QStringLiteral("year");
114 9264 case Unit::Month: return QStringLiteral("month");
115 150 case Unit::Week: return QStringLiteral("week");
116 2192 case Unit::Day: return QStringLiteral("day");
117 4 case Unit::Hour: return QStringLiteral("hour");
118 }
119 ✗ return {};
120 }
121
122 // The query's date range; open ends close at the first hit or today.
123 799 Range SearchTimelineModel::shownRange() const {
124
2/2
✓ Branch 3 → 4 taken 25 times.
✓ Branch 3 → 5 taken 774 times.
799 if (!m_session) return {};
125 774 const QString query = m_session->query();
126
1/1
✓ Branch 7 → 8 taken 774 times.
774 QDate from = SearchTermModel::dateFrom(query);
127
1/1
✓ Branch 8 → 9 taken 774 times.
774 QDate to = SearchTermModel::dateTo(query);
128
7/8
✓ Branch 9 → 10 taken 774 times.
✓ Branch 10 → 11 taken 666 times.
✓ Branch 10 → 14 taken 108 times.
✓ Branch 11 → 12 taken 666 times.
✓ Branch 12 → 13 taken 666 times.
✗ Branch 12 → 14 not taken.
✓ Branch 15 → 16 taken 666 times.
✓ Branch 15 → 22 taken 108 times.
774 if (!from.isValid() && !to.isValid())
129
5/5
✓ Branch 16 → 17 taken 666 times.
✓ Branch 17 → 18 taken 547 times.
✓ Branch 17 → 20 taken 119 times.
✓ Branch 18 → 19 taken 547 times.
✓ Branch 19 → 21 taken 547 times.
666 return m_firstHit.isValid() ? spanForHits(m_firstHit, m_lastHit, maxBucketsForWidth(m_plotWidth), m_weekStart)
130 119 : Range{};
131
2/8
✓ Branch 22 → 23 taken 108 times.
✗ Branch 23 → 24 not taken.
✓ Branch 23 → 32 taken 108 times.
✗ Branch 24 → 25 not taken.
✗ Branch 25 → 26 not taken.
✗ Branch 25 → 30 not taken.
✗ Branch 28 → 29 not taken.
✗ Branch 28 → 30 not taken.
108 if (!from.isValid()) from = m_firstHit.isValid() && m_firstHit < to ? m_firstHit : to;
132
2/7
✓ Branch 32 → 33 taken 108 times.
✗ Branch 33 → 34 not taken.
✓ Branch 33 → 41 taken 108 times.
✗ Branch 34 → 35 not taken.
✗ Branch 35 → 36 not taken.
✗ Branch 35 → 37 not taken.
✗ Branch 38 → 39 not taken.
108 if (!to.isValid()) to = std::max(QDate::currentDate(), m_lastHit.isValid() ? m_lastHit : from);
133 108 return Range{from, to};
134 774 }
135
136 // The same number of bars updates in place (no flicker); another number resets.
137 799 void SearchTimelineModel::rebuild() {
138 799 QList<Bucket> next;
139
6/8
✓ Branch 3 → 4 taken 774 times.
✓ Branch 3 → 15 taken 25 times.
✓ Branch 6 → 7 taken 774 times.
✓ Branch 7 → 8 taken 774 times.
✓ Branch 8 → 9 taken 666 times.
✓ Branch 8 → 14 taken 108 times.
✗ Branch 98 → 99 not taken.
✗ Branch 98 → 100 not taken.
2239 m_constrained = m_session && (SearchTermModel::dateFrom(m_session->query()).isValid() ||
140
7/10
✓ Branch 11 → 12 taken 666 times.
✓ Branch 12 → 13 taken 666 times.
✗ Branch 13 → 14 not taken.
✓ Branch 13 → 15 taken 666 times.
✓ Branch 16 → 17 taken 666 times.
✓ Branch 16 → 18 taken 133 times.
✓ Branch 18 → 19 taken 774 times.
✓ Branch 18 → 20 taken 25 times.
✗ Branch 93 → 94 not taken.
✗ Branch 93 → 95 not taken.
2264 SearchTermModel::dateTo(m_session->query()).isValid());
141
1/1
✓ Branch 20 → 21 taken 799 times.
799 m_range = shownRange();
142
7/7
✓ Branch 21 → 22 taken 799 times.
✓ Branch 22 → 23 taken 655 times.
✓ Branch 22 → 25 taken 144 times.
✓ Branch 23 → 24 taken 578 times.
✓ Branch 23 → 25 taken 77 times.
✓ Branch 26 → 27 taken 578 times.
✓ Branch 26 → 43 taken 221 times.
799 if (m_range.isValid() && m_plotWidth > 0) {
143
1/1
✓ Branch 27 → 28 taken 578 times.
578 const int limit = maxBucketsForWidth(m_plotWidth);
144
1/1
✓ Branch 28 → 29 taken 578 times.
578 m_unit = chooseUnit(m_range, limit, m_weekStart);
145 // Hours need the summary of that very day (the day counts cannot split).
146
8/8
✓ Branch 29 → 30 taken 59 times.
✓ Branch 29 → 34 taken 519 times.
✓ Branch 30 → 31 taken 57 times.
✓ Branch 30 → 33 taken 2 times.
✓ Branch 32 → 33 taken 56 times.
✓ Branch 32 → 34 taken 1 time.
✓ Branch 35 → 36 taken 58 times.
✓ Branch 35 → 37 taken 520 times.
578 if (m_unit == Unit::Hour && (m_stale || m_hourCounts.size() != 24)) m_unit = Unit::Day;
147
3/3
✓ Branch 37 → 38 taken 1 time.
✓ Branch 37 → 39 taken 577 times.
✓ Branch 38 → 40 taken 1 time.
1156 next = m_unit == Unit::Hour ? hourBuckets(m_range.first, m_hourCounts)
148
1/1
✓ Branch 39 → 40 taken 577 times.
578 : buckets(m_dayCounts, m_range, m_unit, m_weekStart);
149 }
150 799 m_maxCount = 0;
151
2/2
✓ Branch 50 → 46 taken 11483 times.
✓ Branch 50 → 51 taken 799 times.
12282 for (const Bucket &bucket : std::as_const(next)) m_maxCount = std::max(m_maxCount, bucket.count);
152
2/2
✓ Branch 53 → 54 taken 578 times.
✓ Branch 53 → 66 taken 221 times.
799 if (next.size() == m_buckets.size()) {
153 578 m_buckets = next;
154
5/5
✓ Branch 56 → 57 taken 380 times.
✓ Branch 56 → 69 taken 198 times.
✓ Branch 60 → 61 taken 380 times.
✓ Branch 62 → 63 taken 380 times.
✓ Branch 63 → 64 taken 380 times.
578 if (!m_buckets.isEmpty()) emit dataChanged(index(0), index(int(m_buckets.size()) - 1));
155 } else {
156
1/1
✓ Branch 66 → 67 taken 221 times.
221 beginResetModel();
157 221 m_buckets = next;
158
1/1
✓ Branch 68 → 69 taken 221 times.
221 endResetModel();
159 }
160
11/11
✓ Branch 69 → 70 taken 799 times.
✓ Branch 73 → 74 taken 799 times.
✓ Branch 74 → 75 taken 799 times.
✓ Branch 76 → 77 taken 799 times.
✓ Branch 77 → 78 taken 799 times.
✓ Branch 78 → 79 taken 799 times.
✓ Branch 79 → 80 taken 799 times.
✓ Branch 80 → 81 taken 799 times.
✓ Branch 81 → 82 taken 799 times.
✓ Branch 86 → 71 taken 799 times.
✓ Branch 86 → 87 taken 799 times.
1598 qCDebug(lcSearch) << "Search timeline: buckets" << m_buckets.size() << "unit" << unitName() << "stale" << m_stale;
161
1/1
✓ Branch 87 → 88 taken 799 times.
799 emit timelineChanged();
162
1/1
✓ Branch 88 → 89 taken 799 times.
799 updateCaret();
163
1/1
✓ Branch 89 → 90 taken 799 times.
799 updateViewport();
164 799 }
165
166 3258 int SearchTimelineModel::bucketFor(const QDateTime &when) const {
167
3/3
✓ Branch 2 → 3 taken 3258 times.
✓ Branch 3 → 4 taken 1653 times.
✓ Branch 3 → 5 taken 1605 times.
3258 if (!when.isValid()) return -1;
168
1/1
✓ Branch 5 → 6 taken 1605 times.
1605 const QDateTime local = when.toLocalTime();
169
2/2
✓ Branch 19 → 7 taken 26918 times.
✓ Branch 19 → 20 taken 286 times.
27204 for (int i = 0; i < m_buckets.size(); ++i) {
170 26918 const Bucket &bucket = m_buckets.at(i);
171
5/8
✗ Branch 8 → 9 not taken.
✓ Branch 8 → 12 taken 26918 times.
✗ Branch 9 → 10 not taken.
✗ Branch 10 → 11 not taken.
✓ Branch 12 → 13 taken 26918 times.
✓ Branch 13 → 14 taken 26918 times.
✓ Branch 15 → 16 taken 1319 times.
✓ Branch 15 → 17 taken 25599 times.
26918 if (m_unit == Unit::Hour ? bucket.hour == local.time().hour() : bucket.range.contains(local.date())) return i;
172 }
173 286 return -1;
174 1605 }
175
176 320 void SearchTimelineModel::setVisibleRows(int first, int last) {
177
4/4
✓ Branch 2 → 3 taken 263 times.
✓ Branch 2 → 5 taken 57 times.
✓ Branch 3 → 4 taken 245 times.
✓ Branch 3 → 5 taken 18 times.
320 if (first == m_visibleFirstRow && last == m_visibleLastRow) return;
178 75 m_visibleFirstRow = first;
179 75 m_visibleLastRow = last;
180 75 updateViewport();
181 }
182
183 // The rows are sorted by date, so the two ends of the visible rows span
184 // the band; an end outside the shown buckets hides it.
185 1208 void SearchTimelineModel::updateViewport() {
186
6/8
✓ Branch 3 → 4 taken 1183 times.
✓ Branch 3 → 8 taken 25 times.
✓ Branch 5 → 6 taken 1183 times.
✓ Branch 6 → 7 taken 1183 times.
✓ Branch 9 → 10 taken 1183 times.
✓ Branch 9 → 11 taken 25 times.
✗ Branch 39 → 40 not taken.
✗ Branch 39 → 41 not taken.
1208 const int newest = m_session ? bucketFor(m_session->dateTimeAt(m_visibleFirstRow)) : -1;
187
6/8
✓ Branch 12 → 13 taken 1183 times.
✓ Branch 12 → 17 taken 25 times.
✓ Branch 14 → 15 taken 1183 times.
✓ Branch 15 → 16 taken 1183 times.
✓ Branch 18 → 19 taken 1183 times.
✓ Branch 18 → 20 taken 25 times.
✗ Branch 43 → 44 not taken.
✗ Branch 43 → 45 not taken.
1208 const int oldest = m_session ? bucketFor(m_session->dateTimeAt(m_visibleLastRow)) : -1;
188
4/4
✓ Branch 20 → 21 taken 437 times.
✓ Branch 20 → 23 taken 771 times.
✓ Branch 21 → 22 taken 426 times.
✓ Branch 21 → 23 taken 11 times.
1208 const bool shown = newest >= 0 && oldest >= 0;
189
2/2
✓ Branch 24 → 25 taken 426 times.
✓ Branch 24 → 27 taken 782 times.
1208 const int first = shown ? std::min(newest, oldest) : -1;
190
2/2
✓ Branch 28 → 29 taken 426 times.
✓ Branch 28 → 31 taken 782 times.
1208 const int last = shown ? std::max(newest, oldest) : -1;
191
3/4
✓ Branch 32 → 33 taken 1090 times.
✓ Branch 32 → 35 taken 118 times.
✓ Branch 33 → 34 taken 1090 times.
✗ Branch 33 → 35 not taken.
1208 if (first == m_viewportFirst && last == m_viewportLast) return;
192 118 m_viewportFirst = first;
193 118 m_viewportLast = last;
194
1/1
✓ Branch 35 → 36 taken 118 times.
118 emit viewportChanged();
195 }
196
197 917 void SearchTimelineModel::updateCaret() {
198
6/8
✓ Branch 3 → 4 taken 892 times.
✓ Branch 3 → 8 taken 25 times.
✓ Branch 5 → 6 taken 892 times.
✓ Branch 6 → 7 taken 892 times.
✓ Branch 9 → 10 taken 892 times.
✓ Branch 9 → 11 taken 25 times.
✗ Branch 15 → 16 not taken.
✗ Branch 15 → 17 not taken.
917 const int caret = m_session ? bucketFor(m_session->selectedDateTime()) : -1;
199
2/2
✓ Branch 11 → 12 taken 759 times.
✓ Branch 11 → 13 taken 158 times.
917 if (caret == m_caret) return;
200 158 m_caret = caret;
201 158 emit caretChanged();
202 }
203
204 2265 QVariantList SearchTimelineModel::levels() const {
205
1/2
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 5 taken 2265 times.
2265 if (!m_session) return {};
206 2265 QVariantList result;
207
8/8
✓ Branch 5 → 6 taken 2265 times.
✓ Branch 6 → 7 taken 256 times.
✓ Branch 6 → 8 taken 2009 times.
✓ Branch 9 → 10 taken 2265 times.
✓ Branch 11 → 12 taken 2265 times.
✓ Branch 12 → 13 taken 2265 times.
✓ Branch 47 → 14 taken 2704 times.
✓ Branch 47 → 48 taken 2265 times.
4969 for (const Level &level : breadcrumb(m_constrained ? m_range : Range{}, m_locale, tr("All time")))
208
2/2
✓ Branch 22 → 23 taken 2704 times.
✓ Branch 31 → 32 taken 2704 times.
10816 result.append(QVariantMap{{QStringLiteral("label"), level.label}, {QStringLiteral("from"), iso(level.range.first)},
209
3/5
✓ Branch 27 → 28 taken 2704 times.
✓ Branch 35 → 36 taken 8112 times.
✓ Branch 35 → 37 taken 2704 times.
✗ Branch 62 → 63 not taken.
✗ Branch 62 → 64 not taken.
18489 {QStringLiteral("to"), iso(level.range.last)}});
210 2265 return result;
211 13081 }
212
213 743 QString SearchTimelineModel::rangeLabel() const {
214
2/2
✓ Branch 3 → 4 taken 530 times.
✓ Branch 3 → 5 taken 213 times.
743 return m_range.isValid() ? TimeHistogram::rangeLabel(m_range, m_locale) : QString();
215 }
216
217
3/4
✓ Branch 2 → 3 taken 46 times.
✓ Branch 2 → 6 taken 699 times.
✓ Branch 4 → 5 taken 46 times.
✗ Branch 4 → 6 not taken.
745 bool SearchTimelineModel::canStep() const { return m_constrained && m_range.isValid(); }
218
219 594 QVariantList SearchTimelineModel::ticks(qreal bucketWidth, qreal charWidth) const {
220 594 QVariantList result;
221 6677 const auto measure = [charWidth](const QString &text) { return text.size() * charWidth; };
222
5/5
✓ Branch 3 → 4 taken 594 times.
✓ Branch 5 → 6 taken 594 times.
✓ Branch 6 → 7 taken 594 times.
✓ Branch 31 → 8 taken 5321 times.
✓ Branch 31 → 32 taken 594 times.
5915 for (const Tick &tick : TimeHistogram::ticks(m_buckets, m_unit, m_locale, bucketWidth, measure))
223
3/5
✓ Branch 19 → 20 taken 5321 times.
✓ Branch 23 → 24 taken 10642 times.
✓ Branch 23 → 25 taken 5321 times.
✗ Branch 43 → 44 not taken.
✗ Branch 43 → 45 not taken.
27199 result.append(QVariantMap{{QStringLiteral("bucket"), tick.bucket}, {QStringLiteral("label"), tick.label}});
224 594 return result;
225 15963 }
226
227 39 void SearchTimelineModel::applyRange(const Range &range) {
228
1/2
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 5 taken 39 times.
39 if (!m_session) return;
229
7/7
✓ Branch 5 → 6 taken 39 times.
✓ Branch 9 → 10 taken 39 times.
✓ Branch 10 → 11 taken 39 times.
✓ Branch 11 → 12 taken 39 times.
✓ Branch 12 → 13 taken 39 times.
✓ Branch 16 → 7 taken 39 times.
✓ Branch 16 → 17 taken 39 times.
78 qCInfo(lcSearch) << "Search timeline: date range set, days" << range.days();
230
3/3
✓ Branch 18 → 19 taken 39 times.
✓ Branch 19 → 20 taken 39 times.
✓ Branch 20 → 21 taken 39 times.
39 m_session->setDateRange(iso(range.first), iso(range.last));
231 39 emit rangeApplied();
232 }
233
234 30 void SearchTimelineModel::drill(int index) {
235
5/6
✓ Branch 2 → 3 taken 30 times.
✗ Branch 2 → 5 not taken.
✓ Branch 4 → 5 taken 1 time.
✓ Branch 4 → 6 taken 29 times.
✓ Branch 7 → 8 taken 1 time.
✓ Branch 7 → 9 taken 29 times.
30 if (index < 0 || index >= m_buckets.size()) return;
236 29 const Bucket &bucket = m_buckets.at(index);
237
2/2
✓ Branch 10 → 11 taken 1 time.
✓ Branch 10 → 13 taken 28 times.
29 if (m_unit == Unit::Hour) { emit hourRequested(bucket.hour); return; }
238
2/6
✗ Branch 13 → 14 not taken.
✓ Branch 13 → 17 taken 28 times.
✗ Branch 15 → 16 not taken.
✗ Branch 15 → 17 not taken.
✗ Branch 18 → 19 not taken.
✓ Branch 18 → 20 taken 28 times.
28 if (m_constrained && bucket.range == m_range) return; // nothing finer to show
239 28 applyRange(bucket.range);
240 }
241
242 3 void SearchTimelineModel::brush(int first, int last) {
243
3/6
✓ Branch 2 → 3 taken 3 times.
✗ Branch 2 → 5 not taken.
✗ Branch 4 → 5 not taken.
✓ Branch 4 → 6 taken 3 times.
✗ Branch 7 → 8 not taken.
✓ Branch 7 → 9 taken 3 times.
3 if (m_unit == Unit::Hour || m_buckets.isEmpty()) return;
244
1/1
✓ Branch 9 → 10 taken 3 times.
3 const Range range = TimeHistogram::brush(m_buckets, first, last);
245
3/4
✓ Branch 10 → 11 taken 3 times.
✓ Branch 11 → 12 taken 3 times.
✗ Branch 11 → 13 not taken.
✓ Branch 12 → 13 taken 3 times.
3 if (range.isValid()) applyRange(range);
246 }
247
248 2 void SearchTimelineModel::zoomOut() {
249
1/2
✗ Branch 2 → 3 not taken.
✓ Branch 2 → 4 taken 2 times.
2 if (!m_constrained) return;
250
2/2
✓ Branch 4 → 5 taken 2 times.
✓ Branch 5 → 6 taken 2 times.
2 applyRange(parentRange(m_range));
251 }
252
253 3 void SearchTimelineModel::zoomToLevel(int level) {
254
1/1
✓ Branch 2 → 3 taken 3 times.
3 const QVariantList all = levels();
255
3/6
✓ Branch 3 → 4 taken 3 times.
✗ Branch 3 → 6 not taken.
✗ Branch 5 → 6 not taken.
✓ Branch 5 → 7 taken 3 times.
✗ Branch 8 → 9 not taken.
✓ Branch 8 → 10 taken 3 times.
3 if (level < 0 || level >= all.size() - 1) return; // the last level is the current one
256
1/1
✓ Branch 11 → 12 taken 3 times.
3 const QVariantMap target = all.at(level).toMap();
257
5/5
✓ Branch 13 → 14 taken 3 times.
✓ Branch 14 → 15 taken 3 times.
✓ Branch 15 → 16 taken 3 times.
✓ Branch 16 → 17 taken 3 times.
✓ Branch 22 → 23 taken 3 times.
9 applyRange(Range{QDate::fromString(target.value("from").toString(), Qt::ISODate),
258
4/4
✓ Branch 18 → 19 taken 3 times.
✓ Branch 19 → 20 taken 3 times.
✓ Branch 20 → 21 taken 3 times.
✓ Branch 21 → 22 taken 3 times.
6 QDate::fromString(target.value("to").toString(), Qt::ISODate)});
259 3 }
260
261 2 void SearchTimelineModel::step(int direction) {
262
3/6
✓ Branch 3 → 4 taken 2 times.
✗ Branch 3 → 5 not taken.
✗ Branch 4 → 5 not taken.
✓ Branch 4 → 6 taken 2 times.
✗ Branch 7 → 8 not taken.
✓ Branch 7 → 9 taken 2 times.
2 if (!canStep() || direction == 0) return;
263
3/4
✓ Branch 9 → 10 taken 2 times.
✗ Branch 9 → 11 not taken.
✓ Branch 12 → 13 taken 2 times.
✓ Branch 13 → 14 taken 2 times.
2 applyRange(steppedRange(m_range, direction > 0 ? 1 : -1, m_weekStart));
264 }
265
266 26 qint64 SearchTimelineModel::countForPeriod(const QString &label) const {
267 26 qint64 count = 0;
268
4/4
✓ Branch 2 → 3 taken 26 times.
✓ Branch 13 → 14 taken 11752 times.
✓ Branch 15 → 4 taken 11726 times.
✓ Branch 15 → 16 taken 26 times.
11752 for (auto it = m_dayCounts.cbegin(); it != m_dayCounts.cend(); ++it)
269
4/4
✓ Branch 5 → 6 taken 11726 times.
✓ Branch 6 → 7 taken 11726 times.
✓ Branch 9 → 10 taken 73 times.
✓ Branch 9 → 12 taken 11653 times.
11726 if (TimeHistogram::rangeLabel(unitRange(it.key(), m_unit, m_weekStart), m_locale) == label) count += it.value();
270 26 return count;
271 }
272
273 1 void SearchTimelineModel::drillPeriod(const QString &label) {
274
3/4
✓ Branch 2 → 3 taken 1 time.
✓ Branch 14 → 15 taken 451 times.
✓ Branch 16 → 4 taken 451 times.
✗ Branch 16 → 17 not taken.
451 for (auto it = m_dayCounts.cbegin(); it != m_dayCounts.cend(); ++it) {
275
1/1
✓ Branch 5 → 6 taken 451 times.
451 const Range range = unitRange(it.key(), m_unit, m_weekStart);
276
4/4
✓ Branch 6 → 7 taken 451 times.
✓ Branch 9 → 10 taken 1 time.
✓ Branch 9 → 12 taken 450 times.
✓ Branch 10 → 11 taken 1 time.
451 if (TimeHistogram::rangeLabel(range, m_locale) == label) { applyRange(range); return; }
277 }
278 }
279
280 36 QString SearchTimelineModel::tooltip(int index) const {
281
3/6
✓ Branch 2 → 3 taken 36 times.
✗ Branch 2 → 5 not taken.
✗ Branch 4 → 5 not taken.
✓ Branch 4 → 6 taken 36 times.
✗ Branch 7 → 8 not taken.
✓ Branch 7 → 9 taken 36 times.
36 if (index < 0 || index >= m_buckets.size()) return {};
282 36 const Bucket &bucket = m_buckets.at(index);
283 //: The read-out of a timeline bar (SD-14): "142 results · March 2025".
284
4/4
✓ Branch 10 → 11 taken 36 times.
✓ Branch 11 → 12 taken 36 times.
✓ Branch 12 → 13 taken 36 times.
✓ Branch 13 → 14 taken 36 times.
36 return tr("%1 · %2").arg(results(bucket.count, m_locale), bucketLabel(bucket, m_unit, m_locale));
285 }
286
287 7 Range SearchTimelineModel::steppedRange(const Range &range, int direction, Qt::DayOfWeek weekStart) {
288
1/2
✗ Branch 3 → 4 not taken.
✓ Branch 3 → 5 taken 7 times.
7 if (!range.isValid()) return range;
289
2/2
✓ Branch 6 → 7 taken 3 times.
✓ Branch 6 → 12 taken 4 times.
7 if (wholeYear(range)) {
290
1/1
✓ Branch 7 → 8 taken 3 times.
3 const QDate first = range.first.addYears(direction);
291
2/2
✓ Branch 8 → 9 taken 3 times.
✓ Branch 9 → 10 taken 3 times.
3 return {first, QDate(first.year(), 12, 31)};
292 }
293
2/2
✓ Branch 13 → 14 taken 2 times.
✓ Branch 13 → 19 taken 2 times.
4 if (wholeMonth(range)) {
294
1/1
✓ Branch 14 → 15 taken 2 times.
2 const QDate first = range.first.addMonths(direction);
295
2/2
✓ Branch 15 → 16 taken 2 times.
✓ Branch 16 → 17 taken 2 times.
4 return {first, first.addMonths(1).addDays(-1)};
296 }
297 Q_UNUSED(weekStart)
298 2 const qint64 days = range.days();
299 2 return {range.first.addDays(direction * days), range.last.addDays(direction * days)};
300 }
301