Files
vdm/gui/src/mainwindow/MainWindow.cpp
T
samiandClaude Sonnet 5 c2eef96175 gui: floating drop target, clipboard global shortcut, theming, UI watchdog
Continues the build order past Options/Scheduler/Speed Limiter/Batch/
Grabber/tray.

- DropTargetWidget: frameless always-on-top drop target (docs/03-gui-spec.md
  §5), position persisted, accepts a dropped http(s) URL or link text and
  opens FileInfoDialog directly (skipping Add URL, since the URL is already
  known). Shown/hidden from general.showDropTarget, live via
  event.settings.changed, same pattern MainWindow already used for
  general.minimizeToTray.
- Clipboard, explicit path #2 (docs/06-risks-and-spikes.md R2):
  GlobalShortcut wraps org.freedesktop.portal.GlobalShortcuts
  (CreateSession -> BindShortcuts -> Activated), triggering the same Add URL
  flow. Guarded end-to-end on `if(TARGET Qt6::DBus)` / VELOX_GUI_HAVE_DBUS
  so a build without the component degrades to "feature skipped," not
  broken (gui/docs/pkg-qa-requests-m1.md R4). Best-effort by design per the
  risk doc: fails silent, never advertised.

  Verified live against the real portal (a real Wayland session, not just
  offscreen): `CreateSession` refuses every caller with "An app id is
  required" — reproduced identically via a bare `busctl` call with no Qt
  involved at all, so this is the portal requiring a sandboxed caller
  identity, not something fixable from an unconfined process. Recorded as
  a partial Spike S2 answer in docs/06-risks-and-spikes.md: this explicit
  path likely doesn't work for Velox as a traditionally-packaged app on
  stock GNOME, only if/when it ships confined. Also fixed a real leak this
  verification caught: QDBusInterface's introspection cache reads as a
  LeakSanitizer leak the first time anything touches D-Bus (tst_rtl went
  red under ASan) — switched to QDBusMessage::createMethodCall, which
  needs no introspection.
- Theming (docs/03-gui-spec.md §7): gui/resources/qss/{idm-like,dark}.qss,
  each with a documented palette block up top (QSS itself has no variable
  syntax), applied by ThemeManager and kept live via
  QStyleHints::colorSchemeChanged. util/Theme.hpp gives the handful of
  inline C++ styles (status dot, offline banner, the eleven identical
  error-label styles across dialogs) named constants instead of a twelfth
  copy of the same hex.
- UiThreadWatchdog: the M1 DoD's 200 ms debug-build watchdog. A background
  std::thread pings the UI thread every 50 ms via a queued invokeMethod and
  warns once (not per-poll) if a ping goes unanswered past 200 ms; no
  QThread, no Qt event loop of its own, so the watchdog itself can never be
  what blocks the thread it watches. No-op in a release build. Proven both
  ways in tst_uithreadwatchdog: fires on a genuinely blocked UI thread
  (synchronous sleep, no processEvents) and stays silent on a responsive
  one.

Full non-conformance suite (55 tests across every lane, `ctest -LE
conformance`) passes clean at this point, including the whole gui label
under ASan+UBSan.

Co-Authored-By: Claude Sonnet 5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01NSCdCWFXBTSBBK3MzWtJiC
2026-09-12 21:30:13 +04:00

622 lines
22 KiB
C++

#include "mainwindow/MainWindow.hpp"
#include <QAction>
#include <QCloseEvent>
#include <QCoreApplication>
#include <QHBoxLayout>
#include <QHeaderView>
#include <QItemSelectionModel>
#include <QJsonArray>
#include <QJsonObject>
#include <QKeySequence>
#include <QLabel>
#include <QLocale>
#include <QMenu>
#include <QMenuBar>
#include <QMessageBox>
#include <QSettings>
#include <QSplitter>
#include <QStatusBar>
#include <QSystemTrayIcon>
#include <QTimer>
#include <QToolBar>
#include <QTreeView>
#include <QVBoxLayout>
#include <QWidget>
#include "dialogs/AddUrlDialog.hpp"
#include "dialogs/BatchDialog.hpp"
#include "dialogs/FileInfoDialog.hpp"
#include "dialogs/GrabberWizard.hpp"
#include "dialogs/OptionsDialog.hpp"
#include "dialogs/ProgressDialog.hpp"
#include "dialogs/SchedulerDialog.hpp"
#include "dialogs/SpeedLimiterDialog.hpp"
#include "mainwindow/CategoryPanel.hpp"
#include "models/DownloadTableModel.hpp"
#include "rpc/RpcClient.hpp"
#include "tray/TrayIcon.hpp"
#include "util/Format.hpp"
#include "util/Theme.hpp"
#include "widgets/DropTargetWidget.hpp"
#include "widgets/ProgressDelegate.hpp"
#ifdef VELOX_GUI_HAVE_DBUS
#include "clipboard/GlobalShortcut.hpp"
#endif
namespace velox::gui {
namespace {
QString dotStyle(const QString &colour) {
return QStringLiteral("color: %1; font-size: 14px;").arg(colour);
}
} // namespace
MainWindow::MainWindow(rpc::RpcClient *client, QWidget *parent)
: QMainWindow(parent),
client_(client),
model_(new DownloadTableModel(this)),
proxy_(new DownloadFilterProxy(this)),
view_(new QTreeView(this)),
panel_(new CategoryPanel(this)),
connDot_(new QLabel(QStringLiteral("●"), this)),
connText_(new QLabel(this)),
countsLabel_(new QLabel(this)),
offlineBanner_(new QWidget(this)),
countsTimer_(new QTimer(this)) {
setWindowTitle(tr("Velox Download Manager"));
resize(1040, 600);
proxy_->setSourceModel(model_);
proxy_->setSortCaseSensitivity(Qt::CaseInsensitive);
view_->setModel(proxy_);
view_->setRootIsDecorated(false);
view_->setAlternatingRowColors(true);
view_->setUniformRowHeights(true); // keeps 10k rows scrolling at 60 fps
view_->setSelectionBehavior(QAbstractItemView::SelectRows);
view_->setSelectionMode(QAbstractItemView::ExtendedSelection);
view_->setSortingEnabled(true);
view_->setItemDelegateForColumn(DownloadTableModel::ColStatus, new ProgressDelegate(this));
view_->setContextMenuPolicy(Qt::CustomContextMenu);
connect(view_, &QWidget::customContextMenuRequested, this, &MainWindow::showTableContextMenu);
connect(view_, &QAbstractItemView::doubleClicked, this,
&MainWindow::openPropertiesForSelection);
view_->header()->setSectionsMovable(true);
view_->header()->setStretchLastSection(true);
// --- offline banner (shown instead of a modal error) -----------------------------
auto *bannerLayout = new QHBoxLayout(offlineBanner_);
bannerLayout->setContentsMargins(10, 6, 10, 6);
auto *bannerLabel = new QLabel(offlineBanner_);
bannerLabel->setObjectName(QStringLiteral("offlineBannerLabel"));
bannerLayout->addWidget(bannerLabel);
bannerLayout->addStretch();
offlineBanner_->setStyleSheet(
QStringLiteral("background: %1; color: %2;")
.arg(QLatin1String(theme::kOfflineBannerBg), QLatin1String(theme::kOfflineBannerText)));
offlineBanner_->setVisible(false);
auto *splitter = new QSplitter(Qt::Horizontal, this);
splitter->addWidget(panel_);
splitter->addWidget(view_);
splitter->setStretchFactor(0, 0);
splitter->setStretchFactor(1, 1);
splitter->setSizes({220, 800});
splitter->setChildrenCollapsible(false);
auto *central = new QWidget(this);
auto *layout = new QVBoxLayout(central);
layout->setContentsMargins(0, 0, 0, 0);
layout->setSpacing(0);
layout->addWidget(offlineBanner_);
layout->addWidget(splitter, 1);
setCentralWidget(central);
buildActions();
buildMenus();
buildToolBar();
buildTray();
buildDropTarget();
// --- status bar -----------------------------------------------------------------
connDot_->setStyleSheet(dotStyle(QLatin1String(theme::kDanger)));
statusBar()->addPermanentWidget(countsLabel_, 1);
statusBar()->addPermanentWidget(connText_);
statusBar()->addPermanentWidget(connDot_);
// Counts walk every row; keep that off the 4 Hz progress path.
countsTimer_->setSingleShot(true);
countsTimer_->setInterval(400);
connect(countsTimer_, &QTimer::timeout, this, &MainWindow::refreshCounts);
// --- wiring --------------------------------------------------------------------
connect(client_, &rpc::RpcClient::stateChanged, this, &MainWindow::onConnectionState);
connect(client_, &rpc::RpcClient::taskListReset, model_, &DownloadTableModel::resetFromJson);
connect(client_, &rpc::RpcClient::taskProgress, model_, &DownloadTableModel::applyProgress);
connect(client_, &rpc::RpcClient::taskAdded, model_, &DownloadTableModel::applyTaskAdded);
connect(client_, &rpc::RpcClient::taskStateChanged, model_,
&DownloadTableModel::applyTaskState);
connect(client_, &rpc::RpcClient::taskRemoved, model_, &DownloadTableModel::applyTaskRemoved);
connect(client_, &rpc::RpcClient::speedGlobal, this, &MainWindow::onSpeedGlobal);
connect(client_, &rpc::RpcClient::settingsChanged, this, &MainWindow::onSettingsChanged);
connect(model_, &QAbstractItemModel::modelReset, this, &MainWindow::scheduleCounts);
connect(model_, &QAbstractItemModel::rowsInserted, this, &MainWindow::scheduleCounts);
connect(model_, &QAbstractItemModel::rowsRemoved, this, &MainWindow::scheduleCounts);
connect(model_, &QAbstractItemModel::dataChanged, this, &MainWindow::scheduleCounts);
connect(panel_, &CategoryPanel::selectionChanged, this, &MainWindow::onFilterChanged);
restoreLayout();
onConnectionState(client_->state());
refreshCounts();
}
MainWindow::~MainWindow() = default;
void MainWindow::buildActions() {
actAddUrl_ = new QAction(tr("Add &URL…"), this);
actAddUrl_->setShortcut(QKeySequence(Qt::CTRL | Qt::Key_N));
connect(actAddUrl_, &QAction::triggered, this, &MainWindow::openAddUrlDialog);
actResume_ = new QAction(tr("&Resume"), this);
actResume_->setShortcut(QKeySequence(Qt::CTRL | Qt::Key_R));
connect(actResume_, &QAction::triggered, this, &MainWindow::resumeSelection);
actPause_ = new QAction(tr("&Pause"), this);
actPause_->setShortcut(QKeySequence(Qt::CTRL | Qt::Key_P));
connect(actPause_, &QAction::triggered, this, &MainWindow::pauseSelection);
actStop_ = new QAction(tr("&Stop"), this);
connect(actStop_, &QAction::triggered, this, &MainWindow::cancelSelection);
actRemove_ = new QAction(tr("&Delete"), this);
actRemove_->setShortcut(QKeySequence::Delete);
connect(actRemove_, &QAction::triggered, this, &MainWindow::removeSelection);
actResumeAll_ = new QAction(tr("Resume A&ll"), this);
connect(actResumeAll_, &QAction::triggered, this, &MainWindow::resumeAll);
actPauseAll_ = new QAction(tr("Pause All"), this);
connect(actPauseAll_, &QAction::triggered, this, &MainWindow::pauseAll);
actProperties_ = new QAction(tr("&Properties…"), this);
connect(actProperties_, &QAction::triggered, this, &MainWindow::openPropertiesForSelection);
actOptions_ = new QAction(tr("&Options…"), this);
connect(actOptions_, &QAction::triggered, this, &MainWindow::openOptionsDialog);
actScheduler_ = new QAction(tr("&Scheduler…"), this);
connect(actScheduler_, &QAction::triggered, this, &MainWindow::openSchedulerDialog);
actSpeedLimiter_ = new QAction(tr("Speed &Limiter…"), this);
connect(actSpeedLimiter_, &QAction::triggered, this, &MainWindow::openSpeedLimiterDialog);
actBatch_ = new QAction(tr("&Batch Download…"), this);
connect(actBatch_, &QAction::triggered, this, &MainWindow::openBatchDialog);
actGrabber_ = new QAction(tr("Site &Grabber…"), this);
connect(actGrabber_, &QAction::triggered, this, &MainWindow::openGrabberWizard);
actShow_ = new QAction(tr("&Show Velox"), this);
connect(actShow_, &QAction::triggered, this, &MainWindow::showAndRaise);
}
void MainWindow::buildMenus() {
QMenu *tasks = menuBar()->addMenu(tr("&Tasks"));
tasks->addAction(actAddUrl_);
tasks->addSeparator();
tasks->addAction(actResume_);
tasks->addAction(actPause_);
tasks->addAction(actStop_);
tasks->addSeparator();
tasks->addAction(actRemove_);
tasks->addAction(actProperties_);
tasks->addSeparator();
tasks->addAction(tr("E&xit"), QKeySequence::Quit, this, &QWidget::close);
QMenu *downloads = menuBar()->addMenu(tr("&Downloads"));
downloads->addAction(actResumeAll_);
downloads->addAction(actPauseAll_);
downloads->addSeparator();
downloads->addAction(actScheduler_);
downloads->addAction(actSpeedLimiter_);
QMenu *tools = menuBar()->addMenu(tr("&Tools"));
tools->addAction(actBatch_);
tools->addAction(actGrabber_);
tools->addSeparator();
tools->addAction(actOptions_);
QMenu *view = menuBar()->addMenu(tr("&View"));
QAction *togglePanel = view->addAction(tr("&Category Panel"));
togglePanel->setCheckable(true);
togglePanel->setChecked(true);
connect(togglePanel, &QAction::toggled, panel_, &QWidget::setVisible);
QMenu *help = menuBar()->addMenu(tr("&Help"));
help->addAction(tr("&About Velox"), this, [this] {
QMessageBox::about(this, tr("About Velox"),
tr("Velox Download Manager — GUI client (M1)."));
});
}
void MainWindow::buildToolBar() {
auto *tb = addToolBar(tr("Main"));
tb->setObjectName(QStringLiteral("mainToolBar"));
tb->setMovable(false);
tb->addAction(actAddUrl_);
tb->addSeparator();
tb->addAction(actResume_);
tb->addAction(actPause_);
tb->addAction(actStop_);
tb->addSeparator();
tb->addAction(actRemove_);
tb->addSeparator();
tb->addAction(actScheduler_);
tb->addAction(actOptions_);
tb->addAction(actGrabber_);
}
void MainWindow::buildTray() {
if (!QSystemTrayIcon::isSystemTrayAvailable()) {
return; // headless/mockd-only test environments, some minimal WMs
}
trayIcon_ = new TrayIcon(client_, actShow_, actAddUrl_, actResumeAll_, actPauseAll_, this);
connect(trayIcon_, &TrayIcon::quitRequested, qApp, &QCoreApplication::quit);
trayIcon_->show();
}
void MainWindow::buildDropTarget() {
// Qt::Tool + a parent keeps it grouped with the main window (no separate taskbar
// entry, destroyed when MainWindow is) while still floating independently per spec.
dropTarget_ = new DropTargetWidget(this);
connect(dropTarget_, &DropTargetWidget::urlDropped, this, &MainWindow::onUrlDropped);
connect(dropTarget_, &DropTargetWidget::addUrlRequested, this, &MainWindow::openAddUrlDialog);
#ifdef VELOX_GUI_HAVE_DBUS
// docs/06-risks-and-spikes.md R2, explicit path #2. Best-effort: requestBinding() is
// silent and permanent-for-this-session on any desktop that lacks the portal or
// declines the prompt — see GlobalShortcut's own header for why that's by design.
globalShortcut_ = new GlobalShortcut(this);
connect(globalShortcut_, &GlobalShortcut::activated, this, &MainWindow::openAddUrlDialog);
globalShortcut_->requestBinding();
#endif
}
void MainWindow::closeEvent(QCloseEvent *event) {
if (minimizeToTrayEnabled_ && trayIcon_ && trayIcon_->isVisible()) {
hide();
event->ignore();
return;
}
saveLayout();
QMainWindow::closeEvent(event);
event->accept();
}
void MainWindow::onConnectionState(rpc::ConnectionState state) {
connText_->setText(tr(rpc::toString(state)));
QString colour = QLatin1String(theme::kDanger);
if (state == rpc::ConnectionState::Connected) {
colour = QLatin1String(theme::kSuccess);
} else if (state != rpc::ConnectionState::Disconnected) {
colour = QLatin1String(theme::kWarning);
}
connDot_->setStyleSheet(dotStyle(colour));
const bool online = state == rpc::ConnectionState::Connected;
offlineBanner_->setVisible(!online);
if (auto *label = offlineBanner_->findChild<QLabel *>(QStringLiteral("offlineBannerLabel"))) {
label->setText(state == rpc::ConnectionState::Reconnecting
? tr("Reconnecting to the Velox service…")
: tr("The Velox service is not running. Downloads are unaffected; "
"this list is read-only until it returns."));
}
for (QAction *a :
{actResume_, actPause_, actStop_, actRemove_, actResumeAll_, actPauseAll_, actAddUrl_,
actProperties_, actScheduler_, actSpeedLimiter_, actBatch_, actGrabber_, actOptions_}) {
a->setEnabled(online);
}
if (online) {
fetchTree();
fetchGeneralUiSettings();
}
}
void MainWindow::fetchTree() {
client_->call(QStringLiteral("category.list"), {}, [this](const rpc::RpcReply &reply) {
if (reply.ok()) {
categoriesCache_ = reply.result.toObject().value("items").toArray();
panel_->setCategories(categoriesCache_);
}
});
client_->call(QStringLiteral("queue.list"), {}, [this](const rpc::RpcReply &reply) {
if (reply.ok()) {
queuesCache_ = reply.result.toObject().value("items").toArray();
panel_->setQueues(queuesCache_);
}
});
}
void MainWindow::fetchGeneralUiSettings() {
client_->call(
QStringLiteral("settings.get"),
QJsonObject{{"keys", QJsonArray{QStringLiteral("general.minimizeToTray"),
QStringLiteral("general.showDropTarget")}}},
[this](const rpc::RpcReply &reply) {
if (!reply.ok()) {
return;
}
const QJsonObject values = reply.result.toObject().value("values").toObject();
minimizeToTrayEnabled_ = values.value("general.minimizeToTray").toBool();
if (dropTarget_) {
dropTarget_->setVisible(values.value("general.showDropTarget").toBool(true));
}
});
}
void MainWindow::onSettingsChanged(const QJsonObject &params) {
for (const QJsonValue &key : params.value("keys").toArray()) {
const QString k = key.toString();
if (k == QLatin1String("general.minimizeToTray") ||
k == QLatin1String("general.showDropTarget")) {
fetchGeneralUiSettings();
break;
}
}
}
void MainWindow::onUrlDropped(const QString &url) {
auto *info = new FileInfoDialog(client_, url, categoriesCache_, queuesCache_, this);
info->setAttribute(Qt::WA_DeleteOnClose);
info->show();
}
void MainWindow::openAddUrlDialog() {
AddUrlDialog dlg(this);
if (dlg.exec() != QDialog::Accepted) {
return;
}
const QString url = dlg.url();
if (url.isEmpty()) {
return;
}
// Non-modal: several File Info dialogs can be open at once (spec §2), and it must
// never block the main window while download.probe is in flight.
auto *info = new FileInfoDialog(client_, url, categoriesCache_, queuesCache_, this);
info->setAttribute(Qt::WA_DeleteOnClose);
info->show();
}
void MainWindow::openPropertiesForSelection() {
const QStringList ids = selectedTaskIds();
if (ids.isEmpty()) {
return;
}
// One Progress dialog per selected task, same as double-clicking each row.
for (const QString &id : ids) {
auto *dlg = new ProgressDialog(client_, id, this);
dlg->show();
}
}
void MainWindow::openOptionsDialog() {
OptionsDialog dlg(client_, this);
dlg.exec();
}
void MainWindow::openSchedulerDialog() {
SchedulerDialog dlg(client_, this);
dlg.exec();
}
void MainWindow::openSpeedLimiterDialog() {
SpeedLimiterDialog dlg(client_, this);
dlg.exec();
}
void MainWindow::openBatchDialog() {
BatchDialog dlg(client_, categoriesCache_, queuesCache_, this);
dlg.exec();
}
void MainWindow::openGrabberWizard() {
auto *wizard = new GrabberWizard(client_, categoriesCache_, this);
wizard->setAttribute(Qt::WA_DeleteOnClose);
wizard->show();
}
void MainWindow::showAndRaise() {
show();
raise();
activateWindow();
}
// docs/03-gui-spec.md §1 lists a fuller row menu (Open · Open With · Open Folder ·
// Move/Rename · Redownload · Refresh Download Address · Add to Queue ▸ · ...). Those all
// need state this build order hasn't reached yet (Options' file-type associations aren't
// per-row actions, and Batch's "Add to Queue" is a different flow than re-filing an
// existing task) — offering them now would be a fake affordance.
void MainWindow::showTableContextMenu(const QPoint &pos) {
const QModelIndex idx = view_->indexAt(pos);
if (idx.isValid() && view_->selectionModel() && !view_->selectionModel()->isSelected(idx)) {
view_->selectionModel()->setCurrentIndex(
idx, QItemSelectionModel::ClearAndSelect | QItemSelectionModel::Rows);
}
if (selectedTaskIds().isEmpty()) {
return;
}
QMenu menu(this);
menu.addAction(actResume_);
menu.addAction(actPause_);
menu.addAction(actStop_);
menu.addSeparator();
menu.addAction(actRemove_);
menu.addSeparator();
menu.addAction(actProperties_);
menu.exec(view_->viewport()->mapToGlobal(pos));
}
void MainWindow::onFilterChanged(const TaskSelection &selection) {
proxy_->setSelection(selection);
// TODO(gui): also push proxy_->asTaskFilter() to a fresh download.list once the daemon
// is serving 100k-row lists and the client should not hold them all. For M1 the whole
// list fits and the proxy is the single source of what's shown.
refreshCounts();
}
void MainWindow::onSpeedGlobal(const QJsonObject &params) {
globalDownBps_ = static_cast<qint64>(params.value("downBps").toDouble());
scheduleCounts();
}
void MainWindow::scheduleCounts() {
// Throttle, not debounce: a restart-on-every-event timer never fires while progress
// ticks arrive faster than its interval. Start it only when it is not already pending.
if (!countsTimer_->isActive()) {
countsTimer_->start();
}
}
void MainWindow::refreshCounts() {
const int total = model_->rowCount();
int active = 0;
int unfinished = 0;
int finished = 0;
QHash<QString, int> byCategory;
QHash<QString, int> byQueue;
for (int i = 0; i < total; ++i) {
const QModelIndex idx = model_->index(i, 0);
const QString state = idx.data(DownloadTableModel::StateRole).toString();
if (state == QLatin1String("downloading")) {
++active;
}
if (state == QLatin1String("complete")) {
++finished;
} else {
++unfinished;
}
const QString cat = idx.data(DownloadTableModel::CategoryIdRole).toString();
if (!cat.isEmpty()) {
++byCategory[cat];
}
const QString queue = idx.data(DownloadTableModel::QueueIdRole).toString();
if (!queue.isEmpty()) {
++byQueue[queue];
}
}
panel_->setCounts(total, unfinished, finished, byCategory, byQueue);
if (trayIcon_) {
trayIcon_->setActiveCount(active);
}
const int shown = proxy_->rowCount();
countsLabel_->setText(tr("%1 of %2 downloads, %3 active ↓ %4")
.arg(shown)
.arg(total)
.arg(active)
.arg(fmt::rate(globalDownBps_)));
}
QStringList MainWindow::selectedTaskIds() const {
QStringList ids;
const auto rows =
view_->selectionModel() ? view_->selectionModel()->selectedRows() : QModelIndexList{};
ids.reserve(rows.size());
for (const QModelIndex &proxyIdx : rows) {
const QString id =
proxy_->mapToSource(proxyIdx).data(DownloadTableModel::TaskIdRole).toString();
if (!id.isEmpty()) {
ids << id;
}
}
return ids;
}
QStringList MainWindow::allTaskIds() const {
QStringList ids;
const int n = model_->rowCount();
ids.reserve(n);
for (int i = 0; i < n; ++i) {
ids << model_->index(i, 0).data(DownloadTableModel::TaskIdRole).toString();
}
return ids;
}
void MainWindow::actOnTasks(const char *methodName, const QStringList &ids) {
if (ids.isEmpty()) {
return;
}
client_->call(QString::fromLatin1(methodName),
QJsonObject{{"taskIds", QJsonArray::fromStringList(ids)}});
}
void MainWindow::pauseSelection() {
actOnTasks(rpc::method::kDownloadPause, selectedTaskIds());
}
void MainWindow::resumeSelection() {
actOnTasks(rpc::method::kDownloadResume, selectedTaskIds());
}
void MainWindow::cancelSelection() {
actOnTasks(rpc::method::kDownloadCancel, selectedTaskIds());
}
void MainWindow::removeSelection() {
const QStringList ids = selectedTaskIds();
if (ids.isEmpty()) {
return;
}
const auto answer = QMessageBox::question(
this, tr("Delete downloads"),
tr("Remove %n download(s) from the list? The partial data is kept.", "", ids.size()));
if (answer == QMessageBox::Yes) {
actOnTasks(rpc::method::kDownloadRemove, ids);
}
}
void MainWindow::resumeAll() {
actOnTasks(rpc::method::kDownloadResume, allTaskIds());
}
void MainWindow::pauseAll() {
actOnTasks(rpc::method::kDownloadPause, allTaskIds());
}
void MainWindow::restoreLayout() {
QSettings settings;
const QByteArray geom = settings.value(QStringLiteral("mainwindow/geometry")).toByteArray();
if (!geom.isEmpty()) {
restoreGeometry(geom);
}
const QByteArray header =
settings.value(QStringLiteral("mainwindow/headerState")).toByteArray();
if (!header.isEmpty()) {
view_->header()->restoreState(header);
return;
}
// First run: widths that fit the content rather than an even split. Description is the
// stretch column, so it takes the remainder.
view_->setColumnWidth(DownloadTableModel::ColName, 280);
view_->setColumnWidth(DownloadTableModel::ColQueue, 32);
view_->setColumnWidth(DownloadTableModel::ColSize, 90);
view_->setColumnWidth(DownloadTableModel::ColStatus, 130);
view_->setColumnWidth(DownloadTableModel::ColTimeLeft, 80);
view_->setColumnWidth(DownloadTableModel::ColSpeed, 90);
view_->setColumnWidth(DownloadTableModel::ColLastTry, 130);
}
void MainWindow::saveLayout() {
QSettings settings;
settings.setValue(QStringLiteral("mainwindow/geometry"), saveGeometry());
settings.setValue(QStringLiteral("mainwindow/headerState"), view_->header()->saveState());
}
} // namespace velox::gui