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 §or) 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 |