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.4% 37 / 0 / 38
Functions: 96.7% 29 / 0 / 30
Branches: -% 0 / 0 / 0

presentation/QuickContactGraph.h
Line Branch Exec Source
1 #pragma once
2
3 #include <QHash>
4 #include <QObject>
5 #include <QPointF>
6 #include <QSizeF>
7 #include <QPair>
8 #include <QStringList>
9 #include <QThreadPool>
10 #include <QVariantList>
11 #include <functional>
12
13 #include "data/RelationGraph.h"
14 #include "data/RelationGraphLayout.h"
15 #include "data/RelationGraphRoutes.h"
16 #include "data/RelationGraphTerritories.h"
17
18 // The relationship graph for QML (Sprint 103 S103.5.1–S103.5.4, D-18,
19 // interaction.md §7; `backend.pim.contactGraph`).
20 //
21 // The input (people of the books in *All contacts* with GENDER,
22 // organisation and groups; resolved edges of RelationIndex) is read on
23 // demand and kept until reload(). The centre follows §7.2: chosen person,
24 // else the previous centre, else the me card. RelationGraph builds the ego
25 // graph (depth 1 or 2, sectors capped), RelationGraphLayout places it;
26 // nodes and edges come out in canvas pixels after pan and zoom.
27 class QuickContactGraph final : public QObject {
28 2439 Q_OBJECT
29 Q_PROPERTY(QString centerKey READ centerKey NOTIFY graphChanged)
30 // F103-12: the overview of everyone with a relationship (no centre), and
31 // whether there is a me card (or the card with an account address).
32 Q_PROPERTY(bool overview READ overview NOTIFY graphChanged)
33 Q_PROPERTY(bool hasMe READ hasMe NOTIFY graphChanged)
34 // [{index, key, name, initials, avatarColor, hasPhoto, x, y, center,
35 // inScope, me, sector, moreCount, degree, relation, role, family, labelWidth, kinship}] — `family`
36 // marks a family card (F103-13: not drawn, it anchors its area); `relation` is the
37 // sentence of the node's edge to the centre (S103.7.2, screen readers); `role`
38 // the short word under the name ("Uncle"), stored or following from others;
39 // `labelWidth` the room for the name beside the nearest neighbours (canvas px; 0: no room, name on hover only);
40 // `kinship` the degree of kinship to the centre (-1: none, or also related otherwise — never filtered).
41 Q_PROPERTY(QVariantList nodes READ nodes NOTIFY layoutChanged)
42 // The first projection after the graph was rebuilt (a new centre, the
43 // family tree, Everyone, new data): people already shown carry `fromX`/
44 // `fromY` (where they stood, canvas px) and glide to their new place, new
45 // ones carry `entering`; false again with the next pan or zoom.
46 Q_PROPERTY(bool transition READ transition NOTIFY layoutChanged)
47 // [{from, to, x1, y1, x2, y2, kind, category, label, sentence, storedOnKey, sourceIndex,
48 // arrowTo, shape, points, path, style, width}] — `shape`/`points` how the line runs
49 // (RelationGraphRoutes: "line", "curve", "elbow", "none", "bundle" — `bundle` lines share
50 // its trunk), `path` the same as a
51 // polyline for hit tests, `style` what the line means (descent, partner, expartner,
52 // sibling, kin, friend, acquaintance, hierarchy, work, other, via, suggestion, layer)
53 // and `width` its stroke; `arrowTo` the node an arrowhead points at (-1: none) — an
54 // arrowed `points`/`path` always ends at that node.
55 Q_PROPERTY(QVariantList edges READ edges NOTIFY layoutChanged)
56 // The texts on the lines, worked out with the projection: the number of
57 // lines on each bundle's trunk and the couples' wedding years above their
58 // rings — [{kind: "count"|"year", x, y, text, lines: [edge indices]}]
59 // (ContactGraphLineLabels).
60 Q_PROPERTY(QVariantList lineLabels READ lineLabels NOTIFY layoutChanged)
61 // F103-10: families as areas: [{key, name, members: [node index]}].
62 Q_PROPERTY(QVariantList areas READ areas NOTIFY layoutChanged)
63 // The family tree's generations, oldest first: [{generation, y, label}] —
64 // `y` the row's avatar centre in canvas pixels, `label` what the row is to
65 // the centre ("Parents", "Ana Silva's generation", "Children"). Empty
66 // outside the family tree.
67 Q_PROPERTY(QVariantList generationRows READ generationRows NOTIFY layoutChanged)
68 // The ego view's side columns named: [{x, y, label, category}] — the
69 // heading's centre in canvas pixels above the column's first person,
70 // `category` its colour (friends, other, work). Empty elsewhere.
71 Q_PROPERTY(QVariantList groupLabels READ groupLabels NOTIFY layoutChanged)
72 // Earlier centres, oldest first: [{key, name, index}] — `index` for backTo().
73 Q_PROPERTY(QVariantList breadcrumb READ breadcrumb NOTIFY graphChanged)
74 Q_PROPERTY(int depth READ depth WRITE setDepth NOTIFY graphChanged)
75 // The 5 people with the most relationships (empty state, §7.2).
76 Q_PROPERTY(QVariantList candidates READ candidates NOTIFY graphChanged)
77 // "How do I know …?" (S103.5.4): the highlighted path and its sentence.
78 Q_PROPERTY(QStringList pathKeys READ pathKeys NOTIFY pathChanged)
79 Q_PROPERTY(QString pathText READ pathText NOTIFY pathChanged)
80 // S103.5.6: the accessible "Relationships" list instead of the canvas (View ▾).
81 Q_PROPERTY(bool relationsList READ relationsList WRITE setRelationsList NOTIFY relationsListChanged)
82 // F103-26: the legend filters — categories ("family", "friends", "work",
83 // "other") switched off: their lines and the people reached only through
84 // them are greyed out, never removed (QSettings contacts/graphHidden).
85 Q_PROPERTY(QStringList hiddenCategories READ hiddenCategories WRITE setHiddenCategories NOTIFY hiddenCategoriesChanged)
86 // S103.5.8 (I-8): relationship suggestions, off by default (QSettings
87 // contacts/graphSuggestions): pairs often together in mails or
88 // appointments, computed on a worker from local data; dashed edges, and
89 // around the centre up to five suggested people in their own row.
90 // `suggestionsExplained`: the one-time note was shown (contacts/graphSuggestionsExplained).
91 Q_PROPERTY(bool suggestions READ suggestions WRITE setSuggestions NOTIFY suggestionsChanged)
92 // F103-32: the family tree around the centre — generations as rows,
93 // descent as arrows (QSettings contacts/graphFamilyTree).
94 Q_PROPERTY(bool familyTree READ familyTree WRITE setFamilyTree NOTIFY graphChanged)
95 Q_PROPERTY(bool suggestionsExplained READ suggestionsExplained WRITE setSuggestionsExplained NOTIFY suggestionsChanged)
96 Q_PROPERTY(int suggestionCount READ suggestionCount NOTIFY suggestionsChanged)
97 // Layout pixels → canvas pixels (zoom): far out, the role line under a name is left out.
98 Q_PROPERTY(double scale READ scale NOTIFY viewChanged)
99 // Sprint 105 S105.4.1 (D-6): node, edge, area and label coordinates are
100 // world coordinates — canvas pixels when the graph was last projected (a
101 // layout change: data, centre, scope, filter, size, fit). Zoom and pan only
102 // change the view transform: canvas = world · viewScale + (viewX, viewY).
103 Q_PROPERTY(double viewScale READ viewScale NOTIFY viewChanged)
104 Q_PROPERTY(double viewX READ viewX NOTIFY viewChanged)
105 Q_PROPERTY(double viewY READ viewY NOTIFY viewChanged)
106 // The zoom limits zoomAt() keeps to (the view scales a wheel burst within them before it re-projects).
107 Q_PROPERTY(double minScale READ minScale NOTIFY layoutChanged)
108 Q_PROPERTY(double maxScale READ maxScale CONSTANT)
109 // The legend (and filter) panel is open (QSettings contacts/graphLegendOpen; closed by default).
110 Q_PROPERTY(bool legendOpen READ legendOpen WRITE setLegendOpen NOTIFY legendOpenChanged)
111 // The family filter (2026-10-08): relatives beyond this degree of kinship
112 // (RelationKinship: parent 1, sibling 2, aunt 3, cousin 4; partners 0) are
113 // left out of the ego graph and the family tree — and the people only
114 // reached through them; 0 shows every relative (QSettings
115 // contacts/graphKinship). People also related to the centre otherwise stay.
116 Q_PROPERTY(int kinshipLimit READ kinshipLimit WRITE setKinshipLimit NOTIFY graphChanged)
117 // The degrees among the centre's relatives as the unfiltered graph shows
118 // them, nearest first: [{degree, count, people: ["Marta Silva (Sister)", …≤ 4]}];
119 // the filter's steps. Empty in the overview.
120 Q_PROPERTY(QVariantList kinshipLevels READ kinshipLevels NOTIFY graphChanged)
121 // The compact instance (F103-52): how many related people its gist leaves
122 // out (*Open in graph* says "+N").
123 Q_PROPERTY(int omittedPeople READ omittedPeople NOTIFY graphChanged)
124
125 public:
126 explicit QuickContactGraph(QString databasePath, QObject *parent = nullptr);
127
128 281 QString centerKey() const { return m_centerKey; }
129 4777 QVariantList nodes() const { return m_nodes; }
130 1210 QVariantList generationRows() const { return m_generationRows; }
131 1162 bool transition() const { return m_transition; }
132 187 QVariantList groupLabels() const { return m_groupLabels; }
133 7078 QVariantList edges() const { return m_edges; }
134 303 QVariantList lineLabels() const { return m_lineLabels; }
135 QVariantList areas() const;
136 QVariantList breadcrumb() const;
137 ✗ int depth() const { return m_depth; }
138 void setDepth(int depth);
139 QVariantList candidates() const;
140 16 QStringList pathKeys() const { return m_pathKeys; }
141 30 QString pathText() const { return m_pathText; }
142 37 bool relationsList() const { return m_relationsList; }
143 void setRelationsList(bool on);
144 11097 QStringList hiddenCategories() const { return m_hiddenCategories; }
145 void setHiddenCategories(const QStringList &categories);
146 // One click on a legend entry switches it off, the next on again.
147 Q_INVOKABLE void toggleCategory(const QString &category);
148 906 bool familyTree() const { return m_familyTree; }
149 void setFamilyTree(bool on);
150 61 bool suggestions() const { return m_suggestions; }
151 void setSuggestions(bool on);
152 bool suggestionsExplained() const;
153 void setSuggestionsExplained(bool shown);
154 27 int suggestionCount() const { return int(m_input.suggestions.size()); }
155 664 double scale() const { return m_scale; }
156 5901 double viewScale() const { return m_scale / m_projectedScale; }
157 double viewX() const;
158 double viewY() const;
159 // The drawn lines in world coordinates, typed (GraphLinesItem reads them
160 // instead of the QVariant `edges`).
161 struct DrawnEdge {
162 int from = -1;
163 int to = -1;
164 int arrowTo = -1;
165 int bundle = 0;
166 int year = 0;
167 double width = 1.5;
168 QString kind;
169 QString shape;
170 QString style;
171 QString category;
172 QList<QPointF> points; // the route (shape-specific)
173 QList<QPointF> path; // the route flattened
174 };
175 1684 const QList<DrawnEdge> &drawnEdges() const { return m_drawnEdges; }
176 // The territories of the last territories() call (world coordinates).
177 32 const QList<RelationGraphTerritories::Territory> &territoryShapes() const { return m_territories; }
178 5 double minScale() const { return m_minScale; }
179 static constexpr double MaxScale = 3.0;
180 5 double maxScale() const { return MaxScale; }
181 28 bool legendOpen() const { return m_legendOpen; }
182 void setLegendOpen(bool open);
183 65 int kinshipLimit() const { return m_kinshipLimit; }
184 void setKinshipLimit(int limit);
185 499 QVariantList kinshipLevels() const { return m_kinshipLevels; }
186 109 int omittedPeople() const { return m_omittedPeople; }
187 // The sources besides contacts.db (the mail cache path may change with the session).
188 void setSuggestionSources(std::function<QString()> mailCachePath, const QString &calendarPath);
189 // A suggestion edge's *Dismiss* (node indices); remembered in graph_dismissed.
190 Q_INVOKABLE bool dismissSuggestion(int from, int to);
191 // Settings › Privacy: dismissed pairs may be suggested again.
192 Q_INVOKABLE void forgetDismissedSuggestions();
193 // F103-40: a second instance (the card's small graph) never suggests and
194 // may zoom out further to fit a small area.
195 // F103-52: it shows the gist — the close family (up to the 2nd degree,
196 // never remembered) and the direct relationships, at most
197 // RelationGraphStar::MaxPeople on an ellipse around the centre — never the family tree.
198 796 void setCompact() {
199 796 m_compact = true;
200 796 m_familyTree = false;
201 796 m_kinshipLimit = CompactKinshipLimit;
202 796 m_suggestionsAllowed = false;
203 796 m_suggestions = false;
204 796 m_minScale = 0.12;
205 796 m_readableScale = 0.12; // the card's small graph always shows everyone
206 796 }
207 // Tests: the worker is done.
208 10 bool computingSuggestions() const { return m_suggestionRun > m_suggestionDone; }
209
210 // Opening the graph centres on me (F103-06); without a me card the
211 // selected person. Later a person becomes the centre explicitly (double
212 // click / Enter, `recenter`); the old centre goes to the breadcrumb.
213 Q_INVOKABLE void show(const QString &key, bool recenter);
214 Q_INVOKABLE void back();
215 // Back to the earlier centre at `index` of the history (the graph's
216 // *Earlier* menu); the centres after it are dropped, as with back().
217 Q_INVOKABLE void backTo(int index);
218 Q_INVOKABLE void showAll();
219 Q_INVOKABLE void showMe();
220 11447 bool overview() const { return m_overview; }
221 bool hasMe() const;
222 Q_INVOKABLE void reload();
223 Q_INVOKABLE void setSize(double width, double height);
224 Q_INVOKABLE void panBy(double dx, double dy);
225 Q_INVOKABLE void zoomAt(double steps, double x, double y);
226 // S105.4.4: the zoom set directly (within min/max), the canvas point (x, y)
227 // kept in place; the current view and a fit's target view
228 // ({scale, offsetX, offsetY}) and setting one, for animated fits.
229 Q_INVOKABLE void zoomTo(double scale, double x, double y);
230 Q_INVOKABLE QVariantMap view() const;
231 Q_INVOKABLE QVariantMap fitTarget(bool everything) const;
232 Q_INVOKABLE void setView(double scale, double offsetX, double offsetY);
233 // The hover card's lines: what the person is to the people they are joined
234 // to, from their own side ("Daughter of Ana Silva", "Introduced by Ben
235 // Weber"), the line to the centre first, at most five plus "+N more".
236 Q_INVOKABLE QStringList about(int index) const;
237 // Shows every person (explicit *Fit*); a new centre or a resize instead
238 // shows a readable size and leaves the rest to dragging and zooming.
239 Q_INVOKABLE void fit();
240 // Path from the me card (or `fromKey`) to `key`; "" pathText when none.
241 Q_INVOKABLE bool how(const QString &key, const QString &fromKey = {});
242 // F103-48: the families' territories (RelationGraphTerritories) for the
243 // boxes the canvas measured — one per node, {x, y, w, h} in world
244 // coordinates, or null when the node is not drawn — around all boxes
245 // (`reach` + `margin` beyond them): per area {loops: [[x0, y0, x1, y1, …],
246 // …]}. The last result stays for territoryAt, territoryLabel and
247 // GraphAreasItem (territoriesChanged).
248 Q_INVOKABLE QVariantList territories(const QVariantList &boxes, double reach, double margin = 0);
249 Q_INVOKABLE int territoryAt(double x, double y) const;
250 // Where the name of `area` (width × height) goes, {x, y, w, h}; empty without a territory.
251 Q_INVOKABLE QVariantMap territoryLabel(int area, double width, double height, const QVariantList &keepClear) const;
252 Q_INVOKABLE void clearPath();
253
254 // F103-06: the key of the card with an account address (used when no me card is set).
255 796 void setMeFallback(std::function<QString()> provider) { m_meFallback = std::move(provider); }
256
257 // Tests.
258 const RelationGraph::Graph &graph() const { return m_graph; }
259
260 signals:
261 void graphChanged();
262 void layoutChanged();
263 void pathChanged();
264 void relationsListChanged();
265 void hiddenCategoriesChanged();
266 void suggestionsChanged();
267 void legendOpenChanged();
268 void viewChanged();
269 void territoriesChanged();
270
271 private:
272 void computeSuggestions();
273 void fitTo(bool everything);
274 void fitValues(bool everything, double *scale, QPointF *offset) const;
275 void reveal(const QString &key);
276 static void insertLabelRoom(QVariantList *nodes, double spacing, double band, double narrowest);
277 QString generationLabel(int generation) const;
278 QString groupLabel(const QString &sector) const;
279 RelationGraph::Layers layers() const;
280 int arrowTarget(const RelationGraph::Edge &edge) const;
281 QString roleOf(int node) const;
282 void collectKinshipLevels(qint64 center);
283 QString suggestionSentence(const RelationGraph::Node &a, const RelationGraph::Node &b) const;
284 void ensureInput();
285 void rebuild();
286 void project();
287 QString meKey() const;
288 QString defaultCenter() const;
289 static constexpr const char *kOverviewKey = "*";
290 qint64 cardIdOf(const QString &key) const;
291
292 QString m_databasePath;
293 std::function<QString()> m_meFallback;
294 bool m_loaded = false;
295 RelationGraph::Input m_input;
296 QHash<qint64, int> m_personIndex; // card id → index in m_input.people
297 QHash<QString, qint64> m_cardIds; // key → card id
298 QHash<qint64, QVariantMap> m_avatars;
299 QString m_centerKey;
300 QStringList m_history; // earlier centres; kOverviewKey for the overview
301 bool m_overview = false;
302 QHash<QString, QPointF> m_overviewPositions;
303 int m_depth = 1;
304 RelationGraph::Graph m_graph;
305 QList<QPointF> m_positions;
306 QList<RelationGraphLayout::Route> m_routes;
307 QList<RelationGraphTerritories::Territory> m_territories; // the last territories() result
308 QList<QRectF> m_territoryBoxes; // the boxes it was built from
309 QSizeF m_size{800, 600};
310 QPointF m_offset;
311 double m_scale = 1.0;
312 // The view when the graph was last projected (world = canvas then).
313 double m_projectedScale = 1.0;
314 QPointF m_projectedOffset;
315 QList<DrawnEdge> m_drawnEdges;
316 QRectF m_territoryRegion;
317 bool m_legendOpen = false;
318 static constexpr int CompactKinshipLimit = 2;
319 bool m_compact = false;
320 int m_kinshipLimit = 0;
321 QHash<qint64, int> m_kinship; // card id → degree of kinship to the centre (only filterable relatives)
322 QVariantList m_kinshipLevels;
323 int m_omittedPeople = 0;
324 QVariantList m_nodes;
325 QVariantList m_generationRows;
326 QHash<QString, QPointF> m_transitionFrom; // key → canvas position before the rebuild
327 bool m_transitionPending = false;
328 bool m_transition = false;
329 QVariantList m_groupLabels;
330 QList<RelationGraphLayout::Group> m_groups;
331 QVariantList m_edges;
332 QVariantList m_lineLabels;
333 QStringList m_pathKeys;
334 QString m_pathText;
335 bool m_relationsList = false;
336 QStringList m_hiddenCategories;
337 bool m_suggestions = false;
338 bool m_familyTree = false;
339 bool m_suggestionsAllowed = true;
340 double m_minScale = 0.25; // the furthest zoom out (and the explicit fit)
341 double m_readableScale = 0.75; // the automatic fit never goes below: names stay whole
342 std::function<QString()> m_mailCachePath;
343 QString m_calendarPath;
344 QHash<QPair<qint64, qint64>, QPair<int, int>> m_suggestionCounts; // pair → mails, appointments
345 int m_suggestionRun = 0; // generation started
346 int m_suggestionDone = 0; // generation answered
347 QThreadPool m_pool;
348 public:
349 ~QuickContactGraph() override;
350 };
351