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:
@@ -0,0 +1,71 @@
|
||||
#include "models/DownloadFilterProxy.hpp"
|
||||
|
||||
#include <QJsonArray>
|
||||
#include <QJsonObject>
|
||||
|
||||
#include "models/DownloadTableModel.hpp"
|
||||
|
||||
namespace velox::gui {
|
||||
namespace {
|
||||
|
||||
// "Finished" is the completed pile; everything else — active, paused, queued, errored,
|
||||
// cancelled — is "Unfinished", matching IDM's two top-level nodes.
|
||||
bool isFinished(const QString &state) {
|
||||
return state == QLatin1String("complete");
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
DownloadFilterProxy::DownloadFilterProxy(QObject *parent) : QSortFilterProxyModel(parent) {
|
||||
setDynamicSortFilter(true);
|
||||
}
|
||||
|
||||
void DownloadFilterProxy::setSelection(const TaskSelection &selection) {
|
||||
if (selection_ == selection) {
|
||||
return;
|
||||
}
|
||||
selection_ = selection;
|
||||
beginFilterChange();
|
||||
endFilterChange(QSortFilterProxyModel::Direction::Rows);
|
||||
}
|
||||
|
||||
bool DownloadFilterProxy::filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const {
|
||||
if (selection_.kind == TaskSelection::All) {
|
||||
return true;
|
||||
}
|
||||
const QModelIndex idx = sourceModel()->index(sourceRow, 0, sourceParent);
|
||||
switch (selection_.kind) {
|
||||
case TaskSelection::All:
|
||||
return true;
|
||||
case TaskSelection::Unfinished:
|
||||
return !isFinished(idx.data(DownloadTableModel::StateRole).toString());
|
||||
case TaskSelection::Finished:
|
||||
return isFinished(idx.data(DownloadTableModel::StateRole).toString());
|
||||
case TaskSelection::Category:
|
||||
return idx.data(DownloadTableModel::CategoryIdRole).toString() == selection_.id;
|
||||
case TaskSelection::Queue:
|
||||
return idx.data(DownloadTableModel::QueueIdRole).toString() == selection_.id;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
QJsonObject DownloadFilterProxy::asTaskFilter() const {
|
||||
switch (selection_.kind) {
|
||||
case TaskSelection::All:
|
||||
return {};
|
||||
case TaskSelection::Finished:
|
||||
return QJsonObject{{"states", QJsonArray{"complete"}}};
|
||||
case TaskSelection::Unfinished:
|
||||
return QJsonObject{
|
||||
{"states",
|
||||
QJsonArray{"new", "probing", "queued", "connecting", "downloading", "paused",
|
||||
"retry_wait", "assembling", "verifying", "failed", "cancelled"}}};
|
||||
case TaskSelection::Category:
|
||||
return QJsonObject{{"categoryId", selection_.id}};
|
||||
case TaskSelection::Queue:
|
||||
return QJsonObject{{"queueId", selection_.id}};
|
||||
}
|
||||
return {};
|
||||
}
|
||||
|
||||
} // namespace velox::gui
|
||||
@@ -0,0 +1,44 @@
|
||||
// Category-tree filtering for the download table. Lane GUI.
|
||||
//
|
||||
// docs/03-gui-spec.md §1: the left tree selects which rows the table shows. The daemon
|
||||
// does the heavy filtering for a 100k-row list; this proxy is the client-side view on top
|
||||
// of whatever download.list returned, so a task that changes state or category mid-view
|
||||
// leaves the filtered set immediately rather than lingering until the next re-list.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QSortFilterProxyModel>
|
||||
#include <QString>
|
||||
|
||||
namespace velox::gui {
|
||||
|
||||
/// One node of the category tree, expressed as a filter.
|
||||
struct TaskSelection {
|
||||
enum Kind { All, Unfinished, Finished, Category, Queue };
|
||||
Kind kind = All;
|
||||
QString id; // categoryId or queueId, when kind is Category or Queue
|
||||
|
||||
bool operator==(const TaskSelection &) const = default;
|
||||
};
|
||||
|
||||
class DownloadFilterProxy : public QSortFilterProxyModel {
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit DownloadFilterProxy(QObject *parent = nullptr);
|
||||
|
||||
void setSelection(const TaskSelection &selection);
|
||||
TaskSelection selection() const { return selection_; }
|
||||
|
||||
/// Equivalent TaskFilter params for a server-side download.list, so a caller can push
|
||||
/// the same filter to the daemon when paging matters. Empty object for `All`.
|
||||
QJsonObject asTaskFilter() const;
|
||||
|
||||
protected:
|
||||
bool filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const override;
|
||||
|
||||
private:
|
||||
TaskSelection selection_;
|
||||
};
|
||||
|
||||
} // namespace velox::gui
|
||||
@@ -89,6 +89,10 @@ QVariant DownloadTableModel::data(const QModelIndex &index, int role) const {
|
||||
return r.taskId;
|
||||
case StateRole:
|
||||
return r.state;
|
||||
case CategoryIdRole:
|
||||
return r.categoryId;
|
||||
case QueueIdRole:
|
||||
return r.queueId;
|
||||
case ProgressRole:
|
||||
return progressOf(r);
|
||||
case Qt::TextAlignmentRole:
|
||||
|
||||
@@ -34,7 +34,9 @@ class DownloadTableModel : public QAbstractTableModel {
|
||||
enum Roles {
|
||||
ProgressRole = Qt::UserRole + 1, ///< double in [0,1], -1 when the size is unknown
|
||||
TaskIdRole,
|
||||
StateRole, ///< the TaskState as a lowercase string
|
||||
StateRole, ///< the TaskState as a lowercase string
|
||||
CategoryIdRole, ///< categoryId, or empty
|
||||
QueueIdRole, ///< queueId, or empty
|
||||
};
|
||||
|
||||
explicit DownloadTableModel(QObject *parent = nullptr);
|
||||
|
||||
Reference in New Issue
Block a user