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 ¤t, 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 : : " "
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 : : " "
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 : : " "
1551 : : "Fortschritt: "
1552 : : "<a href='action:progress-down' style='color:%2;'>"
1553 : : "−</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 : }
|