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
+31 -7
View File
@@ -1,22 +1,26 @@
// The main window. Lane GUI.
//
// M1 slice: category tree, dialogs and the tray come later. This is the table driven by
// mockd, a status-bar connection dot, an offline banner instead of a modal, and the three
// selection actions that need no dialog (pause / resume / cancel).
// M1 slice: dialogs and the tray still come later. This is the menus + toolbar + splitter
// (category tree | table), the table driven by mockd, a status-bar connection dot, an
// offline banner instead of a modal, and the actions that need no dialog.
#pragma once
#include <QHash>
#include <QMainWindow>
#include "models/DownloadFilterProxy.hpp"
#include "rpc/Protocol.hpp"
class QAction;
class QLabel;
class QSortFilterProxyModel;
class QTimer;
class QTreeView;
namespace velox::gui {
class DownloadTableModel;
class CategoryPanel;
namespace rpc {
class RpcClient;
} // namespace rpc
@@ -34,27 +38,47 @@ class MainWindow : public QMainWindow {
private slots:
void onConnectionState(rpc::ConnectionState state);
void onSpeedGlobal(const QJsonObject &params);
void updateCounts();
void onFilterChanged(const velox::gui::TaskSelection &selection);
void scheduleCounts(); // throttled: fires refreshCounts() at most every 400 ms
void refreshCounts();
void pauseSelection();
void resumeSelection();
void cancelSelection();
void removeSelection();
void resumeAll();
void pauseAll();
private:
void buildActions();
void buildMenus();
void buildToolBar();
void fetchTree();
QStringList selectedTaskIds() const;
void actOnSelection(const char *methodName);
QStringList allTaskIds() const;
void actOnTasks(const char *methodName, const QStringList &ids);
void restoreLayout();
void saveLayout();
rpc::RpcClient *client_;
DownloadTableModel *model_;
QSortFilterProxyModel *proxy_;
DownloadFilterProxy *proxy_;
QTreeView *view_;
CategoryPanel *panel_;
QAction *actAddUrl_ = nullptr;
QAction *actResume_ = nullptr;
QAction *actPause_ = nullptr;
QAction *actStop_ = nullptr;
QAction *actRemove_ = nullptr;
QAction *actResumeAll_ = nullptr;
QAction *actPauseAll_ = nullptr;
QLabel *connDot_;
QLabel *connText_;
QLabel *countsLabel_;
QWidget *offlineBanner_;
QTimer *countsTimer_;
qint64 globalDownBps_ = 0;
};