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
This commit is contained in:
@@ -37,8 +37,14 @@
|
||||
#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 {
|
||||
|
||||
@@ -88,7 +94,9 @@ MainWindow::MainWindow(rpc::RpcClient *client, QWidget *parent)
|
||||
bannerLabel->setObjectName(QStringLiteral("offlineBannerLabel"));
|
||||
bannerLayout->addWidget(bannerLabel);
|
||||
bannerLayout->addStretch();
|
||||
offlineBanner_->setStyleSheet(QStringLiteral("background: #5a3a00; color: #ffd9a0;"));
|
||||
offlineBanner_->setStyleSheet(
|
||||
QStringLiteral("background: %1; color: %2;")
|
||||
.arg(QLatin1String(theme::kOfflineBannerBg), QLatin1String(theme::kOfflineBannerText)));
|
||||
offlineBanner_->setVisible(false);
|
||||
|
||||
auto *splitter = new QSplitter(Qt::Horizontal, this);
|
||||
@@ -111,9 +119,10 @@ MainWindow::MainWindow(rpc::RpcClient *client, QWidget *parent)
|
||||
buildMenus();
|
||||
buildToolBar();
|
||||
buildTray();
|
||||
buildDropTarget();
|
||||
|
||||
// --- status bar -----------------------------------------------------------------
|
||||
connDot_->setStyleSheet(dotStyle(QStringLiteral("#c0392b")));
|
||||
connDot_->setStyleSheet(dotStyle(QLatin1String(theme::kDanger)));
|
||||
statusBar()->addPermanentWidget(countsLabel_, 1);
|
||||
statusBar()->addPermanentWidget(connText_);
|
||||
statusBar()->addPermanentWidget(connDot_);
|
||||
@@ -261,6 +270,23 @@ void MainWindow::buildTray() {
|
||||
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();
|
||||
@@ -275,11 +301,11 @@ void MainWindow::closeEvent(QCloseEvent *event) {
|
||||
void MainWindow::onConnectionState(rpc::ConnectionState state) {
|
||||
connText_->setText(tr(rpc::toString(state)));
|
||||
|
||||
QString colour = QStringLiteral("#c0392b"); // red
|
||||
QString colour = QLatin1String(theme::kDanger);
|
||||
if (state == rpc::ConnectionState::Connected) {
|
||||
colour = QStringLiteral("#27ae60"); // green
|
||||
colour = QLatin1String(theme::kSuccess);
|
||||
} else if (state != rpc::ConnectionState::Disconnected) {
|
||||
colour = QStringLiteral("#e67e22"); // amber
|
||||
colour = QLatin1String(theme::kWarning);
|
||||
}
|
||||
connDot_->setStyleSheet(dotStyle(colour));
|
||||
|
||||
@@ -299,7 +325,7 @@ void MainWindow::onConnectionState(rpc::ConnectionState state) {
|
||||
|
||||
if (online) {
|
||||
fetchTree();
|
||||
fetchMinimizeToTraySetting();
|
||||
fetchGeneralUiSettings();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -318,29 +344,40 @@ void MainWindow::fetchTree() {
|
||||
});
|
||||
}
|
||||
|
||||
void MainWindow::fetchMinimizeToTraySetting() {
|
||||
client_->call(QStringLiteral("settings.get"),
|
||||
QJsonObject{{"keys", QJsonArray{QStringLiteral("general.minimizeToTray")}}},
|
||||
[this](const rpc::RpcReply &reply) {
|
||||
if (reply.ok()) {
|
||||
minimizeToTrayEnabled_ = reply.result.toObject()
|
||||
.value("values")
|
||||
.toObject()
|
||||
.value("general.minimizeToTray")
|
||||
.toBool();
|
||||
}
|
||||
});
|
||||
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 ¶ms) {
|
||||
for (const QJsonValue &key : params.value("keys").toArray()) {
|
||||
if (key.toString() == QLatin1String("general.minimizeToTray")) {
|
||||
fetchMinimizeToTraySetting();
|
||||
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) {
|
||||
|
||||
Reference in New Issue
Block a user