Branch data Line data Source code
1 : : #include "ContactCompleter.h"
2 : :
3 : : #include <QEvent>
4 : : #include <QKeyEvent>
5 : : #include <QLineEdit>
6 : : #include <QListWidget>
7 : : #include <QApplication>
8 : :
9 : : #include "data/ContactStore.h"
10 : :
11 : 69 : ContactCompleter::ContactCompleter(ContactStore *store, QLineEdit *target,
12 : 69 : QObject *parent)
13 : 69 : : QObject(parent), m_store(store), m_target(target) {
14 [ + - + - : 69 : m_popup = new QListWidget();
- + - - ]
15 [ + - ]: 69 : m_popup->setWindowFlags(Qt::ToolTip | Qt::FramelessWindowHint);
16 [ + - ]: 69 : m_popup->setFocusPolicy(Qt::NoFocus);
17 [ + - ]: 69 : m_popup->setMaximumHeight(220);
18 [ + - ]: 69 : m_popup->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
19 : : // Styled via main.qss (QListWidget#completerPopup, 67.B3)
20 [ + - ]: 138 : m_popup->setObjectName(QStringLiteral("completerPopup"));
21 [ + - ]: 69 : m_popup->hide();
22 : :
23 : 69 : connect(m_target, &QLineEdit::textChanged, this,
24 [ + - ]: 69 : &ContactCompleter::onTextChanged);
25 : 69 : connect(m_popup, &QListWidget::itemActivated, this,
26 [ + - ]: 69 : &ContactCompleter::onItemActivated);
27 : :
28 [ + - ]: 69 : m_target->installEventFilter(this);
29 : 69 : }
30 : :
31 : 132 : ContactCompleter::~ContactCompleter() {
32 [ + - ]: 69 : delete m_popup;
33 : 132 : }
34 : :
35 : 1018 : bool ContactCompleter::eventFilter(QObject *obj, QEvent *event) {
36 [ + - + + : 1018 : if (obj != m_target || !m_popup->isVisible())
+ + ]
37 : 942 : return QObject::eventFilter(obj, event);
38 : :
39 [ + + ]: 76 : if (event->type() == QEvent::KeyPress) {
40 : 9 : auto *ke = static_cast<QKeyEvent *>(event);
41 [ + + + + : 9 : switch (ke->key()) {
+ ]
42 : 2 : case Qt::Key_Down:
43 : 2 : navigatePopup(1);
44 : 2 : return true;
45 : 1 : case Qt::Key_Up:
46 : 1 : navigatePopup(-1);
47 : 1 : return true;
48 : 1 : case Qt::Key_Return:
49 : : case Qt::Key_Enter:
50 : : case Qt::Key_Tab:
51 [ + - ]: 1 : if (m_popup->currentItem()) {
52 : 1 : onItemActivated(m_popup->currentItem());
53 : 1 : return true;
54 : : }
55 : 0 : break;
56 : 1 : case Qt::Key_Escape:
57 : 1 : hidePopup();
58 : 1 : return true;
59 : 4 : default:
60 : 4 : break;
61 : : }
62 : : }
63 : :
64 : 71 : return QObject::eventFilter(obj, event);
65 : : }
66 : :
67 : 30 : QString ContactCompleter::currentToken() const {
68 [ + - ]: 30 : QString text = m_target->text();
69 [ + - ]: 30 : int cursorPos = m_target->cursorPosition();
70 : :
71 : : // Find the start of the current token (after last comma/semicolon)
72 : 30 : int start = 0;
73 [ + + ]: 202 : for (int i = cursorPos - 1; i >= 0; --i) {
74 [ + - + + : 179 : if (text[i] == ',' || text[i] == ';') {
+ - - + +
+ ]
75 : 7 : start = i + 1;
76 : 7 : break;
77 : : }
78 : : }
79 : :
80 : : // Skip leading whitespace
81 [ + + + - : 36 : while (start < cursorPos && text[start].isSpace())
+ + + + ]
82 : 6 : ++start;
83 : :
84 : 30 : m_tokenStart = start;
85 [ + - + - ]: 60 : return text.mid(start, cursorPos - start).trimmed();
86 : 30 : }
87 : :
88 : 28 : void ContactCompleter::onTextChanged(const QString &) {
89 [ + - ]: 28 : QString token = currentToken();
90 [ + + ]: 28 : if (token.length() < 2) {
91 [ + - ]: 10 : hidePopup();
92 : 10 : return;
93 : : }
94 [ + - ]: 18 : updateSuggestions(token);
95 [ + + ]: 28 : }
96 : :
97 : 18 : void ContactCompleter::updateSuggestions(const QString &query) {
98 [ + - + - : 18 : if (!m_store || !m_store->isOpen()) {
- + - + ]
99 [ # # ]: 0 : hidePopup();
100 : 8 : return;
101 : : }
102 : :
103 [ + - ]: 18 : QList<Contact> results = m_store->search(query, 8);
104 [ + + ]: 18 : if (results.isEmpty()) {
105 [ + - ]: 8 : hidePopup();
106 : 8 : return;
107 : : }
108 : :
109 [ + - ]: 10 : m_popup->clear();
110 [ + - + - : 23 : for (const auto &c : results) {
+ + ]
111 : 13 : QString display;
112 [ + - + - : 13 : if (!c.displayName.isEmpty() && c.displayName != c.email) {
+ - ]
113 [ + - ]: 13 : display = QStringLiteral("%1 <%2>").arg(c.displayName, c.email);
114 : : } else {
115 : 0 : display = c.email;
116 : : }
117 [ + - + - : 13 : auto *item = new QListWidgetItem(display, m_popup);
- + - - ]
118 [ + - ]: 13 : item->setData(Qt::UserRole, display);
119 : 13 : }
120 : :
121 [ + - ]: 10 : showPopup();
122 [ + + ]: 18 : }
123 : :
124 : 10 : void ContactCompleter::showPopup() {
125 [ + - - + ]: 10 : if (m_popup->count() == 0) {
126 [ # # ]: 0 : hidePopup();
127 : 0 : return;
128 : : }
129 : :
130 : : // Position below the line edit
131 [ + - ]: 10 : QPoint pos = m_target->mapToGlobal(
132 : 10 : QPoint(0, m_target->height()));
133 [ + - ]: 10 : m_popup->setFixedWidth(m_target->width());
134 [ + - ]: 10 : m_popup->move(pos);
135 [ + - ]: 10 : m_popup->setCurrentRow(0);
136 [ + - ]: 10 : m_popup->show();
137 : : }
138 : :
139 : 21 : void ContactCompleter::hidePopup() {
140 : 21 : m_popup->hide();
141 : 21 : }
142 : :
143 : 2 : void ContactCompleter::onItemActivated(QListWidgetItem *item) {
144 [ - + ]: 2 : if (!item)
145 : 0 : return;
146 [ + - + - : 2 : acceptSuggestion(item->data(Qt::UserRole).toString());
+ - ]
147 : : }
148 : :
149 : 2 : void ContactCompleter::acceptSuggestion(const QString &formatted) {
150 [ + - ]: 2 : QString text = m_target->text();
151 [ + - ]: 2 : int cursorPos = m_target->cursorPosition();
152 : :
153 : : // Find current token boundaries
154 [ + - ]: 2 : currentToken(); // updates m_tokenStart
155 : :
156 : : // Replace from token start to cursor with the formatted contact
157 [ + - ]: 2 : QString before = text.left(m_tokenStart);
158 [ + - ]: 2 : QString after = text.mid(cursorPos);
159 : :
160 : : // Add comma+space after the inserted address if there's more text
161 : 2 : QString separator;
162 [ + - + - ]: 2 : if (after.trimmed().isEmpty()) {
163 : 2 : separator = QStringLiteral(", ");
164 : : }
165 : :
166 [ + - + - : 2 : QString newText = before + formatted + separator + after.trimmed();
+ - + - ]
167 [ + - ]: 2 : m_target->setText(newText);
168 [ + - ]: 4 : m_target->setCursorPosition(before.length() + formatted.length() +
169 : 2 : separator.length());
170 : :
171 [ + - ]: 2 : hidePopup();
172 : 2 : }
173 : :
174 : 3 : void ContactCompleter::navigatePopup(int delta) {
175 [ + - ]: 3 : int row = m_popup->currentRow() + delta;
176 [ + - + - ]: 3 : row = qBound(0, row, m_popup->count() - 1);
177 [ + - ]: 3 : m_popup->setCurrentRow(row);
178 : 3 : }
|