gui: category tree, menus, toolbar, splitter — build-order step 3

- CategoryPanel: the left tree — All Downloads / Unfinished / Finished,
  then Categories and Queues populated from category.list / queue.list,
  with per-node task counts. Selecting a node emits a TaskSelection.
- DownloadFilterProxy: QSortFilterProxyModel keyed off that selection.
  Client-side for M1 (the whole list fits); asTaskFilter() exposes the
  equivalent TaskFilter for a server-side download.list once paging lands.
- MainWindow: menu bar (Tasks / Downloads / View / Help) sharing QAction
  objects with the toolbar; QSplitter [panel | table]; Delete with a
  confirm; Resume/Pause All; View menu toggles the panel. Actions
  disabled while offline.
- Counts are computed off a throttled 400 ms timer, not the 4 Hz progress
  path. Fixed a debounce-vs-throttle bug found in the first screenshot:
  restarting the timer on every progress tick meant it never fired and
  the status bar sat at "0 of 0 downloads".
- First-run column widths that fit the content.
- tst_downloadfilterproxy: nodes filter to their own rows, the
  Finished/Unfinished split is correct, and the filter is dynamic (a row
  that finishes leaves the Unfinished node with no re-list). Verified
  against mockd --tasks 400 (screenshot: tree counts 75/84/89/83/69 sum
  to 400, status bar "400 of 400, 21 active").

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_016Ne28kx4VreeBWZv82Nksd
This commit is contained in:
2026-09-10 15:41:29 +04:00
co-authored by Claude Sonnet 5
parent 170bcfdb3e
commit 51fa1201bd
11 changed files with 655 additions and 44 deletions
+47
View File
@@ -0,0 +1,47 @@
// The left-hand category tree. Lane GUI.
//
// docs/03-gui-spec.md §1: All Downloads / Unfinished / Finished, then Categories and
// Queues from category.list / queue.list. Selecting a node emits the filter the table
// should apply. Per-node counts come later (they need the full task set, not just the
// filtered page).
#pragma once
#include <QHash>
#include <QJsonArray>
#include <QWidget>
#include "models/DownloadFilterProxy.hpp"
class QTreeWidget;
class QTreeWidgetItem;
namespace velox::gui {
class CategoryPanel : public QWidget {
Q_OBJECT
public:
explicit CategoryPanel(QWidget *parent = nullptr);
public slots:
void setCategories(const QJsonArray &items);
void setQueues(const QJsonArray &items);
/// Per-node task counts, keyed by the node's filter. Missing keys render no count.
void setCounts(int all, int unfinished, int finished, const QHash<QString, int> &byCategory,
const QHash<QString, int> &byQueue);
signals:
void selectionChanged(const velox::gui::TaskSelection &selection);
private:
QTreeWidgetItem *makeItem(QTreeWidgetItem *parent, const QString &label,
const TaskSelection &sel);
void onCurrentItemChanged(QTreeWidgetItem *current);
QTreeWidget *tree_;
QTreeWidgetItem *categoriesRoot_;
QTreeWidgetItem *queuesRoot_;
};
} // namespace velox::gui