Branch data Line data Source code
1 : : #include "MentionTextEdit.h"
2 : :
3 : : #include <QKeyEvent>
4 : : #include <QListWidget>
5 : : #include <QLoggingCategory>
6 : : #include <QScrollBar>
7 : : #include <QTextCursor>
8 : :
9 : : #include "data/ContactStore.h"
10 : :
11 [ + + + - : 63 : Q_LOGGING_CATEGORY(lcMention, "mailjd.mention")
+ - - - ]
12 : :
13 : 79 : MentionTextEdit::MentionTextEdit(QWidget *parent) : QTextEdit(parent) {
14 [ + - + - : 79 : m_popup = new QListWidget(this);
- + - - ]
15 [ + - ]: 79 : m_popup->setWindowFlags(Qt::ToolTip | Qt::FramelessWindowHint);
16 [ + - ]: 79 : m_popup->setFocusPolicy(Qt::NoFocus);
17 [ + - ]: 79 : m_popup->setMaximumHeight(180);
18 [ + - ]: 79 : m_popup->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
19 : : // Styled via main.qss (QListWidget#completerPopup, 67.B3)
20 [ + - ]: 158 : m_popup->setObjectName(QStringLiteral("completerPopup"));
21 [ + - ]: 79 : m_popup->hide();
22 : :
23 : 79 : connect(m_popup, &QListWidget::itemActivated, this,
24 [ + - ]: 79 : &MentionTextEdit::acceptMention);
25 : :
26 : : // Detect '@' via textChanged – works with any input method
27 : 79 : connect(this, &QTextEdit::textChanged, this,
28 [ + - ]: 79 : &MentionTextEdit::onTextChanged);
29 : 79 : }
30 : :
31 : 152 : MentionTextEdit::~MentionTextEdit() {
32 : : // m_popup is child of this, auto-deleted
33 : 152 : }
34 : :
35 : 25 : void MentionTextEdit::setContactStore(ContactStore *store) {
36 : 25 : m_store = store;
37 [ + - + - : 50 : qCDebug(lcMention) << "setContactStore called, store="
+ - + + ]
38 [ + - + - ]: 25 : << (store ? "valid" : "null")
39 [ + - + - : 25 : << "isOpen=" << (store ? store->isOpen() : false);
+ - + - +
- ]
40 : 25 : }
41 : :
42 : : // keyPressEvent only handles popup navigation (Up/Down/Enter/Esc)
43 : 79 : void MentionTextEdit::keyPressEvent(QKeyEvent *event) {
44 [ + + + + : 79 : if (m_mentionActive && m_popup->isVisible()) {
+ + ]
45 [ - - - + : 8 : switch (event->key()) {
+ ]
46 : 0 : case Qt::Key_Down: {
47 : 0 : int row = m_popup->currentRow() + 1;
48 [ # # ]: 0 : if (row < m_popup->count())
49 : 0 : m_popup->setCurrentRow(row);
50 : 0 : return;
51 : : }
52 : 0 : case Qt::Key_Up: {
53 : 0 : int row = m_popup->currentRow() - 1;
54 [ # # ]: 0 : if (row >= 0)
55 : 0 : m_popup->setCurrentRow(row);
56 : 0 : return;
57 : : }
58 : 0 : case Qt::Key_Return:
59 : : case Qt::Key_Enter:
60 : : case Qt::Key_Tab:
61 [ # # ]: 0 : if (m_popup->currentItem()) {
62 : 0 : acceptMention(m_popup->currentItem());
63 : 0 : return;
64 : : }
65 : 0 : break;
66 : 2 : case Qt::Key_Escape:
67 : 2 : endMention();
68 : 2 : return;
69 : 6 : default:
70 : 6 : break;
71 : : }
72 : : }
73 : :
74 : 77 : QTextEdit::keyPressEvent(event);
75 : : }
76 : :
77 : 110 : void MentionTextEdit::onTextChanged() {
78 [ + - ]: 110 : QString text = toPlainText();
79 [ + - + - ]: 110 : int curPos = textCursor().position();
80 : :
81 : : // If mention is active, update or cancel
82 [ + + ]: 110 : if (m_mentionActive) {
83 : : // Cancel if cursor is before the '@' or text was deleted past it
84 [ + - - + : 13 : if (curPos <= m_mentionStart - 1 || m_mentionStart - 1 >= text.length()) {
- + ]
85 [ # # ]: 0 : endMention();
86 : 0 : return;
87 : : }
88 : : // Check the char at mentionStart-1 is still '@'
89 [ + - - + : 13 : if (m_mentionStart > 0 && text.at(m_mentionStart - 1) != QLatin1Char('@')) {
- + ]
90 [ # # ]: 0 : endMention();
91 : 0 : return;
92 : : }
93 [ + - ]: 13 : updateMentionPopup();
94 : 13 : return;
95 : : }
96 : :
97 : : // Not in mention mode – check if '@' was just typed
98 [ + + + + ]: 97 : if (!m_store || curPos <= 0)
99 : 61 : return;
100 : :
101 : : // Look at the character just before the cursor
102 : 36 : QChar ch = text.at(curPos - 1);
103 [ + + ]: 36 : if (ch == QLatin1Char('@')) {
104 : : // Only trigger if '@' is at start of text or preceded by whitespace
105 [ + + + - : 4 : if (curPos == 1 || text.at(curPos - 2).isSpace()) {
+ - ]
106 [ + - + - : 8 : qCDebug(lcMention) << "@ detected at position" << curPos;
+ - + - +
+ ]
107 [ + - ]: 4 : startMention(curPos);
108 : : }
109 : : }
110 : :
111 : 36 : m_lastLength = text.length();
112 [ + + ]: 110 : }
113 : :
114 : 4 : void MentionTextEdit::startMention(int atPos) {
115 : 4 : m_mentionActive = true;
116 : 4 : m_mentionStart = atPos; // position right after '@'
117 [ + - + - : 8 : qCDebug(lcMention) << "startMention at" << atPos;
+ - + - +
+ ]
118 : 4 : updateMentionPopup();
119 : 4 : }
120 : :
121 : 3 : void MentionTextEdit::endMention() {
122 : 3 : m_mentionActive = false;
123 : 3 : m_mentionStart = -1;
124 : 3 : m_popup->hide();
125 : 3 : }
126 : :
127 : 17 : void MentionTextEdit::updateMentionPopup() {
128 [ + - + - : 17 : if (!m_store || !m_mentionActive || m_mentionStart < 0) {
- + ]
129 [ # # ]: 0 : m_popup->hide();
130 : 8 : return;
131 : : }
132 : :
133 [ + - + - ]: 17 : int curPos = textCursor().position();
134 [ - + ]: 17 : if (curPos < m_mentionStart) {
135 [ # # ]: 0 : endMention();
136 : 0 : return;
137 : : }
138 : :
139 : : // Extract query text after '@'
140 [ + - ]: 17 : QString fullText = toPlainText();
141 [ + - ]: 17 : QString query = fullText.mid(m_mentionStart, curPos - m_mentionStart);
142 : :
143 [ + - + - : 34 : qCDebug(lcMention) << "updateMentionPopup query=" << query
+ - + - +
+ ]
144 [ + - + - ]: 17 : << "curPos=" << curPos
145 [ + - + - ]: 17 : << "mentionStart=" << m_mentionStart;
146 : :
147 [ + + ]: 17 : if (query.isEmpty()) {
148 : : // Show all recent contacts when just '@' is typed
149 [ + - ]: 4 : QList<Contact> results = m_store->search(QString(), 6);
150 : : // Even with empty query, show popup with recent contacts
151 : : // (search returns empty for empty query, so use allContacts fallback)
152 [ + - ]: 4 : if (results.isEmpty()) {
153 [ + - ]: 4 : m_popup->hide();
154 : 4 : return;
155 : : }
156 [ - + ]: 4 : }
157 : :
158 [ + - ]: 13 : QList<Contact> results = m_store->search(query, 6);
159 [ + - + - : 26 : qCDebug(lcMention) << "search returned" << results.size() << "results";
+ - + - +
- + + ]
160 : :
161 [ + + ]: 13 : if (results.isEmpty()) {
162 [ + - ]: 4 : m_popup->hide();
163 : 4 : return;
164 : : }
165 : :
166 [ + - ]: 9 : m_popup->clear();
167 [ + - + - : 18 : for (const auto &c : results) {
+ + ]
168 : 9 : QString display;
169 [ + - + - : 9 : if (!c.displayName.isEmpty() && c.displayName != c.email) {
+ - ]
170 [ + - ]: 9 : display = QStringLiteral("%1 <%2>").arg(c.displayName, c.email);
171 : : } else {
172 : 0 : display = c.email;
173 : : }
174 [ + - + - : 9 : auto *item = new QListWidgetItem(display, m_popup);
- + - - ]
175 [ + - ]: 9 : item->setData(Qt::UserRole, c.email);
176 [ + - ]: 9 : item->setData(Qt::UserRole + 1, c.displayName);
177 : 9 : }
178 : :
179 [ + - ]: 9 : m_popup->setCurrentRow(0);
180 [ + - ]: 9 : positionPopup();
181 [ + - ]: 9 : m_popup->show();
182 [ + + + + : 29 : }
+ + ]
183 : :
184 : 1 : void MentionTextEdit::acceptMention(QListWidgetItem *item) {
185 [ - + ]: 1 : if (!item)
186 : 0 : return;
187 : :
188 [ + - + - ]: 1 : QString email = item->data(Qt::UserRole).toString();
189 [ + - + - ]: 1 : QString displayName = item->data(Qt::UserRole + 1).toString();
190 : :
191 : : // Replace @query with @DisplayName
192 [ + - ]: 1 : QTextCursor cursor = textCursor();
193 [ + - ]: 1 : int currentPos = cursor.position();
194 : :
195 : : // Select from '@' (one char before m_mentionStart) to current position
196 [ + - ]: 1 : cursor.setPosition(m_mentionStart - 1); // position of '@'
197 [ + - ]: 1 : cursor.setPosition(currentPos, QTextCursor::KeepAnchor);
198 : :
199 : 2 : QString replacement = QStringLiteral("@%1 ").arg(
200 [ - + + - ]: 1 : displayName.isEmpty() ? email : displayName);
201 : :
202 : : // Block textChanged during replacement to avoid re-triggering
203 : 1 : blockSignals(true);
204 [ + - ]: 1 : cursor.insertText(replacement);
205 : 1 : blockSignals(false);
206 : :
207 [ + - ]: 1 : endMention();
208 : :
209 [ + - ]: 1 : emit mentionSelected(email, displayName);
210 : 1 : }
211 : :
212 : 9 : void MentionTextEdit::positionPopup() {
213 [ + - ]: 9 : QTextCursor cursor = textCursor();
214 [ + - ]: 9 : QRect cursorRect = this->cursorRect(cursor);
215 : :
216 : : // Position popup below cursor
217 [ + - ]: 9 : QPoint pos = mapToGlobal(cursorRect.bottomLeft());
218 [ + - ]: 9 : m_popup->setFixedWidth(280);
219 [ + - ]: 9 : m_popup->move(pos);
220 : 9 : }
|