MailJD nbsp;·nbsp; Test Dashboard nbsp;·nbsp; Coverage
LCOV - code coverage report
Current view: top level - ui - SearchPanel.cpp (source / functions) Coverage Total Hit
Test: MailJD Coverage (Unit + E2E) Lines: 92.8 % 166 154
Test Date: 2026-06-21 21:10:19 Functions: 100.0 % 21 21
Legend: Lines:     hit not hit
Branches: + taken - not taken # not executed
Branches: 48.2 % 334 161

             Branch data     Line data    Source code
       1                 :             : #include "SearchPanel.h"
       2                 :             : #include "FilterChipWidget.h"
       3                 :             : #include "FilterPopoverWidget.h"
       4                 :             : #include "FlowLayout.h"
       5                 :             : #include "app/SearchQuery.h"
       6                 :             : 
       7                 :             : #include <QLineEdit>
       8                 :             : #include <QToolButton>
       9                 :             : #include <QVBoxLayout>
      10                 :             : #include <QHBoxLayout>
      11                 :             : #include <QEvent>
      12                 :             : #include <QSignalBlocker>
      13                 :             : #include <QMouseEvent>
      14                 :             : #include <QStyle>
      15                 :             : 
      16                 :             : using Tri = MailCache::SearchFilter::Tri;
      17                 :             : 
      18         [ +  - ]:          66 : SearchPanel::SearchPanel(QWidget *parent) : QWidget(parent) {
      19         [ +  - ]:          66 :   setupUi();
      20                 :          66 : }
      21                 :             : 
      22                 :          66 : void SearchPanel::setupUi() {
      23   [ +  -  -  +  :          66 :   auto *outerLayout = new QVBoxLayout(this);
                   -  - ]
      24                 :          66 :   outerLayout->setContentsMargins(6, 6, 6, 6);
      25                 :          66 :   outerLayout->setSpacing(0);
      26                 :             : 
      27                 :             :   // --- Fake Search Bar Container ---
      28   [ +  -  -  +  :          66 :   m_searchBarContainer = new QWidget(this);
                   -  - ]
      29         [ +  - ]:         132 :   m_searchBarContainer->setObjectName("searchBarContainer");
      30   [ +  -  +  - ]:          66 :   m_searchBarContainer->setCursor(Qt::IBeamCursor);
      31                 :             :   // Styled to look like a line edit in AppStyle.cpp
      32                 :             :   
      33   [ +  -  -  +  :          66 :   m_flowLayout = new FlowLayout(m_searchBarContainer, 4, 4, 4);
                   -  - ]
      34                 :             : 
      35   [ +  -  -  +  :          66 :   m_freeText = new QLineEdit(m_searchBarContainer);
                   -  - ]
      36                 :          66 :   m_freeText->setFrame(false);
      37                 :          66 :   m_freeText->setAttribute(Qt::WA_MacShowFocusRect, false);
      38         [ +  - ]:         132 :   m_freeText->setObjectName("searchPanelFreeText");
      39   [ +  -  +  - ]:          66 :   m_freeText->setPlaceholderText(tr("Search…"));
      40                 :             :   // Let the QLineEdit expand if there is room
      41                 :          66 :   m_freeText->setSizePolicy(QSizePolicy::MinimumExpanding, QSizePolicy::Fixed);
      42                 :          66 :   m_freeText->setMinimumWidth(100);
      43                 :             : 
      44                 :          66 :   m_flowLayout->addWidget(m_freeText);
      45                 :             : 
      46                 :             :   // Popover button
      47   [ +  -  -  +  :          66 :   m_popoverButton = new QToolButton(m_searchBarContainer);
                   -  - ]
      48         [ +  - ]:         132 :   m_popoverButton->setObjectName("searchPanelPopoverButton");
      49   [ +  -  +  - ]:          66 :   m_popoverButton->setCursor(Qt::PointingHandCursor);
      50   [ +  -  +  - ]:          66 :   m_popoverButton->setToolTip(tr("Advanced Filters"));
      51   [ +  -  +  - ]:          66 :   m_popoverButton->setText("▼"); // or an icon
      52                 :          66 :   m_flowLayout->addWidget(m_popoverButton);
      53                 :             : 
      54         [ +  - ]:          66 :   outerLayout->addWidget(m_searchBarContainer);
      55                 :             : 
      56   [ +  -  -  +  :          66 :   m_popover = new FilterPopoverWidget(this);
                   -  - ]
      57                 :             :   
      58         [ +  - ]:          66 :   connect(m_popoverButton, &QToolButton::clicked, this, [this]() {
      59         [ +  - ]:           1 :     m_popover->adjustSize();
      60         [ +  - ]:           1 :     QPoint pos = m_searchBarContainer->mapToGlobal(QPoint(m_searchBarContainer->width() - m_popover->width(), m_searchBarContainer->height()));
      61         [ +  - ]:           1 :     m_popover->move(pos);
      62         [ +  - ]:           1 :     m_popover->show();
      63                 :           1 :   });
      64                 :             : 
      65         [ +  - ]:          66 :   connect(m_freeText, &QLineEdit::returnPressed, this, &SearchPanel::searchRequested);
      66         [ +  - ]:          66 :   connect(m_freeText, &QLineEdit::textChanged, this, &SearchPanel::onQueryFieldEdited);
      67         [ +  - ]:          66 :   connect(m_popover, &FilterPopoverWidget::filterEdited, this, &SearchPanel::onPopoverFilterEdited);
      68                 :          66 : }
      69                 :             : 
      70                 :           1 : void SearchPanel::mousePressEvent(QMouseEvent *event) {
      71   [ +  -  +  - ]:           1 :   if (m_searchBarContainer->geometry().contains(event->pos())) {
      72                 :           1 :     m_freeText->setFocus();
      73                 :             :   }
      74                 :           1 :   QWidget::mousePressEvent(event);
      75                 :           1 : }
      76                 :             : 
      77                 :        1561 : void SearchPanel::changeEvent(QEvent *event) {
      78         [ +  + ]:        1561 :   if (event->type() == QEvent::LanguageChange)
      79                 :          86 :     retranslateUi();
      80                 :        1561 :   QWidget::changeEvent(event);
      81                 :        1561 : }
      82                 :             : 
      83                 :          86 : void SearchPanel::retranslateUi() {
      84   [ +  -  +  - ]:          86 :   m_freeText->setPlaceholderText(tr("Search…"));
      85   [ +  -  +  - ]:          86 :   m_popoverButton->setToolTip(tr("Advanced Filters"));
      86                 :          86 : }
      87                 :             : 
      88                 :          53 : QString SearchPanel::freeText() const {
      89                 :          53 :   MailCache::SearchFilter ignored;
      90   [ +  -  +  - ]:         106 :   return parseSearchQuery(m_freeText->text(), ignored);
      91                 :          53 : }
      92                 :             : 
      93                 :          99 : MailCache::SearchFilter SearchPanel::filter() const {
      94                 :          99 :   return m_popover->filter();
      95                 :             : }
      96                 :             : 
      97                 :          92 : void SearchPanel::setState(const QString &freeText, const MailCache::SearchFilter &f) {
      98                 :             :   {
      99                 :          92 :     const QSignalBlocker block(m_freeText);
     100         [ +  - ]:          92 :     QString newText = buildSearchQuery(freeText, f);
     101   [ +  -  +  + ]:          92 :     if (m_freeText->text() != newText) {
     102         [ +  - ]:          45 :       int cursorPos = m_freeText->cursorPosition();
     103         [ +  - ]:          45 :       m_freeText->setText(newText);
     104         [ +  - ]:          45 :       m_freeText->setCursorPosition(cursorPos);
     105                 :             :     }
     106                 :          92 :   }
     107                 :          92 :   applyFilterToControls(f);
     108                 :          92 : }
     109                 :             : 
     110                 :          96 : void SearchPanel::applyFilterToControls(const MailCache::SearchFilter &f) {
     111                 :          96 :   const QSignalBlocker block(m_popover);
     112         [ +  - ]:          96 :   m_popover->setFilter(f);
     113         [ +  - ]:          96 :   syncChipsFromFilter(f);
     114                 :          96 : }
     115                 :             : 
     116                 :         132 : void SearchPanel::clearChips() {
     117   [ +  -  +  -  :         148 :   for (auto *chip : m_chips) {
                   +  + ]
     118         [ +  - ]:          16 :     m_flowLayout->removeWidget(chip);
     119         [ +  - ]:          16 :     chip->deleteLater();
     120                 :             :   }
     121                 :         132 :   m_chips.clear();
     122                 :         132 : }
     123                 :             : 
     124                 :         132 : void SearchPanel::syncChipsFromFilter(const MailCache::SearchFilter &f) {
     125         [ +  - ]:         132 :   clearChips();
     126                 :             : 
     127                 :         413 :   auto addChip = [this](const QString &field, const QString &val) {
     128         [ +  + ]:         413 :     if (val.isEmpty()) return;
     129   [ +  -  -  +  :          31 :     auto *chip = new FilterChipWidget(field, val, m_searchBarContainer);
                   -  - ]
     130         [ +  - ]:          31 :     connect(chip, &FilterChipWidget::removeRequested, this, [this, field]() {
     131                 :           3 :       onChipRemoveRequested(field);
     132                 :           3 :     });
     133                 :             :     // Insert before the text edit
     134                 :          31 :     m_flowLayout->takeAt(m_flowLayout->count() - 2); // remove popover
     135                 :          31 :     m_flowLayout->takeAt(m_flowLayout->count() - 1); // remove text edit
     136                 :             :     
     137                 :          31 :     m_flowLayout->addWidget(chip);
     138                 :          31 :     m_flowLayout->addWidget(m_freeText);
     139                 :          31 :     m_flowLayout->addWidget(m_popoverButton);
     140                 :          31 :     m_chips.append(chip);
     141                 :         132 :   };
     142                 :             : 
     143   [ +  -  +  - ]:         132 :   addChip("Subject", f.subjectFilter);
     144   [ +  -  +  - ]:         132 :   addChip("From", f.fromFilter);
     145   [ +  -  +  - ]:         132 :   addChip("To", f.toFilter);
     146                 :             :   
     147         [ +  + ]:         132 :   if (!f.folderPatterns.isEmpty()) {
     148   [ +  -  +  -  :           4 :     addChip("Folder", f.folderPatterns.join(", "));
             +  -  +  - ]
     149                 :             :   }
     150         [ +  + ]:         132 :   if (!f.tags.isEmpty()) {
     151   [ +  -  +  -  :           1 :     addChip("Tags", f.tags.join(", "));
             +  -  +  - ]
     152                 :             :   }
     153                 :             : 
     154   [ +  +  +  -  :         132 :   if (f.unread != Tri::Any) addChip("Unread", f.unread == Tri::Yes ? "Yes" : "No");
          +  -  +  -  +  
                      - ]
     155   [ +  +  -  +  :         132 :   if (f.flagged != Tri::Any) addChip("Flagged", f.flagged == Tri::Yes ? "Yes" : "No");
          +  -  +  -  +  
                      - ]
     156   [ +  +  +  -  :         132 :   if (f.answered != Tri::Any) addChip("Answered", f.answered == Tri::Yes ? "Yes" : "No");
          +  -  +  -  +  
                      - ]
     157   [ +  +  +  +  :         132 :   if (f.hasAttachment != Tri::Any) addChip("Attachment", f.hasAttachment == Tri::Yes ? "Yes" : "No");
          +  -  +  -  +  
                      - ]
     158                 :             : 
     159   [ +  -  +  +  :         132 :   if (f.dateFrom.isValid() || f.dateTo.isValid()) {
          +  -  -  +  +  
                      + ]
     160                 :           1 :     QString dateStr;
     161   [ +  -  +  -  :           1 :     if (f.dateFrom.isValid() && f.dateTo.isValid()) {
          +  -  +  -  +  
                      - ]
     162   [ +  -  +  -  :           1 :       dateStr = f.dateFrom.date().toString(Qt::ISODate) + " - " + f.dateTo.date().toString(Qt::ISODate);
          +  -  +  -  +  
                -  +  - ]
     163   [ #  #  #  # ]:           0 :     } else if (f.dateFrom.isValid()) {
     164   [ #  #  #  #  :           0 :       dateStr = "> " + f.dateFrom.date().toString(Qt::ISODate);
                   #  # ]
     165                 :             :     } else {
     166   [ #  #  #  #  :           0 :       dateStr = "< " + f.dateTo.date().toString(Qt::ISODate);
                   #  # ]
     167                 :             :     }
     168   [ +  -  +  - ]:           1 :     addChip("Date", dateStr);
     169                 :           1 :   }
     170                 :         132 : }
     171                 :             : 
     172                 :           3 : void SearchPanel::removeFilterField(MailCache::SearchFilter &f, const QString &field) {
     173         [ +  + ]:           3 :   if (field == "Subject") f.subjectFilter.clear();
     174         [ +  + ]:           2 :   else if (field == "From") f.fromFilter.clear();
     175         [ +  - ]:           1 :   else if (field == "To") f.toFilter.clear();
     176         [ #  # ]:           0 :   else if (field == "Folder") f.folderPatterns.clear();
     177         [ #  # ]:           0 :   else if (field == "Tags") f.tags.clear();
     178         [ #  # ]:           0 :   else if (field == "Unread") f.unread = Tri::Any;
     179         [ #  # ]:           0 :   else if (field == "Flagged") f.flagged = Tri::Any;
     180         [ #  # ]:           0 :   else if (field == "Answered") f.answered = Tri::Any;
     181         [ #  # ]:           0 :   else if (field == "Attachment") f.hasAttachment = Tri::Any;
     182         [ #  # ]:           0 :   else if (field == "Date") {
     183                 :           0 :     f.dateFrom = QDateTime();
     184                 :           0 :     f.dateTo = QDateTime();
     185                 :             :   }
     186                 :           3 : }
     187                 :             : 
     188                 :           3 : void SearchPanel::onChipRemoveRequested(const QString &field) {
     189         [ +  - ]:           3 :   MailCache::SearchFilter f = filter();
     190         [ +  - ]:           3 :   removeFilterField(f, field);
     191                 :             :   
     192                 :             :   // Re-sync UI
     193         [ +  - ]:           3 :   applyFilterToControls(f);
     194         [ +  - ]:           3 :   rebuildQueryField();
     195                 :             :   
     196         [ +  - ]:           3 :   emit filterEdited();
     197         [ +  - ]:           3 :   emit searchRequested();
     198                 :           3 : }
     199                 :             : 
     200                 :          39 : void SearchPanel::rebuildQueryField() {
     201                 :          39 :   MailCache::SearchFilter ignored;
     202   [ +  -  +  - ]:          39 :   const QString freeRemainder = parseSearchQuery(m_freeText->text(), ignored);
     203                 :          39 :   const QSignalBlocker block(m_freeText);
     204   [ +  -  +  - ]:          39 :   QString newText = buildSearchQuery(freeRemainder, filter());
     205   [ +  -  +  + ]:          39 :   if (m_freeText->text() != newText) {
     206         [ +  - ]:           5 :     int cursorPos = m_freeText->cursorPosition();
     207         [ +  - ]:           5 :     m_freeText->setText(newText);
     208         [ +  - ]:           5 :     m_freeText->setCursorPosition(cursorPos);
     209                 :             :   }
     210                 :          39 : }
     211                 :             : 
     212                 :           1 : void SearchPanel::onQueryFieldEdited() {
     213         [ +  - ]:           1 :   int cursorPos = m_freeText->cursorPosition();
     214                 :           1 :   MailCache::SearchFilter f;
     215   [ +  -  +  - ]:           1 :   parseSearchQuery(m_freeText->text(), f);
     216         [ +  - ]:           1 :   applyFilterToControls(f);
     217         [ +  - ]:           1 :   m_freeText->setCursorPosition(cursorPos);
     218         [ +  - ]:           1 :   emit filterEdited();
     219                 :           1 : }
     220                 :             : 
     221                 :          36 : void SearchPanel::onPopoverFilterEdited() {
     222         [ +  - ]:          36 :   MailCache::SearchFilter f = m_popover->filter();
     223         [ +  - ]:          36 :   syncChipsFromFilter(f);
     224         [ +  - ]:          36 :   rebuildQueryField();
     225         [ +  - ]:          36 :   emit filterEdited();
     226                 :          36 : }
     227                 :             : 
     228                 :          36 : void SearchPanel::setKnownFolders(const QStringList &paths) {
     229                 :          36 :   m_popover->setKnownFolders(paths);
     230                 :          36 : }
     231                 :             : 
     232                 :          36 : void SearchPanel::setKnownTags(const QStringList &tags) {
     233                 :          36 :   m_popover->setKnownTags(tags);
     234                 :          36 : }
        

Generated by: LCOV version 2.0-1