MailJD nbsp;·nbsp; Test Dashboard nbsp;·nbsp; Coverage
LCOV - code coverage report
Current view: top level - ui - MentionTextEdit.cpp (source / functions) Coverage Total Hit
Test: MailJD Coverage (Unit + E2E) Lines: 82.9 % 140 116
Test Date: 2026-06-21 21:10:19 Functions: 100.0 % 11 11
Legend: Lines:     hit not hit
Branches: + taken - not taken # not executed
Branches: 53.8 % 277 149

             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 : }
        

Generated by: LCOV version 2.0-1