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: 95.1 % 1237 1176
Test Date: 2026-06-21 21:10:19 Functions: 100.0 % 109 109
Legend: Lines:     hit not hit
Branches: + taken - not taken # not executed
Branches: 54.6 % 2428 1326

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

Generated by: LCOV version 2.0-1