Files
vdm/gui/src/dialogs/SpeedLimiterDialog.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

105 lines
3.6 KiB
C++

#include "dialogs/SpeedLimiterDialog.hpp"
#include <algorithm>
#include <QCheckBox>
#include <QDialogButtonBox>
#include <QFormLayout>
#include <QLabel>
#include <QPointer>
#include <QSpinBox>
#include <QVBoxLayout>
#include "rpc/Protocol.hpp"
#include "rpc/RpcClient.hpp"
#include "util/Theme.hpp"
namespace velox::gui {
QJsonObject SpeedLimiterDialog::buildParams(bool enabled, int kibps, bool applyToRunning) {
const qint64 bps = enabled ? static_cast<qint64>(std::max(1, kibps)) * 1024 : 0;
return QJsonObject{
{"enabled", enabled},
{"globalBps", bps},
{"applyToRunning", applyToRunning},
};
}
SpeedLimiterDialog::SpeedLimiterDialog(rpc::RpcClient *client, QWidget *parent)
: QDialog(parent),
client_(client),
enabled_(new QCheckBox(tr("Limit download speed"), this)),
kibps_(new QSpinBox(this)),
applyToRunning_(new QCheckBox(tr("Apply to running downloads now"), this)),
statusLabel_(new QLabel(this)) {
setWindowTitle(tr("Speed Limiter"));
kibps_->setRange(1, 1000000);
kibps_->setSuffix(tr(" KiB/s"));
kibps_->setEnabled(false);
connect(enabled_, &QCheckBox::toggled, kibps_, &QWidget::setEnabled);
statusLabel_->setStyleSheet(theme::errorLabelStyle());
statusLabel_->hide();
auto *form = new QFormLayout;
form->addRow(enabled_);
form->addRow(tr("Limit to:"), kibps_);
form->addRow(applyToRunning_);
auto *buttons = new QDialogButtonBox(QDialogButtonBox::Ok | QDialogButtonBox::Cancel, this);
connect(buttons, &QDialogButtonBox::accepted, this, &SpeedLimiterDialog::save);
connect(buttons, &QDialogButtonBox::rejected, this, &QDialog::reject);
auto *layout = new QVBoxLayout(this);
layout->addLayout(form);
layout->addWidget(statusLabel_);
layout->addWidget(buttons);
setEnabled(false);
QPointer<SpeedLimiterDialog> self(this);
client_->call(QString::fromLatin1(rpc::method::kLimiterGet), {},
[self](const rpc::RpcReply &reply) {
if (!self) {
return;
}
self->setEnabled(true);
if (!reply.ok()) {
self->statusLabel_->setText(
tr("Could not load the limiter: %1").arg(reply.error.message));
self->statusLabel_->show();
return;
}
self->onLoaded(reply.result.toObject());
});
}
void SpeedLimiterDialog::onLoaded(const QJsonObject &limiter) {
const bool on = limiter.value("enabled").toBool();
const qint64 bps = static_cast<qint64>(limiter.value("globalBps").toDouble());
enabled_->setChecked(on);
kibps_->setValue(std::max<qint64>(1, bps / 1024));
kibps_->setEnabled(on);
}
void SpeedLimiterDialog::save() {
const QJsonObject params =
buildParams(enabled_->isChecked(), kibps_->value(), applyToRunning_->isChecked());
QPointer<SpeedLimiterDialog> self(this);
client_->call(QString::fromLatin1(rpc::method::kLimiterSet), params,
[self](const rpc::RpcReply &reply) {
if (!self) {
return;
}
if (!reply.ok()) {
self->statusLabel_->setText(
tr("Could not apply the limit: %1").arg(reply.error.message));
self->statusLabel_->show();
return;
}
self->accept();
});
}
} // namespace velox::gui