MailJD nbsp;·nbsp; Test Dashboard nbsp;·nbsp; Coverage
LCOV - code coverage report
Current view: top level - ui - TaskListWidget.cpp (source / functions) Coverage Total Hit
Test: MailJD Coverage (Unit + E2E) Lines: 93.6 % 1270 1189
Test Date: 2026-07-27 17:53:44 Functions: 99.1 % 109 108
Legend: Lines:     hit not hit
Branches: + taken - not taken # not executed
Branches: 53.8 % 2468 1328

             Branch data     Line data    Source code
       1                 :             : #include "TaskListWidget.h"
       2                 :             : 
       3                 :             : #include "ui/ThemeManager.h"
       4                 :             : #include "util/TaskDates.h"
       5                 :             : 
       6                 :             : #include <QCursor>
       7                 :             : #include <QFont>
       8                 :             : #include <QDesktopServices>
       9                 :             : #include <QHeaderView>
      10                 :             : #include <QKeyEvent>
      11                 :             : #include <QLocale>
      12                 :             : #include <QMenu>
      13                 :             : #include <QMessageBox>
      14                 :             : #include <QMouseEvent>
      15                 :             : #include <QPainter>
      16                 :             : #include <QPushButton>
      17                 :             : #include <QSettings>
      18                 :             : #include <QTimeZone>
      19                 :             : #include <QVBoxLayout>
      20                 :             : 
      21                 :             : #include "data/CalendarStore.h"
      22                 :             : #include "ui/MarkdownHighlighter.h"
      23                 :             : #include "ui/MarkdownRenderer.h"
      24                 :             : #include "ui/PlainTextMessageBox.h"
      25                 :             : #include "ui/UrlSchemeFilter.h"
      26                 :             : #include <QToolButton>
      27                 :             : #include <QFrame>
      28                 :             : #include <QScrollBar>
      29                 :             : #include <QTimer>
      30                 :             : #include <QUuid>
      31                 :             : #include <QUrlQuery>
      32                 :             : 
      33                 :             : // 67.B3: all colors come from ThemeManager tokens (docs/DESIGN.md §2);
      34                 :             : // widget stylesheets and detail-view HTML are rebuilt per render with
      35                 :             : // the active palette.
      36                 :        1710 : static QString tok(const char *token) {
      37   [ +  -  +  - ]:        1710 :   return ThemeManager::instance().color(QLatin1String(token));
      38                 :             : }
      39                 :             : 
      40                 :             : // ═══════════════════════════════════════════════════════
      41                 :             : // TaskListModel (Sprint 37 – T-455: extended columns)
      42                 :             : // ═══════════════════════════════════════════════════════
      43                 :             : 
      44                 :          33 : TaskListModel::TaskListModel(QObject *parent)
      45                 :          33 :     : QAbstractItemModel(parent) {}
      46                 :             : 
      47                 :         104 : void TaskListModel::setTasks(const QList<CalendarTask> &tasks) {
      48                 :         104 :   beginResetModel();
      49                 :         104 :   m_tasks = tasks;
      50                 :         104 :   endResetModel();
      51                 :         104 : }
      52                 :             : 
      53                 :        1793 : QModelIndex TaskListModel::index(int row, int col,
      54                 :             :                                   const QModelIndex &parent) const {
      55   [ +  -  +  -  :        1793 :   if (parent.isValid() || row < 0 || row >= m_tasks.size() || col < 0 ||
          +  -  +  -  -  
                +  -  + ]
      56                 :             :       col >= ColCount)
      57                 :           0 :     return {};
      58                 :        1793 :   return createIndex(row, col);
      59                 :             : }
      60                 :             : 
      61                 :        2812 : QModelIndex TaskListModel::parent(const QModelIndex &) const { return {}; }
      62                 :             : 
      63                 :         818 : int TaskListModel::rowCount(const QModelIndex &parent) const {
      64         [ +  + ]:         818 :   return parent.isValid() ? 0 : m_tasks.size();
      65                 :             : }
      66                 :             : 
      67                 :         798 : int TaskListModel::columnCount(const QModelIndex &) const {
      68                 :         798 :   return ColCount;
      69                 :             : }
      70                 :             : 
      71                 :        5775 : QVariant TaskListModel::data(const QModelIndex &index, int role) const {
      72   [ +  +  -  +  :        5775 :   if (!index.isValid() || index.row() >= m_tasks.size())
                   +  + ]
      73                 :           1 :     return {};
      74                 :             : 
      75                 :        5774 :   const auto &task = m_tasks[index.row()];
      76                 :             : 
      77         [ +  + ]:        5774 :   if (role == Qt::DisplayRole) {
      78   [ +  +  +  +  :         749 :     switch (index.column()) {
                +  +  - ]
      79                 :         149 :     case ColStatus: {
      80                 :         149 :       QString icon;
      81         [ +  + ]:         149 :       if (task.status == QStringLiteral("COMPLETED"))
      82                 :          16 :         icon = QStringLiteral("✓");
      83         [ +  + ]:         133 :       else if (task.status == QStringLiteral("IN-PROCESS"))
      84                 :          25 :         icon = QStringLiteral("▶");
      85         [ -  + ]:         108 :       else if (task.status == QStringLiteral("CANCELLED"))
      86                 :           0 :         icon = QStringLiteral("✗");
      87                 :             :       else
      88                 :         108 :         icon = QStringLiteral("○");
      89                 :             :       // Starred indicator
      90         [ +  + ]:         149 :       if (task.isStarred())
      91         [ +  - ]:          38 :         icon += QStringLiteral(" ★");
      92                 :         149 :       return icon;
      93                 :         149 :     }
      94                 :         150 :     case ColSummary:
      95                 :         150 :       return task.summary;
      96                 :           3 :     case ColProgress:
      97                 :             :       // Drawn by delegate, but provide tooltip data
      98                 :           3 :       return task.percentComplete > 0
      99   [ +  +  +  -  :           7 :                  ? QString::number(task.percentComplete) +
          +  -  +  +  -  
                      - ]
     100   [ +  +  +  +  :           4 :                        QStringLiteral("%")
             -  -  -  - ]
     101                 :           3 :                  : QString();
     102                 :         149 :     case ColDue:
     103         [ +  + ]:         149 :       if (task.due.isValid()) {
     104         [ +  - ]:         115 :         QDate today = QDate::currentDate();
     105   [ +  -  +  - ]:         115 :         QDate dueDate = task.due.toLocalTime().date();
     106         [ -  + ]:         115 :         if (dueDate == today)
     107                 :           0 :           return QStringLiteral("Heute");
     108   [ +  -  +  - ]:         230 :         return QLocale().toString(dueDate, QStringLiteral("dd.MM.yy"));
     109                 :             :       }
     110                 :          34 :       return QStringLiteral("—");
     111                 :         149 :     case ColPriority:
     112   [ +  +  +  + ]:         149 :       if (task.priority > 0 && task.priority <= 4)
     113                 :          38 :         return QStringLiteral("!!!");
     114         [ +  + ]:         111 :       if (task.priority == 5)
     115                 :          29 :         return QStringLiteral("!!");
     116   [ -  +  -  - ]:          82 :       if (task.priority >= 6 && task.priority <= 9)
     117                 :           0 :         return QStringLiteral("!");
     118                 :          82 :       return {};
     119                 :         149 :     case ColCalendar:
     120                 :         149 :       return task.calendarDisplayName.isEmpty()
     121   [ +  +  +  - ]:         298 :                  ? task.calendarPath.section(QLatin1Char('/'), -2, -2)
     122                 :         149 :                  : task.calendarDisplayName;
     123                 :             :     }
     124                 :             :   }
     125                 :             : 
     126         [ +  + ]:        5025 :   if (role == Qt::ForegroundRole) {
     127                 :             :     // 67.B6: theme tokens instead of fixed RGB — readable in dark mode
     128   [ +  +  -  +  :        2911 :     if (task.status == QStringLiteral("COMPLETED") ||
             +  -  +  + ]
     129   [ +  +  +  +  :        1415 :         task.status == QStringLiteral("CANCELLED")) {
                   +  - ]
     130   [ +  -  +  - ]:          81 :       return QColor(tok("@text_muted"));
     131                 :             :     }
     132   [ +  +  +  +  :         667 :     if (index.column() == ColDue && task.due.isValid()) {
                   +  + ]
     133         [ +  - ]:         105 :       QDate today = QDate::currentDate();
     134   [ +  -  +  - ]:         105 :       QDate dueDate = task.due.toLocalTime().date();
     135         [ +  + ]:         105 :       if (dueDate < today)
     136   [ +  -  +  - ]:          33 :         return QColor(tok("@danger"));  // overdue
     137         [ -  + ]:          72 :       if (dueDate == today)
     138   [ #  #  #  # ]:           0 :         return QColor(tok("@warning")); // today
     139                 :             :     }
     140   [ +  +  +  +  :         634 :     if (index.column() == ColStatus && task.isStarred())
                   +  + ]
     141   [ +  -  +  - ]:          38 :       return QColor(tok("@star_active")); // starred
     142                 :             :   }
     143                 :             : 
     144         [ +  + ]:        4873 :   if (role == Qt::TextAlignmentRole) {
     145   [ +  +  +  +  :        1198 :     if (index.column() == ColStatus || index.column() == ColPriority ||
             +  +  +  + ]
     146                 :         450 :         index.column() == ColProgress)
     147                 :         301 :       return Qt::AlignCenter;
     148                 :             :   }
     149                 :             : 
     150         [ +  + ]:        4572 :   if (role == Qt::FontRole) {
     151         [ +  + ]:         748 :     if (task.status == QStringLiteral("COMPLETED")) {
     152         [ +  - ]:          81 :       QFont f;
     153         [ +  - ]:          81 :       f.setStrikeOut(true);
     154         [ +  - ]:          81 :       return f;
     155                 :          81 :     }
     156                 :             :   }
     157                 :             : 
     158   [ +  +  +  +  :        4491 :   if (role == Qt::ToolTipRole && index.column() == ColProgress) {
                   +  + ]
     159         [ +  + ]:           3 :     if (task.percentComplete > 0)
     160         [ +  - ]:           2 :       return QStringLiteral("Fortschritt: %1%").arg(task.percentComplete);
     161                 :             :   }
     162                 :             : 
     163                 :             :   // Store percentComplete for delegate via UserRole
     164   [ +  +  +  +  :        4490 :   if (role == Qt::UserRole && index.column() == ColProgress)
                   +  + ]
     165                 :          59 :     return task.percentComplete;
     166                 :             : 
     167                 :             :   // Store color for delegate via UserRole+1
     168   [ +  +  +  -  :        4431 :   if (role == Qt::UserRole + 1 && index.column() == ColProgress)
                   +  + ]
     169                 :          13 :     return task.color;
     170                 :             : 
     171                 :             :   // Store color for calendar column (circle, not square)
     172   [ +  +  +  +  :        4418 :   if (role == Qt::DecorationRole && index.column() == ColCalendar) {
                   +  + ]
     173         [ +  + ]:         149 :     if (!task.color.isEmpty()) {
     174         [ +  - ]:         125 :       QPixmap px(10, 10);
     175         [ +  - ]:         125 :       px.fill(Qt::transparent);
     176         [ +  - ]:         125 :       QPainter p(&px);
     177         [ +  - ]:         125 :       p.setRenderHint(QPainter::Antialiasing);
     178   [ +  -  +  - ]:         125 :       p.setBrush(QColor(task.color));
     179         [ +  - ]:         125 :       p.setPen(Qt::NoPen);
     180         [ +  - ]:         125 :       p.drawEllipse(0, 0, 10, 10);
     181         [ +  - ]:         125 :       return px;
     182                 :         125 :     }
     183                 :             :   }
     184                 :             : 
     185                 :        4293 :   return {};
     186                 :             : }
     187                 :             : 
     188                 :        4578 : QVariant TaskListModel::headerData(int section, Qt::Orientation orientation,
     189                 :             :                                     int role) const {
     190   [ +  -  +  + ]:        4578 :   if (orientation != Qt::Horizontal || role != Qt::DisplayRole)
     191                 :        3582 :     return {};
     192   [ +  +  +  +  :         996 :   switch (section) {
                +  +  - ]
     193                 :         166 :   case ColStatus:
     194                 :         166 :     return QStringLiteral(" ");
     195                 :         166 :   case ColSummary:
     196                 :         166 :     return QStringLiteral("Aufgabe");
     197                 :         166 :   case ColProgress:
     198                 :         166 :     return QStringLiteral("◔");
     199                 :         166 :   case ColDue:
     200                 :         166 :     return QStringLiteral("Fällig");
     201                 :         166 :   case ColPriority:
     202                 :         166 :     return QStringLiteral("P");
     203                 :         166 :   case ColCalendar:
     204                 :         166 :     return QStringLiteral("Kalender");
     205                 :             :   }
     206                 :           0 :   return {};
     207                 :             : }
     208                 :             : 
     209                 :             : // ═══════════════════════════════════════════════════════
     210                 :             : // TaskProgressDelegate (Sprint 37 – T-455)
     211                 :             : // ═══════════════════════════════════════════════════════
     212                 :             : 
     213                 :          56 : void TaskProgressDelegate::paint(QPainter *painter,
     214                 :             :                                   const QStyleOptionViewItem &option,
     215                 :             :                                   const QModelIndex &index) const {
     216                 :             :   // Draw background
     217         [ +  - ]:          56 :   QStyledItemDelegate::paint(painter, option, QModelIndex());
     218                 :             : 
     219   [ +  -  +  - ]:          56 :   int percent = index.data(Qt::UserRole).toInt();
     220         [ +  + ]:          56 :   if (percent <= 0)
     221                 :          43 :     return;
     222                 :             : 
     223         [ +  - ]:          13 :   painter->save();
     224         [ +  - ]:          13 :   painter->setRenderHint(QPainter::Antialiasing);
     225                 :             : 
     226                 :          13 :   int diameter = 14;
     227                 :             :   QRectF circleRect(
     228                 :          13 :       option.rect.center().x() - diameter / 2.0,
     229                 :          13 :       option.rect.center().y() - diameter / 2.0,
     230                 :          39 :       diameter, diameter);
     231                 :             : 
     232                 :             :   // Background circle
     233         [ +  - ]:          13 :   painter->setPen(Qt::NoPen);
     234   [ +  -  +  -  :          13 :   painter->setBrush(QColor(tok("@border_medium")));
                   +  - ]
     235         [ +  - ]:          13 :   painter->drawEllipse(circleRect);
     236                 :             : 
     237                 :             :   // Progress arc
     238   [ +  -  +  - ]:          13 :   QString colorStr = index.data(Qt::UserRole + 1).toString();
     239                 :             :   QColor progressColor =
     240   [ +  +  +  -  :          13 :       colorStr.isEmpty() ? QColor(tok("@accent")) : QColor(colorStr);
             +  +  -  - ]
     241   [ +  -  +  - ]:          13 :   painter->setBrush(progressColor);
     242                 :             : 
     243                 :          13 :   int startAngle = 90 * 16; // 12 o'clock
     244                 :          13 :   int spanAngle = -qRound(percent * 360.0 / 100.0) * 16;
     245         [ +  - ]:          13 :   painter->drawPie(circleRect, startAngle, spanAngle);
     246                 :             : 
     247                 :             :   // Center hole (donut effect) — theme background, not palette base
     248                 :             :   // (the palette stays light even in dark mode, 67.B6)
     249                 :          13 :   QRectF inner(circleRect.adjusted(3, 3, -3, -3));
     250   [ +  -  +  -  :          13 :   painter->setBrush(QColor(tok("@bg_main")));
                   +  - ]
     251         [ +  - ]:          13 :   painter->drawEllipse(inner);
     252                 :             : 
     253         [ +  - ]:          13 :   painter->restore();
     254                 :          13 : }
     255                 :             : 
     256                 :          90 : QSize TaskProgressDelegate::sizeHint(const QStyleOptionViewItem &,
     257                 :             :                                       const QModelIndex &) const {
     258                 :          90 :   return QSize(24, 20);
     259                 :             : }
     260                 :             : 
     261                 :             : // ═══════════════════════════════════════════════════════
     262                 :             : // TaskListWidget (Sprint 37 – T-453/454/456)
     263                 :             : // ═══════════════════════════════════════════════════════
     264                 :             : 
     265                 :             : 
     266         [ +  - ]:          30 : TaskListWidget::TaskListWidget(QWidget *parent) : QWidget(parent) {
     267   [ +  -  +  -  :          30 :   m_model = new TaskListModel(this);
             -  +  -  - ]
     268                 :             : 
     269                 :             :   // --- 3-Pane Layout ---
     270   [ +  -  +  -  :          30 :   m_mainSplitter = new QSplitter(Qt::Horizontal, this);
             -  +  -  - ]
     271   [ +  -  +  -  :          30 :   m_rightSplitter = new QSplitter(Qt::Vertical);
             -  +  -  - ]
     272                 :             : 
     273         [ +  - ]:          30 :   setupSidebar();
     274         [ +  - ]:          30 :   setupTaskList();
     275         [ +  - ]:          30 :   setupDetailPanel();
     276                 :             : 
     277                 :             :   // Left: sidebar, Right: list + detail
     278         [ +  - ]:          30 :   m_rightSplitter->addWidget(m_treeView);
     279         [ +  - ]:          30 :   m_rightSplitter->addWidget(m_detailContainer);
     280         [ +  - ]:          30 :   m_rightSplitter->setStretchFactor(0, 3); // list 60%
     281         [ +  - ]:          30 :   m_rightSplitter->setStretchFactor(1, 2); // detail 40%
     282                 :             : 
     283         [ +  - ]:          30 :   m_mainSplitter->addWidget(m_sidebarContainer);
     284         [ +  - ]:          30 :   m_mainSplitter->addWidget(m_rightSplitter);
     285         [ +  - ]:          30 :   m_mainSplitter->setStretchFactor(0, 0); // sidebar fixed
     286         [ +  - ]:          30 :   m_mainSplitter->setStretchFactor(1, 1); // right stretch
     287                 :             : 
     288   [ +  -  +  -  :          30 :   auto *layout = new QVBoxLayout(this);
             -  +  -  - ]
     289         [ +  - ]:          30 :   layout->setContentsMargins(0, 0, 0, 0);
     290         [ +  - ]:          30 :   layout->setSpacing(0);
     291         [ +  - ]:          30 :   layout->addWidget(m_mainSplitter);
     292                 :             : 
     293                 :             :   // Selection → detail
     294         [ +  - ]:          30 :   connect(m_treeView->selectionModel(),
     295                 :             :           &QItemSelectionModel::currentRowChanged, this,
     296         [ +  - ]:          30 :           [this](const QModelIndex &current, const QModelIndex &) {
     297         [ +  - ]:          99 :             if (current.isValid())
     298                 :          99 :               showDetail(current.row());
     299                 :             :             else
     300         [ #  # ]:           0 :               m_detailBrowser->setHtml(
     301                 :           0 :                   QStringLiteral("<p style='color:%1;text-align:center;'>"
     302                 :             :                                  "Keine Aufgabe ausgewählt</p>")
     303   [ #  #  #  # ]:           0 :                       .arg(tok("@text_secondary")));
     304                 :          99 :           });
     305                 :             : 
     306         [ +  - ]:          30 :   setFocusProxy(m_treeView);
     307                 :             : 
     308                 :          30 :   m_currentFilter = QStringLiteral("all");
     309         [ +  - ]:          30 :   restoreSettings();
     310                 :          30 : }
     311                 :             : 
     312                 :             : // --- Sidebar (T-454) ---
     313                 :             : 
     314                 :          30 : void TaskListWidget::setupSidebar() {
     315   [ +  -  -  +  :          30 :   auto *sidebarContainer = new QWidget();
                   -  - ]
     316   [ +  -  -  +  :          30 :   auto *sidebarLayout = new QVBoxLayout(sidebarContainer);
                   -  - ]
     317                 :          30 :   sidebarLayout->setContentsMargins(0, 0, 0, 0);
     318                 :          30 :   sidebarLayout->setSpacing(0);
     319                 :             : 
     320   [ +  -  -  +  :          30 :   m_sidebarList = new QListWidget();
                   -  - ]
     321         [ +  - ]:          60 :   m_sidebarList->setObjectName(QStringLiteral("taskSidebar"));
     322                 :          30 :   m_sidebarList->setMinimumWidth(150);
     323                 :          30 :   m_sidebarList->setMaximumWidth(280);
     324                 :          30 :   m_sidebarList->setFocusPolicy(Qt::ClickFocus);
     325                 :          30 :   m_sidebarList->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
     326                 :          30 :   m_sidebarList->setHorizontalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
     327                 :             :   // Sprint 69: object name set so main.qss QListWidget#taskSidebar rule applies
     328                 :             :   // (was missing before — the QSS rule was dead and inline QSS took over).
     329         [ +  - ]:          60 :   m_sidebarList->setObjectName(QStringLiteral("taskSidebar"));
     330         [ +  - ]:          30 :   sidebarLayout->addWidget(m_sidebarList, 1);
     331                 :             : 
     332                 :             :   // Sprint 39/56: "Neue Aufgabe" button — uses main.qss QPushButton#primaryButton.
     333   [ +  -  -  +  :          60 :   auto *addBtn = new QPushButton(QStringLiteral("+ Neue Aufgabe"), this);
                   -  - ]
     334         [ +  - ]:          60 :   addBtn->setObjectName(QStringLiteral("primaryButton"));
     335   [ +  -  +  - ]:          30 :   addBtn->setCursor(Qt::PointingHandCursor);
     336                 :          30 :   connect(addBtn, &QPushButton::clicked, this,
     337         [ +  - ]:          31 :           [this]() { emit taskCreateRequested(); });
     338         [ +  - ]:          30 :   sidebarLayout->addWidget(addBtn);
     339                 :             : 
     340                 :             :   // Use the container as the sidebar widget
     341                 :          30 :   sidebarContainer->setMinimumWidth(150);
     342                 :          30 :   sidebarContainer->setMaximumWidth(280);
     343                 :             : 
     344                 :          30 :   connect(m_sidebarList, &QListWidget::currentRowChanged, this,
     345         [ +  - ]:          30 :           &TaskListWidget::onSidebarClicked);
     346                 :             : 
     347                 :             :   // T-71.5b: native checkbox toggle for calendar visibility. Meta-folder
     348                 :             :   // rows (Alle/Aktuell/…) carry no UserRole path and are skipped. We do NOT
     349                 :             :   // call updateSidebar() here — it rebuilds items and would re-trigger
     350                 :             :   // itemChanged. The checkbox state itself is the primary indicator; the
     351                 :             :   // existing context-menu path still calls updateSidebar for a full resync.
     352                 :          30 :   connect(m_sidebarList, &QListWidget::itemChanged, this,
     353         [ +  - ]:          30 :           [this](QListWidgetItem *item) {
     354         [ -  + ]:           1 :     if (!item) return;
     355   [ +  -  +  - ]:           1 :     const QString path = item->data(Qt::UserRole).toString();
     356         [ -  + ]:           1 :     if (path.isEmpty()) return;                 // meta-folder, ignore
     357         [ +  - ]:           1 :     const bool nowHidden = item->checkState() == Qt::Unchecked;
     358                 :           1 :     const bool wasHidden = m_hiddenCalendars.contains(path);
     359         [ -  + ]:           1 :     if (nowHidden == wasHidden) return;         // no change
     360   [ +  -  +  - ]:           1 :     if (nowHidden) m_hiddenCalendars.insert(path);
     361         [ #  # ]:           0 :     else           m_hiddenCalendars.remove(path);
     362         [ +  - ]:           1 :     saveSettings();
     363         [ +  - ]:           1 :     applyFilter();
     364         [ +  - ]:           1 :   });
     365                 :             : 
     366                 :             :   // Sprint 56: Unified sidebar context menu
     367                 :          30 :   m_sidebarList->setContextMenuPolicy(Qt::CustomContextMenu);
     368                 :          30 :   connect(m_sidebarList, &QWidget::customContextMenuRequested, this,
     369         [ +  - ]:          30 :           [this](const QPoint &pos) {
     370         [ +  - ]:           3 :     QMenu menu;
     371                 :             : 
     372                 :             :     // Show completed toggle (always available)
     373         [ +  - ]:           3 :     auto *showCompletedAction = menu.addAction(
     374         [ +  - ]:           6 :         tr("Show completed tasks\tShift+F"));
     375         [ +  - ]:           3 :     showCompletedAction->setCheckable(true);
     376         [ +  - ]:           3 :     showCompletedAction->setChecked(m_showCompleted);
     377                 :           3 :     connect(showCompletedAction, &QAction::toggled, this,
     378         [ +  - ]:           3 :             &TaskListWidget::setShowCompleted);
     379                 :             : 
     380                 :             :     // Calendar-specific actions
     381         [ +  - ]:           3 :     auto *item = m_sidebarList->itemAt(pos);
     382         [ +  + ]:           3 :     if (item) {
     383   [ +  -  +  - ]:           2 :       QString path = item->data(Qt::UserRole).toString();
     384         [ +  - ]:           2 :       if (!path.isEmpty()) {
     385         [ +  - ]:           2 :         menu.addSeparator();
     386                 :           2 :         bool hidden = m_hiddenCalendars.contains(path);
     387         [ +  - ]:           2 :         menu.addAction(
     388   [ +  -  +  - ]:           4 :             hidden ? tr("Show calendar")
     389                 :             :                    : tr("Hide calendar"),
     390   [ +  +  -  - ]:           4 :             [this, path, hidden]() {
     391         [ +  + ]:           2 :           if (hidden)
     392                 :           1 :             m_hiddenCalendars.remove(path);
     393                 :             :           else
     394         [ +  - ]:           1 :             m_hiddenCalendars.insert(path);
     395                 :           2 :           saveSettings();
     396                 :           2 :           applyFilter();
     397                 :           2 :           updateSidebar();
     398                 :           2 :         });
     399                 :             :       }
     400                 :           2 :     }
     401   [ +  -  +  -  :           3 :     menu.exec(m_sidebarList->viewport()->mapToGlobal(pos));
                   +  - ]
     402                 :           3 :   });
     403                 :             : 
     404                 :             :   // Sprint 56: Shift+F shortcut for show-completed toggle
     405   [ +  -  -  +  :          30 :   auto *toggleCompletedAction = new QAction(this);
                   -  - ]
     406         [ +  - ]:          30 :   toggleCompletedAction->setShortcut(
     407         [ +  - ]:          60 :       QKeySequence(Qt::SHIFT | Qt::Key_F));
     408                 :          30 :   connect(toggleCompletedAction, &QAction::triggered, this,
     409         [ +  - ]:          31 :           [this]() { setShowCompleted(!m_showCompleted); });
     410                 :          30 :   addAction(toggleCompletedAction);
     411                 :             : 
     412                 :             :   // Store container as the actual sidebar widget for splitter
     413                 :          30 :   m_sidebarContainer = sidebarContainer;
     414                 :          30 : }
     415                 :             : 
     416                 :             : // --- Task List (T-453/455) ---
     417                 :             : 
     418                 :          30 : void TaskListWidget::setupTaskList() {
     419   [ +  -  -  +  :          30 :   m_treeView = new QTreeView();
                   -  - ]
     420         [ +  - ]:          60 :   m_treeView->setObjectName(QStringLiteral("taskList"));
     421                 :          30 :   m_treeView->setModel(m_model);
     422                 :             : 
     423                 :          30 :   m_treeView->setRootIsDecorated(false);
     424                 :          30 :   m_treeView->setAlternatingRowColors(true);
     425                 :          30 :   m_treeView->setSelectionMode(QAbstractItemView::SingleSelection);
     426                 :          30 :   m_treeView->setSelectionBehavior(QAbstractItemView::SelectRows);
     427                 :          30 :   m_treeView->setSortingEnabled(true);
     428                 :          30 :   m_treeView->setFocusPolicy(Qt::StrongFocus);
     429         [ +  - ]:          30 :   m_treeView->setEditTriggers(QAbstractItemView::NoEditTriggers);
     430                 :             :   // Sprint 69: styling via main.qss QTreeView#taskList — no inline QSS.
     431                 :             : 
     432                 :             :   // Sprint 39: Double-click to edit task
     433                 :          30 :   connect(m_treeView, &QTreeView::doubleClicked, this,
     434         [ +  - ]:          30 :           [this](const QModelIndex &idx) {
     435         [ -  + ]:           1 :     if (!idx.isValid()) return;
     436                 :           1 :     const auto &task = m_model->taskAt(idx.row());
     437         [ +  - ]:           1 :     if (!task.uid.isEmpty())
     438                 :           1 :       emit taskUpdated(task);
     439                 :             :   });
     440                 :             : 
     441                 :             :   // Column widths
     442                 :          30 :   auto *header = m_treeView->header();
     443                 :          30 :   header->setStretchLastSection(false);
     444                 :          30 :   header->setSectionResizeMode(TaskListModel::ColStatus, QHeaderView::Fixed);
     445                 :          30 :   header->setSectionResizeMode(TaskListModel::ColSummary, QHeaderView::Stretch);
     446                 :          30 :   header->setSectionResizeMode(TaskListModel::ColProgress, QHeaderView::Fixed);
     447                 :          30 :   header->setSectionResizeMode(TaskListModel::ColDue, QHeaderView::Fixed);
     448                 :          30 :   header->setSectionResizeMode(TaskListModel::ColPriority, QHeaderView::Fixed);
     449                 :          30 :   header->setSectionResizeMode(TaskListModel::ColCalendar, QHeaderView::Fixed);
     450                 :          30 :   header->resizeSection(TaskListModel::ColStatus, 40);
     451                 :          30 :   header->resizeSection(TaskListModel::ColProgress, 28);
     452                 :          30 :   header->resizeSection(TaskListModel::ColDue, 80);
     453                 :          30 :   header->resizeSection(TaskListModel::ColPriority, 30);
     454                 :          30 :   header->resizeSection(TaskListModel::ColCalendar, 100);
     455                 :             : 
     456                 :             :   // Progress circle delegate
     457   [ +  -  -  +  :          30 :   m_treeView->setItemDelegateForColumn(
                   -  - ]
     458         [ +  - ]:          30 :       TaskListModel::ColProgress, new TaskProgressDelegate(m_treeView));
     459                 :             : 
     460                 :             :   // Sprint 56: Click on status column → toggle task
     461                 :          30 :   connect(m_treeView, &QTreeView::clicked, this,
     462         [ +  - ]:          30 :           [this](const QModelIndex &idx) {
     463         [ +  - ]:           1 :     if (idx.column() == TaskListModel::ColStatus)
     464                 :           1 :       toggleCurrentTask();
     465                 :           1 :   });
     466                 :             : 
     467                 :             :   // Sprint 56: Context menu (edit / delete)
     468                 :          30 :   m_treeView->setContextMenuPolicy(Qt::CustomContextMenu);
     469                 :          30 :   connect(m_treeView, &QWidget::customContextMenuRequested, this,
     470         [ +  - ]:          30 :           [this](const QPoint &pos) {
     471         [ +  - ]:           1 :     auto idx = m_treeView->indexAt(pos);
     472         [ -  + ]:           1 :     if (!idx.isValid()) return;
     473                 :           1 :     const auto &task = m_model->taskAt(idx.row());
     474         [ -  + ]:           1 :     if (task.uid.isEmpty()) return;
     475         [ +  - ]:           1 :     QMenu menu;
     476   [ +  -  +  - ]:           1 :     menu.addAction(tr("Edit task"), [this, task]() {
     477                 :           1 :       emit taskUpdated(task);
     478                 :           1 :     });
     479         [ +  - ]:           1 :     menu.addSeparator();
     480                 :             : 
     481                 :             :     // Toggle completion
     482                 :           1 :     bool isDone = (task.status == QStringLiteral("COMPLETED"));
     483   [ -  -  +  -  :           1 :     menu.addAction(isDone ? tr("○ Mark as open")
                   +  - ]
     484         [ -  + ]:           1 :                           : tr("✓ Mark as completed"), [this]() {
     485                 :           1 :       toggleCurrentTask();
     486                 :           1 :     });
     487                 :             : 
     488                 :             :     // Toggle starred
     489                 :           1 :     bool starred = task.isStarred();
     490   [ -  -  +  -  :           1 :     menu.addAction(starred ? tr("☆ Remove star")
                   +  - ]
     491         [ -  + ]:           1 :                            : tr("★ Add star"), [this, starred]() {
     492         [ +  - ]:           1 :       modifyCurrentTask([starred](CalendarTask &t) {
     493         [ -  + ]:           1 :         t.priority = starred ? 0 : 1;
     494                 :           1 :       });
     495                 :           1 :     });
     496                 :             : 
     497         [ +  - ]:           1 :     menu.addSeparator();
     498                 :             : 
     499                 :             :     // Priority submenu
     500   [ +  -  +  - ]:           1 :     auto *prioMenu = menu.addMenu(tr("Priority"));
     501                 :           5 :     auto addPrio = [&](const QString &label, int val) {
     502         [ +  - ]:           5 :       auto *a = prioMenu->addAction(label, [this, val]() {
     503         [ +  - ]:           1 :         modifyCurrentTask([val](CalendarTask &t) { t.priority = val; });
     504                 :           1 :       });
     505         [ +  + ]:           5 :       if (task.priority == val) a->setEnabled(false);
     506                 :           5 :     };
     507   [ +  -  +  - ]:           1 :     addPrio(tr("★ Important (1)"), 1);
     508   [ +  -  +  - ]:           1 :     addPrio(tr("↑ High (4)"), 4);
     509   [ +  -  +  - ]:           1 :     addPrio(tr("● Medium (5)"), 5);
     510   [ +  -  +  - ]:           1 :     addPrio(tr("↓ Low (6)"), 6);
     511   [ +  -  +  - ]:           1 :     addPrio(tr("○ None (0)"), 0);
     512                 :             : 
     513                 :             :     // Progress submenu
     514   [ +  -  +  - ]:           1 :     auto *progMenu = menu.addMenu(tr("Progress"));
     515         [ +  + ]:           6 :     for (int pct : {0, 25, 50, 75, 100}) {
     516         [ +  - ]:           5 :       auto *a = progMenu->addAction(
     517         [ +  - ]:          20 :           QStringLiteral("%1%").arg(pct), [this, pct]() {
     518         [ +  - ]:           1 :         modifyCurrentTask([pct](CalendarTask &t) {
     519                 :           1 :           t.percentComplete = pct;
     520         [ -  + ]:           1 :           if (pct == 100) t.status = QStringLiteral("COMPLETED");
     521         [ +  - ]:           2 :           else if (pct > 0) t.status = QStringLiteral("IN-PROCESS");
     522                 :           1 :         });
     523                 :           1 :       });
     524   [ +  +  +  - ]:           5 :       if (task.percentComplete == pct) a->setEnabled(false);
     525                 :             :     }
     526                 :             : 
     527                 :             :     // Due date submenu
     528   [ +  -  +  - ]:           1 :     auto *dueMenu = menu.addMenu(tr("Due"));
     529         [ +  - ]:           1 :     QDateTime now = QDateTime::currentDateTimeUtc();
     530   [ +  -  +  - ]:           1 :     dueMenu->addAction(tr("Today"), [this, now]() {
     531   [ +  -  +  - ]:           1 :       modifyCurrentTask([now](CalendarTask &t) {
     532   [ +  -  +  - ]:           1 :         t.due = TaskDates::endOfLocalDay(QDate::currentDate());
     533                 :           1 :       });
     534                 :           1 :     });
     535   [ +  -  +  - ]:           1 :     dueMenu->addAction(tr("Tomorrow"), [this, now]() {
     536   [ +  -  +  - ]:           1 :       modifyCurrentTask([now](CalendarTask &t) {
     537   [ +  -  +  -  :           1 :         t.due = TaskDates::endOfLocalDay(QDate::currentDate().addDays(1));
                   +  - ]
     538                 :           1 :       });
     539                 :           1 :     });
     540   [ +  -  +  - ]:           1 :     dueMenu->addAction(tr("Next week"), [this, now]() {
     541   [ +  -  +  - ]:           1 :       modifyCurrentTask([now](CalendarTask &t) {
     542   [ +  -  +  -  :           1 :         t.due = TaskDates::endOfLocalDay(QDate::currentDate().addDays(7));
                   +  - ]
     543                 :           1 :       });
     544                 :           1 :     });
     545   [ +  -  +  - ]:           1 :     if (task.due.isValid()) {
     546         [ +  - ]:           1 :       dueMenu->addSeparator();
     547   [ +  -  +  - ]:           1 :       dueMenu->addAction(tr("Remove"), [this]() {
     548         [ +  - ]:           2 :         modifyCurrentTask([](CalendarTask &t) { t.due = {}; });
     549                 :           1 :       });
     550                 :             :     }
     551                 :             : 
     552         [ +  - ]:           1 :     menu.addSeparator();
     553   [ +  -  +  - ]:           1 :     menu.addAction(tr("Delete task"), [this]() {
     554                 :           0 :       deleteCurrentTask();
     555                 :           0 :     });
     556   [ +  -  +  -  :           1 :     menu.exec(m_treeView->viewport()->mapToGlobal(pos));
                   +  - ]
     557                 :           1 :   });
     558                 :          30 : }
     559                 :             : 
     560                 :             : // --- Detail Panel (T-456) ---
     561                 :             : 
     562                 :          30 : void TaskListWidget::setupDetailPanel() {
     563                 :             :   // Container for header + body/editor stack
     564   [ +  -  -  +  :          30 :   auto *detailContainer = new QWidget();
                   -  - ]
     565   [ +  -  -  +  :          30 :   auto *detailLayout = new QVBoxLayout(detailContainer);
                   -  - ]
     566                 :          30 :   detailLayout->setContentsMargins(0, 0, 0, 0);
     567                 :          30 :   detailLayout->setSpacing(0);
     568                 :             : 
     569                 :             :   // ── Persistent header (always visible, even during editing) ──
     570   [ +  -  -  +  :          30 :   m_headerBrowser = new QTextBrowser();
                   -  - ]
     571                 :          30 :   m_headerBrowser->setOpenExternalLinks(false);
     572                 :          30 :   m_headerBrowser->setOpenLinks(false);
     573                 :          30 :   m_headerBrowser->setFrameShape(QFrame::NoFrame);
     574                 :          30 :   m_headerBrowser->setVerticalScrollBarPolicy(Qt::ScrollBarAlwaysOff);
     575                 :             :   // Sprint 69: global QTextBrowser rule covers border:none; document margin
     576                 :             :   // is set via setDocumentMargin(0) below — no inline QSS needed.
     577                 :          30 :   m_headerBrowser->setSizePolicy(QSizePolicy::Preferred, QSizePolicy::Minimum);
     578                 :          30 :   m_headerBrowser->document()->setDocumentMargin(0);
     579                 :          30 :   m_headerBrowser->setVisible(false);
     580                 :             : 
     581                 :             :   // Handle action links from the header (status, priority, due, progress, star)
     582                 :          30 :   connect(m_headerBrowser, &QTextBrowser::anchorClicked, this,
     583         [ +  - ]:          60 :           [this](const QUrl &url) {
     584   [ +  -  +  + ]:          30 :     if (url.scheme() != QStringLiteral("action")) return;
     585   [ +  -  +  - ]:          29 :     QString act = url.toString().mid(7); // strip "action:"
     586         [ +  + ]:          29 :     if (act == QStringLiteral("toggle-status")) {
     587         [ +  - ]:           2 :       toggleCurrentTask();
     588         [ +  + ]:          27 :     } else if (act == QStringLiteral("toggle-star")) {
     589         [ +  - ]:           1 :       modifyCurrentTask([](CalendarTask &t) {
     590         [ -  + ]:           1 :         t.priority = t.isStarred() ? 0 : 1;
     591                 :           1 :       });
     592         [ +  + ]:          26 :     } else if (act == QStringLiteral("cycle-priority")) {
     593         [ +  - ]:           4 :       modifyCurrentTask([](CalendarTask &t) {
     594   [ +  -  +  + ]:           4 :         if (t.priority == 0 || t.priority == 1) t.priority = 4;
     595         [ +  + ]:           3 :         else if (t.priority <= 4) t.priority = 5;
     596         [ +  + ]:           2 :         else if (t.priority == 5) t.priority = 6;
     597                 :           1 :         else t.priority = 0;
     598                 :           4 :       });
     599         [ +  + ]:          22 :     } else if (act == QStringLiteral("progress-up")) {
     600         [ +  - ]:          10 :       modifyCurrentTask([](CalendarTask &t) {
     601                 :          10 :         t.percentComplete = qMin(100, t.percentComplete + 10);
     602         [ +  + ]:          10 :         if (t.percentComplete == 100)
     603                 :           1 :           t.status = QStringLiteral("COMPLETED");
     604         [ +  - ]:           9 :         else if (t.percentComplete > 0)
     605                 :           9 :           t.status = QStringLiteral("IN-PROCESS");
     606                 :          10 :       });
     607         [ +  + ]:          12 :     } else if (act == QStringLiteral("progress-down")) {
     608         [ +  - ]:          10 :       modifyCurrentTask([](CalendarTask &t) {
     609                 :          10 :         t.percentComplete = qMax(0, t.percentComplete - 10);
     610         [ +  + ]:          10 :         if (t.percentComplete == 0)
     611                 :           1 :           t.status = QStringLiteral("NEEDS-ACTION");
     612                 :          10 :       });
     613         [ +  + ]:           2 :     } else if (act == QStringLiteral("due-menu")) {
     614                 :             :       // Show due date popup near the browser
     615         [ +  - ]:           1 :       QMenu dueMenu;
     616   [ +  -  +  - ]:           1 :       dueMenu.addAction(tr("Today"), [this]() {
     617         [ +  - ]:           1 :         modifyCurrentTask([](CalendarTask &t) {
     618   [ +  -  +  - ]:           1 :           t.due = TaskDates::endOfLocalDay(QDate::currentDate());
     619                 :           1 :         });
     620                 :           1 :       });
     621   [ +  -  +  - ]:           1 :       dueMenu.addAction(tr("Tomorrow"), [this]() {
     622         [ +  - ]:           1 :         modifyCurrentTask([](CalendarTask &t) {
     623   [ +  -  +  -  :           1 :           t.due = TaskDates::endOfLocalDay(QDate::currentDate().addDays(1));
                   +  - ]
     624                 :           1 :         });
     625                 :           1 :       });
     626   [ +  -  +  - ]:           1 :       dueMenu.addAction(tr("End of week"), [this]() {
     627         [ +  - ]:           1 :         modifyCurrentTask([](CalendarTask &t) {
     628   [ +  -  +  - ]:           1 :           int daysToFri = 5 - QDate::currentDate().dayOfWeek();
     629         [ -  + ]:           1 :           if (daysToFri <= 0) daysToFri += 7;
     630   [ +  -  +  -  :           1 :           t.due = TaskDates::endOfLocalDay(QDate::currentDate().addDays(daysToFri));
                   +  - ]
     631                 :           1 :         });
     632                 :           1 :       });
     633   [ +  -  +  - ]:           1 :       dueMenu.addAction(tr("Next week"), [this]() {
     634         [ +  - ]:           1 :         modifyCurrentTask([](CalendarTask &t) {
     635   [ +  -  +  -  :           1 :           t.due = TaskDates::endOfLocalDay(QDate::currentDate().addDays(7));
                   +  - ]
     636                 :           1 :         });
     637                 :           1 :       });
     638                 :             :       // Check if task has due date for clear option
     639         [ +  - ]:           1 :       auto treeIdx = m_treeView->currentIndex();
     640         [ +  - ]:           1 :       if (treeIdx.isValid()) {
     641                 :           1 :         const auto &curTask = m_model->taskAt(treeIdx.row());
     642   [ +  -  +  - ]:           1 :         if (curTask.due.isValid()) {
     643         [ +  - ]:           1 :           dueMenu.addSeparator();
     644   [ +  -  +  - ]:           1 :           dueMenu.addAction(tr("Remove"), [this]() {
     645         [ +  - ]:           2 :             modifyCurrentTask([](CalendarTask &t) { t.due = {}; });
     646                 :           1 :           });
     647                 :             :         }
     648                 :             :       }
     649   [ +  -  +  - ]:           1 :       dueMenu.exec(QCursor::pos());
     650         [ +  - ]:           2 :     } else if (act == QStringLiteral("due-clear")) {
     651         [ +  - ]:           2 :       modifyCurrentTask([](CalendarTask &t) { t.due = {}; });
     652                 :             :     }
     653                 :          29 :   });
     654                 :             : 
     655         [ +  - ]:          30 :   detailLayout->addWidget(m_headerBrowser);
     656                 :             : 
     657                 :             :   // ── Body/editor stack (toggles between rendered view and editor) ──
     658   [ +  -  -  +  :          30 :   m_detailStack = new QStackedWidget();
                   -  - ]
     659                 :             : 
     660                 :             :   // Page 0: Rendered Markdown view (body only, no header)
     661   [ +  -  -  +  :          30 :   m_detailBrowser = new QTextBrowser();
                   -  - ]
     662         [ +  - ]:          60 :   m_detailBrowser->setObjectName(QStringLiteral("taskDetailBrowser"));
     663                 :          30 :   m_detailBrowser->setOpenExternalLinks(false);
     664                 :          30 :   m_detailBrowser->setOpenLinks(false);
     665                 :             :   // Sprint 69: styling via main.qss QTextBrowser#taskDetailBrowser.
     666         [ +  - ]:          60 :   m_detailBrowser->setObjectName(QStringLiteral("taskDetailBrowser"));
     667                 :          30 :   m_detailBrowser->document()->setDocumentMargin(0);
     668         [ +  - ]:          60 :   m_detailBrowser->setHtml(
     669                 :          60 :       QStringLiteral("<p style='color:%1;text-align:center;'>"
     670                 :             :                      "Keine Aufgabe ausgewählt</p>")
     671   [ +  -  +  - ]:          60 :           .arg(tok("@text_secondary")));
     672                 :             :   // Handle checkbox toggles and external links (body content)
     673                 :          30 :   connect(m_detailBrowser, &QTextBrowser::anchorClicked, this,
     674         [ +  - ]:          37 :           [this](const QUrl &url) {
     675   [ +  -  +  + ]:           7 :     if (url.scheme() == QStringLiteral("mailjd-task-toggle")) {
     676         [ +  - ]:           3 :       const QUrlQuery query(url);
     677                 :           3 :       bool indexOk = false;
     678                 :             :       const int cbIdx =
     679   [ +  -  +  - ]:           3 :           query.queryItemValue(QStringLiteral("index")).toInt(&indexOk);
     680                 :             :       const QString token =
     681         [ +  - ]:           3 :           query.queryItemValue(QStringLiteral("token"));
     682   [ +  -  +  -  :           5 :       if (!indexOk || token.isEmpty() || token != m_checkboxActionToken ||
             +  +  +  + ]
     683         [ -  + ]:           2 :           !m_renderedCheckboxIndexes.contains(cbIdx))
     684                 :           1 :         return;
     685                 :             : 
     686         [ +  - ]:           2 :       auto treeIdx = m_treeView->currentIndex();
     687         [ -  + ]:           2 :       if (!treeIdx.isValid()) return;
     688                 :           2 :       CalendarTask task = m_model->taskAt(treeIdx.row());
     689                 :             :       const bool sameTask =
     690                 :           4 :           (m_renderedCheckboxTaskId > 0 &&
     691   [ +  -  -  + ]:           2 :            task.id == m_renderedCheckboxTaskId) ||
     692   [ #  #  #  # ]:           0 :           (m_renderedCheckboxTaskId <= 0 &&
     693         [ #  # ]:           0 :            task.uid == m_renderedCheckboxTaskUid &&
     694         [ #  # ]:           0 :            task.accountId == m_renderedCheckboxAccountId &&
     695                 :           0 :            task.calendarPath == m_renderedCheckboxCalendarPath);
     696   [ +  -  -  +  :           2 :       if (!sameTask || task.description != m_renderedCheckboxSource)
                   -  + ]
     697                 :           0 :         return;
     698                 :             :       // Toggle checkbox in markdown source. Descriptions are unescaped
     699                 :             :       // centrally at parse time (T-79.C1/H5) — real newlines here.
     700         [ +  - ]:           2 :       QStringList lines = task.description.split(QLatin1Char('\n'));
     701                 :           2 :       int cbCount = 0;
     702                 :             :       static QRegularExpression cbRe(
     703   [ +  +  +  -  :           3 :           QStringLiteral("^(\\s*- )\\[([ xX])\\](\\s+.*)$"));
             +  -  -  - ]
     704                 :           2 :       bool toggled = false;
     705         [ +  - ]:           5 :       for (int i = 0; i < lines.size(); ++i) {
     706   [ +  -  +  - ]:           5 :         auto m = cbRe.match(lines[i]);
     707   [ +  -  +  + ]:           5 :         if (m.hasMatch()) {
     708         [ +  + ]:           3 :           if (cbCount == cbIdx) {
     709   [ +  -  +  - ]:           2 :             bool wasChecked = m.captured(2).toLower() == QStringLiteral("x");
     710   [ +  -  +  - ]:           6 :             lines[i] = m.captured(1) +
     711   [ +  +  +  -  :          10 :                 (wasChecked ? QStringLiteral("[ ]") : QStringLiteral("[x]")) +
          +  +  +  +  -  
                -  -  - ]
     712   [ +  -  +  - ]:           8 :                 m.captured(3);
     713                 :           2 :             toggled = true;
     714                 :           2 :             break;
     715                 :             :           }
     716                 :           1 :           cbCount++;
     717                 :             :         }
     718         [ +  + ]:           5 :       }
     719         [ -  + ]:           2 :       if (!toggled)
     720                 :           0 :         return;
     721         [ +  - ]:           2 :       task.description = lines.join(QLatin1Char('\n'));
     722         [ +  - ]:           2 :       task.lastModified = QDateTime::currentDateTimeUtc();
     723                 :             :       // Save scroll position before re-render
     724   [ +  -  +  - ]:           2 :       int scrollPos = m_detailBrowser->verticalScrollBar()->value();
     725         [ +  - ]:           2 :       emit taskSaveRequested(task);
     726                 :             :       // Restore scroll position after the re-render triggered by save
     727         [ +  - ]:           2 :       QTimer::singleShot(0, this, [this, scrollPos]() {
     728                 :           0 :         m_detailBrowser->verticalScrollBar()->setValue(scrollPos);
     729                 :           0 :       });
     730   [ +  -  +  -  :           8 :     } else if (isAllowedExternalScheme(url.scheme().toLower())) {
          +  +  +  +  +  
          -  +  -  +  -  
                   +  + ]
     731                 :           1 :       QDesktopServices::openUrl(url);
     732                 :             :     }
     733                 :             :   });
     734                 :             :   // Click on body area → open editor
     735                 :          30 :   m_detailBrowser->viewport()->installEventFilter(this);
     736                 :             : 
     737                 :             :   // Page 1: Markdown editor with syntax highlighting (Sprint 57)
     738   [ +  -  -  +  :          30 :   auto *editorContainer = new QWidget();
                   -  - ]
     739   [ +  -  -  +  :          30 :   auto *editorLayout = new QVBoxLayout(editorContainer);
                   -  - ]
     740                 :          30 :   editorLayout->setContentsMargins(0, 0, 0, 0);
     741                 :          30 :   editorLayout->setSpacing(0);
     742                 :             : 
     743                 :          30 :   setupEditorToolbar();
     744         [ +  - ]:          30 :   editorLayout->addWidget(m_editorToolbar);
     745                 :             : 
     746   [ +  -  -  +  :          30 :   m_descriptionEditor = new QTextEdit();
                   -  - ]
     747         [ +  - ]:          60 :   m_descriptionEditor->setObjectName(QStringLiteral("taskDescriptionEditor"));
     748                 :          30 :   m_descriptionEditor->setAcceptRichText(false);
     749   [ +  -  +  - ]:          30 :   m_descriptionEditor->setPlaceholderText(tr("Description (Markdown)..."));
     750                 :             :   // Sprint 69: styling via main.qss (QTextEdit#taskDescriptionEditor).
     751   [ +  -  +  -  :          30 :   m_highlighter = new MarkdownHighlighter(m_descriptionEditor->document());
             -  +  -  - ]
     752                 :          30 :   m_descriptionEditor->installEventFilter(this);
     753         [ +  - ]:          30 :   editorLayout->addWidget(m_descriptionEditor, 1);
     754                 :             : 
     755                 :          30 :   m_detailStack->addWidget(m_detailBrowser);   // page 0
     756                 :          30 :   m_detailStack->addWidget(editorContainer);   // page 1
     757                 :          30 :   m_detailStack->setCurrentIndex(0);
     758                 :             : 
     759         [ +  - ]:          30 :   detailLayout->addWidget(m_detailStack, 1);
     760                 :             : 
     761                 :             :   // Store container — used by the right splitter
     762                 :          30 :   m_detailStack->setParent(detailContainer);
     763                 :             :   // Replace the old m_detailStack reference for the splitter
     764                 :             :   // We add detailContainer to the splitter instead
     765                 :          30 :   m_detailContainer = detailContainer;
     766                 :          30 : }
     767                 :             : 
     768                 :             : // --- Editor Toolbar (Sprint 57) ---
     769                 :             : 
     770                 :          30 : void TaskListWidget::setupEditorToolbar() {
     771   [ +  -  +  -  :          30 :   m_editorToolbar = new QWidget();
             -  +  -  - ]
     772         [ +  - ]:          60 :   m_editorToolbar->setObjectName(QStringLiteral("taskEditorToolbar"));
     773         [ +  - ]:          30 :   m_editorToolbar->setFixedHeight(32);
     774                 :             :   // Sprint 69: styling via main.qss (QWidget#taskEditorToolbar).
     775                 :             : 
     776   [ +  -  +  -  :          30 :   auto *layout = new QHBoxLayout(m_editorToolbar);
             -  +  -  - ]
     777         [ +  - ]:          30 :   layout->setContentsMargins(8, 0, 8, 0);
     778         [ +  - ]:          30 :   layout->setSpacing(2);
     779                 :             : 
     780                 :         210 :   auto addBtn = [layout](const QString &label, const QString &tooltip,
     781                 :             :                           auto slot) {
     782   [ +  -  -  +  :         210 :     auto *btn = new QToolButton();
                   -  - ]
     783                 :         210 :     btn->setText(label);
     784                 :         210 :     btn->setToolTip(tooltip);
     785         [ +  - ]:         210 :     QObject::connect(btn, &QToolButton::clicked, slot);
     786         [ +  - ]:         210 :     layout->addWidget(btn);
     787                 :         210 :     return btn;
     788                 :          30 :   };
     789                 :             : 
     790   [ +  -  +  - ]:          60 :   auto *boldBtn = addBtn(QStringLiteral("B"), tr("Bold (Ctrl+B)"), [this]() {
     791         [ +  - ]:           3 :     insertMarkdownWrap(QStringLiteral("**"), QStringLiteral("**"));
     792                 :           1 :   });
     793                 :          30 :   m_boldBtn = boldBtn;
     794   [ +  -  +  - ]:          30 :   QFont bf = boldBtn->font();
     795         [ +  - ]:          30 :   bf.setBold(true);
     796         [ +  - ]:          30 :   boldBtn->setFont(bf);
     797                 :             : 
     798   [ +  -  +  - ]:          60 :   auto *italicBtn = addBtn(QStringLiteral("I"), tr("Italic (Ctrl+I)"), [this]() {
     799         [ +  - ]:           3 :     insertMarkdownWrap(QStringLiteral("*"), QStringLiteral("*"));
     800                 :           1 :   });
     801                 :          30 :   m_italicBtn = italicBtn;
     802   [ +  -  +  - ]:          30 :   QFont itf = italicBtn->font();
     803         [ +  - ]:          30 :   itf.setItalic(true);
     804         [ +  - ]:          30 :   italicBtn->setFont(itf);
     805                 :             : 
     806   [ +  -  +  - ]:          60 :   m_codeBtn = addBtn(QStringLiteral("<>"), tr("Code (Ctrl+E)"), [this]() {
     807         [ +  - ]:           3 :     insertMarkdownWrap(QStringLiteral("`"), QStringLiteral("`"));
     808                 :           1 :   });
     809                 :             : 
     810                 :             :   // Separator
     811   [ +  -  +  -  :          30 :   auto *sep = new QFrame();
             -  +  -  - ]
     812         [ +  - ]:          60 :   sep->setObjectName(QStringLiteral("taskToolbarSeparator"));
     813         [ +  - ]:          30 :   sep->setFrameShape(QFrame::VLine);
     814         [ +  - ]:          30 :   sep->setFixedWidth(1);
     815         [ +  - ]:          30 :   layout->addWidget(sep);
     816                 :             : 
     817   [ +  -  +  - ]:          60 :   m_headingBtn = addBtn(QStringLiteral("H"), tr("Heading"), [this]() {
     818         [ +  - ]:           2 :     insertMarkdownPrefix(QStringLiteral("## "));
     819                 :           1 :   });
     820                 :             : 
     821   [ +  -  +  - ]:          60 :   m_checkboxBtn = addBtn(QStringLiteral("\u2610"), tr("Checkbox"), [this]() {
     822         [ +  - ]:           2 :     insertMarkdownPrefix(QStringLiteral("- [ ] "));
     823                 :           1 :   });
     824                 :             : 
     825   [ +  -  +  - ]:          60 :   m_dividerBtn = addBtn(QStringLiteral("\u2014"), tr("Separator"), [this]() {
     826         [ +  - ]:           1 :     QTextCursor cursor = m_descriptionEditor->textCursor();
     827         [ +  - ]:           1 :     cursor.movePosition(QTextCursor::EndOfBlock);
     828         [ +  - ]:           1 :     cursor.insertText(QStringLiteral("\n---\n"));
     829         [ +  - ]:           1 :     m_descriptionEditor->setTextCursor(cursor);
     830                 :           1 :   });
     831                 :             : 
     832   [ +  -  +  - ]:          60 :   m_linkBtn = addBtn(QStringLiteral("\U0001F517"), tr("Link (Ctrl+K)"), [this]() {
     833         [ +  - ]:           2 :     QTextCursor cursor = m_descriptionEditor->textCursor();
     834   [ +  -  +  + ]:           2 :     if (cursor.hasSelection()) {
     835         [ +  - ]:           1 :       QString sel = cursor.selectedText();
     836   [ +  -  +  - ]:           2 :       cursor.insertText(QStringLiteral("[%1](url)").arg(sel));
     837                 :           1 :     } else {
     838         [ +  - ]:           1 :       int pos = cursor.position();
     839         [ +  - ]:           1 :       cursor.insertText(QStringLiteral("[Text](url)"));
     840         [ +  - ]:           1 :       cursor.setPosition(pos + 7);
     841         [ +  - ]:           1 :       cursor.setPosition(pos + 10, QTextCursor::KeepAnchor);
     842         [ +  - ]:           1 :       m_descriptionEditor->setTextCursor(cursor);
     843                 :             :     }
     844                 :           2 :   });
     845                 :             : 
     846         [ +  - ]:          30 :   layout->addStretch();
     847                 :          30 : }
     848                 :             : 
     849                 :           7 : void TaskListWidget::insertMarkdownWrap(const QString &before,
     850                 :             :                                          const QString &after) {
     851         [ +  - ]:           7 :   QTextCursor cursor = m_descriptionEditor->textCursor();
     852   [ +  -  +  + ]:           7 :   if (cursor.hasSelection()) {
     853         [ +  - ]:           1 :     QString sel = cursor.selectedText();
     854   [ +  -  +  -  :           1 :     cursor.insertText(before + sel + after);
                   +  - ]
     855                 :           1 :   } else {
     856         [ +  - ]:           6 :     int pos = cursor.position();
     857   [ +  -  +  - ]:           6 :     cursor.insertText(before + after);
     858         [ +  - ]:           6 :     cursor.setPosition(pos + before.length());
     859         [ +  - ]:           6 :     m_descriptionEditor->setTextCursor(cursor);
     860                 :             :   }
     861         [ +  - ]:           7 :   m_descriptionEditor->setFocus();
     862                 :           7 : }
     863                 :             : 
     864                 :           2 : void TaskListWidget::insertMarkdownPrefix(const QString &prefix) {
     865         [ +  - ]:           2 :   QTextCursor cursor = m_descriptionEditor->textCursor();
     866         [ +  - ]:           2 :   cursor.movePosition(QTextCursor::StartOfBlock);
     867         [ +  - ]:           2 :   cursor.insertText(prefix);
     868         [ +  - ]:           2 :   cursor.movePosition(QTextCursor::EndOfBlock);
     869         [ +  - ]:           2 :   m_descriptionEditor->setTextCursor(cursor);
     870         [ +  - ]:           2 :   m_descriptionEditor->setFocus();
     871                 :           2 : }
     872                 :             : 
     873                 :             : 
     874                 :             : 
     875                 :          27 : void TaskListWidget::setCalendarStore(CalendarStore *store) {
     876                 :          27 :   m_store = store;
     877                 :          27 :   reload();
     878                 :          27 : }
     879                 :             : 
     880                 :          84 : void TaskListWidget::reload() {
     881   [ +  -  +  +  :          84 :   if (!m_store || !m_store->isOpen()) {
                   +  + ]
     882         [ +  - ]:           4 :     m_model->setTasks({});
     883                 :           4 :     m_allTasks.clear();
     884         [ +  - ]:           8 :     m_detailBrowser->setHtml(
     885                 :           8 :         QStringLiteral("<p style='color:%1;text-align:center;'>"
     886                 :             :                        "Keine Aufgabe ausgewählt</p>")
     887   [ +  -  +  - ]:           8 :             .arg(tok("@text_secondary")));
     888                 :           4 :     return;
     889                 :             :   }
     890                 :             :   // Sprint 56: Always load all tasks — applyFilter() handles visibility
     891                 :             :   // of completed tasks based on m_showCompleted, isSelected, and sidebar filter
     892         [ +  - ]:          80 :   m_allTasks = m_store->allTasks();
     893                 :          80 :   updateSidebar();
     894                 :          80 :   applyFilter();
     895                 :             : }
     896                 :             : 
     897                 :           9 : void TaskListWidget::setShowCompleted(bool show) {
     898         [ +  + ]:           9 :   if (m_showCompleted != show) {
     899                 :           8 :     m_showCompleted = show;
     900                 :           8 :     saveSettings();
     901                 :           8 :     reload();
     902                 :             :   }
     903                 :           9 : }
     904                 :             : 
     905                 :           4 : void TaskListWidget::setFilterText(const QString &text) {
     906                 :           4 :   m_filterText = text;
     907                 :           4 :   applyFilter();
     908                 :           4 : }
     909                 :             : 
     910                 :           4 : void TaskListWidget::setSearchResults(const QList<CalendarTask> &results) {
     911                 :           4 :   m_model->setTasks(results);
     912   [ +  -  +  - ]:           4 :   if (m_model->rowCount() > 0)
     913   [ +  -  +  - ]:           4 :     m_treeView->setCurrentIndex(m_model->index(0, 0));
     914                 :           4 : }
     915                 :             : 
     916                 :           6 : void TaskListWidget::toggleCurrentTask() {
     917         [ +  - ]:          12 :   modifyCurrentTask([](CalendarTask &t) {
     918         [ +  + ]:           6 :     if (t.status == QStringLiteral("COMPLETED")) {
     919                 :           1 :       t.status = QStringLiteral("NEEDS-ACTION");
     920                 :           1 :       t.percentComplete = 0;
     921                 :           1 :       t.completedAt = {};
     922                 :             :     } else {
     923                 :           5 :       t.status = QStringLiteral("COMPLETED");
     924                 :           5 :       t.percentComplete = 100;
     925         [ +  - ]:           5 :       t.completedAt = QDateTime::currentDateTimeUtc();
     926                 :             :     }
     927                 :           6 :   });
     928                 :           6 : }
     929                 :             : 
     930                 :           2 : void TaskListWidget::deleteCurrentTask() {
     931         [ +  - ]:           2 :   auto idx = m_treeView->currentIndex();
     932         [ -  + ]:           2 :   if (!idx.isValid()) return;
     933                 :           2 :   const auto &task = m_model->taskAt(idx.row());
     934         [ -  + ]:           2 :   if (task.uid.isEmpty()) return;
     935                 :             : 
     936         [ +  - ]:           4 :   auto answer = PlainTextMessageBox::question(
     937         [ +  - ]:           4 :       this, tr("Delete task"),
     938   [ +  -  +  - ]:           6 :       tr("Really delete task \"%1\"?").arg(task.summary),
     939                 :             :       QMessageBox::Yes | QMessageBox::No, QMessageBox::No);
     940         [ +  + ]:           2 :   if (answer == QMessageBox::Yes)
     941         [ +  - ]:           1 :     emit taskDeleteRequested(task);
     942                 :             : }
     943                 :             : 
     944                 :          51 : void TaskListWidget::modifyCurrentTask(
     945                 :             :     const std::function<void(CalendarTask&)> &mutator) {
     946         [ +  - ]:          51 :   auto idx = m_treeView->currentIndex();
     947         [ -  + ]:          51 :   if (!idx.isValid()) return;
     948                 :          51 :   CalendarTask task = m_model->taskAt(idx.row());
     949         [ -  + ]:          51 :   if (task.uid.isEmpty()) return;
     950         [ +  - ]:          51 :   mutator(task);
     951         [ +  - ]:          51 :   task.lastModified = QDateTime::currentDateTimeUtc();
     952         [ +  - ]:          51 :   emit taskSaveRequested(task);
     953         [ +  - ]:          51 : }
     954                 :             : 
     955                 :           6 : void TaskListWidget::startDescriptionEdit() {
     956         [ -  + ]:           6 :   if (m_editing) return;
     957         [ +  - ]:           6 :   auto idx = m_treeView->currentIndex();
     958         [ -  + ]:           6 :   if (!idx.isValid()) return;
     959                 :             : 
     960                 :           6 :   const auto &task = m_model->taskAt(idx.row());
     961                 :           6 :   m_editingTaskId = task.id;
     962                 :           6 :   m_editingUid = task.uid;
     963                 :           6 :   m_editingAccountId = task.accountId;
     964                 :           6 :   m_editingCalendarPath = task.calendarPath;
     965         [ +  - ]:           6 :   m_descriptionEditor->setPlainText(task.description);
     966         [ +  - ]:           6 :   m_detailStack->setCurrentIndex(1);
     967         [ +  - ]:           6 :   m_descriptionEditor->setFocus();
     968                 :           6 :   m_editing = true;
     969                 :             : }
     970                 :             : 
     971                 :           7 : void TaskListWidget::finishDescriptionEdit(bool save) {
     972         [ +  + ]:           7 :   if (!m_editing) return;
     973                 :           6 :   m_editing = false;
     974         [ +  - ]:           6 :   m_detailStack->setCurrentIndex(0);
     975                 :             : 
     976         [ +  + ]:           6 :   if (save) {
     977                 :           4 :     CalendarTask task;
     978                 :           4 :     bool found = false;
     979   [ +  -  +  - ]:           8 :     for (int row = 0; row < m_model->rowCount(); ++row) {
     980                 :           8 :       const auto &candidate = m_model->taskAt(row);
     981   [ +  -  +  + ]:           8 :       if (matchesEditingTask(candidate)) {
     982                 :           4 :         task = candidate;
     983                 :           4 :         found = true;
     984                 :           4 :         break;
     985                 :             :       }
     986                 :             :     }
     987         [ -  + ]:           4 :     if (!found) {
     988   [ #  #  #  #  :           0 :       for (const auto &candidate : m_allTasks) {
                   #  # ]
     989   [ #  #  #  # ]:           0 :         if (matchesEditingTask(candidate)) {
     990                 :           0 :           task = candidate;
     991                 :           0 :           found = true;
     992                 :           0 :           break;
     993                 :             :         }
     994                 :             :       }
     995                 :             :     }
     996                 :             : 
     997         [ +  - ]:           4 :     QString newDesc = m_descriptionEditor->toPlainText();
     998   [ +  -  +  -  :           4 :     if (found && newDesc != task.description) {
                   +  - ]
     999                 :           4 :       task.description = newDesc;
    1000         [ +  - ]:           4 :       task.lastModified = QDateTime::currentDateTimeUtc();
    1001         [ +  - ]:           4 :       emit taskSaveRequested(task);
    1002                 :             :     }
    1003                 :           4 :   }
    1004                 :           6 :   m_editingTaskId = 0;
    1005                 :           6 :   m_editingUid.clear();
    1006                 :           6 :   m_editingAccountId.clear();
    1007                 :           6 :   m_editingCalendarPath.clear();
    1008                 :             :   // Re-render the detail view
    1009         [ +  - ]:           6 :   auto idx = m_treeView->currentIndex();
    1010         [ +  - ]:           6 :   if (idx.isValid())
    1011         [ +  - ]:           6 :     showDetail(idx.row());
    1012         [ +  - ]:           6 :   m_treeView->setFocus();
    1013                 :             : }
    1014                 :             : 
    1015                 :           8 : bool TaskListWidget::matchesEditingTask(const CalendarTask &task) const {
    1016   [ +  -  +  + ]:           8 :   if (m_editingTaskId > 0 && task.id == m_editingTaskId)
    1017                 :           4 :     return true;
    1018                 :             : 
    1019   [ +  -  +  -  :           4 :   if (m_editingUid.isEmpty() || task.uid != m_editingUid)
                   +  - ]
    1020                 :           4 :     return false;
    1021                 :             : 
    1022   [ #  #  #  #  :           0 :   if (!m_editingAccountId.isEmpty() && task.accountId != m_editingAccountId)
                   #  # ]
    1023                 :           0 :     return false;
    1024   [ #  #  #  # ]:           0 :   if (!m_editingCalendarPath.isEmpty() &&
    1025         [ #  # ]:           0 :       task.calendarPath != m_editingCalendarPath)
    1026                 :           0 :     return false;
    1027                 :             : 
    1028                 :           0 :   return true;
    1029                 :             : }
    1030                 :             : 
    1031                 :         740 : bool TaskListWidget::eventFilter(QObject *obj, QEvent *event) {
    1032                 :             :   // Esc in description editor → finish editing (don't propagate to MainWindow)
    1033   [ +  +  +  +  :         740 :   if (obj == m_descriptionEditor && event->type() == QEvent::FocusOut) {
                   +  + ]
    1034                 :             :     // Sprint 57b: Finish editing when clicking outside the editor
    1035                 :           1 :     finishDescriptionEdit(true);
    1036                 :           1 :     return false;
    1037                 :             :   }
    1038   [ +  +  +  +  :         739 :   if (obj == m_descriptionEditor && event->type() == QEvent::KeyPress) {
                   +  + ]
    1039                 :          20 :     auto *ke = static_cast<QKeyEvent *>(event);
    1040         [ +  + ]:          20 :     if (ke->key() == Qt::Key_Escape) {
    1041                 :           1 :       finishDescriptionEdit(true);
    1042                 :           1 :       return true; // consumed — don't propagate
    1043                 :             :     }
    1044                 :             :     // Sprint 57: Markdown keyboard shortcuts
    1045         [ +  + ]:          19 :     if (ke->modifiers() == Qt::ControlModifier) {
    1046   [ +  +  +  +  :          12 :       switch (ke->key()) {
                      + ]
    1047                 :           1 :       case Qt::Key_B:
    1048         [ +  - ]:           2 :         insertMarkdownWrap(QStringLiteral("**"), QStringLiteral("**"));
    1049                 :           1 :         return true;
    1050                 :           1 :       case Qt::Key_I:
    1051         [ +  - ]:           2 :         insertMarkdownWrap(QStringLiteral("*"), QStringLiteral("*"));
    1052                 :           1 :         return true;
    1053                 :           1 :       case Qt::Key_E:
    1054         [ +  - ]:           2 :         insertMarkdownWrap(QStringLiteral("`"), QStringLiteral("`"));
    1055                 :           1 :         return true;
    1056                 :           3 :       case Qt::Key_K: {
    1057         [ +  - ]:           3 :         QTextCursor cursor = m_descriptionEditor->textCursor();
    1058   [ +  -  +  + ]:           3 :         if (cursor.hasSelection()) {
    1059         [ +  - ]:           1 :           QString sel = cursor.selectedText();
    1060   [ +  -  +  - ]:           2 :           cursor.insertText(QStringLiteral("[%1](url)").arg(sel));
    1061                 :           1 :         } else {
    1062         [ +  - ]:           2 :           cursor.insertText(QStringLiteral("[Text](url)"));
    1063                 :             :         }
    1064         [ +  - ]:           3 :         m_descriptionEditor->setTextCursor(cursor);
    1065                 :           3 :         return true;
    1066                 :           3 :       }
    1067                 :           6 :       default:
    1068                 :           6 :         break;
    1069                 :             :       }
    1070                 :             :     }
    1071                 :             :     // Sprint 57b: Auto-continue lists on Enter
    1072   [ +  +  -  +  :          13 :     if (ke->key() == Qt::Key_Return || ke->key() == Qt::Key_Enter) {
                   +  + ]
    1073         [ +  - ]:           7 :       QTextCursor cursor = m_descriptionEditor->textCursor();
    1074         [ +  - ]:           7 :       cursor.movePosition(QTextCursor::StartOfBlock, QTextCursor::KeepAnchor);
    1075         [ +  - ]:           7 :       QString currentLine = cursor.selectedText();
    1076         [ +  - ]:           7 :       cursor.clearSelection();
    1077         [ +  - ]:           7 :       cursor.movePosition(QTextCursor::EndOfBlock);
    1078         [ +  - ]:           7 :       m_descriptionEditor->setTextCursor(cursor);
    1079                 :             : 
    1080                 :             :       // Checkbox: "- [ ] " or "- [x] "
    1081   [ +  +  +  -  :           8 :       static QRegularExpression cbRe(QStringLiteral("^(\\s*)- \\[[xX ]\\]\\s"));
             +  -  -  - ]
    1082         [ +  - ]:           7 :       auto cbMatch = cbRe.match(currentLine);
    1083   [ +  -  +  + ]:           7 :       if (cbMatch.hasMatch()) {
    1084   [ +  -  +  -  :           3 :         if (currentLine.mid(cbMatch.capturedEnd()).trimmed().isEmpty()) {
             +  -  +  + ]
    1085         [ +  - ]:           1 :           cursor.movePosition(QTextCursor::StartOfBlock, QTextCursor::KeepAnchor);
    1086         [ +  - ]:           1 :           cursor.removeSelectedText();
    1087                 :           1 :           return true;
    1088                 :             :         }
    1089   [ +  -  +  -  :           4 :         cursor.insertText(QStringLiteral("\n%1- [ ] ").arg(cbMatch.captured(1)));
                   +  - ]
    1090         [ +  - ]:           2 :         m_descriptionEditor->setTextCursor(cursor);
    1091                 :           2 :         return true;
    1092                 :             :       }
    1093                 :             : 
    1094                 :             :       // Unordered list: "- " or "* " or "+ "
    1095   [ +  +  +  -  :           5 :       static QRegularExpression ulRe(QStringLiteral("^(\\s*)([-*+])\\s"));
             +  -  -  - ]
    1096         [ +  - ]:           4 :       auto ulMatch = ulRe.match(currentLine);
    1097   [ +  -  +  + ]:           4 :       if (ulMatch.hasMatch()) {
    1098   [ +  -  +  -  :           2 :         if (currentLine.mid(ulMatch.capturedEnd()).trimmed().isEmpty()) {
             +  -  +  + ]
    1099         [ +  - ]:           1 :           cursor.movePosition(QTextCursor::StartOfBlock, QTextCursor::KeepAnchor);
    1100         [ +  - ]:           1 :           cursor.removeSelectedText();
    1101                 :           1 :           return true;
    1102                 :             :         }
    1103   [ +  -  +  -  :           1 :         cursor.insertText(QStringLiteral("\n%1%2 ").arg(ulMatch.captured(1), ulMatch.captured(2)));
             +  -  +  - ]
    1104         [ +  - ]:           1 :         m_descriptionEditor->setTextCursor(cursor);
    1105                 :           1 :         return true;
    1106                 :             :       }
    1107                 :             : 
    1108                 :             :       // Ordered list: "1. " -> increment number
    1109   [ +  +  +  -  :           3 :       static QRegularExpression olRe(QStringLiteral("^(\\s*)(\\d+)\\.\\s"));
             +  -  -  - ]
    1110         [ +  - ]:           2 :       auto olMatch = olRe.match(currentLine);
    1111   [ +  -  +  - ]:           2 :       if (olMatch.hasMatch()) {
    1112   [ +  -  +  -  :           2 :         if (currentLine.mid(olMatch.capturedEnd()).trimmed().isEmpty()) {
             +  -  +  + ]
    1113         [ +  - ]:           1 :           cursor.movePosition(QTextCursor::StartOfBlock, QTextCursor::KeepAnchor);
    1114         [ +  - ]:           1 :           cursor.removeSelectedText();
    1115                 :           1 :           return true;
    1116                 :             :         }
    1117   [ +  -  +  - ]:           1 :         int nextNum = olMatch.captured(2).toInt() + 1;
    1118   [ +  -  +  -  :           1 :         cursor.insertText(QStringLiteral("\n%1%2. ").arg(olMatch.captured(1), QString::number(nextNum)));
             +  -  +  - ]
    1119         [ +  - ]:           1 :         m_descriptionEditor->setTextCursor(cursor);
    1120                 :           1 :         return true;
    1121                 :             :       }
    1122   [ -  +  -  +  :          27 :     }
          -  +  -  +  -  
                      + ]
    1123                 :             :   }
    1124                 :             : 
    1125                 :             :   // Click on detail browser body → open editor at click position
    1126   [ +  +  +  +  :        1030 :   if (obj == m_detailBrowser->viewport() &&
                   +  + ]
    1127                 :         305 :       event->type() == QEvent::MouseButtonPress) {
    1128                 :           1 :     auto *me = static_cast<QMouseEvent *>(event);
    1129         [ +  - ]:           1 :     if (me->button() == Qt::LeftButton) {
    1130                 :             :       // Check if clicking on an anchor (checkbox, external link) — let it through
    1131         [ +  - ]:           1 :       QPoint pos = me->pos();
    1132         [ +  - ]:           1 :       QString anchor = m_detailBrowser->anchorAt(pos);
    1133         [ -  + ]:           1 :       if (!anchor.isEmpty())
    1134                 :           0 :         return false; // let anchorClicked handle it
    1135                 :             : 
    1136                 :             :       // Only start edit if a task is selected
    1137         [ +  - ]:           1 :       auto idx = m_treeView->currentIndex();
    1138         [ +  - ]:           1 :       if (idx.isValid()) {
    1139                 :             :         // Get the clicked text from the rendered view for matching
    1140         [ +  - ]:           1 :         QTextCursor htmlCursor = m_detailBrowser->cursorForPosition(pos);
    1141   [ +  -  +  -  :           1 :         QString clickedBlockText = htmlCursor.block().text().trimmed();
                   +  - ]
    1142         [ +  - ]:           1 :         int clickCol = htmlCursor.positionInBlock();
    1143                 :             : 
    1144                 :             :         // Count which occurrence of this text we clicked (for duplicates)
    1145                 :           1 :         int occurrence = 0;
    1146                 :             :         {
    1147   [ +  -  +  - ]:           1 :           QTextBlock blk = m_detailBrowser->document()->begin();
    1148         [ +  - ]:           1 :           QTextBlock clickedBlk = htmlCursor.block();
    1149   [ +  -  +  -  :           1 :           while (blk.isValid() && blk != clickedBlk) {
             -  +  -  + ]
    1150   [ #  #  #  #  :           0 :             if (blk.text().trimmed() == clickedBlockText)
                   #  # ]
    1151                 :           0 :               ++occurrence;
    1152         [ #  # ]:           0 :             blk = blk.next();
    1153                 :             :           }
    1154                 :             :         }
    1155                 :             : 
    1156         [ +  - ]:           1 :         startDescriptionEdit();
    1157                 :             :         // Find the Nth matching line in the raw Markdown
    1158   [ +  -  +  -  :           1 :         if (m_descriptionEditor && !clickedBlockText.isEmpty()) {
                   +  - ]
    1159                 :             :           static QRegularExpression syntaxRe(
    1160                 :           2 :               QStringLiteral("^(#{1,6}\\s+|[-*+]\\s+|\\d+\\.\\s+|"
    1161   [ +  -  +  -  :           3 :                              "- \\[[ xX]\\]\\s+|>\\s*)"));
             +  -  -  - ]
    1162         [ +  - ]:           1 :           QString plainMd = m_descriptionEditor->toPlainText();
    1163         [ +  - ]:           1 :           QStringList mdLines = plainMd.split(QLatin1Char('\n'));
    1164                 :           1 :           int targetLine = -1;
    1165                 :           1 :           int matchCount = 0;
    1166         [ +  - ]:           1 :           for (int i = 0; i < mdLines.size(); ++i) {
    1167   [ +  -  +  - ]:           1 :             QString stripped = mdLines[i].trimmed();
    1168         [ +  - ]:           1 :             stripped.remove(syntaxRe);
    1169   [ +  -  +  - ]:           2 :             if (!stripped.isEmpty() &&
    1170   [ +  -  -  + ]:           1 :                 (clickedBlockText.contains(stripped) ||
    1171   [ #  #  #  # ]:           0 :                  stripped.contains(clickedBlockText))) {
    1172         [ +  - ]:           1 :               if (matchCount == occurrence) {
    1173                 :           1 :                 targetLine = i;
    1174                 :           1 :                 break;
    1175                 :             :               }
    1176                 :           0 :               ++matchCount;
    1177                 :             :             }
    1178         [ -  + ]:           1 :           }
    1179         [ +  - ]:           1 :           if (targetLine >= 0) {
    1180         [ +  - ]:           1 :             QTextCursor editorCursor = m_descriptionEditor->textCursor();
    1181         [ +  - ]:           1 :             editorCursor.movePosition(QTextCursor::Start);
    1182         [ -  + ]:           1 :             for (int i = 0; i < targetLine; ++i) {
    1183   [ #  #  #  # ]:           0 :               if (!editorCursor.movePosition(QTextCursor::NextBlock))
    1184                 :           0 :                 break;
    1185                 :             :             }
    1186                 :             :             // Column offset: account for Markdown prefix length
    1187         [ +  - ]:           1 :             int prefixLen = mdLines[targetLine].length() -
    1188   [ +  -  +  - ]:           1 :                             mdLines[targetLine].trimmed().length();
    1189   [ +  -  +  - ]:           1 :             QString stripped = mdLines[targetLine].trimmed();
    1190         [ +  - ]:           1 :             stripped.remove(syntaxRe);
    1191   [ +  -  +  - ]:           1 :             int syntaxLen = mdLines[targetLine].trimmed().length() -
    1192                 :           1 :                             stripped.length();
    1193                 :           1 :             int col = prefixLen + syntaxLen + clickCol;
    1194   [ +  -  +  - ]:           1 :             int lineLen = editorCursor.block().length() - 1;
    1195         [ +  - ]:           1 :             editorCursor.movePosition(QTextCursor::Right,
    1196                 :             :                                       QTextCursor::MoveAnchor,
    1197                 :           1 :                                       qMin(col, lineLen));
    1198         [ +  - ]:           1 :             m_descriptionEditor->setTextCursor(editorCursor);
    1199                 :           1 :           }
    1200                 :           1 :         }
    1201                 :           1 :         return true;
    1202                 :           1 :       }
    1203         [ -  + ]:           1 :     }
    1204                 :             :   }
    1205                 :             : 
    1206                 :         724 :   return QWidget::eventFilter(obj, event);
    1207                 :             : }
    1208                 :           3 : void TaskListWidget::moveSelectionBy(int delta) {
    1209         [ +  - ]:           3 :   auto idx = m_treeView->currentIndex();
    1210         [ +  - ]:           3 :   int newRow = idx.isValid() ? idx.row() + delta : 0;
    1211   [ +  -  +  - ]:           3 :   newRow = qBound(0, newRow, m_model->rowCount() - 1);
    1212   [ +  -  +  - ]:           3 :   if (m_model->rowCount() > 0)
    1213   [ +  -  +  - ]:           3 :     m_treeView->setCurrentIndex(m_model->index(newRow, 0));
    1214                 :           3 : }
    1215                 :             : 
    1216                 :             : // --- Sidebar updating (T-454) ---
    1217                 :             : 
    1218                 :          82 : void TaskListWidget::updateSidebar() {
    1219                 :          82 :   m_sidebarList->blockSignals(true);
    1220         [ +  - ]:          82 :   int prevRow = m_sidebarList->currentRow();
    1221         [ +  - ]:          82 :   m_sidebarList->clear();
    1222                 :             : 
    1223   [ +  -  +  -  :          82 :   if (!m_store || !m_store->isOpen()) {
             -  +  -  + ]
    1224                 :           0 :     m_sidebarList->blockSignals(false);
    1225                 :           0 :     return;
    1226                 :             :   }
    1227                 :             : 
    1228                 :             :   // Count tasks for meta-folders (respecting hidden calendars)
    1229                 :          82 :   int totalOpen = 0, currentCount = 0, urgentCount = 0, completedCount = 0;
    1230         [ +  - ]:          82 :   QDateTime now = QDateTime::currentDateTimeUtc();
    1231         [ +  - ]:          82 :   QDateTime thirtyDays = now.addDays(30);
    1232         [ +  - ]:          82 :   QDateTime sevenDays = now.addDays(7);
    1233                 :             : 
    1234   [ +  -  +  -  :         465 :   for (const auto &task : m_allTasks) {
                   +  + ]
    1235         [ +  + ]:         383 :     if (m_hiddenCalendars.contains(task.calendarPath))
    1236                 :           4 :       continue;
    1237   [ +  +  +  - ]:        1434 :     bool isOpen = task.status != QStringLiteral("COMPLETED") &&
    1238   [ +  -  +  +  :         676 :                   task.status != QStringLiteral("CANCELLED");
             +  +  +  - ]
    1239         [ +  + ]:         379 :     if (isOpen) {
    1240                 :         297 :       totalOpen++;
    1241   [ +  -  +  +  :         318 :       if ((task.due.isValid() && task.due <= thirtyDays) ||
          +  -  +  +  -  
                      + ]
    1242   [ +  +  +  +  :         318 :           task.status == QStringLiteral("IN-PROCESS"))
          +  +  -  -  -  
                      - ]
    1243                 :         276 :         currentCount++;
    1244   [ +  -  +  +  :         297 :       if (task.due.isValid() && task.due <= sevenDays)
          +  -  +  +  +  
                      + ]
    1245                 :         276 :         urgentCount++;
    1246                 :             :     } else {
    1247                 :          82 :       completedCount++;
    1248                 :             :     }
    1249                 :             :   }
    1250                 :             : 
    1251                 :             :   // Meta-folders — use color-dot icons for consistent layout
    1252                 :         328 :   auto makeDotIcon = [](const QColor &color) -> QIcon {
    1253         [ +  - ]:         328 :     QPixmap px(16, 16);
    1254         [ +  - ]:         328 :     px.fill(Qt::transparent);
    1255         [ +  - ]:         328 :     QPainter p(&px);
    1256         [ +  - ]:         328 :     p.setRenderHint(QPainter::Antialiasing);
    1257   [ +  -  +  - ]:         328 :     p.setBrush(color);
    1258         [ +  - ]:         328 :     p.setPen(Qt::NoPen);
    1259         [ +  - ]:         328 :     p.drawEllipse(1, 1, 14, 14);
    1260         [ +  - ]:         656 :     return QIcon(px);
    1261                 :         328 :   };
    1262                 :             : 
    1263                 :         328 :   auto addMeta = [this, &makeDotIcon](const QString &label, int count,
    1264                 :             :                                        const QString &filter,
    1265                 :             :                                        const QColor &dotColor) {
    1266                 :             :     auto *item = new QListWidgetItem(
    1267   [ +  -  +  -  :        1312 :         QStringLiteral("%1 (%2)").arg(label).arg(count));
          +  -  -  +  -  
                      - ]
    1268         [ +  - ]:         328 :     item->setData(Qt::UserRole, QString()); // empty path = meta
    1269         [ +  - ]:         328 :     item->setData(Qt::UserRole + 1, filter);
    1270   [ +  -  +  - ]:         328 :     item->setIcon(makeDotIcon(dotColor));
    1271                 :         328 :     m_sidebarList->addItem(item);
    1272                 :         328 :   };
    1273         [ +  - ]:          82 :   addMeta(QStringLiteral("Alle"), totalOpen,
    1274         [ +  - ]:         246 :           QStringLiteral("all"), QColor(tok("@text_secondary")));
    1275         [ +  - ]:          82 :   addMeta(QStringLiteral("Aktuell"), currentCount,
    1276         [ +  - ]:         246 :           QStringLiteral("current"), QColor(tok("@accent")));
    1277         [ +  - ]:          82 :   addMeta(QStringLiteral("Dringend"), urgentCount,
    1278         [ +  - ]:         246 :           QStringLiteral("urgent"), QColor(tok("@danger")));
    1279         [ +  - ]:          82 :   addMeta(QStringLiteral("Fertiggestellt"), completedCount,
    1280         [ +  - ]:         246 :           QStringLiteral("completed"), QColor(tok("@success")));
    1281                 :             : 
    1282                 :             :   // Separator
    1283   [ +  -  +  -  :          82 :   auto *sep = new QListWidgetItem();
             -  +  -  - ]
    1284         [ +  - ]:          82 :   sep->setFlags(Qt::NoItemFlags);
    1285         [ +  - ]:          82 :   sep->setSizeHint(QSize(0, 8));
    1286         [ +  - ]:          82 :   m_sidebarList->addItem(sep);
    1287                 :             : 
    1288                 :             :   // Calendar list
    1289         [ +  - ]:          82 :   auto counts = m_store->openTaskCountByCalendar();
    1290         [ +  - ]:          82 :   auto calendars = m_store->allCalendars();
    1291   [ +  -  +  -  :         237 :   for (const auto &cal : calendars) {
                   +  + ]
    1292         [ +  - ]:         155 :     int count = counts.value(cal.path, 0);
    1293                 :         155 :     QString label = cal.displayName.isEmpty()
    1294         [ -  + ]:         155 :                         ? cal.path.section(QLatin1Char('/'), -2, -2)
    1295         [ -  - ]:         155 :                         : cal.displayName;
    1296                 :             :     auto *item = new QListWidgetItem(
    1297   [ +  -  +  -  :         620 :         QStringLiteral("%1 (%2)").arg(label).arg(count));
          +  -  +  -  -  
                +  -  - ]
    1298         [ +  - ]:         155 :     item->setData(Qt::UserRole, cal.path);
    1299         [ +  - ]:         155 :     item->setData(Qt::UserRole + 1, cal.path);
    1300                 :             : 
    1301                 :             :     // Color marker
    1302   [ -  -  -  +  :         310 :     QColor calColor = cal.color.isEmpty() ? QColor(tok("@text_muted"))
                   -  - ]
    1303         [ -  + ]:         155 :                                           : QColor(cal.color);
    1304                 :             :     {
    1305         [ +  - ]:         155 :       QPixmap px(16, 16);
    1306         [ +  - ]:         155 :       px.fill(Qt::transparent);
    1307         [ +  - ]:         155 :       QPainter p(&px);
    1308         [ +  - ]:         155 :       p.setRenderHint(QPainter::Antialiasing);
    1309   [ +  -  +  - ]:         155 :       p.setBrush(calColor);
    1310         [ +  - ]:         155 :       p.setPen(Qt::NoPen);
    1311         [ +  - ]:         155 :       p.drawEllipse(1, 1, 14, 14);
    1312   [ +  -  +  - ]:         155 :       item->setIcon(QIcon(px));
    1313                 :         155 :     }
    1314                 :             : 
    1315                 :             :     // Hidden calendar styling
    1316         [ +  + ]:         155 :     if (m_hiddenCalendars.contains(cal.path)) {
    1317   [ +  -  +  -  :           1 :       item->setForeground(QColor(tok("@text_muted")));
                   +  - ]
    1318         [ +  - ]:           1 :       QFont f = item->font();
    1319         [ +  - ]:           1 :       f.setItalic(true);
    1320         [ +  - ]:           1 :       item->setFont(f);
    1321                 :           1 :     }
    1322                 :             : 
    1323                 :             :     // T-71.5b: native checkbox mirroring the CheckableFolderCombo pattern.
    1324                 :             :     // The check state is the primary visibility indicator; the existing
    1325                 :             :     // context-menu toggle (Sprint 56) remains a second way to change it.
    1326                 :             :     // Only calendar rows are checkable — meta-folders use addMeta and keep
    1327                 :             :     // the default (non-checkable) flags.
    1328         [ +  - ]:         155 :     item->setFlags(item->flags() | Qt::ItemIsUserCheckable);
    1329   [ +  +  +  - ]:         155 :     item->setCheckState(m_hiddenCalendars.contains(cal.path)
    1330                 :             :                             ? Qt::Unchecked : Qt::Checked);
    1331                 :             : 
    1332         [ +  - ]:         155 :     m_sidebarList->addItem(item);
    1333                 :         155 :   }
    1334                 :             : 
    1335                 :             :   // Restore selection
    1336   [ +  +  +  -  :          82 :   if (prevRow >= 0 && prevRow < m_sidebarList->count())
             +  -  +  + ]
    1337         [ +  - ]:          56 :     m_sidebarList->setCurrentRow(prevRow);
    1338                 :             :   else
    1339         [ +  - ]:          26 :     m_sidebarList->setCurrentRow(0); // "Alle" by default
    1340                 :             : 
    1341                 :          82 :   m_sidebarList->blockSignals(false);
    1342                 :          82 : }
    1343                 :             : 
    1344                 :           6 : void TaskListWidget::onSidebarClicked(int row) {
    1345   [ +  -  -  +  :           6 :   if (row < 0 || row >= m_sidebarList->count())
                   -  + ]
    1346                 :           0 :     return;
    1347                 :           6 :   auto *item = m_sidebarList->item(row);
    1348   [ +  -  +  +  :           6 :   if (!item || !(item->flags() & Qt::ItemIsEnabled))
                   +  + ]
    1349                 :           1 :     return;
    1350                 :             : 
    1351   [ +  -  +  - ]:           5 :   m_currentFilter = item->data(Qt::UserRole + 1).toString();
    1352                 :           5 :   applyFilter();
    1353                 :             : }
    1354                 :             : 
    1355                 :             : // --- Filtering (T-453) ---
    1356                 :             : 
    1357                 :          92 : void TaskListWidget::applyFilter() {
    1358                 :          92 :   QList<CalendarTask> filtered;
    1359                 :             : 
    1360                 :             :   // Sprint 56: Remember currently selected task so it stays visible
    1361                 :             :   // even after marking completed (user stays on it until they navigate away)
    1362                 :          92 :   QString selectedUid;
    1363         [ +  - ]:          92 :   auto selIdx = m_treeView->currentIndex();
    1364         [ +  + ]:          92 :   if (selIdx.isValid())
    1365                 :          63 :     selectedUid = m_model->taskAt(selIdx.row()).uid;
    1366                 :             : 
    1367   [ +  -  +  -  :         510 :   for (const auto &task : m_allTasks) {
                   +  + ]
    1368                 :             :     // Hidden calendar check
    1369         [ +  + ]:         418 :     if (m_hiddenCalendars.contains(task.calendarPath))
    1370                 :           4 :       continue;
    1371                 :             : 
    1372                 :             :     // Meta-folder filter
    1373   [ +  +  -  +  :        1567 :     bool isCompleted = task.status == QStringLiteral("COMPLETED") ||
                   +  - ]
    1374   [ +  +  +  +  :         739 :                        task.status == QStringLiteral("CANCELLED");
                   +  - ]
    1375                 :             : 
    1376                 :             :     // Keep the currently selected task visible regardless of filter
    1377   [ +  +  +  + ]:         414 :     bool isSelected = (!selectedUid.isEmpty() && task.uid == selectedUid);
    1378                 :             : 
    1379         [ +  + ]:         414 :     if (m_currentFilter == QStringLiteral("all")) {
    1380                 :             :       // "Alle" shows based on showCompleted toggle
    1381   [ +  +  +  +  :         389 :       if (isCompleted && !m_showCompleted && !isSelected)
                   +  + ]
    1382                 :          25 :         continue;
    1383         [ +  + ]:          25 :     } else if (m_currentFilter == QStringLiteral("current")) {
    1384   [ +  +  +  - ]:           5 :       if (isCompleted && !isSelected)
    1385                 :           1 :         continue;
    1386   [ +  -  +  - ]:           4 :       QDateTime thirtyDays = QDateTime::currentDateTimeUtc().addDays(30);
    1387   [ +  -  +  -  :           4 :       bool isCurrent = (task.due.isValid() && task.due <= thirtyDays) ||
          +  -  -  +  -  
                      - ]
    1388   [ -  +  -  +  :           4 :                        task.status == QStringLiteral("IN-PROCESS");
             -  -  -  - ]
    1389         [ -  + ]:           4 :       if (!isCurrent)
    1390                 :           0 :         continue;
    1391   [ +  -  +  + ]:          24 :     } else if (m_currentFilter == QStringLiteral("urgent")) {
    1392   [ +  +  +  - ]:           5 :       if (isCompleted && !isSelected)
    1393                 :           1 :         continue;
    1394   [ +  -  +  - ]:           4 :       QDateTime sevenDays = QDateTime::currentDateTimeUtc().addDays(7);
    1395   [ +  -  +  -  :           4 :       if (!task.due.isValid() || task.due > sevenDays)
          +  -  -  +  -  
                      + ]
    1396                 :           0 :         continue;
    1397   [ +  -  +  + ]:          19 :     } else if (m_currentFilter == QStringLiteral("completed")) {
    1398         [ +  + ]:           5 :       if (!isCompleted)
    1399                 :           4 :         continue;
    1400                 :             :     } else {
    1401                 :             :       // Calendar path filter
    1402         [ +  + ]:          10 :       if (task.calendarPath != m_currentFilter)
    1403                 :           5 :         continue;
    1404   [ +  +  +  -  :           5 :       if (isCompleted && !m_showCompleted && !isSelected)
                   -  + ]
    1405                 :           0 :         continue;
    1406                 :             :     }
    1407                 :             : 
    1408                 :             :     // Text filter
    1409         [ +  + ]:         378 :     if (!m_filterText.isEmpty()) {
    1410                 :             :       bool matches =
    1411   [ +  -  +  + ]:           7 :           task.summary.contains(m_filterText, Qt::CaseInsensitive) ||
    1412   [ +  -  -  + ]:           3 :           task.description.contains(m_filterText, Qt::CaseInsensitive);
    1413         [ +  + ]:           4 :       if (!matches)
    1414                 :           3 :         continue;
    1415                 :             :     }
    1416                 :             : 
    1417         [ +  - ]:         375 :     filtered.append(task);
    1418                 :             :   }
    1419                 :             : 
    1420                 :             :   // Remember current selection to restore after reload
    1421                 :          92 :   QString prevUid;
    1422         [ +  - ]:          92 :   auto curIdx = m_treeView->currentIndex();
    1423         [ +  + ]:          92 :   if (curIdx.isValid())
    1424                 :          63 :     prevUid = m_model->taskAt(curIdx.row()).uid;
    1425                 :             : 
    1426         [ +  - ]:          92 :   m_model->setTasks(filtered);
    1427                 :             :   // Restore previous selection if still in list, else select first
    1428                 :          92 :   bool restored = false;
    1429         [ +  + ]:          92 :   if (!prevUid.isEmpty()) {
    1430   [ +  -  +  + ]:         139 :     for (int i = 0; i < m_model->rowCount(); ++i) {
    1431         [ +  + ]:         136 :       if (m_model->taskAt(i).uid == prevUid) {
    1432   [ +  -  +  - ]:          60 :         m_treeView->setCurrentIndex(m_model->index(i, 0));
    1433                 :          60 :         restored = true;
    1434                 :          60 :         break;
    1435                 :             :       }
    1436                 :             :     }
    1437                 :             :   }
    1438   [ +  +  +  -  :          92 :   if (!restored && m_model->rowCount() > 0)
             +  +  +  + ]
    1439   [ +  -  +  - ]:          25 :     m_treeView->setCurrentIndex(m_model->index(0, 0));
    1440                 :          92 : }
    1441                 :             : 
    1442                 :             : // --- Detail Panel (T-456) ---
    1443                 :             : 
    1444                 :         105 : void TaskListWidget::showDetail(int row) {
    1445                 :             :   // Sprint 56: Finish any active inline edit before switching
    1446         [ +  + ]:         105 :   if (m_editing)
    1447         [ +  - ]:           2 :     finishDescriptionEdit(true);
    1448                 :             : 
    1449   [ +  -  +  -  :         105 :   if (row < 0 || row >= m_model->rowCount()) {
             -  +  -  + ]
    1450                 :           0 :     m_checkboxActionToken.clear();
    1451                 :           0 :     m_renderedCheckboxIndexes.clear();
    1452                 :           0 :     m_renderedCheckboxSource.clear();
    1453         [ #  # ]:           0 :     m_detailBrowser->setHtml(
    1454                 :           0 :         QStringLiteral("<p style='color:%1;text-align:center;'>"
    1455                 :             :                        "Keine Aufgabe ausgewählt</p>")
    1456   [ #  #  #  # ]:           0 :             .arg(tok("@text_secondary")));
    1457         [ #  # ]:           0 :     m_headerBrowser->setVisible(false);
    1458                 :           0 :     return;
    1459                 :             :   }
    1460                 :         105 :   const auto &task = m_model->taskAt(row);
    1461         [ +  - ]:         105 :   emit taskSelected(task);
    1462                 :             : 
    1463                 :             :   // Build HTML matching MailView's header style; colors come from the
    1464                 :             :   // active theme palette (67.B3). Font stack per DESIGN.md §3.
    1465                 :         210 :   QString html = QStringLiteral(
    1466                 :             :       "<style>"
    1467                 :             :       "body { margin:0; padding:0; "
    1468                 :             :       "  font-family: 'Inter', 'Noto Sans', sans-serif; }"
    1469                 :             :       ".header { background-color:%1;"
    1470                 :             :       "  border-bottom:1px solid %2;"
    1471                 :             :       "  padding:8px 8px 6px 8px; }"
    1472                 :             :       ".subject { font-size:16px; font-weight:bold; color:%3;"
    1473                 :             :       "  margin:0 0 4px 0; }"
    1474                 :             :       ".meta { font-size:12px; color:%4; padding:1px 0; }"
    1475                 :             :       ".meta a { color:inherit; text-decoration:none; cursor:pointer; }"
    1476                 :             :       ".meta .val { color:%3; }"
    1477                 :             :       ".overdue { color:%5; font-weight:bold; }"
    1478                 :             :       ".starred { color:%6; }"
    1479                 :             :       ".body { padding:12px 16px; font-size:13px; line-height:1.45;"
    1480                 :             :       "  color:%7; max-width:720px; }"
    1481                 :             :       "</style>")
    1482   [ +  -  +  -  :         210 :       .arg(tok("@bg_sidebar"), tok("@border_light"), tok("@text_primary"),
                   +  - ]
    1483   [ +  -  +  -  :         210 :            tok("@text_secondary"), tok("@danger"), tok("@star_active"),
                   +  - ]
    1484   [ +  -  +  - ]:         315 :            tok("@md_text"));
    1485                 :             : 
    1486                 :             :   // Header block (like MailView's m_headerFrame)
    1487         [ +  - ]:         105 :   html += QStringLiteral("<div class='header'>");
    1488                 :             : 
    1489                 :             :   // Task title (like MailView's m_subjectLabel)
    1490                 :         210 :   html += QStringLiteral("<div class='subject'>%1</div>")
    1491   [ +  -  +  -  :         210 :               .arg(task.summary.toHtmlEscaped());
                   +  - ]
    1492                 :             : 
    1493                 :             :   // Meta row 1: Status · Priorität · Favorit
    1494                 :             :   {
    1495         [ +  - ]:         105 :     html += QStringLiteral("<div class='meta'>");
    1496                 :             : 
    1497                 :             :     // Status — clickable toggle
    1498                 :         105 :     QString statusText;
    1499         [ +  + ]:         105 :     if (task.status == QStringLiteral("COMPLETED"))
    1500                 :          16 :       statusText = QStringLiteral("\u2713 Abgeschlossen");
    1501         [ +  + ]:          89 :     else if (task.status == QStringLiteral("IN-PROCESS"))
    1502                 :          11 :       statusText = QStringLiteral("\u25b6 In Bearbeitung");
    1503         [ -  + ]:          78 :     else if (task.status == QStringLiteral("CANCELLED"))
    1504                 :           0 :       statusText = QStringLiteral("\u2717 Abgebrochen");
    1505                 :             :     else
    1506                 :          78 :       statusText = QStringLiteral("\u25cb Offen");
    1507                 :         210 :     html += QStringLiteral(
    1508                 :             :         "Status: <a href='action:toggle-status'>"
    1509                 :             :         "<span class='val'>%1</span></a>")
    1510   [ +  -  +  - ]:         105 :         .arg(statusText);
    1511                 :             : 
    1512                 :             :     // Priorität — clickable cycle
    1513                 :         105 :     QString prioText;
    1514         [ +  + ]:         105 :     if (task.priority == 1)
    1515                 :          54 :       prioText = QStringLiteral("\u2605 Wichtig");
    1516   [ +  +  +  + ]:          51 :     else if (task.priority <= 4 && task.priority > 0)
    1517                 :           1 :       prioText = QStringLiteral("\u2191 Hoch");
    1518         [ +  + ]:          50 :     else if (task.priority == 5)
    1519                 :           5 :       prioText = QStringLiteral("\u25cf Mittel");
    1520         [ +  + ]:          45 :     else if (task.priority > 5)
    1521                 :           1 :       prioText = QStringLiteral("\u2193 Niedrig");
    1522                 :             :     else
    1523                 :          44 :       prioText = QStringLiteral("\u25cb Keine");
    1524                 :         210 :     html += QStringLiteral(
    1525                 :             :         "&nbsp;&nbsp;&nbsp;&nbsp;"
    1526                 :             :         "Priorit\u00e4t: <a href='action:cycle-priority'>"
    1527                 :             :         "<span class='val'>%1</span></a>")
    1528   [ +  -  +  - ]:         105 :         .arg(prioText);
    1529                 :             : 
    1530                 :             :     // Starred — clickable toggle
    1531         [ +  + ]:         105 :     if (task.isStarred()) {
    1532         [ +  - ]:          54 :       html += QStringLiteral(
    1533                 :             :           "&nbsp;&nbsp;&nbsp;&nbsp;"
    1534                 :             :           "<a href='action:toggle-star'>"
    1535                 :             :           "<span class='starred'>\u2605 Favorit</span></a>");
    1536                 :             :     }
    1537                 :             : 
    1538         [ +  - ]:         105 :     html += QStringLiteral("</div>");
    1539                 :         105 :   }
    1540                 :             : 
    1541                 :             :   // Meta row 2: Fällig · Fortschritt
    1542                 :             :   {
    1543         [ +  - ]:         105 :     html += QStringLiteral("<div class='meta'>");
    1544                 :             : 
    1545                 :             :     // Due date — clickable
    1546         [ +  - ]:         105 :     html += QStringLiteral("F\u00e4llig: ");
    1547   [ +  -  +  + ]:         105 :     if (task.due.isValid()) {
    1548         [ +  - ]:          94 :       QString dueStr = QLocale().toString(
    1549   [ +  -  +  - ]:          94 :           task.due.toLocalTime(), QStringLiteral("dd.MM.yyyy HH:mm"));
    1550   [ +  -  +  -  :         237 :       bool overdue = task.due < QDateTime::currentDateTimeUtc() &&
             +  +  -  - ]
    1551   [ +  -  +  +  :         143 :                      task.status != QStringLiteral("COMPLETED");
          +  +  +  -  -  
                -  -  - ]
    1552         [ +  + ]:          94 :       if (overdue) {
    1553                 :          98 :         html += QStringLiteral(
    1554                 :             :             "<a href='action:due-menu'>"
    1555                 :             :             "<span class='overdue'>%1 (\u00fcberf\u00e4llig)</span></a>"
    1556                 :             :             " <a href='action:due-clear' style='color:%2;"
    1557                 :             :             "font-size:10px;'>(\u00d7)</a>")
    1558   [ +  -  +  -  :          98 :             .arg(dueStr, tok("@danger"));
                   +  - ]
    1559                 :             :       } else {
    1560                 :          90 :         html += QStringLiteral(
    1561                 :             :             "<a href='action:due-menu'>"
    1562                 :             :             "<span class='val'>%1</span></a>"
    1563                 :             :             " <a href='action:due-clear' style='color:%2;"
    1564                 :             :             "font-size:10px;'>(\u00d7)</a>")
    1565   [ +  -  +  -  :          90 :             .arg(dueStr, tok("@danger"));
                   +  - ]
    1566                 :             :       }
    1567                 :          94 :     } else {
    1568                 :          22 :       html += QStringLiteral(
    1569                 :             :           "<a href='action:due-menu' style='color:%1;'>"
    1570   [ +  -  +  -  :          22 :           "+ setzen</a>").arg(tok("@link"));
                   +  - ]
    1571                 :             :     }
    1572                 :             : 
    1573                 :             :     // Fortschritt — clickable +/-
    1574                 :             :     {
    1575                 :         105 :       int pct = task.percentComplete;
    1576                 :         210 :       html += QStringLiteral(
    1577                 :             :           "&nbsp;&nbsp;&nbsp;&nbsp;"
    1578                 :             :           "Fortschritt: "
    1579                 :             :           "<a href='action:progress-down' style='color:%2;'>"
    1580                 :             :           "&minus;</a>"
    1581   [ +  -  +  -  :         315 :           " <span class='val'>%1</span>").arg(pct).arg(tok("@link"));
             +  -  +  - ]
    1582                 :         210 :       html += QStringLiteral(
    1583                 :             :           "%"
    1584                 :             :           " <a href='action:progress-up' style='color:%1;'>"
    1585   [ +  -  +  -  :         210 :           "+</a>").arg(tok("@link"));
                   +  - ]
    1586                 :             :     }
    1587                 :             : 
    1588         [ +  - ]:         105 :     html += QStringLiteral("</div>");
    1589                 :             :   }
    1590                 :             : 
    1591                 :             :   // Meta row 3: Kalender · sekundäre Infos (nur wenn Daten vorhanden)
    1592                 :             :   {
    1593                 :         105 :     QStringList parts;
    1594                 :             :     // Calendar
    1595   [ +  +  +  +  :         105 :     if (!task.calendarDisplayName.isEmpty() || !task.calendarPath.isEmpty()) {
                   +  + ]
    1596                 :         104 :       QString calName = task.calendarDisplayName.isEmpty()
    1597         [ +  + ]:         107 :                             ? task.calendarPath.section(QLatin1Char('/'), -2, -2)
    1598         [ +  - ]:         107 :                             : task.calendarDisplayName;
    1599                 :         104 :       QString colorDot;
    1600         [ +  + ]:         104 :       if (!task.color.isEmpty()) {
    1601                 :             :         // SEC-2026-07-21-19: Normalize the server-supplied color through
    1602                 :             :         // QColor so it cannot inject additional CSS properties into the
    1603                 :             :         // style attribute (e.g. "red; background: yellow"). QColor::name()
    1604                 :             :         // returns a validated "#RRGGBB" string or an empty string for
    1605                 :             :         // invalid input.
    1606                 :         101 :         QColor normalized(task.color);
    1607         [ +  - ]:         101 :         if (normalized.isValid())
    1608                 :         202 :           colorDot = QStringLiteral(
    1609                 :             :               "<span style='color:%1;'>\u25cf</span> ")
    1610   [ +  -  +  - ]:         202 :               .arg(normalized.name());
    1611                 :             :       }
    1612                 :         208 :       parts << QStringLiteral("Kalender: <span class='val'>%1%2</span>")
    1613   [ +  -  +  -  :         104 :                    .arg(colorDot, calName.toHtmlEscaped());
                   +  - ]
    1614                 :         104 :     }
    1615   [ +  -  -  + ]:         105 :     if (task.dtStart.isValid())
    1616         [ #  # ]:           0 :       parts << QStringLiteral("Start: <span class='val'>%1</span>").arg(
    1617   [ #  #  #  #  :           0 :           QLocale().toString(task.dtStart.toLocalTime(),
                   #  # ]
    1618         [ #  # ]:           0 :                              QStringLiteral("dd.MM.yyyy")));
    1619         [ -  + ]:         105 :     if (!task.organizer.isEmpty())
    1620                 :           0 :       parts << QStringLiteral("Ersteller: <span class='val'>%1</span>")
    1621   [ #  #  #  #  :           0 :                    .arg(task.organizer.toHtmlEscaped());
                   #  # ]
    1622   [ +  -  +  + ]:         105 :     if (task.created.isValid())
    1623         [ +  - ]:          16 :       parts << QStringLiteral("Erstellt: <span class='val'>%1</span>").arg(
    1624   [ +  -  +  -  :          16 :           QLocale().toString(task.created.toLocalTime(),
                   +  - ]
    1625         [ +  - ]:          12 :                              QStringLiteral("dd.MM.yyyy")));
    1626   [ +  -  +  + ]:         105 :     if (task.completedAt.isValid())
    1627   [ +  -  +  -  :           5 :       parts << QStringLiteral("Erledigt: <span class='val'>%1</span>").arg(QLocale().toString(
                   +  - ]
    1628   [ +  -  +  - ]:           3 :           task.completedAt.toLocalTime(), QStringLiteral("dd.MM.yyyy")));
    1629                 :             : 
    1630         [ +  + ]:         105 :     if (!parts.isEmpty())
    1631                 :         208 :       html += QStringLiteral("<div class='secondary'>%1</div>")
    1632   [ +  -  +  -  :         208 :                   .arg(parts.join(QStringLiteral(" · ")));
                   +  - ]
    1633                 :         105 :   }
    1634                 :             : 
    1635         [ +  - ]:         105 :   html += QStringLiteral("</div>"); // close .header
    1636                 :             : 
    1637                 :             :   // Set header HTML and auto-resize to content
    1638         [ +  - ]:         105 :   m_headerBrowser->setHtml(html);
    1639         [ +  - ]:         105 :   m_headerBrowser->setVisible(true);
    1640   [ +  -  +  -  :         105 :   m_headerBrowser->document()->setTextWidth(m_headerBrowser->viewport()->width());
                   +  - ]
    1641                 :         105 :   int headerHeight = static_cast<int>(
    1642   [ +  -  +  - ]:         105 :       m_headerBrowser->document()->size().height()) + 4;
    1643         [ +  - ]:         105 :   m_headerBrowser->setFixedHeight(headerHeight);
    1644                 :             : 
    1645                 :             :   // Body (Markdown rendered) — separate QTextBrowser, click to edit.
    1646                 :             :   // Font stack per DESIGN.md §3.
    1647                 :         210 :   QString bodyHtml = QStringLiteral(
    1648                 :             :       "<style>"
    1649                 :             :       "body { margin:0; padding:0; "
    1650                 :             :       "  font-family: 'Inter', 'Noto Sans', sans-serif; }"
    1651                 :             :       ".body { padding:12px 16px; font-size:13px; line-height:1.45;"
    1652                 :             :       "  color:%1; max-width:720px; }"
    1653   [ +  -  +  - ]:         105 :       "</style>").arg(tok("@md_text"));
    1654         [ +  + ]:         105 :   if (!task.description.isEmpty()) {
    1655                 :             :     m_checkboxActionToken =
    1656   [ +  -  +  - ]:         101 :         QUuid::createUuid().toString(QUuid::WithoutBraces);
    1657                 :             :     const auto rendered = MarkdownRenderer::toInteractiveHtml(
    1658         [ +  - ]:         101 :         task.description, m_checkboxActionToken);
    1659                 :         101 :     m_renderedCheckboxIndexes = rendered.checkboxIndexes;
    1660                 :         101 :     m_renderedCheckboxTaskId = task.id;
    1661                 :         101 :     m_renderedCheckboxTaskUid = task.uid;
    1662                 :         101 :     m_renderedCheckboxAccountId = task.accountId;
    1663                 :         101 :     m_renderedCheckboxCalendarPath = task.calendarPath;
    1664                 :         101 :     m_renderedCheckboxSource = task.description;
    1665         [ +  - ]:         101 :     bodyHtml += QStringLiteral("<div class='body' "
    1666                 :             :         "style='cursor:text;min-height:40px;'>");
    1667         [ +  - ]:         101 :     bodyHtml += rendered.html;
    1668         [ +  - ]:         101 :     bodyHtml += QStringLiteral("</div>");
    1669                 :         101 :   } else {
    1670                 :           4 :     m_checkboxActionToken.clear();
    1671                 :           4 :     m_renderedCheckboxIndexes.clear();
    1672                 :           4 :     m_renderedCheckboxSource.clear();
    1673                 :           8 :     bodyHtml += QStringLiteral(
    1674                 :             :         "<div class='body' style='cursor:text;min-height:40px;'>"
    1675                 :             :         "<p style='color:%1;font-style:italic;'>"
    1676                 :             :         "Klicken, um Beschreibung hinzuzuf\u00fcgen...</p>"
    1677   [ +  -  +  -  :           8 :         "</div>").arg(tok("@text_muted"));
                   +  - ]
    1678                 :             :   }
    1679                 :             : 
    1680         [ +  - ]:         105 :   m_detailBrowser->setHtml(bodyHtml);
    1681                 :         105 : }
    1682                 :             : 
    1683                 :             : // --- Settings persistence ---
    1684                 :             : 
    1685                 :          11 : void TaskListWidget::saveSettings() {
    1686         [ +  - ]:          11 :   QSettings s;
    1687         [ +  - ]:          11 :   s.beginGroup(QStringLiteral("taskView"));
    1688   [ +  -  +  - ]:          22 :   s.setValue(QStringLiteral("splitterH"), m_mainSplitter->saveState());
    1689   [ +  -  +  - ]:          22 :   s.setValue(QStringLiteral("splitterV"), m_rightSplitter->saveState());
    1690         [ +  - ]:          22 :   s.setValue(QStringLiteral("showCompleted"), m_showCompleted);
    1691         [ +  - ]:          22 :   s.setValue(QStringLiteral("hiddenCalendars"),
    1692   [ +  -  +  -  :          22 :              QStringList(m_hiddenCalendars.begin(), m_hiddenCalendars.end()));
                   +  - ]
    1693         [ +  - ]:          11 :   s.endGroup();
    1694                 :          11 : }
    1695                 :             : 
    1696                 :          30 : void TaskListWidget::restoreSettings() {
    1697         [ +  - ]:          30 :   QSettings s;
    1698         [ +  - ]:          30 :   s.beginGroup(QStringLiteral("taskView"));
    1699                 :             : 
    1700   [ +  -  +  + ]:          30 :   if (s.contains(QStringLiteral("splitterH")))
    1701   [ +  -  +  -  :          34 :     m_mainSplitter->restoreState(s.value(QStringLiteral("splitterH")).toByteArray());
                   +  - ]
    1702                 :             :   else
    1703   [ +  -  +  - ]:          13 :     m_mainSplitter->setSizes({220, 580});
    1704                 :             : 
    1705   [ +  -  +  + ]:          30 :   if (s.contains(QStringLiteral("splitterV")))
    1706   [ +  -  +  -  :          34 :     m_rightSplitter->restoreState(s.value(QStringLiteral("splitterV")).toByteArray());
                   +  - ]
    1707                 :             : 
    1708   [ +  -  +  - ]:          60 :   m_showCompleted = s.value(QStringLiteral("showCompleted"), false).toBool();
    1709                 :             : 
    1710   [ +  -  +  - ]:          30 :   auto hidden = s.value(QStringLiteral("hiddenCalendars")).toStringList();
    1711   [ +  -  +  -  :          30 :   m_hiddenCalendars = QSet<QString>(hidden.begin(), hidden.end());
                   +  - ]
    1712                 :             : 
    1713         [ +  - ]:          30 :   s.endGroup();
    1714                 :          30 : }
    1715                 :             : 
    1716                 :             : // --- Keyboard navigation ---
    1717                 :             : 
    1718                 :          27 : void TaskListWidget::keyPressEvent(QKeyEvent *event) {
    1719   [ +  +  +  -  :          27 :   switch (event->key()) {
          +  +  +  +  +  
          +  +  -  +  +  
                +  +  + ]
    1720                 :           2 :   case Qt::Key_J:
    1721                 :             :   case Qt::Key_Down:
    1722                 :           2 :     moveSelectionBy(1);
    1723                 :           2 :     break;
    1724                 :           1 :   case Qt::Key_K:
    1725                 :             :   case Qt::Key_Up:
    1726                 :           1 :     moveSelectionBy(-1);
    1727                 :           1 :     break;
    1728                 :           2 :   case Qt::Key_X:
    1729                 :             :   case Qt::Key_Space:
    1730                 :           2 :     toggleCurrentTask();
    1731                 :           2 :     break;
    1732                 :           0 :   case Qt::Key_D:
    1733                 :           0 :     deleteCurrentTask();
    1734                 :           0 :     break;
    1735                 :             :   // Sprint 56: Inline property shortcuts
    1736                 :           1 :   case Qt::Key_S:
    1737         [ +  - ]:           1 :     modifyCurrentTask([](CalendarTask &t) {
    1738         [ +  - ]:           1 :       t.priority = t.isStarred() ? 0 : 1;
    1739                 :           1 :     });
    1740                 :           1 :     break;
    1741                 :           1 :   case Qt::Key_0:
    1742         [ +  - ]:           1 :     modifyCurrentTask([](CalendarTask &t) { t.priority = 0; });
    1743                 :           1 :     break;
    1744                 :           1 :   case Qt::Key_1:
    1745         [ +  - ]:           1 :     modifyCurrentTask([](CalendarTask &t) { t.priority = 1; }); // starred
    1746                 :           1 :     break;
    1747                 :           1 :   case Qt::Key_2:
    1748         [ +  - ]:           1 :     modifyCurrentTask([](CalendarTask &t) { t.priority = 5; }); // mittel
    1749                 :           1 :     break;
    1750                 :           1 :   case Qt::Key_3:
    1751         [ +  - ]:           1 :     modifyCurrentTask([](CalendarTask &t) { t.priority = 6; }); // niedrig
    1752                 :           1 :     break;
    1753                 :           1 :   case Qt::Key_Plus:
    1754                 :             :   case Qt::Key_Equal: // unshifted + on US layouts
    1755         [ +  - ]:           1 :     modifyCurrentTask([](CalendarTask &t) {
    1756                 :           1 :       t.percentComplete = qMin(100, t.percentComplete + 10);
    1757         [ -  + ]:           1 :       if (t.percentComplete == 100)
    1758                 :           0 :         t.status = QStringLiteral("COMPLETED");
    1759         [ +  - ]:           1 :       else if (t.percentComplete > 0)
    1760                 :           1 :         t.status = QStringLiteral("IN-PROCESS");
    1761                 :           1 :     });
    1762                 :           1 :     break;
    1763                 :           1 :   case Qt::Key_Minus:
    1764         [ +  - ]:           1 :     modifyCurrentTask([](CalendarTask &t) {
    1765                 :           1 :       t.percentComplete = qMax(0, t.percentComplete - 10);
    1766         [ +  - ]:           1 :       if (t.percentComplete == 0)
    1767                 :           1 :         t.status = QStringLiteral("NEEDS-ACTION");
    1768                 :           1 :     });
    1769                 :           1 :     break;
    1770                 :           0 :   case Qt::Key_F:
    1771   [ #  #  #  # ]:           0 :     if (event->modifiers() & Qt::ShiftModifier)
    1772                 :           0 :       setShowCompleted(!m_showCompleted); // Shift+F → toggle completed
    1773                 :             :     else
    1774                 :           0 :       event->ignore(); // f → handled by MainWindow (search)
    1775                 :           0 :     break;
    1776                 :           1 :   case Qt::Key_Return:
    1777                 :             :   case Qt::Key_Enter: {
    1778                 :             :     // Sprint 39: Edit selected task
    1779         [ +  - ]:           1 :     auto idx = m_treeView->currentIndex();
    1780         [ +  - ]:           1 :     if (idx.isValid()) {
    1781                 :           1 :       const auto &task = m_model->taskAt(idx.row());
    1782         [ +  - ]:           1 :       if (!task.uid.isEmpty())
    1783         [ +  - ]:           1 :         emit taskUpdated(task);
    1784                 :             :     }
    1785                 :           1 :     break;
    1786                 :             :   }
    1787                 :           1 :   case Qt::Key_E: {
    1788                 :             :     // Sprint 39: 'e' = Edit selected task
    1789         [ +  - ]:           1 :     auto idx = m_treeView->currentIndex();
    1790         [ +  - ]:           1 :     if (idx.isValid()) {
    1791                 :           1 :       const auto &task = m_model->taskAt(idx.row());
    1792         [ +  - ]:           1 :       if (!task.uid.isEmpty())
    1793         [ +  - ]:           1 :         emit taskUpdated(task);
    1794                 :             :     }
    1795                 :           1 :     break;
    1796                 :             :   }
    1797                 :           2 :   case Qt::Key_N: {
    1798                 :             :     // Sprint 39: 'n' = New task
    1799                 :           2 :     emit taskCreateRequested();
    1800                 :           2 :     break;
    1801                 :             :   }
    1802                 :           1 :   case Qt::Key_Escape:
    1803         [ -  + ]:           1 :     if (m_editing) {
    1804                 :           0 :       finishDescriptionEdit(true);
    1805                 :           0 :       break;
    1806                 :             :     }
    1807                 :           1 :     event->ignore(); // Delegate to MainWindow
    1808                 :           1 :     break;
    1809                 :          10 :   default:
    1810                 :          10 :     event->ignore(); // Let MainWindow handle (CommandBar etc.)
    1811                 :          10 :     break;
    1812                 :             :   }
    1813                 :          27 : }
    1814                 :             : 
    1815                 :             : // T-76.B3: Runtime language switching
    1816                 :          36 : void TaskListWidget::changeEvent(QEvent *event) {
    1817                 :          36 :   QWidget::changeEvent(event);
    1818         [ -  + ]:          36 :   if (event->type() == QEvent::LanguageChange)
    1819                 :           0 :     retranslateUi();
    1820                 :          36 : }
    1821                 :             : 
    1822                 :           0 : void TaskListWidget::retranslateUi() {
    1823   [ #  #  #  # ]:           0 :   m_descriptionEditor->setPlaceholderText(tr("Description (Markdown)..."));
    1824   [ #  #  #  # ]:           0 :   m_boldBtn->setToolTip(tr("Bold (Ctrl+B)"));
    1825   [ #  #  #  # ]:           0 :   m_italicBtn->setToolTip(tr("Italic (Ctrl+I)"));
    1826   [ #  #  #  # ]:           0 :   m_codeBtn->setToolTip(tr("Code (Ctrl+E)"));
    1827   [ #  #  #  # ]:           0 :   m_headingBtn->setToolTip(tr("Heading"));
    1828   [ #  #  #  # ]:           0 :   m_checkboxBtn->setToolTip(tr("Checkbox"));
    1829   [ #  #  #  # ]:           0 :   m_dividerBtn->setToolTip(tr("Separator"));
    1830   [ #  #  #  # ]:           0 :   m_linkBtn->setToolTip(tr("Link (Ctrl+K)"));
    1831                 :           0 : }
        

Generated by: LCOV version 2.0-1