presentation/QuickContactModel.h
| Line | Branch | Exec | Source |
|---|---|---|---|
| 1 | #pragma once | ||
| 2 | |||
| 3 | #include <QAbstractListModel> | ||
| 4 | #include <QList> | ||
| 5 | #include <QThreadPool> | ||
| 6 | #include <QVariantMap> | ||
| 7 | #include <functional> | ||
| 8 | #include <memory> | ||
| 9 | |||
| 10 | #include "data/ContactQueries.h" | ||
| 11 | |||
| 12 | class ContactStore; | ||
| 13 | |||
| 14 | // Keyed, paged contact list for the Quick frontend (Sprint 97 S97.1.7, | ||
| 15 | // S97.2.2; Q09 before). | ||
| 16 | // | ||
| 17 | // Rows are cards (one per person, D-1) or collected addresses, identified by | ||
| 18 | // key ("<bookId>/<uid>", "collected:<address>") — never by row index | ||
| 19 | // (CB-08). A worker with its own store connection loads keyset pages (100 | ||
| 20 | // rows) on demand: the list fetches the next page when it scrolls | ||
| 21 | // (canFetchMore/fetchMore, CD-07). Card changes arrive as keys | ||
| 22 | // (applyCardChanges): loaded rows update in place, move to their new sort | ||
| 23 | // position or disappear, without a model reset — scroll position and | ||
| 24 | // selection survive a background sync (G15). A reset remains for a view or | ||
| 25 | // filter change and for changes of more than half the loaded rows. | ||
| 26 | class QuickContactModel final : public QAbstractListModel { | ||
| 27 | 17 | Q_OBJECT | |
| 28 | Q_PROPERTY(bool loading READ loading NOTIFY loadingChanged) | ||
| 29 | Q_PROPERTY(int totalCount READ totalCount NOTIFY totalCountChanged) | ||
| 30 | Q_PROPERTY(bool hasMore READ hasMore NOTIFY totalCountChanged) | ||
| 31 | Q_PROPERTY(QString filter READ filter WRITE setFilter NOTIFY filterChanged) | ||
| 32 | // {kind: "all"|"frequent"|"collected"|"book"|"group", bookId, group} | ||
| 33 | Q_PROPERTY(QVariantMap view READ view WRITE setView NOTIFY viewChanged) | ||
| 34 | Q_PROPERTY(bool sortByFamilyName READ sortByFamilyName WRITE setSortByFamilyName NOTIFY viewChanged) | ||
| 35 | // Bounded recipient suggestions for the compose fields: {name, email, | ||
| 36 | // insert, label, book, collected, initials, avatarColor, key}. | ||
| 37 | Q_PROPERTY(QVariantList completions READ completions NOTIFY completionsChanged) | ||
| 38 | // F100-10: the list's optional columns in their order (ids of | ||
| 39 | // availableColumns()); the name column is always first. | ||
| 40 | Q_PROPERTY(QStringList columns READ columns WRITE setColumns NOTIFY columnsChanged) | ||
| 41 | // F100-13: width of the docked card/editor pane in logical pixels. | ||
| 42 | Q_PROPERTY(int paneWidth READ paneWidth WRITE setPaneWidth NOTIFY paneWidthChanged) | ||
| 43 | public: | ||
| 44 | enum Role { | ||
| 45 | CardKey = Qt::UserRole + 1, | ||
| 46 | DisplayName, | ||
| 47 | PrimaryAddress, | ||
| 48 | AddressCount, | ||
| 49 | Organization, | ||
| 50 | BookName, | ||
| 51 | BookReadOnly, | ||
| 52 | LocalBook, | ||
| 53 | Section, | ||
| 54 | Initials, | ||
| 55 | AvatarColor, | ||
| 56 | HasPhoto, | ||
| 57 | PhotoTag, | ||
| 58 | IsGroup, | ||
| 59 | SyncState, | ||
| 60 | Collected, | ||
| 61 | Writable, | ||
| 62 | BookId, | ||
| 63 | Uid, | ||
| 64 | Phone, | ||
| 65 | JobTitle, | ||
| 66 | Locality, | ||
| 67 | Birthday, | ||
| 68 | Groups, | ||
| 69 | }; | ||
| 70 | static constexpr int PageSize = 100; | ||
| 71 | |||
| 72 | explicit QuickContactModel(QString databasePath, QObject *parent = nullptr); | ||
| 73 | ~QuickContactModel() override; | ||
| 74 | QHash<int, QByteArray> roleNames() const override; | ||
| 75 | int rowCount(const QModelIndex &parent = {}) const override; | ||
| 76 | QVariant data(const QModelIndex &index, int role) const override; | ||
| 77 | bool canFetchMore(const QModelIndex &parent) const override; | ||
| 78 | void fetchMore(const QModelIndex &parent) override; | ||
| 79 | |||
| 80 | 1186 | bool loading() const { return m_loading; } | |
| 81 | 35 | int totalCount() const { return m_totalCount; } | |
| 82 | 54 | bool hasMore() const { return m_hasMore; } | |
| 83 | 332 | QString filter() const { return m_filter; } | |
| 84 | void setFilter(const QString &filter); | ||
| 85 | QVariantMap view() const; | ||
| 86 | void setView(const QVariantMap &view); | ||
| 87 | 16 | bool sortByFamilyName() const { return m_view.byFamilyName; } | |
| 88 | void setSortByFamilyName(bool byFamily); | ||
| 89 | 2355 | QString databasePath() const { return m_databasePath; } | |
| 90 | 4059 | QStringList columns() const { return m_columns; } | |
| 91 | void setColumns(const QStringList &columns); | ||
| 92 | 403 | int paneWidth() const { return m_paneWidth; } | |
| 93 | void setPaneWidth(int width); | ||
| 94 | // S106-354: the occasion of a "how we met" record by its event UID. | ||
| 95 | 797 | void setEventTitleResolver(std::function<QString(const QString &)> resolver) { m_eventTitle = std::move(resolver); } | |
| 96 | // Every optional column id in its default order: email, phone, | ||
| 97 | // organization, title, city, birthday, groups, book. | ||
| 98 | Q_INVOKABLE static QStringList availableColumns(); | ||
| 99 | |||
| 100 | // Restarts the projection (view/filter unchanged). | ||
| 101 | Q_INVOKABLE void reload(); | ||
| 102 | // Kept for callers of the pre-Sprint-97 API; fetchMore is automatic. | ||
| 103 |
1/1✓ Branch 3 → 4 taken 2 times.
|
2 | Q_INVOKABLE void loadMore() { fetchMore({}); } |
| 104 | // Row of a key in the loaded rows, -1 when not loaded. | ||
| 105 | Q_INVOKABLE int indexOfKey(const QString &key) const; | ||
| 106 | // Key of a loaded row ("" outside), for range selection over rows the | ||
| 107 | // view has not instantiated. | ||
| 108 | Q_INVOKABLE QString keyAt(int row) const; | ||
| 109 | // The full card of a key for the card pane and the editor | ||
| 110 | // (ContactEditorFields map plus book, sync state, writable); {} if gone. | ||
| 111 | Q_INVOKABLE QVariantMap card(const QString &key) const; | ||
| 112 | // Per-card updates after a write or a sync. | ||
| 113 | void applyCardChanges(const QStringList &keys); | ||
| 114 | Q_INVOKABLE void requestCompletions(const QString &fragment); | ||
| 115 | 645 | QVariantList completions() const { return m_completions; } | |
| 116 | |||
| 117 | signals: | ||
| 118 | void loadingChanged(); | ||
| 119 | void totalCountChanged(); | ||
| 120 | void filterChanged(); | ||
| 121 | void viewChanged(); | ||
| 122 | void completionsChanged(); | ||
| 123 | void columnsChanged(); | ||
| 124 | void paneWidthChanged(); | ||
| 125 | // A changed card moved to another row (the view keeps it selected). | ||
| 126 | void cardMoved(const QString &key, int row); | ||
| 127 | |||
| 128 | private: | ||
| 129 | void restartProjection(); | ||
| 130 | void loadPage(); | ||
| 131 | bool ensureReader() const; | ||
| 132 | int insertPosition(const ContactQueries::Row &row) const; | ||
| 133 | void refreshCount(); | ||
| 134 | QVariant rowData(const ContactQueries::Row &row, int role) const; | ||
| 135 | |||
| 136 | QString m_databasePath; | ||
| 137 | QString m_filter; | ||
| 138 | ContactQueries::View m_view; | ||
| 139 | QList<ContactQueries::Row> m_rows; | ||
| 140 | // GUI-thread read connection for per-card updates and card(); the paged | ||
| 141 | // reads use isolated worker connections. | ||
| 142 | mutable std::unique_ptr<ContactStore> m_reader; | ||
| 143 | std::function<QString(const QString &)> m_eventTitle; | ||
| 144 | QThreadPool m_pool; | ||
| 145 | // P90.13: latest-wins lane for recipient completion. | ||
| 146 | QThreadPool m_completionPool; | ||
| 147 | int m_totalCount = 0; | ||
| 148 | bool m_hasMore = false; | ||
| 149 | bool m_loading = false; | ||
| 150 | quint64 m_generation = 0; | ||
| 151 | QVariantList m_completions; | ||
| 152 | quint64 m_completionGeneration = 0; | ||
| 153 | QStringList m_columns; | ||
| 154 | int m_paneWidth = 440; | ||
| 155 | }; | ||
| 156 |