From 92252c36a3b7fc249580668634f08227085b216c Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Tue, 5 Nov 2024 12:14:45 -0800 Subject: [PATCH 01/52] build: fix gcc --- src/crash/CMakeLists.txt | 5 +++-- src/services/pam/CMakeLists.txt | 5 ++++- src/services/pipewire/CMakeLists.txt | 1 + src/wayland/hyprland/global_shortcuts/CMakeLists.txt | 1 + src/wayland/session_lock/CMakeLists.txt | 5 ++++- 5 files changed, 13 insertions(+), 4 deletions(-) diff --git a/src/crash/CMakeLists.txt b/src/crash/CMakeLists.txt index 522b5b0..442859a 100644 --- a/src/crash/CMakeLists.txt +++ b/src/crash/CMakeLists.txt @@ -4,13 +4,14 @@ qt_add_library(quickshell-crash STATIC handler.cpp ) -qs_pch(quickshell-crash) +qs_pch(quickshell-crash SET large) find_package(PkgConfig REQUIRED) pkg_check_modules(breakpad REQUIRED IMPORTED_TARGET breakpad) # only need client?? take only includes from pkg config todo target_link_libraries(quickshell-crash PRIVATE PkgConfig::breakpad -lbreakpad_client) -target_link_libraries(quickshell-crash PRIVATE quickshell-build Qt6::Widgets) +# quick linked for pch compat +target_link_libraries(quickshell-crash PRIVATE quickshell-build Qt::Quick Qt::Widgets) target_link_libraries(quickshell-core PRIVATE quickshell-crash) diff --git a/src/services/pam/CMakeLists.txt b/src/services/pam/CMakeLists.txt index c35e74a..ec300ee 100644 --- a/src/services/pam/CMakeLists.txt +++ b/src/services/pam/CMakeLists.txt @@ -13,7 +13,10 @@ qt_add_qml_module(quickshell-service-pam install_qml_module(quickshell-service-pam) -target_link_libraries(quickshell-service-pam PRIVATE Qt::Qml pam ${PAM_LIBRARIES}) +target_link_libraries(quickshell-service-pam PRIVATE + Qt::Qml pam ${PAM_LIBRARIES} + Qt::Quick # pch +) qs_module_pch(quickshell-service-pam) diff --git a/src/services/pipewire/CMakeLists.txt b/src/services/pipewire/CMakeLists.txt index 35aaa13..fddca6f 100644 --- a/src/services/pipewire/CMakeLists.txt +++ b/src/services/pipewire/CMakeLists.txt @@ -25,6 +25,7 @@ install_qml_module(quickshell-service-pipewire) target_link_libraries(quickshell-service-pipewire PRIVATE Qt::Qml PkgConfig::pipewire + Qt::Quick # pch ) qs_module_pch(quickshell-service-pipewire) diff --git a/src/wayland/hyprland/global_shortcuts/CMakeLists.txt b/src/wayland/hyprland/global_shortcuts/CMakeLists.txt index 8b2aa94..986f2d8 100644 --- a/src/wayland/hyprland/global_shortcuts/CMakeLists.txt +++ b/src/wayland/hyprland/global_shortcuts/CMakeLists.txt @@ -19,6 +19,7 @@ wl_proto(quickshell-hyprland-global-shortcuts target_link_libraries(quickshell-hyprland-global-shortcuts PRIVATE Qt::Qml Qt::WaylandClient Qt::WaylandClientPrivate wayland-client + Qt::Quick # pch ) qs_module_pch(quickshell-hyprland-global-shortcuts) diff --git a/src/wayland/session_lock/CMakeLists.txt b/src/wayland/session_lock/CMakeLists.txt index 63dc129..245d1f2 100644 --- a/src/wayland/session_lock/CMakeLists.txt +++ b/src/wayland/session_lock/CMakeLists.txt @@ -7,7 +7,10 @@ qt_add_library(quickshell-wayland-sessionlock STATIC ) wl_proto(quickshell-wayland-sessionlock ext-session-lock-v1 "${WAYLAND_PROTOCOLS}/staging/ext-session-lock/ext-session-lock-v1.xml") -target_link_libraries(quickshell-wayland-sessionlock PRIVATE ${QT_DEPS} wayland-client) + +target_link_libraries(quickshell-wayland-sessionlock PRIVATE + Qt::Quick Qt::WaylandClient Qt::WaylandClientPrivate wayland-client +) qs_pch(quickshell-wayland-sessionlock SET large) From b528be94260b572919ff47d2f5e3150ebc1ee3e9 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Tue, 5 Nov 2024 13:31:24 -0800 Subject: [PATCH 02/52] all: fix gcc warnings --- CMakeLists.txt | 3 +++ src/core/logging.cpp | 8 +++++++- src/core/model.hpp | 5 +++-- src/core/paths.cpp | 2 ++ src/core/util.hpp | 14 ++++++++++++++ src/io/fileview.cpp | 2 +- src/io/fileview.hpp | 5 +++++ src/io/ipchandler.cpp | 2 +- src/io/ipchandler.hpp | 4 +++- src/services/notifications/notification.cpp | 2 +- src/services/pipewire/device.hpp | 3 +-- src/services/pipewire/link.hpp | 3 +-- src/services/pipewire/metadata.hpp | 4 +--- src/services/pipewire/node.hpp | 3 +-- src/services/pipewire/qml.cpp | 2 +- src/services/pipewire/registry.cpp | 6 ++++++ src/services/pipewire/registry.hpp | 8 ++++---- src/wayland/hyprland/focus_grab/grab.cpp | 2 +- src/widgets/CMakeLists.txt | 1 + 19 files changed, 57 insertions(+), 22 deletions(-) diff --git a/CMakeLists.txt b/CMakeLists.txt index f951d96..23e6add 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -69,6 +69,9 @@ include(cmake/util.cmake) add_compile_options(-Wall -Wextra) +# pipewire defines this, breaking PCH +add_compile_definitions(_REENTRANT) + if (FRAME_POINTERS) add_compile_options(-fno-omit-frame-pointer) endif() diff --git a/src/core/logging.cpp b/src/core/logging.cpp index 1564e89..45f5b3e 100644 --- a/src/core/logging.cpp +++ b/src/core/logging.cpp @@ -369,6 +369,7 @@ void ThreadLogging::initFs() { .l_whence = SEEK_SET, .l_start = 0, .l_len = 0, + .l_pid = 0, }; if (fcntl(detailedFile->handle(), F_SETLK, &lock) != 0) { // NOLINT @@ -455,6 +456,8 @@ CompressedLogType compressedTypeOf(QtMsgType type) { case QtCriticalMsg: case QtFatalMsg: return CompressedLogType::Critical; } + + return CompressedLogType::Info; // unreachable under normal conditions } QtMsgType typeOfCompressed(CompressedLogType type) { @@ -464,6 +467,8 @@ QtMsgType typeOfCompressed(CompressedLogType type) { case CompressedLogType::Warn: return QtWarningMsg; case CompressedLogType::Critical: return QtCriticalMsg; } + + return QtInfoMsg; // unreachable under normal conditions } void WriteBuffer::setDevice(QIODevice* device) { this->device = device; } @@ -636,7 +641,7 @@ bool EncodedLogReader::read(LogMessage* slot) { if (!this->readVarInt(&secondDelta)) return false; } - if (index < 0 || index >= this->recentMessages.size()) return false; + if (index >= this->recentMessages.size()) return false; *slot = this->recentMessages.at(index); this->lastMessageTime = this->lastMessageTime.addSecs(static_cast(secondDelta)); slot->time = this->lastMessageTime; @@ -858,6 +863,7 @@ void LogFollower::FcntlWaitThread::run() { .l_whence = SEEK_SET, .l_start = 0, .l_len = 0, + .l_pid = 0, }; auto r = fcntl(this->follower->reader->file->handle(), F_SETLKW, &lock); // NOLINT diff --git a/src/core/model.hpp b/src/core/model.hpp index 5ab3e79..d0981fd 100644 --- a/src/core/model.hpp +++ b/src/core/model.hpp @@ -1,5 +1,6 @@ #pragma once +#include #include #include #include @@ -85,11 +86,11 @@ class ObjectModel: public UntypedObjectModel { explicit ObjectModel(QObject* parent): UntypedObjectModel(parent) {} [[nodiscard]] QVector& valueList() { - return *reinterpret_cast*>(&this->valuesList); // NOLINT + return *std::bit_cast*>(&this->valuesList); } [[nodiscard]] const QVector& valueList() const { - return *reinterpret_cast*>(&this->valuesList); // NOLINT + return *std::bit_cast*>(&this->valuesList); } void insertObject(T* object, qsizetype index = -1) { diff --git a/src/core/paths.cpp b/src/core/paths.cpp index e2b1530..e108da0 100644 --- a/src/core/paths.cpp +++ b/src/core/paths.cpp @@ -242,6 +242,7 @@ void QsPaths::createLock() { .l_whence = SEEK_SET, .l_start = 0, .l_len = 0, + .l_pid = 0, }; if (fcntl(file->handle(), F_SETLK, &lock) != 0) { // NOLINT @@ -268,6 +269,7 @@ bool QsPaths::checkLock(const QString& path, InstanceLockInfo* info) { .l_whence = SEEK_SET, .l_start = 0, .l_len = 0, + .l_pid = 0, }; fcntl(file.handle(), F_GETLK, &lock); // NOLINT diff --git a/src/core/util.hpp b/src/core/util.hpp index 3ca095e..1ff9b22 100644 --- a/src/core/util.hpp +++ b/src/core/util.hpp @@ -1,10 +1,24 @@ #pragma once +#include #include +#include #include #include #include +template +struct StringLiteral { + constexpr StringLiteral(const char (&str)[Length]) { // NOLINT + std::copy_n(str, Length, this->value); + } + + constexpr operator const char*() const noexcept { return this->value; } + operator QLatin1StringView() const { return QLatin1String(this->value, Length); } + + char value[Length]; // NOLINT +}; + // NOLINTBEGIN #define DROP_EMIT(object, func) \ DropEmitter(object, static_cast([](typeof(object) o) { o->func(); })) diff --git a/src/io/fileview.cpp b/src/io/fileview.cpp index 40dde6d..6cfe4bc 100644 --- a/src/io/fileview.cpp +++ b/src/io/fileview.cpp @@ -153,7 +153,7 @@ void FileView::loadSync() { auto state = FileViewState(); this->updateState(state); } else if (!this->blockUntilLoaded()) { - auto state = FileViewState {.path = this->targetPath}; + auto state = FileViewState(this->targetPath); FileViewReader::read(state, false); this->updateState(state); } diff --git a/src/io/fileview.hpp b/src/io/fileview.hpp index 04ed421..715962f 100644 --- a/src/io/fileview.hpp +++ b/src/io/fileview.hpp @@ -1,5 +1,7 @@ #pragma once +#include + #include #include #include @@ -15,6 +17,9 @@ namespace qs::io { struct FileViewState { + FileViewState() = default; + explicit FileViewState(QString path): path(std::move(path)) {} + QString path; QString text; QByteArray data; diff --git a/src/io/ipchandler.cpp b/src/io/ipchandler.cpp index d2a549b..510b205 100644 --- a/src/io/ipchandler.cpp +++ b/src/io/ipchandler.cpp @@ -247,7 +247,7 @@ void IpcHandlerRegistry::deregisterHandler(IpcHandler* handler) { } } - handler->registeredState = {.enabled = false, .target = ""}; + handler->registeredState = IpcHandler::RegistrationState(false); } QString IpcHandler::listMembers(qsizetype indent) { diff --git a/src/io/ipchandler.hpp b/src/io/ipchandler.hpp index 9751980..cc4ee5f 100644 --- a/src/io/ipchandler.hpp +++ b/src/io/ipchandler.hpp @@ -172,12 +172,14 @@ class IpcHandler void updateRegistration(bool destroying = false); struct RegistrationState { + explicit RegistrationState(bool enabled = false): enabled(enabled) {} + bool enabled = false; QString target; }; RegistrationState registeredState; - RegistrationState targetState {.enabled = true}; + RegistrationState targetState {true}; bool complete = false; QHash functionMap; diff --git a/src/services/notifications/notification.cpp b/src/services/notifications/notification.cpp index c090c13..18c8ff1 100644 --- a/src/services/notifications/notification.cpp +++ b/src/services/notifications/notification.cpp @@ -85,7 +85,7 @@ void Notification::updateProperties( qint32 expireTimeout ) { auto urgency = hints.contains("urgency") ? hints.value("urgency").value() - : NotificationUrgency::Normal; + : static_cast(NotificationUrgency::Normal); auto hasActionIcons = hints.value("action-icons").value(); auto resident = hints.value("resident").value(); diff --git a/src/services/pipewire/device.hpp b/src/services/pipewire/device.hpp index ed6b6c5..2e14d61 100644 --- a/src/services/pipewire/device.hpp +++ b/src/services/pipewire/device.hpp @@ -17,8 +17,7 @@ namespace qs::service::pipewire { class PwDevice; -constexpr const char TYPE_INTERFACE_Device[] = PW_TYPE_INTERFACE_Device; // NOLINT -class PwDevice: public PwBindable { +class PwDevice: public PwBindable { Q_OBJECT; public: diff --git a/src/services/pipewire/link.hpp b/src/services/pipewire/link.hpp index 0c7bde2..55bbcf0 100644 --- a/src/services/pipewire/link.hpp +++ b/src/services/pipewire/link.hpp @@ -35,8 +35,7 @@ class PwLinkState: public QObject { Q_INVOKABLE static QString toString(qs::service::pipewire::PwLinkState::Enum value); }; -constexpr const char TYPE_INTERFACE_Link[] = PW_TYPE_INTERFACE_Link; // NOLINT -class PwLink: public PwBindable { // NOLINT +class PwLink: public PwBindable { Q_OBJECT; public: diff --git a/src/services/pipewire/metadata.hpp b/src/services/pipewire/metadata.hpp index 812a853..f257dc2 100644 --- a/src/services/pipewire/metadata.hpp +++ b/src/services/pipewire/metadata.hpp @@ -11,9 +11,7 @@ namespace qs::service::pipewire { -constexpr const char TYPE_INTERFACE_Metadata[] = PW_TYPE_INTERFACE_Metadata; // NOLINT -class PwMetadata - : public PwBindable { // NOLINT +class PwMetadata: public PwBindable { Q_OBJECT; public: diff --git a/src/services/pipewire/node.hpp b/src/services/pipewire/node.hpp index 783614a..5a67db7 100644 --- a/src/services/pipewire/node.hpp +++ b/src/services/pipewire/node.hpp @@ -156,8 +156,7 @@ private slots: PwNode* node; }; -constexpr const char TYPE_INTERFACE_Node[] = PW_TYPE_INTERFACE_Node; // NOLINT -class PwNode: public PwBindable { // NOLINT +class PwNode: public PwBindable { Q_OBJECT; public: diff --git a/src/services/pipewire/qml.cpp b/src/services/pipewire/qml.cpp index a8186ea..be50ec6 100644 --- a/src/services/pipewire/qml.cpp +++ b/src/services/pipewire/qml.cpp @@ -429,7 +429,7 @@ void PwObjectTracker::setObjects(const QList& objects) { // connect destroy for (auto* object: objects) { - if (auto* pwObject = dynamic_cast(object)) { + if (dynamic_cast(object) != nullptr) { QObject::connect(object, &QObject::destroyed, this, &PwObjectTracker::objectDestroyed); } } diff --git a/src/services/pipewire/registry.cpp b/src/services/pipewire/registry.cpp index 1370fa1..04bd9ac 100644 --- a/src/services/pipewire/registry.cpp +++ b/src/services/pipewire/registry.cpp @@ -63,6 +63,12 @@ void PwBindableObject::unref() { if (this->refcount == 0) this->unbind(); } +void PwBindableObject::registryBind(const char* interface, quint32 version) { + // NOLINTNEXTLINE + auto* object = pw_registry_bind(this->registry->object, this->id, interface, version, 0); + this->object = static_cast(object); +} + void PwBindableObject::bind() { qCDebug(logRegistry) << "Bound object" << this; this->bindHooks(); diff --git a/src/services/pipewire/registry.hpp b/src/services/pipewire/registry.hpp index c61773b..59aac75 100644 --- a/src/services/pipewire/registry.hpp +++ b/src/services/pipewire/registry.hpp @@ -12,6 +12,7 @@ #include #include +#include "../../core/util.hpp" #include "core.hpp" namespace qs::service::pipewire { @@ -50,6 +51,7 @@ class PwBindableObject: public QObject { void destroying(PwBindableObject* self); protected: + void registryBind(const char* interface, quint32 version); virtual void bind(); void unbind(); virtual void bindHooks() {}; @@ -62,7 +64,7 @@ class PwBindableObject: public QObject { QDebug operator<<(QDebug debug, const PwBindableObject* object); -template +template class PwBindable: public PwBindableObject { public: T* proxy() { @@ -72,9 +74,7 @@ class PwBindable: public PwBindableObject { protected: void bind() override { if (this->object != nullptr) return; - auto* object = - pw_registry_bind(this->registry->object, this->id, INTERFACE, VERSION, 0); // NOLINT - this->object = static_cast(object); + this->registryBind(INTERFACE, VERSION); this->PwBindableObject::bind(); } diff --git a/src/wayland/hyprland/focus_grab/grab.cpp b/src/wayland/hyprland/focus_grab/grab.cpp index 6229869..188c206 100644 --- a/src/wayland/hyprland/focus_grab/grab.cpp +++ b/src/wayland/hyprland/focus_grab/grab.cpp @@ -39,7 +39,7 @@ void FocusGrab::addWindow(QWindow* window) { if (auto* waylandWindow = dynamic_cast(window->handle())) { tryAddWayland(waylandWindow); } else { - QObject::connect(window, &QWindow::visibleChanged, this, [this, window, tryAddWayland]() { + QObject::connect(window, &QWindow::visibleChanged, this, [window, tryAddWayland]() { if (window->isVisible()) { if (window->handle() == nullptr) { window->create(); diff --git a/src/widgets/CMakeLists.txt b/src/widgets/CMakeLists.txt index 226d950..def0aaf 100644 --- a/src/widgets/CMakeLists.txt +++ b/src/widgets/CMakeLists.txt @@ -11,4 +11,5 @@ install_qml_module(quickshell-widgets) qs_module_pch(quickshell-widgets) +target_link_libraries(quickshell-widgets PRIVATE Qt::Quick) target_link_libraries(quickshell PRIVATE quickshell-widgetsplugin) From 74f371850df400a9b403ce61a290149e2fc08323 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Mon, 11 Nov 2024 22:01:08 -0800 Subject: [PATCH 03/52] launch: fix use after free of command options --- src/launch/launch_p.hpp | 2 ++ src/launch/parsecommand.cpp | 30 ++++++++++++++++-------------- 2 files changed, 18 insertions(+), 14 deletions(-) diff --git a/src/launch/launch_p.hpp b/src/launch/launch_p.hpp index d1916d5..d752edb 100644 --- a/src/launch/launch_p.hpp +++ b/src/launch/launch_p.hpp @@ -1,5 +1,6 @@ #pragma once +#include #include #include @@ -26,6 +27,7 @@ class QStringOption { }; struct CommandState { + std::unique_ptr app; struct { int argc = 0; char** argv = nullptr; diff --git a/src/launch/parsecommand.cpp b/src/launch/parsecommand.cpp index 14fd920..91f7dc0 100644 --- a/src/launch/parsecommand.cpp +++ b/src/launch/parsecommand.cpp @@ -1,5 +1,6 @@ #include #include +#include #include #include // NOLINT: Need to include this for impls of some CLI11 classes @@ -90,30 +91,31 @@ int parseCommand(int argc, char** argv, CommandState& state) { return group; }; - auto cli = CLI::App(); + state.app = std::make_unique(); + auto* cli = state.app.get(); // Require 0-1 subcommands. Without this, positionals can be parsed as more subcommands. - cli.require_subcommand(0, 1); + cli->require_subcommand(0, 1); - addConfigSelection(&cli); - addLoggingOptions(&cli, false); - addDebugOptions(&cli); + addConfigSelection(cli); + addLoggingOptions(cli, false); + addDebugOptions(cli); { - cli.add_option_group("")->add_flag("--private-check-compat", state.misc.checkCompat); + cli->add_option_group("")->add_flag("--private-check-compat", state.misc.checkCompat); - cli.add_flag("-V,--version", state.misc.printVersion) + cli->add_flag("-V,--version", state.misc.printVersion) ->description("Print quickshell's version and exit."); - cli.add_flag("-n,--no-duplicate", state.misc.noDuplicate) + cli->add_flag("-n,--no-duplicate", state.misc.noDuplicate) ->description("Exit immediately if another instance of the given config is running."); - cli.add_flag("-d,--daemonize", state.misc.daemonize) + cli->add_flag("-d,--daemonize", state.misc.daemonize) ->description("Detach from the controlling terminal."); } { - auto* sub = cli.add_subcommand("log", "Print quickshell logs."); + auto* sub = cli->add_subcommand("log", "Print quickshell logs."); auto* file = sub->add_option("file", state.log.file, "Log file to read."); @@ -135,7 +137,7 @@ int parseCommand(int argc, char** argv, CommandState& state) { } { - auto* sub = cli.add_subcommand("list", "List running quickshell instances."); + auto* sub = cli->add_subcommand("list", "List running quickshell instances."); auto* all = sub->add_flag("-a,--all", state.instance.all) ->description("List all instances.\n" @@ -151,7 +153,7 @@ int parseCommand(int argc, char** argv, CommandState& state) { } { - auto* sub = cli.add_subcommand("kill", "Kill quickshell instances."); + auto* sub = cli->add_subcommand("kill", "Kill quickshell instances."); //sub->add_flag("-a,--all", "Kill all matching instances instead of just one."); auto* instance = addInstanceSelection(sub); addConfigSelection(sub)->excludes(instance); @@ -161,7 +163,7 @@ int parseCommand(int argc, char** argv, CommandState& state) { } { - auto* sub = cli.add_subcommand("msg", "Send messages to IpcHandlers.")->require_option(); + auto* sub = cli->add_subcommand("msg", "Send messages to IpcHandlers.")->require_option(); auto* target = sub->add_option("target", state.ipc.target, "The target to message."); @@ -188,7 +190,7 @@ int parseCommand(int argc, char** argv, CommandState& state) { state.subcommand.msg = sub; } - CLI11_PARSE(cli, argc, argv); + CLI11_PARSE(*cli, argc, argv); return 0; } From 2c0e46cedb6305671b1eafa1e0c6071a8ab48b27 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Tue, 12 Nov 2024 03:23:59 -0800 Subject: [PATCH 04/52] core/lazyloader: fix incubator UAF in forceCompletion The incubator was deleted via onIncubationCompleted before it was done working when completed via forceCompletion(). --- src/core/lazyloader.cpp | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/core/lazyloader.cpp b/src/core/lazyloader.cpp index 7631722..be0eb78 100644 --- a/src/core/lazyloader.cpp +++ b/src/core/lazyloader.cpp @@ -179,7 +179,9 @@ void LazyLoader::incubateIfReady(bool overrideReloadCheck) { void LazyLoader::onIncubationCompleted() { this->setItem(this->incubator->object()); - delete this->incubator; + // The incubator is not necessarily inert at the time of this callback, + // so deleteLater is required. + this->incubator->deleteLater(); this->incubator = nullptr; this->targetLoading = false; emit this->loadingChanged(); From 0dd19d4a181378df5f61de9b5f54b3f2bba96e8a Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Tue, 12 Nov 2024 04:35:42 -0800 Subject: [PATCH 05/52] core/proxywindow: remove blank frame when destroying window Removes the blank frame caused by removing the content item from the window. Fixes an issue with hyprland's window exit animations. --- src/window/proxywindow.cpp | 12 +++++++----- src/window/proxywindow.hpp | 4 ++-- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/src/window/proxywindow.cpp b/src/window/proxywindow.cpp index 07f8a23..3d01224 100644 --- a/src/window/proxywindow.cpp +++ b/src/window/proxywindow.cpp @@ -44,7 +44,7 @@ ProxyWindowBase::ProxyWindowBase(QObject* parent) // clang-format on } -ProxyWindowBase::~ProxyWindowBase() { this->deleteWindow(); } +ProxyWindowBase::~ProxyWindowBase() { this->deleteWindow(true); } void ProxyWindowBase::onReload(QObject* oldInstance) { this->window = this->retrieveWindow(oldInstance); @@ -90,9 +90,9 @@ void ProxyWindowBase::createWindow() { emit this->windowConnected(); } -void ProxyWindowBase::deleteWindow() { +void ProxyWindowBase::deleteWindow(bool keepItemOwnership) { if (this->window != nullptr) emit this->windowDestroyed(); - if (auto* window = this->disownWindow()) { + if (auto* window = this->disownWindow(keepItemOwnership)) { if (auto* generation = EngineGeneration::findObjectGeneration(this)) { generation->deregisterIncubationController(window->incubationController()); } @@ -101,12 +101,14 @@ void ProxyWindowBase::deleteWindow() { } } -QQuickWindow* ProxyWindowBase::disownWindow() { +QQuickWindow* ProxyWindowBase::disownWindow(bool keepItemOwnership) { if (this->window == nullptr) return nullptr; QObject::disconnect(this->window, nullptr, this, nullptr); - this->mContentItem->setParentItem(nullptr); + if (!keepItemOwnership) { + this->mContentItem->setParentItem(nullptr); + } auto* window = this->window; this->window = nullptr; diff --git a/src/window/proxywindow.hpp b/src/window/proxywindow.hpp index dbbf191..79d326e 100644 --- a/src/window/proxywindow.hpp +++ b/src/window/proxywindow.hpp @@ -57,10 +57,10 @@ class ProxyWindowBase: public Reloadable { void onReload(QObject* oldInstance) override; void createWindow(); - void deleteWindow(); + void deleteWindow(bool keepItemOwnership = false); // Disown the backing window and delete all its children. - virtual QQuickWindow* disownWindow(); + virtual QQuickWindow* disownWindow(bool keepItemOwnership = false); virtual QQuickWindow* retrieveWindow(QObject* oldInstance); virtual QQuickWindow* createQQuickWindow(); From 60dfa67ec7f20e574a1ecf112a759aac30523ce2 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Thu, 14 Nov 2024 17:54:16 -0800 Subject: [PATCH 06/52] io/fileview: support zero-sized files (/proc) --- src/io/fileview.cpp | 46 ++++++++++++++++++++++++++++++--------------- 1 file changed, 31 insertions(+), 15 deletions(-) diff --git a/src/io/fileview.cpp b/src/io/fileview.cpp index 6cfe4bc..063ea83 100644 --- a/src/io/fileview.cpp +++ b/src/io/fileview.cpp @@ -1,4 +1,5 @@ #include "fileview.hpp" +#include #include #include @@ -68,22 +69,37 @@ void FileViewReader::read(FileViewState& state, bool doStringConversion) { } auto& data = state.data; - data = QByteArray(file.size(), Qt::Uninitialized); - - qint64 i = 0; - - while (true) { - auto r = file.read(data.data() + i, data.length() - i); // NOLINT - - if (r == -1) { - qCCritical(logFileView) << "Failed to read" << state.path; - goto error; - } else if (r == 0) { - data.resize(i); - break; + if (file.size() != 0) { + data = QByteArray(file.size(), Qt::Uninitialized); + qint64 i = 0; + + while (true) { + auto r = file.read(data.data() + i, data.length() - i); // NOLINT + + if (r == -1) { + qCCritical(logFileView) << "Failed to read" << state.path; + goto error; + } else if (r == 0) { + data.resize(i); + break; + } + + i += r; + } + } else { + auto buf = std::array(); + + while (true) { + auto r = file.read(buf.data(), buf.size()); // NOLINT + + if (r == -1) { + qCCritical(logFileView) << "Failed to read" << state.path; + goto error; + } else { + data.append(buf.data(), r); + if (r == 0) break; + } } - - i += r; } if (doStringConversion) { From 0445eee33a5c2e88ce5b23fbe4924185420e7983 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Sun, 17 Nov 2024 00:47:22 -0800 Subject: [PATCH 07/52] io/process: support commands at file:// and root:// paths. --- src/io/process.cpp | 11 +++++++++++ src/io/process.hpp | 8 ++++++++ 2 files changed, 19 insertions(+) diff --git a/src/io/process.cpp b/src/io/process.cpp index 9fae90e..3e3292d 100644 --- a/src/io/process.cpp +++ b/src/io/process.cpp @@ -12,6 +12,7 @@ #include #include +#include "../core/generation.hpp" #include "../core/qmlglobal.hpp" #include "datastream.hpp" @@ -53,6 +54,16 @@ QList Process::command() const { return this->mCommand; } void Process::setCommand(QList command) { if (this->mCommand == command) return; this->mCommand = std::move(command); + + auto& cmd = this->mCommand.first(); + if (cmd.startsWith("file://")) { + cmd = cmd.sliced(7); + } else if (cmd.startsWith("root://")) { + cmd = cmd.sliced(7); + auto& root = EngineGeneration::findObjectGeneration(this)->rootPath; + cmd = root.filePath(cmd.startsWith('/') ? cmd.sliced(1) : cmd); + } + emit this->commandChanged(); this->startProcessIfReady(); diff --git a/src/io/process.hpp b/src/io/process.hpp index 521ee2c..43db316 100644 --- a/src/io/process.hpp +++ b/src/io/process.hpp @@ -51,6 +51,14 @@ class Process: public QObject { /// started process. If the property has been changed after starting a process it will /// return the new value, not the one for the currently running process. /// + /// > [!WARNING] This does not run command in a shell. All arguments to the command + /// > must be in separate values in the list, e.g. `["echo", "hello"]` + /// > and not `["echo hello"]`. + /// > + /// > Additionally, shell scripts must be run by your shell, + /// > e.g. `["sh", "script.sh"]` instead of `["script.sh"]` unless the script + /// > has a shebang. + /// /// > [!INFO] You can use `["sh", "-c", ]` to execute your command with /// > the system shell. Q_PROPERTY(QList command READ command WRITE setCommand NOTIFY commandChanged); From 36d1dbeb69854767c70b44b520c7cd1a44a36f93 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Sun, 17 Nov 2024 01:30:54 -0800 Subject: [PATCH 08/52] service/tray: report misbehaving tray hosts I've debugged broken tray items that just end up being a bad host far too many times. --- src/dbus/properties.cpp | 3 ++- src/dbus/properties.hpp | 1 + src/services/status_notifier/item.cpp | 16 ++++++++++++++++ src/services/status_notifier/item.hpp | 1 + src/services/status_notifier/watcher.cpp | 4 +++- src/services/status_notifier/watcher.hpp | 2 ++ 6 files changed, 25 insertions(+), 2 deletions(-) diff --git a/src/dbus/properties.cpp b/src/dbus/properties.cpp index 1a40ca2..6156b2a 100644 --- a/src/dbus/properties.cpp +++ b/src/dbus/properties.cpp @@ -268,14 +268,15 @@ void DBusPropertyGroup::updateAllViaGetAll() { qCWarning(logDbusProperties).noquote() << "Error updating properties of" << this->toString() << "via GetAll"; qCWarning(logDbusProperties) << reply.error(); + emit this->getAllFailed(reply.error()); } else { qCDebug(logDbusProperties).noquote() << "Received GetAll property set for" << this->toString(); this->updatePropertySet(reply.value(), true); + emit this->getAllFinished(); } delete call; - emit this->getAllFinished(); }; QObject::connect(call, &QDBusPendingCallWatcher::finished, this, responseCallback); diff --git a/src/dbus/properties.hpp b/src/dbus/properties.hpp index e24d23f..65f51af 100644 --- a/src/dbus/properties.hpp +++ b/src/dbus/properties.hpp @@ -135,6 +135,7 @@ class DBusPropertyGroup: public QObject { signals: void getAllFinished(); + void getAllFailed(QDBusError error); private slots: void onPropertiesChanged( diff --git a/src/services/status_notifier/item.cpp b/src/services/status_notifier/item.cpp index 7f990a9..f6e16a2 100644 --- a/src/services/status_notifier/item.cpp +++ b/src/services/status_notifier/item.cpp @@ -26,6 +26,7 @@ #include "dbus_item.h" #include "dbus_item_types.hpp" #include "host.hpp" +#include "watcher.hpp" using namespace qs::dbus; using namespace qs::dbus::dbusmenu; @@ -76,6 +77,7 @@ StatusNotifierItem::StatusNotifierItem(const QString& address, QObject* parent) QObject::connect(&this->overlayIconPixmaps, &AbstractDBusProperty::changed, this, &StatusNotifierItem::updateIcon); QObject::connect(&this->properties, &DBusPropertyGroup::getAllFinished, this, &StatusNotifierItem::onGetAllFinished); + QObject::connect(&this->properties, &DBusPropertyGroup::getAllFailed, this, &StatusNotifierItem::onGetAllFailed); QObject::connect(&this->menuPath, &AbstractDBusProperty::changed, this, &StatusNotifierItem::onMenuPathChanged); // clang-format on @@ -246,6 +248,19 @@ void StatusNotifierItem::onGetAllFinished() { emit this->ready(); } +void StatusNotifierItem::onGetAllFailed() { + // Not changing the item to ready, as it is almost definitely broken. + if (!this->mReady) { + qWarning(logStatusNotifierItem) << "Failed to load tray item" << this->properties.toString(); + + if (!StatusNotifierWatcher::instance()->isRegistered()) { + qWarning(logStatusNotifierItem) + << "Another StatusNotifier host seems to be running. Please disable it and check that " + "the problem persists before reporting an issue."; + } + } +} + TrayImageHandle::TrayImageHandle(StatusNotifierItem* item) : QsImageHandle(QQmlImageProviderBase::Pixmap, item) , item(item) {} @@ -257,6 +272,7 @@ TrayImageHandle::requestPixmap(const QString& /*unused*/, QSize* size, const QSi auto pixmap = this->item->createPixmap(targetSize); if (pixmap.isNull()) { + qCWarning(logStatusNotifierItem) << "Unable to create pixmap for tray icon" << this->item; pixmap = IconImageProvider::missingPixmap(targetSize); } diff --git a/src/services/status_notifier/item.hpp b/src/services/status_notifier/item.hpp index efe3159..2a22e2e 100644 --- a/src/services/status_notifier/item.hpp +++ b/src/services/status_notifier/item.hpp @@ -75,6 +75,7 @@ class StatusNotifierItem: public QObject { private slots: void updateIcon(); void onGetAllFinished(); + void onGetAllFailed(); void onMenuPathChanged(); private: diff --git a/src/services/status_notifier/watcher.cpp b/src/services/status_notifier/watcher.cpp index a6fd217..4917077 100644 --- a/src/services/status_notifier/watcher.cpp +++ b/src/services/status_notifier/watcher.cpp @@ -47,12 +47,15 @@ StatusNotifierWatcher::StatusNotifierWatcher(QObject* parent): QObject(parent) { this->tryRegister(); } +bool StatusNotifierWatcher::isRegistered() const { return this->registered; } + void StatusNotifierWatcher::tryRegister() { // NOLINT auto bus = QDBusConnection::sessionBus(); auto success = bus.registerService("org.kde.StatusNotifierWatcher"); if (success) { qCDebug(logStatusNotifierWatcher) << "Registered watcher at org.kde.StatusNotifierWatcher"; + this->registered = true; } else { qCDebug(logStatusNotifierWatcher) << "Could not register watcher at org.kde.StatusNotifierWatcher, presumably because one is " @@ -68,7 +71,6 @@ void StatusNotifierWatcher::onServiceUnregistered(const QString& service) { << "Active StatusNotifierWatcher unregistered, attempting registration"; this->tryRegister(); return; - ; } else { QString qualifiedItem; this->items.removeIf([&](const QString& item) { diff --git a/src/services/status_notifier/watcher.hpp b/src/services/status_notifier/watcher.hpp index 5fd41e5..4a04225 100644 --- a/src/services/status_notifier/watcher.hpp +++ b/src/services/status_notifier/watcher.hpp @@ -29,6 +29,7 @@ class StatusNotifierWatcher [[nodiscard]] qint32 protocolVersion() const { return 0; } // NOLINT [[nodiscard]] bool isHostRegistered() const; [[nodiscard]] QList registeredItems() const; + [[nodiscard]] bool isRegistered() const; // NOLINTBEGIN void RegisterStatusNotifierHost(const QString& host); @@ -54,6 +55,7 @@ private slots: QDBusServiceWatcher serviceWatcher; QList hosts; QList items; + bool registered = false; }; } // namespace qs::service::sni From 29d31f5d3bde681facbd623f0d0fca1652f77fae Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Sun, 17 Nov 2024 01:36:25 -0800 Subject: [PATCH 09/52] docs: add note that private qt headers are required for some libs --- BUILD.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/BUILD.md b/BUILD.md index 5a0652f..8fa7884 100644 --- a/BUILD.md +++ b/BUILD.md @@ -44,6 +44,12 @@ Quickshell has a set of base dependencies you will always need, names vary by di - `pkg-config` - `cli11` +On some distros, private Qt headers are in separate packages which you may have to install. +We currently require private headers for the following libraries: + +- `qt6declarative` +- `qt6wayland` + We recommend an implicit dependency on `qt6svg`. If it is not installed, svg images and svg icons will not work, including system ones. From 7db37726412fd071886cf2303e1da4a54bcd1c05 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Sun, 17 Nov 2024 01:46:49 -0800 Subject: [PATCH 10/52] core/generation: short circuit findObjectGeneration if only one exists --- src/core/generation.cpp | 8 +++++--- src/core/generation.hpp | 4 ++-- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/src/core/generation.cpp b/src/core/generation.cpp index 147e2f9..f4fcde6 100644 --- a/src/core/generation.cpp +++ b/src/core/generation.cpp @@ -24,7 +24,7 @@ #include "reload.hpp" #include "scan.hpp" -static QHash g_generations; // NOLINT +static QHash g_generations; // NOLINT EngineGeneration::EngineGeneration(const QDir& rootPath, QmlScanner scanner) : rootPath(rootPath) @@ -326,11 +326,13 @@ EngineGeneration* EngineGeneration::currentGeneration() { } else return nullptr; } -EngineGeneration* EngineGeneration::findEngineGeneration(QQmlEngine* engine) { +EngineGeneration* EngineGeneration::findEngineGeneration(const QQmlEngine* engine) { return g_generations.value(engine); } -EngineGeneration* EngineGeneration::findObjectGeneration(QObject* object) { +EngineGeneration* EngineGeneration::findObjectGeneration(const QObject* object) { + if (g_generations.size() == 1) return EngineGeneration::currentGeneration(); + while (object != nullptr) { auto* context = QQmlEngine::contextForObject(object); diff --git a/src/core/generation.hpp b/src/core/generation.hpp index 043d2f7..2d84282 100644 --- a/src/core/generation.hpp +++ b/src/core/generation.hpp @@ -45,8 +45,8 @@ class EngineGeneration: public QObject { void registerExtension(const void* key, EngineGenerationExt* extension); EngineGenerationExt* findExtension(const void* key); - static EngineGeneration* findEngineGeneration(QQmlEngine* engine); - static EngineGeneration* findObjectGeneration(QObject* object); + static EngineGeneration* findEngineGeneration(const QQmlEngine* engine); + static EngineGeneration* findObjectGeneration(const QObject* object); // Returns the current generation if there is only one generation, // otherwise null. From d2667369e18d44974a34921707e9a114b5534271 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Sun, 17 Nov 2024 01:49:27 -0800 Subject: [PATCH 11/52] core/qmlglobal: add shellRoot property --- src/core/qmlglobal.cpp | 6 ++++++ src/core/qmlglobal.hpp | 7 +++++++ 2 files changed, 13 insertions(+) diff --git a/src/core/qmlglobal.cpp b/src/core/qmlglobal.cpp index 3321fcf..9328d90 100644 --- a/src/core/qmlglobal.cpp +++ b/src/core/qmlglobal.cpp @@ -166,6 +166,12 @@ void QuickshellGlobal::reload(bool hard) { root->reloadGraph(hard); } +QString QuickshellGlobal::shellRoot() const { + auto* generation = EngineGeneration::findObjectGeneration(this); + // already canonical + return generation->rootPath.path(); +} + QString QuickshellGlobal::workingDirectory() const { // NOLINT return QuickshellSettings::instance()->workingDirectory(); } diff --git a/src/core/qmlglobal.hpp b/src/core/qmlglobal.hpp index fb1853f..7efdb41 100644 --- a/src/core/qmlglobal.hpp +++ b/src/core/qmlglobal.hpp @@ -98,6 +98,11 @@ class QuickshellGlobal: public QObject { /// This creates an instance of your window once on every screen. /// As screens are added or removed your window will be created or destroyed on those screens. Q_PROPERTY(QQmlListProperty screens READ screens NOTIFY screensChanged); + /// The full path to the root directory of your shell. + /// + /// The root directory is the folder containing the entrypoint to your shell, often referred + /// to as `shell.qml`. + Q_PROPERTY(QString shellRoot READ shellRoot CONSTANT); /// Quickshell's working directory. Defaults to whereever quickshell was launched from. Q_PROPERTY(QString workingDirectory READ workingDirectory WRITE setWorkingDirectory NOTIFY workingDirectoryChanged); /// If true then the configuration will be reloaded whenever any files change. @@ -133,6 +138,8 @@ class QuickshellGlobal: public QObject { /// > of your icon theme. Q_INVOKABLE static QString iconPath(const QString& icon); + [[nodiscard]] QString shellRoot() const; + [[nodiscard]] QString workingDirectory() const; void setWorkingDirectory(QString workingDirectory); From 68ba5005ce02d251c40a6ad941bb17f064eab878 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Sun, 17 Nov 2024 14:38:29 -0800 Subject: [PATCH 12/52] core/icon: ability to specify a fallback or check if an icon exists --- src/core/iconimageprovider.cpp | 21 +++++++++++++++++++-- src/core/iconimageprovider.hpp | 7 ++++++- src/core/qmlglobal.cpp | 12 +++++++++++- src/core/qmlglobal.hpp | 6 ++++++ 4 files changed, 42 insertions(+), 4 deletions(-) diff --git a/src/core/iconimageprovider.cpp b/src/core/iconimageprovider.cpp index f4710fb..cf24d37 100644 --- a/src/core/iconimageprovider.cpp +++ b/src/core/iconimageprovider.cpp @@ -11,7 +11,9 @@ QPixmap IconImageProvider::requestPixmap(const QString& id, QSize* size, const QSize& requestedSize) { QString iconName; + QString fallbackName; QString path; + auto splitIdx = id.indexOf("?path="); if (splitIdx != -1) { iconName = id.sliced(0, splitIdx); @@ -19,10 +21,17 @@ IconImageProvider::requestPixmap(const QString& id, QSize* size, const QSize& re qWarning() << "Searching custom icon paths is not yet supported. Icon path will be ignored for" << id; } else { - iconName = id; + splitIdx = id.indexOf("?fallback="); + if (splitIdx != -1) { + iconName = id.sliced(0, splitIdx); + fallbackName = id.sliced(splitIdx + 10); + } else { + iconName = id; + } } auto icon = QIcon::fromTheme(iconName); + if (icon.isNull()) icon = QIcon::fromTheme(fallbackName); auto targetSize = requestedSize.isValid() ? requestedSize : QSize(100, 100); if (targetSize.width() == 0 || targetSize.height() == 0) targetSize = QSize(2, 2); @@ -55,12 +64,20 @@ QPixmap IconImageProvider::missingPixmap(const QSize& size) { return pixmap; } -QString IconImageProvider::requestString(const QString& icon, const QString& path) { +QString IconImageProvider::requestString( + const QString& icon, + const QString& path, + const QString& fallback +) { auto req = "image://icon/" + icon; if (!path.isEmpty()) { req += "?path=" + path; } + if (!fallback.isEmpty()) { + req += "?fallback=" + fallback; + } + return req; } diff --git a/src/core/iconimageprovider.hpp b/src/core/iconimageprovider.hpp index 167d93b..57e2604 100644 --- a/src/core/iconimageprovider.hpp +++ b/src/core/iconimageprovider.hpp @@ -10,5 +10,10 @@ class IconImageProvider: public QQuickImageProvider { QPixmap requestPixmap(const QString& id, QSize* size, const QSize& requestedSize) override; static QPixmap missingPixmap(const QSize& size); - static QString requestString(const QString& icon, const QString& path); + + static QString requestString( + const QString& icon, + const QString& path = QString(), + const QString& fallback = QString() + ); }; diff --git a/src/core/qmlglobal.cpp b/src/core/qmlglobal.cpp index 9328d90..23f238d 100644 --- a/src/core/qmlglobal.cpp +++ b/src/core/qmlglobal.cpp @@ -5,6 +5,7 @@ #include #include #include +#include #include #include #include @@ -196,7 +197,16 @@ QVariant QuickshellGlobal::env(const QString& variable) { // NOLINT } QString QuickshellGlobal::iconPath(const QString& icon) { - return IconImageProvider::requestString(icon, ""); + return IconImageProvider::requestString(icon); +} + +QString QuickshellGlobal::iconPath(const QString& icon, bool check) { + if (check && QIcon::fromTheme(icon).isNull()) return ""; + return IconImageProvider::requestString(icon); +} + +QString QuickshellGlobal::iconPath(const QString& icon, const QString& fallback) { + return IconImageProvider::requestString(icon, "", fallback); } QuickshellGlobal* QuickshellGlobal::create(QQmlEngine* engine, QJSEngine* /*unused*/) { diff --git a/src/core/qmlglobal.hpp b/src/core/qmlglobal.hpp index 7efdb41..4f46d23 100644 --- a/src/core/qmlglobal.hpp +++ b/src/core/qmlglobal.hpp @@ -137,6 +137,12 @@ class QuickshellGlobal: public QObject { /// > at the top of your root config file or set the `QS_ICON_THEME` variable to the name /// > of your icon theme. Q_INVOKABLE static QString iconPath(const QString& icon); + /// Setting the `check` parameter of `iconPath` to true will return an empty string + /// if the icon does not exist, instead of an image showing a missing texture. + Q_INVOKABLE static QString iconPath(const QString& icon, bool check); + /// Setting the `fallback` parameter of `iconPath` will attempt to load the fallback + /// icon if the requested one could not be loaded. + Q_INVOKABLE static QString iconPath(const QString& icon, const QString& fallback); [[nodiscard]] QString shellRoot() const; From fdc13023b7a2cf11a68756c673e6bf0f9d7a37fc Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Sun, 17 Nov 2024 17:05:44 -0800 Subject: [PATCH 13/52] widgets: add ClippingRectangle --- BUILD.md | 1 + CMakeLists.txt | 2 +- default.nix | 3 ++ src/widgets/CMakeLists.txt | 20 +++++++- src/widgets/ClippingRectangle.qml | 82 +++++++++++++++++++++++++++++++ src/widgets/cliprect.cpp | 1 + src/widgets/cliprect.hpp | 20 ++++++++ src/widgets/module.md | 1 + src/widgets/shaders/cliprect.frag | 40 +++++++++++++++ 9 files changed, 168 insertions(+), 2 deletions(-) create mode 100644 src/widgets/ClippingRectangle.qml create mode 100644 src/widgets/cliprect.cpp create mode 100644 src/widgets/cliprect.hpp create mode 100644 src/widgets/shaders/cliprect.frag diff --git a/BUILD.md b/BUILD.md index 8fa7884..2085a5b 100644 --- a/BUILD.md +++ b/BUILD.md @@ -41,6 +41,7 @@ Quickshell has a set of base dependencies you will always need, names vary by di - `cmake` - `qt6base` - `qt6declarative` +- `qtshadertools` (build-time only) - `pkg-config` - `cli11` diff --git a/CMakeLists.txt b/CMakeLists.txt index 23e6add..61cc296 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -91,7 +91,7 @@ if (NOT CMAKE_BUILD_TYPE) set(CMAKE_BUILD_TYPE Debug) endif() -set(QT_FPDEPS Gui Qml Quick QuickControls2 Widgets) +set(QT_FPDEPS Gui Qml Quick QuickControls2 Widgets ShaderTools) include(cmake/pch.cmake) diff --git a/default.nix b/default.nix index 298e561..83c5e54 100644 --- a/default.nix +++ b/default.nix @@ -8,6 +8,7 @@ cmake, ninja, qt6, + spirv-tools, cli11, breakpad, jemalloc, @@ -45,6 +46,8 @@ nativeBuildInputs = with pkgs; [ cmake ninja + qt6.qtshadertools + spirv-tools qt6.wrapQtAppsHook pkg-config ] ++ (lib.optionals withWayland [ diff --git a/src/widgets/CMakeLists.txt b/src/widgets/CMakeLists.txt index def0aaf..5fbcc5b 100644 --- a/src/widgets/CMakeLists.txt +++ b/src/widgets/CMakeLists.txt @@ -1,10 +1,28 @@ -qt_add_library(quickshell-widgets STATIC) +qt_add_library(quickshell-widgets STATIC + cliprect.cpp +) qt_add_qml_module(quickshell-widgets URI Quickshell.Widgets VERSION 0.1 QML_FILES IconImage.qml + ClippingRectangle.qml +) + +qt6_add_shaders(quickshell-widgets "widgets-cliprect" + NOHLSL NOMSL BATCHABLE PRECOMPILE OPTIMIZED QUIET + PREFIX "/Quickshell/Widgets" + FILES shaders/cliprect.frag + OUTPUTS shaders/cliprect.frag.qsb +) + +qt6_add_shaders(quickshell-widgets "widgets-cliprect-ub" + NOHLSL NOMSL BATCHABLE PRECOMPILE OPTIMIZED QUIET + PREFIX "/Quickshell/Widgets" + FILES shaders/cliprect.frag + OUTPUTS shaders/cliprect-ub.frag.qsb + DEFINES CONTENT_UNDER_BORDER ) install_qml_module(quickshell-widgets) diff --git a/src/widgets/ClippingRectangle.qml b/src/widgets/ClippingRectangle.qml new file mode 100644 index 0000000..ca8ae5a --- /dev/null +++ b/src/widgets/ClippingRectangle.qml @@ -0,0 +1,82 @@ +import QtQuick + +///! Rectangle capable of clipping content inside its border. +/// > [!WARNING] This type requires at least Qt 6.7. +/// +/// This is a specialized version of @@QtQuick.Rectangle that clips content +/// inside of its border, including rounded rectangles. It costs more than +/// @@QtQuick.Rectangle, so it should not be used unless you need to clip +/// items inside of it to the border. +Item { + id: root + + /// If content should be displayed underneath the border. + /// + /// Defaults to false, does nothing if the border is opaque. + property bool contentUnderBorder: false; + /// If the content item should be resized to fit inside the border. + /// + /// Defaults to `!contentUnderBorder`. Most useful when combined with + /// `anchors.fill: parent` on an item passed to the ClippingRectangle. + property bool contentInsideBorder: !root.contentUnderBorder; + /// If the rectangle should be antialiased. + /// + /// Defaults to true if any corner has a non-zero radius, otherwise false. + property /*bool*/alias antialiasing: rectangle.antialiasing; + /// The background color of the rectangle, which goes under its content. + property /*color*/alias color: shader.backgroundColor; + /// See @@QtQuick.Rectangle.border. + property clippingRectangleBorder border; + /// Radius of all corners. Defaults to 0. + property /*real*/alias radius: rectangle.radius + /// Radius of the top left corner. Defaults to @@radius. + property /*real*/alias topLeftRadius: rectangle.topLeftRadius + /// Radius of the top right corner. Defaults to @@radius. + property /*real*/alias topRightRadius: rectangle.topRightRadius + /// Radius of the bottom left corner. Defaults to @@radius. + property /*real*/alias bottomLeftRadius: rectangle.bottomLeftRadius + /// Radius of the bottom right corner. Defaults to @@radius. + property /*real*/alias borromRightRadius: rectangle.bottomRightRadius + + /// Visual children of the ClippingRectangle's @@contentItem. (`list`). + /// + /// See @@QtQuick.Item.children for details. + default property alias children: contentItem.children; + /// The item containing the rectangle's content. + /// There is usually no reason to use this directly. + readonly property alias contentItem: contentItem; + + Rectangle { + id: rectangle + anchors.fill: root + color: "#ffff0000" + border.color: "#ff00ff00" + border.pixelAligned: root.border.pixelAligned + border.width: root.border.width + layer.enabled: true + visible: false + } + + Item { + id: contentItemContainer + anchors.fill: root + layer.enabled: true + visible: false + + Item { + id: contentItem + anchors.fill: parent + anchors.margins: root.contentInsideBorder ? root.border.width : 0 + } + } + + ShaderEffect { + id: shader + anchors.fill: root + fragmentShader: `qrc:/Quickshell/Widgets/shaders/cliprect${root.contentUnderBorder ? "-ub" : ""}.frag.qsb` + property Rectangle rect: rectangle; + property color backgroundColor; + property color borderColor: root.border.color; + property Item content: contentItemContainer; + } +} diff --git a/src/widgets/cliprect.cpp b/src/widgets/cliprect.cpp new file mode 100644 index 0000000..a66147e --- /dev/null +++ b/src/widgets/cliprect.cpp @@ -0,0 +1 @@ +#include "cliprect.hpp" // NOLINT diff --git a/src/widgets/cliprect.hpp b/src/widgets/cliprect.hpp new file mode 100644 index 0000000..adc1381 --- /dev/null +++ b/src/widgets/cliprect.hpp @@ -0,0 +1,20 @@ +#pragma once + +#include +#include +#include +#include +#include + +class ClippingRectangleBorder { + Q_GADGET; + Q_PROPERTY(QColor color MEMBER color); + Q_PROPERTY(bool pixelAligned MEMBER pixelAligned); + Q_PROPERTY(int width MEMBER width); + QML_VALUE_TYPE(clippingRectangleBorder); + +public: + QColor color = Qt::black; + bool pixelAligned = true; + int width = 0; +}; diff --git a/src/widgets/module.md b/src/widgets/module.md index 9a51894..c24bb87 100644 --- a/src/widgets/module.md +++ b/src/widgets/module.md @@ -2,5 +2,6 @@ name = "Quickshell.Widgets" description = "Bundled widgets" qml_files = [ "IconImage.qml", + "ClippingRectangle.qml", ] ----- diff --git a/src/widgets/shaders/cliprect.frag b/src/widgets/shaders/cliprect.frag new file mode 100644 index 0000000..f1c004a --- /dev/null +++ b/src/widgets/shaders/cliprect.frag @@ -0,0 +1,40 @@ +#version 440 +layout(location = 0) in vec2 qt_TexCoord0; +layout(location = 0) out vec4 fragColor; + +layout(std140, binding = 0) uniform buf { + mat4 qt_Matrix; + float qt_Opacity; + vec4 backgroundColor; + vec4 borderColor; +}; + +layout(binding = 1) uniform sampler2D rect; +layout(binding = 2) uniform sampler2D content; + +vec4 overlay(vec4 base, vec4 overlay) { + if (overlay.a == 0.0) return base; + + float baseMul = 1.0 - overlay.a; + float newAlpha = overlay.a + base.a * baseMul; + vec3 rgb = (overlay.rgb * overlay.a + base.rgb * base.a * baseMul) / newAlpha; + return vec4(rgb, newAlpha); +} + +void main() { + vec4 contentColor = texture(content, qt_TexCoord0.xy); + vec4 rectColor = texture(rect, qt_TexCoord0.xy); + +#ifdef CONTENT_UNDER_BORDER + float contentAlpha = rectColor.a; +#else + float contentAlpha = rectColor.r; +#endif + + float borderAlpha = rectColor.g; + + vec4 innerColor = overlay(backgroundColor, contentColor) * contentAlpha; + vec4 borderColor = borderColor * borderAlpha; + + fragColor = (innerColor * (1.0 - borderColor.a) + borderColor) * qt_Opacity; +} From 36174854ada58d7fdf47b92edadf200e2084d03a Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Sun, 17 Nov 2024 17:06:06 -0800 Subject: [PATCH 14/52] services/tray: fix const lint in item --- src/services/status_notifier/item.cpp | 2 +- src/services/status_notifier/item.hpp | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/services/status_notifier/item.cpp b/src/services/status_notifier/item.cpp index f6e16a2..e3bb012 100644 --- a/src/services/status_notifier/item.cpp +++ b/src/services/status_notifier/item.cpp @@ -248,7 +248,7 @@ void StatusNotifierItem::onGetAllFinished() { emit this->ready(); } -void StatusNotifierItem::onGetAllFailed() { +void StatusNotifierItem::onGetAllFailed() const { // Not changing the item to ready, as it is almost definitely broken. if (!this->mReady) { qWarning(logStatusNotifierItem) << "Failed to load tray item" << this->properties.toString(); diff --git a/src/services/status_notifier/item.hpp b/src/services/status_notifier/item.hpp index 2a22e2e..eccf79b 100644 --- a/src/services/status_notifier/item.hpp +++ b/src/services/status_notifier/item.hpp @@ -75,7 +75,7 @@ class StatusNotifierItem: public QObject { private slots: void updateIcon(); void onGetAllFinished(); - void onGetAllFailed(); + void onGetAllFailed() const; void onMenuPathChanged(); private: From 79fca3cab88e1911d4203507ccce47ebaeaaff9c Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Sun, 17 Nov 2024 21:38:17 -0800 Subject: [PATCH 15/52] docs: mention spirv-tools in BUILD.md --- BUILD.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/BUILD.md b/BUILD.md index 2085a5b..659a616 100644 --- a/BUILD.md +++ b/BUILD.md @@ -42,7 +42,8 @@ Quickshell has a set of base dependencies you will always need, names vary by di - `qt6base` - `qt6declarative` - `qtshadertools` (build-time only) -- `pkg-config` +- `spirv-tools` (build-time only) +- `pkg-config` (build-time only) - `cli11` On some distros, private Qt headers are in separate packages which you may have to install. From 401ee4cec6dec6eb846e7b525231a259f6da8549 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Tue, 19 Nov 2024 02:02:55 -0800 Subject: [PATCH 16/52] widgets: add wrapper components and managers --- src/widgets/CMakeLists.txt | 4 + src/widgets/WrapperItem.qml | 44 ++++++++++ src/widgets/WrapperRectangle.qml | 35 ++++++++ src/widgets/marginwrapper.cpp | 146 +++++++++++++++++++++++++++++++ src/widgets/marginwrapper.hpp | 75 ++++++++++++++++ src/widgets/module.md | 8 ++ src/widgets/wrapper.cpp | 127 +++++++++++++++++++++++++++ src/widgets/wrapper.hpp | 139 +++++++++++++++++++++++++++++ 8 files changed, 578 insertions(+) create mode 100644 src/widgets/WrapperItem.qml create mode 100644 src/widgets/WrapperRectangle.qml create mode 100644 src/widgets/marginwrapper.cpp create mode 100644 src/widgets/marginwrapper.hpp create mode 100644 src/widgets/wrapper.cpp create mode 100644 src/widgets/wrapper.hpp diff --git a/src/widgets/CMakeLists.txt b/src/widgets/CMakeLists.txt index 5fbcc5b..3f8de41 100644 --- a/src/widgets/CMakeLists.txt +++ b/src/widgets/CMakeLists.txt @@ -1,5 +1,7 @@ qt_add_library(quickshell-widgets STATIC cliprect.cpp + wrapper.cpp + marginwrapper.cpp ) qt_add_qml_module(quickshell-widgets @@ -8,6 +10,8 @@ qt_add_qml_module(quickshell-widgets QML_FILES IconImage.qml ClippingRectangle.qml + WrapperItem.qml + WrapperRectangle.qml ) qt6_add_shaders(quickshell-widgets "widgets-cliprect" diff --git a/src/widgets/WrapperItem.qml b/src/widgets/WrapperItem.qml new file mode 100644 index 0000000..dfa7c0f --- /dev/null +++ b/src/widgets/WrapperItem.qml @@ -0,0 +1,44 @@ +import QtQuick +import Quickshell.Widgets + +///! Item that handles sizes and positioning for a single visual child. +/// This component is useful when you need to wrap a single component in +/// an item, or give a single component a margin. See [QtQuick.Layouts] +/// for positioning multiple items. +/// +/// > [!NOTE] WrapperItem is a @@MarginWrapperManager based component. +/// > You should read its documentation as well. +/// +/// ### Example: Adding a margin to an item +/// The snippet below adds a 10px margin to all sides of the @@QtQuick.Text item. +/// +/// ```qml +/// WrapperItem { +/// margin: 10 +/// +/// @@QtQuick.Text { text: "Hello!" } +/// } +/// ``` +/// +/// > [!NOTE] The child item can be specified by writing it inline in the wrapper, +/// > as in the example above, or by using the @@child property. See +/// > @@WrapperManager.child for details. +/// +/// > [!WARNING] You should not set @@Item.x, @@Item.y, @@Item.width, +/// > @@Item.height or @@Item.anchors on the child item, as they are used +/// > by WrapperItem to position it. Instead set @@Item.implicitWidth and +/// > @@Item.implicitHeight. +/// +/// [QtQuick.Layouts]: https://doc.qt.io/qt-6/qtquicklayouts-index.html +Item { + /// The minimum margin between the child item and the WrapperItem's edges. + /// Defaults to 0. + property /*real*/alias margin: manager.margin + /// If the child item should be resized larger than its implicit size if + /// the WrapperItem is resized larger than its implicit size. Defaults to false. + property /*bool*/alias resizeChild: manager.resizeChild + /// See @@WrapperManager.child for details. + property /*Item*/alias child: manager.child + + MarginWrapperManager { id: manager } +} diff --git a/src/widgets/WrapperRectangle.qml b/src/widgets/WrapperRectangle.qml new file mode 100644 index 0000000..c198c47 --- /dev/null +++ b/src/widgets/WrapperRectangle.qml @@ -0,0 +1,35 @@ +import QtQuick +import Quickshell.Widgets + +///! Rectangle that handles sizes and positioning for a single visual child. +/// This component is useful for adding a border or background rectangle to +/// a child item. +/// +/// > [!NOTE] WrapperRectangle is a @@MarginWrapperManager based component. +/// > You should read its documentation as well. +/// +/// > [!WARNING] You should not set @@Item.x, @@Item.y, @@Item.width, +/// > @@Item.height or @@Item.anchors on the child item, as they are used +/// > by WrapperItem to position it. Instead set @@Item.implicitWidth and +/// > @@Item.implicitHeight. +Rectangle { + id: root + + /// If true (default), the rectangle's border width will be added + /// to the margin. + property bool contentInsideBorder: true + /// The minimum margin between the child item and the WrapperRectangle's + /// edges. If @@contentInsideBorder is true, this excludes the border, + /// otherwise it includes it. Defaults to 0. + property real margin: 0 + /// If the child item should be resized larger than its implicit size if + /// the WrapperRectangle is resized larger than its implicit size. Defaults to false. + property /*bool*/alias resizeChild: manager.resizeChild + /// See @@WrapperManager.child for details. + property alias child: manager.child + + MarginWrapperManager { + id: manager + margin: (root.contentInsideBorder ? root.border.width : 0) + root.margin + } +} diff --git a/src/widgets/marginwrapper.cpp b/src/widgets/marginwrapper.cpp new file mode 100644 index 0000000..b960a9f --- /dev/null +++ b/src/widgets/marginwrapper.cpp @@ -0,0 +1,146 @@ +#include "marginwrapper.hpp" +#include + +#include +#include +#include +#include + +#include "wrapper.hpp" + +namespace qs::widgets { + +MarginWrapperManager::MarginWrapperManager(QObject* parent): WrapperManager(parent) { + QObject::connect( + this, + &WrapperManager::initializedChildChanged, + this, + &MarginWrapperManager::onChildChanged + ); +} + +void MarginWrapperManager::componentComplete() { + if (this->mWrapper) { + QObject::connect( + this->mWrapper, + &QQuickItem::widthChanged, + this, + &MarginWrapperManager::onWrapperWidthChanged + ); + + QObject::connect( + this->mWrapper, + &QQuickItem::heightChanged, + this, + &MarginWrapperManager::onWrapperHeightChanged + ); + } + + this->WrapperManager::componentComplete(); + + if (!this->mChild) this->updateGeometry(); +} + +qreal MarginWrapperManager::margin() const { return this->mMargin; } + +void MarginWrapperManager::setMargin(qreal margin) { + if (margin == this->mMargin) return; + this->mMargin = margin; + this->updateGeometry(); + emit this->marginChanged(); +} + +bool MarginWrapperManager::resizeChild() const { return this->mResizeChild; } + +void MarginWrapperManager::setResizeChild(bool resizeChild) { + if (resizeChild == this->mResizeChild) return; + this->mResizeChild = resizeChild; + this->updateGeometry(); + emit this->resizeChildChanged(); +} + +void MarginWrapperManager::onChildChanged() { + // QObject::disconnect in MarginWrapper handles disconnecting old item + + if (this->mChild) { + QObject::connect( + this->mChild, + &QQuickItem::implicitWidthChanged, + this, + &MarginWrapperManager::onChildImplicitWidthChanged + ); + + QObject::connect( + this->mChild, + &QQuickItem::implicitHeightChanged, + this, + &MarginWrapperManager::onChildImplicitHeightChanged + ); + } + + this->updateGeometry(); +} + +qreal MarginWrapperManager::targetChildWidth() const { + auto max = this->mWrapper->width() - this->mMargin * 2; + + if (this->mResizeChild) return max; + else return std::min(this->mChild->implicitWidth(), max); +} + +qreal MarginWrapperManager::targetChildHeight() const { + auto max = this->mWrapper->height() - this->mMargin * 2; + + if (this->mResizeChild) return max; + else return std::min(this->mChild->implicitHeight(), max); +} + +qreal MarginWrapperManager::targetChildX() const { + if (this->mResizeChild) return this->mMargin; + else return this->mWrapper->width() / 2 - this->mChild->implicitWidth() / 2; +} + +qreal MarginWrapperManager::targetChildY() const { + if (this->mResizeChild) return this->mMargin; + else return this->mWrapper->height() / 2 - this->mChild->implicitHeight() / 2; +} + +void MarginWrapperManager::onWrapperWidthChanged() { + if (!this->mChild || !this->mWrapper) return; + this->mChild->setX(this->targetChildX()); + this->mChild->setWidth(this->targetChildWidth()); +} + +void MarginWrapperManager::onWrapperHeightChanged() { + if (!this->mChild || !this->mWrapper) return; + this->mChild->setY(this->targetChildY()); + this->mChild->setHeight(this->targetChildHeight()); +} + +void MarginWrapperManager::onChildImplicitWidthChanged() { + if (!this->mChild || !this->mWrapper) return; + this->mWrapper->setImplicitWidth(this->mChild->implicitWidth() + this->mMargin * 2); +} + +void MarginWrapperManager::onChildImplicitHeightChanged() { + if (!this->mChild || !this->mWrapper) return; + this->mWrapper->setImplicitHeight(this->mChild->implicitHeight() + this->mMargin * 2); +} + +void MarginWrapperManager::updateGeometry() { + if (!this->mWrapper) return; + + if (this->mChild) { + this->mWrapper->setImplicitWidth(this->mChild->implicitWidth() + this->mMargin * 2); + this->mWrapper->setImplicitHeight(this->mChild->implicitHeight() + this->mMargin * 2); + this->mChild->setX(this->targetChildX()); + this->mChild->setY(this->targetChildY()); + this->mChild->setWidth(this->targetChildWidth()); + this->mChild->setHeight(this->targetChildHeight()); + } else { + this->mWrapper->setImplicitWidth(this->mMargin * 2); + this->mWrapper->setImplicitHeight(this->mMargin * 2); + } +} + +} // namespace qs::widgets diff --git a/src/widgets/marginwrapper.hpp b/src/widgets/marginwrapper.hpp new file mode 100644 index 0000000..7946951 --- /dev/null +++ b/src/widgets/marginwrapper.hpp @@ -0,0 +1,75 @@ +#pragma once + +#include +#include +#include +#include + +#include "wrapper.hpp" + +namespace qs::widgets { + +///! Helper object for applying sizes and margins to a single child item. +/// > [!NOTE] MarginWrapperManager is an extension of @@WrapperManager. +/// > You should read its documentation to understand wrapper types. +/// +/// MarginWrapperManager can be used to apply margins to a child item, +/// in addition to handling the size / implicit size relationship +/// between the parent and the child. @@WrapperItem and @@WrapperRectangle +/// exist for Item and Rectangle implementations respectively. +/// +/// > [!WARNING] MarginWrapperManager based types set the child item's +/// > @@QtQuick.Item.x, @@QtQuick.Item.y, @@QtQuick.Item.width, @@QtQuick.Item.height +/// > or @@QtQuick.Item.anchors properties. Do not set them yourself, +/// > instead set @@Item.implicitWidth and @@Item.implicitHeight. +/// +/// ### Implementing a margin wrapper type +/// Follow the directions in @@WrapperManager$'s documentation, and or +/// alias the @@margin property if you wish to expose it. +class MarginWrapperManager: public WrapperManager { + Q_OBJECT; + // clang-format off + /// The minimum margin between the child item and the parent item's edges. + /// Defaults to 0. + Q_PROPERTY(qreal margin READ margin WRITE setMargin NOTIFY marginChanged FINAL); + /// If the child item should be resized larger than its implicit size if + /// the parent is resized larger than its implicit size. Defaults to false. + Q_PROPERTY(bool resizeChild READ resizeChild WRITE setResizeChild NOTIFY resizeChildChanged FINAL); + // clang-format on + QML_ELEMENT; + +public: + explicit MarginWrapperManager(QObject* parent = nullptr); + + void componentComplete() override; + + [[nodiscard]] qreal margin() const; + void setMargin(qreal margin); + + [[nodiscard]] bool resizeChild() const; + void setResizeChild(bool resizeChild); + +signals: + void marginChanged(); + void resizeChildChanged(); + +private slots: + void onChildChanged(); + void onWrapperWidthChanged(); + void onWrapperHeightChanged(); + void onChildImplicitWidthChanged(); + void onChildImplicitHeightChanged(); + +private: + void updateGeometry(); + + [[nodiscard]] qreal targetChildX() const; + [[nodiscard]] qreal targetChildY() const; + [[nodiscard]] qreal targetChildWidth() const; + [[nodiscard]] qreal targetChildHeight() const; + + qreal mMargin = 0; + bool mResizeChild = false; +}; + +} // namespace qs::widgets diff --git a/src/widgets/module.md b/src/widgets/module.md index c24bb87..77d4a3a 100644 --- a/src/widgets/module.md +++ b/src/widgets/module.md @@ -1,7 +1,15 @@ name = "Quickshell.Widgets" description = "Bundled widgets" + +headers = [ + "wrapper.hpp", + "marginwrapper.hpp", +] + qml_files = [ "IconImage.qml", "ClippingRectangle.qml", + "WrapperItem.qml", + "WrapperRectangle.qml", ] ----- diff --git a/src/widgets/wrapper.cpp b/src/widgets/wrapper.cpp new file mode 100644 index 0000000..4e502ce --- /dev/null +++ b/src/widgets/wrapper.cpp @@ -0,0 +1,127 @@ +#include "wrapper.hpp" + +#include +#include +#include +#include +#include +#include +#include + +namespace qs::widgets { + +void WrapperManager::componentComplete() { + this->mWrapper = qobject_cast(this->parent()); + + if (!this->mWrapper) { + QString pstr; + QDebug(&pstr) << this->parent(); + + qmlWarning(this) << "Parent of WrapperManager is not a QQuickItem. Parent: " << pstr; + return; + } + + QQuickItem* child = this->mChild; + this->mChild = nullptr; // avoids checks for the old item in setChild. + + const auto& childItems = this->mWrapper->childItems(); + + if (childItems.length() == 1) { + this->mDefaultChild = childItems.first(); + } else if (childItems.length() != 0) { + this->flags.setFlag(WrapperManager::HasMultipleChildren); + + if (!child && !this->flags.testFlags(WrapperManager::NullChild)) { + this->printChildCountWarning(); + } + } + + for (auto* item: childItems) { + if (item != child) item->setParentItem(nullptr); + } + + if (child && !this->flags.testFlag(WrapperManager::NullChild)) { + this->setChild(child); + } +} + +QQuickItem* WrapperManager::child() const { return this->mChild; } + +void WrapperManager::setChild(QQuickItem* child) { + if (child && child == this->mChild) return; + + if (this->mChild != nullptr) { + QObject::disconnect(this->mChild, nullptr, this, nullptr); + + if (this->mChild->parentItem() == this->mWrapper) { + this->mChild->setParentItem(nullptr); + } + } + + this->mChild = child; + this->flags.setFlag(WrapperManager::NullChild, child == nullptr); + + if (child) { + QObject::connect( + child, + &QObject::destroyed, + this, + &WrapperManager::onChildDestroyed, + Qt::UniqueConnection + ); + + if (auto* wrapper = this->mWrapper) { + child->setParentItem(wrapper); + } + } + + emit this->initializedChildChanged(); + emit this->childChanged(); +} + +void WrapperManager::setProspectiveChild(QQuickItem* child) { + if (child && child == this->mChild) return; + + if (!this->mWrapper) { + if (this->mChild) { + QObject::disconnect(this->mChild, nullptr, this, nullptr); + } + + this->mChild = child; + this->flags.setFlag(WrapperManager::NullChild, child == nullptr); + + if (child) { + QObject::connect(child, &QObject::destroyed, this, &WrapperManager::onChildDestroyed); + } + } else { + this->setChild(child); + } +} + +void WrapperManager::unsetChild() { + if (!this->mWrapper) { + this->setProspectiveChild(nullptr); + } else { + this->setChild(this->mDefaultChild); + + if (!this->mDefaultChild && this->flags.testFlag(WrapperManager::HasMultipleChildren)) { + this->printChildCountWarning(); + } + } + + this->flags.setFlag(WrapperManager::NullChild, false); +} + +void WrapperManager::onChildDestroyed() { + this->mChild = nullptr; + this->unsetChild(); + emit this->childChanged(); +} + +void WrapperManager::printChildCountWarning() const { + qmlWarning(this->mWrapper) << "Wrapper component cannot have more than one visual child."; + qmlWarning(this->mWrapper) << "Remove all additional children, or pick a specific component " + "to wrap using the child property."; +} + +} // namespace qs::widgets diff --git a/src/widgets/wrapper.hpp b/src/widgets/wrapper.hpp new file mode 100644 index 0000000..95b3ade --- /dev/null +++ b/src/widgets/wrapper.hpp @@ -0,0 +1,139 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "../core/doc.hpp" + +namespace qs::widgets { + +///! Helper object for creating components with a single visual child. +/// WrapperManager determines which child of an Item should be its visual +/// child, and exposes it for further operations. See @@MarginWrapperManager +/// for a subclass that implements automatic sizing and margins. +/// +/// ### Using wrapper types +/// WrapperManager based types have a single visual child item. +/// You can specify the child item using the default property, or by +/// setting the @@child property. You must use the @@child property if +/// the widget has more than one @@QtQuick.Item based child. +/// +/// #### Example using the default property +/// ```qml +/// WrapperWidget { // a widget that uses WrapperManager +/// // Putting the item inline uses the default property of WrapperWidget. +/// @@QtQuick.Text { text: "Hello" } +/// +/// // Scope does not extend Item, so it can be placed in the +/// // default property without issue. +/// @@Quickshell.Scope {} +/// } +/// ``` +/// +/// #### Example using the child property +/// ```qml +/// WrapperWidget { +/// @@QtQuick.Text { +/// id: text +/// text: "Hello" +/// } +/// +/// @@QtQuick.Text { +/// id: otherText +/// text: "Other Text" +/// } +/// +/// // Both text and otherText extend Item, so one must be specified. +/// child: text +/// } +/// ``` +/// +/// See @@child for more details on how the child property can be used. +/// +/// ### Implementing wrapper types +/// In addition to the bundled wrapper types, you can make your own using +/// WrapperManager. To implement a wrapper, create a WrapperManager inside +/// your wrapper component 's default property, then alias a new property +/// to the WrapperManager's @@child property. +/// +/// #### Example +/// ```qml +/// Item { // your wrapper component +/// WrapperManager { id: wrapperManager } +/// +/// // Allows consumers of your wrapper component to use the child property. +/// property alias child: wrapperManager.child +/// +/// // The rest of your component logic. You can use +/// // `wrapperManager.child` or `this.child` to refer to the selected child. +/// } +/// ``` +/// +/// ### See also +/// - @@WrapperItem - A @@MarginWrapperManager based component that sizes itself +/// to its child. +/// - @@WrapperRectangle - A @@MarginWrapperManager based component that sizes +/// itself to its child, and provides an option to use its border as an inset. +class WrapperManager + : public QObject + , public QQmlParserStatus { + Q_OBJECT; + // clang-format off + /// The wrapper component's selected child. + /// + /// Setting this property override's WrapperManager's default selection, + /// and resolve ambiguity when more than one visual child is present. + /// The property can additionally be defined inline or reference a component + /// that is not already a child of the wrapper, in which case it will be + /// reparented to the wrapper. Setting child to `null` will select no child, + /// and `undefined` will restore the default child. + /// + /// When read, `child` will always return the (potentially null) selected child, + /// and not `undefined`. + Q_PROPERTY(QQuickItem* child READ child WRITE setProspectiveChild RESET unsetChild NOTIFY childChanged FINAL); + // clang-format on + QML_ELEMENT; + +public: + explicit WrapperManager(QObject* parent = nullptr): QObject(parent) {} + + void classBegin() override {} + void componentComplete() override; + + [[nodiscard]] QQuickItem* child() const; + void setChild(QQuickItem* child); + void setProspectiveChild(QQuickItem* child); + void unsetChild(); + +signals: + void childChanged(); + QSDOC_HIDE void initializedChildChanged(); + +private slots: + void onChildDestroyed(); + +protected: + enum Flag : quint8 { + NoFlags = 0x0, + NullChild = 0x1, + HasMultipleChildren = 0x2, + }; + Q_DECLARE_FLAGS(Flags, Flag); + + void printChildCountWarning() const; + void updateGeometry(); + + QQuickItem* mWrapper = nullptr; + QPointer mDefaultChild; + QQuickItem* mChild = nullptr; + Flags flags; +}; + +} // namespace qs::widgets From 033e8108716bb348b97574c5a978ba715fd4edd4 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Tue, 19 Nov 2024 02:52:49 -0800 Subject: [PATCH 17/52] widgets: add ClippingWrapperRectangle --- src/widgets/CMakeLists.txt | 1 + src/widgets/ClippingWrapperRectangle.qml | 35 ++++++++++++++++++++++++ src/widgets/WrapperItem.qml | 1 - src/widgets/WrapperRectangle.qml | 4 +-- src/widgets/module.md | 1 + src/widgets/wrapper.cpp | 17 +++++++++++- src/widgets/wrapper.hpp | 8 ++++++ 7 files changed, 63 insertions(+), 4 deletions(-) create mode 100644 src/widgets/ClippingWrapperRectangle.qml diff --git a/src/widgets/CMakeLists.txt b/src/widgets/CMakeLists.txt index 3f8de41..29e760a 100644 --- a/src/widgets/CMakeLists.txt +++ b/src/widgets/CMakeLists.txt @@ -12,6 +12,7 @@ qt_add_qml_module(quickshell-widgets ClippingRectangle.qml WrapperItem.qml WrapperRectangle.qml + ClippingWrapperRectangle.qml ) qt6_add_shaders(quickshell-widgets "widgets-cliprect" diff --git a/src/widgets/ClippingWrapperRectangle.qml b/src/widgets/ClippingWrapperRectangle.qml new file mode 100644 index 0000000..26014a6 --- /dev/null +++ b/src/widgets/ClippingWrapperRectangle.qml @@ -0,0 +1,35 @@ +import QtQuick + +///! ClippingRectangle that handles sizes and positioning for a single visual child. +/// This component is useful for adding a clipping border or background rectangle to +/// a child item. If you don't need clipping, use @@WrapperRectangle. +/// +/// > [!NOTE] ClippingWrapperRectangle is a @@MarginWrapperManager based component. +/// > You should read its documentation as well. +/// +/// > [!WARNING] You should not set @@Item.x, @@Item.y, @@Item.width, +/// > @@Item.height or @@Item.anchors on the child item, as they are used +/// > by WrapperItem to position it. Instead set @@Item.implicitWidth and +/// > @@Item.implicitHeight. +ClippingRectangle { + id: root + + /// The minimum margin between the child item and the ClippingWrapperRectangle's + /// edges. Defaults to 0. + property /*real*/alias margin: manager.margin + /// If the child item should be resized larger than its implicit size if + /// the WrapperRectangle is resized larger than its implicit size. Defaults to false. + property /*bool*/alias resizeChild: manager.resizeChild + /// See @@WrapperManager.child for details. + property alias child: manager.child + + implicitWidth: root.contentItem.implicitWidth + (root.contentInsideBorder ? root.border.width * 2 : 0) + implicitHeight: root.contentItem.implicitHeight + (root.contentInsideBorder ? root.border.width * 2 : 0) + + resources: [ + MarginWrapperManager { + id: manager + wrapper: root.contentItem + } + ] +} diff --git a/src/widgets/WrapperItem.qml b/src/widgets/WrapperItem.qml index dfa7c0f..e1701ca 100644 --- a/src/widgets/WrapperItem.qml +++ b/src/widgets/WrapperItem.qml @@ -1,5 +1,4 @@ import QtQuick -import Quickshell.Widgets ///! Item that handles sizes and positioning for a single visual child. /// This component is useful when you need to wrap a single component in diff --git a/src/widgets/WrapperRectangle.qml b/src/widgets/WrapperRectangle.qml index c198c47..e1c2c83 100644 --- a/src/widgets/WrapperRectangle.qml +++ b/src/widgets/WrapperRectangle.qml @@ -1,9 +1,9 @@ import QtQuick -import Quickshell.Widgets ///! Rectangle that handles sizes and positioning for a single visual child. /// This component is useful for adding a border or background rectangle to -/// a child item. +/// a child item. If you need to clip the child item to the rectangle's +/// border, see @@ClippingWrapperRectangle. /// /// > [!NOTE] WrapperRectangle is a @@MarginWrapperManager based component. /// > You should read its documentation as well. diff --git a/src/widgets/module.md b/src/widgets/module.md index 77d4a3a..4009b79 100644 --- a/src/widgets/module.md +++ b/src/widgets/module.md @@ -11,5 +11,6 @@ qml_files = [ "ClippingRectangle.qml", "WrapperItem.qml", "WrapperRectangle.qml", + "ClippingWrapperRectangle.qml", ] ----- diff --git a/src/widgets/wrapper.cpp b/src/widgets/wrapper.cpp index 4e502ce..40d7755 100644 --- a/src/widgets/wrapper.cpp +++ b/src/widgets/wrapper.cpp @@ -11,7 +11,11 @@ namespace qs::widgets { void WrapperManager::componentComplete() { - this->mWrapper = qobject_cast(this->parent()); + if (this->mAssignedWrapper) { + this->mWrapper = this->mAssignedWrapper; + } else { + this->mWrapper = qobject_cast(this->parent()); + } if (!this->mWrapper) { QString pstr; @@ -118,6 +122,17 @@ void WrapperManager::onChildDestroyed() { emit this->childChanged(); } +QQuickItem* WrapperManager::wrapper() const { return this->mWrapper; } + +void WrapperManager::setWrapper(QQuickItem* wrapper) { + if (this->mWrapper) { + qmlWarning(this) << "Cannot set wrapper after WrapperManager initialization."; + return; + } + + this->mAssignedWrapper = wrapper; +} + void WrapperManager::printChildCountWarning() const { qmlWarning(this->mWrapper) << "Wrapper component cannot have more than one visual child."; qmlWarning(this->mWrapper) << "Remove all additional children, or pick a specific component " diff --git a/src/widgets/wrapper.hpp b/src/widgets/wrapper.hpp index 95b3ade..993cfd5 100644 --- a/src/widgets/wrapper.hpp +++ b/src/widgets/wrapper.hpp @@ -98,6 +98,9 @@ class WrapperManager /// When read, `child` will always return the (potentially null) selected child, /// and not `undefined`. Q_PROPERTY(QQuickItem* child READ child WRITE setProspectiveChild RESET unsetChild NOTIFY childChanged FINAL); + /// The wrapper managed by this manager. Defaults to the manager's parent. + /// This property may not be changed after Component.onCompleted. + Q_PROPERTY(QQuickItem* wrapper READ wrapper WRITE setWrapper NOTIFY wrapperChanged FINAL); // clang-format on QML_ELEMENT; @@ -112,8 +115,12 @@ class WrapperManager void setProspectiveChild(QQuickItem* child); void unsetChild(); + [[nodiscard]] QQuickItem* wrapper() const; + void setWrapper(QQuickItem* wrapper); + signals: void childChanged(); + void wrapperChanged(); QSDOC_HIDE void initializedChildChanged(); private slots: @@ -131,6 +138,7 @@ private slots: void updateGeometry(); QQuickItem* mWrapper = nullptr; + QQuickItem* mAssignedWrapper = nullptr; QPointer mDefaultChild; QQuickItem* mChild = nullptr; Flags flags; From ee93306312f0a253ab6bf2af4f8b140fb0772c66 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Tue, 19 Nov 2024 02:57:04 -0800 Subject: [PATCH 18/52] widgets/wrapper: fix margin wrapper reactvity and margins Fixed reactivity of the paren't actual size not working before child had been assigned. Fixed incorrect margins when actual size is less than implicit size. --- src/widgets/marginwrapper.cpp | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/src/widgets/marginwrapper.cpp b/src/widgets/marginwrapper.cpp index b960a9f..92de293 100644 --- a/src/widgets/marginwrapper.cpp +++ b/src/widgets/marginwrapper.cpp @@ -20,6 +20,8 @@ MarginWrapperManager::MarginWrapperManager(QObject* parent): WrapperManager(pare } void MarginWrapperManager::componentComplete() { + this->WrapperManager::componentComplete(); + if (this->mWrapper) { QObject::connect( this->mWrapper, @@ -36,8 +38,6 @@ void MarginWrapperManager::componentComplete() { ); } - this->WrapperManager::componentComplete(); - if (!this->mChild) this->updateGeometry(); } @@ -97,12 +97,19 @@ qreal MarginWrapperManager::targetChildHeight() const { qreal MarginWrapperManager::targetChildX() const { if (this->mResizeChild) return this->mMargin; - else return this->mWrapper->width() / 2 - this->mChild->implicitWidth() / 2; + else { + return std::max(this->mMargin, this->mWrapper->width() / 2 - this->mChild->implicitWidth() / 2); + } } qreal MarginWrapperManager::targetChildY() const { if (this->mResizeChild) return this->mMargin; - else return this->mWrapper->height() / 2 - this->mChild->implicitHeight() / 2; + else { + return std::max( + this->mMargin, + this->mWrapper->height() / 2 - this->mChild->implicitHeight() / 2 + ); + } } void MarginWrapperManager::onWrapperWidthChanged() { From f4066cb4edd96b0152ab9b2b1bec821cd5c9da57 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Tue, 19 Nov 2024 03:29:31 -0800 Subject: [PATCH 19/52] core/popupanchor: add anchoring signal for last second repositioning --- src/core/popupanchor.cpp | 2 ++ src/core/popupanchor.hpp | 19 +++++++++++++++++-- src/wayland/popupanchor.cpp | 3 +++ 3 files changed, 22 insertions(+), 2 deletions(-) diff --git a/src/core/popupanchor.cpp b/src/core/popupanchor.cpp index 0dc9c4a..aa570b3 100644 --- a/src/core/popupanchor.cpp +++ b/src/core/popupanchor.cpp @@ -151,6 +151,8 @@ void PopupPositioner::reposition(PopupAnchor* anchor, QWindow* window, bool only if (onlyIfDirty && !anchor->isDirty()) return; anchor->markClean(); + emit anchor->anchoring(); + auto adjustment = anchor->adjustment(); auto screenGeometry = parentWindow->screen()->geometry(); auto anchorRectGeometry = anchor->rect().qrect().translated(parentGeometry.topLeft()); diff --git a/src/core/popupanchor.hpp b/src/core/popupanchor.hpp index a0f6353..f9a4b99 100644 --- a/src/core/popupanchor.hpp +++ b/src/core/popupanchor.hpp @@ -77,9 +77,18 @@ class PopupAnchor: public QObject { /// determined by the @@edges, @@gravity, and @@adjustment. /// /// If you leave @@edges, @@gravity and @@adjustment at their default values, - /// setting more than `x` and `y` does not matter. + /// setting more than `x` and `y` does not matter. The anchor rect cannot + /// be smaller than 1x1 pixels. /// - /// > [!INFO] The anchor rect cannot be smaller than 1x1 pixels. + /// > [!INFO] To position a popup relative to an item inside a window, + /// > you can use [coordinate mapping functions] (note the warning below). + /// + /// > [!WARNING] Using [coordinate mapping functions] in a binding to + /// > this property will position the anchor incorrectly. + /// > If you want to use them, do so in @@anchoring(s), or use + /// > @@TransformWatcher if you need real-time updates to mapped coordinates. + /// + /// [coordinate mapping functions]: https://doc.qt.io/qt-6/qml-qtquick-item.html#mapFromItem-method Q_PROPERTY(Box rect READ rect WRITE setRect NOTIFY rectChanged); /// The point on the anchor rectangle the popup should anchor to. /// Opposing edges suchs as `Edges.Left | Edges.Right` are not allowed. @@ -127,6 +136,12 @@ class PopupAnchor: public QObject { void updatePlacement(const QPoint& anchorpoint, const QSize& size); signals: + /// Emitted when this anchor is about to be used. Mostly useful for modifying + /// the anchor @@rect using [coordinate mapping functions], which are not reactive. + /// + /// [coordinate mapping functions]: https://doc.qt.io/qt-6/qml-qtquick-item.html#mapFromItem-method + void anchoring(); + void windowChanged(); QSDOC_HIDE void backingWindowVisibilityChanged(); void rectChanged(); diff --git a/src/wayland/popupanchor.cpp b/src/wayland/popupanchor.cpp index e38eeff..b13fb48 100644 --- a/src/wayland/popupanchor.cpp +++ b/src/wayland/popupanchor.cpp @@ -41,6 +41,8 @@ void WaylandPopupPositioner::reposition(PopupAnchor* anchor, QWindow* window, bo positioner.set_constraint_adjustment(anchor->adjustment().toInt()); + emit anchor->anchoring(); + auto anchorRect = anchor->rect(); if (auto* p = window->transientParent()) { @@ -101,6 +103,7 @@ void WaylandPopupPositioner::reposition(PopupAnchor* anchor, QWindow* window, bo bool WaylandPopupPositioner::shouldRepositionOnMove() const { return true; } void WaylandPopupPositioner::setFlags(PopupAnchor* anchor, QWindow* window) { + emit anchor->anchoring(); auto anchorRect = anchor->rect(); if (auto* p = window->transientParent()) { From 66b494d760b0f99d51b477e61fd6498bbbc68b70 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Tue, 19 Nov 2024 13:58:34 -0800 Subject: [PATCH 20/52] build: add qs_add_link_dependencies Further inspection as to what libraries actually require which others will be required before this can be used as a hint for shared builds. --- cmake/util.cmake | 9 +++++++++ src/CMakeLists.txt | 2 +- src/crash/CMakeLists.txt | 2 +- src/dbus/dbusmenu/CMakeLists.txt | 3 ++- src/services/mpris/CMakeLists.txt | 1 + src/services/upower/CMakeLists.txt | 3 ++- 6 files changed, 16 insertions(+), 4 deletions(-) diff --git a/cmake/util.cmake b/cmake/util.cmake index 5d261a4..14fa7c2 100644 --- a/cmake/util.cmake +++ b/cmake/util.cmake @@ -1,3 +1,12 @@ +# Adds a dependency hint to the link order, but does not block build on the dependency. +function (qs_add_link_dependencies target) + set_property( + TARGET ${target} + APPEND PROPERTY INTERFACE_LINK_LIBRARIES + ${ARGN} + ) +endfunction() + function (qs_append_qmldir target text) get_property(qmldir_content TARGET ${target} PROPERTY _qt_internal_qmldir_content) diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index c518a1c..707bc37 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -2,8 +2,8 @@ qt_add_executable(quickshell main.cpp) install(TARGETS quickshell RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}) -add_subdirectory(launch) add_subdirectory(build) +add_subdirectory(launch) add_subdirectory(core) add_subdirectory(ipc) add_subdirectory(window) diff --git a/src/crash/CMakeLists.txt b/src/crash/CMakeLists.txt index 442859a..7fdd830 100644 --- a/src/crash/CMakeLists.txt +++ b/src/crash/CMakeLists.txt @@ -14,4 +14,4 @@ target_link_libraries(quickshell-crash PRIVATE PkgConfig::breakpad -lbreakpad_cl # quick linked for pch compat target_link_libraries(quickshell-crash PRIVATE quickshell-build Qt::Quick Qt::Widgets) -target_link_libraries(quickshell-core PRIVATE quickshell-crash) +target_link_libraries(quickshell PRIVATE quickshell-crash) diff --git a/src/dbus/dbusmenu/CMakeLists.txt b/src/dbus/dbusmenu/CMakeLists.txt index ac50b28..61cee42 100644 --- a/src/dbus/dbusmenu/CMakeLists.txt +++ b/src/dbus/dbusmenu/CMakeLists.txt @@ -27,7 +27,8 @@ install_qml_module(quickshell-dbusmenu) # dbus headers target_include_directories(quickshell-dbusmenu PRIVATE ${CMAKE_CURRENT_BINARY_DIR}) -target_link_libraries(quickshell-dbusmenu PRIVATE Qt::Quick Qt::DBus quickshell-dbus) +target_link_libraries(quickshell-dbusmenu PRIVATE Qt::Quick Qt::DBus) +qs_add_link_dependencies(quickshell-dbusmenu quickshell-dbus) qs_module_pch(quickshell-dbusmenu SET dbus) diff --git a/src/services/mpris/CMakeLists.txt b/src/services/mpris/CMakeLists.txt index 122a0c5..4440c0a 100644 --- a/src/services/mpris/CMakeLists.txt +++ b/src/services/mpris/CMakeLists.txt @@ -38,6 +38,7 @@ qs_add_module_deps_light(quickshell-service-mpris Quickshell) install_qml_module(quickshell-service-mpris) target_link_libraries(quickshell-service-mpris PRIVATE Qt::Qml Qt::DBus) +qs_add_link_dependencies(quickshell-service-mpris quickshell-dbus) qs_module_pch(quickshell-service-mpris SET dbus) diff --git a/src/services/upower/CMakeLists.txt b/src/services/upower/CMakeLists.txt index fd0da2a..ca87f6a 100644 --- a/src/services/upower/CMakeLists.txt +++ b/src/services/upower/CMakeLists.txt @@ -37,7 +37,8 @@ qs_add_module_deps_light(quickshell-service-upower Quickshell) install_qml_module(quickshell-service-upower) -target_link_libraries(quickshell-service-upower PRIVATE Qt::Qml Qt::DBus quickshell-dbus) +target_link_libraries(quickshell-service-upower PRIVATE Qt::Qml Qt::DBus) +qs_add_link_dependencies(quickshell-service-upower quickshell-dbus) target_link_libraries(quickshell PRIVATE quickshell-service-upowerplugin) qs_module_pch(quickshell-service-upower SET dbus) From 6ceee06884eace853c9179764d8b8677e3cba2ee Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Tue, 19 Nov 2024 15:25:42 -0800 Subject: [PATCH 21/52] debug: add lint for zero sized items --- src/CMakeLists.txt | 1 + src/debug/CMakeLists.txt | 7 ++++ src/debug/lint.cpp | 81 ++++++++++++++++++++++++++++++++++++++ src/debug/lint.hpp | 11 ++++++ src/window/CMakeLists.txt | 2 + src/window/proxywindow.cpp | 6 +++ src/window/proxywindow.hpp | 1 + 7 files changed, 109 insertions(+) create mode 100644 src/debug/CMakeLists.txt create mode 100644 src/debug/lint.cpp create mode 100644 src/debug/lint.hpp diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 707bc37..882d2ba 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -5,6 +5,7 @@ install(TARGETS quickshell RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}) add_subdirectory(build) add_subdirectory(launch) add_subdirectory(core) +add_subdirectory(debug) add_subdirectory(ipc) add_subdirectory(window) add_subdirectory(io) diff --git a/src/debug/CMakeLists.txt b/src/debug/CMakeLists.txt new file mode 100644 index 0000000..55da4fc --- /dev/null +++ b/src/debug/CMakeLists.txt @@ -0,0 +1,7 @@ +qt_add_library(quickshell-debug STATIC + lint.cpp +) + +qs_pch(quickshell-debug) +target_link_libraries(quickshell-debug PRIVATE Qt::Quick) +target_link_libraries(quickshell PRIVATE quickshell-debug) diff --git a/src/debug/lint.cpp b/src/debug/lint.cpp new file mode 100644 index 0000000..1ec0086 --- /dev/null +++ b/src/debug/lint.cpp @@ -0,0 +1,81 @@ +#include "lint.hpp" +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +namespace qs::debug { + +Q_LOGGING_CATEGORY(logLint, "quickshell.linter", QtWarningMsg); + +void lintZeroSized(QQuickItem* item); +bool isRenderable(QQuickItem* item); + +void lintObjectTree(QObject* object) { + if (!logLint().isWarningEnabled()) return; + + qCDebug(logLint) << "Walking children of object" << object; + + for (auto* child: object->children()) { + if (child->isQuickItemType()) { + auto* item = static_cast(child); // NOLINT; + lintItemTree(item); + } else { + lintObjectTree(child); + } + } +} + +void lintItemTree(QQuickItem* item) { + if (!logLint().isWarningEnabled()) return; + + qCDebug(logLint) << "Running lints for item" << item; + lintZeroSized(item); + + qCDebug(logLint) << "Walking visual children of item" << item; + for (auto* child: item->childItems()) { + lintItemTree(child); + } +} + +void lintZeroSized(QQuickItem* item) { + if (!item->isEnabled() || !item->isVisible()) return; + if (item->childItems().isEmpty()) return; + + auto zeroWidth = item->width() == 0; + auto zeroHeight = item->height() == 0; + + if (!zeroWidth && !zeroHeight) return; + + if (!isRenderable(item)) return; + + auto* ctx = QQmlEngine::contextForObject(item); + if (!ctx || ctx->baseUrl().scheme() != QStringLiteral("qsintercept")) return; + + qmlWarning(item) << "Item is visible and has visible children, but has zero " + << (zeroWidth && zeroHeight ? "width and height" + : zeroWidth ? "width" + : "height"); +} + +bool isRenderable(QQuickItem* item) { + if (!item->isEnabled() || !item->isVisible()) return false; + + if (item->flags().testFlags(QQuickItem::ItemHasContents)) { + return true; + } + + return std::ranges::any_of(item->childItems(), [](auto* item) { return isRenderable(item); }); +} + +} // namespace qs::debug diff --git a/src/debug/lint.hpp b/src/debug/lint.hpp new file mode 100644 index 0000000..5b5420e --- /dev/null +++ b/src/debug/lint.hpp @@ -0,0 +1,11 @@ +#pragma once + +#include +#include + +namespace qs::debug { + +void lintObjectTree(QObject* object); +void lintItemTree(QQuickItem* item); + +} // namespace qs::debug diff --git a/src/window/CMakeLists.txt b/src/window/CMakeLists.txt index 89b2233..47b546d 100644 --- a/src/window/CMakeLists.txt +++ b/src/window/CMakeLists.txt @@ -22,6 +22,8 @@ target_link_libraries(quickshell-window PRIVATE Qt::Core Qt::Gui Qt::Quick Qt6::QuickPrivate ) +qs_add_link_dependencies(quickshell-window quickshell-debug) + target_link_libraries(quickshell-window-init PRIVATE Qt::Qml) qs_module_pch(quickshell-window SET large) diff --git a/src/window/proxywindow.cpp b/src/window/proxywindow.cpp index 3d01224..426b405 100644 --- a/src/window/proxywindow.cpp +++ b/src/window/proxywindow.cpp @@ -19,6 +19,7 @@ #include "../core/qmlscreen.hpp" #include "../core/region.hpp" #include "../core/reload.hpp" +#include "../debug/lint.hpp" #include "windowinterface.hpp" ProxyWindowBase::ProxyWindowBase(QObject* parent) @@ -214,6 +215,11 @@ void ProxyWindowBase::polishItems() { // This hack manually polishes the item tree right before showing the window so it will // always be created with the correct size. QQuickWindowPrivate::get(this->window)->polishItems(); + + if (!this->ranLints) { + qs::debug::lintItemTree(this->mContentItem); + this->ranLints = true; + } } qint32 ProxyWindowBase::x() const { diff --git a/src/window/proxywindow.hpp b/src/window/proxywindow.hpp index 79d326e..8ab8bfd 100644 --- a/src/window/proxywindow.hpp +++ b/src/window/proxywindow.hpp @@ -130,6 +130,7 @@ protected slots: QQuickWindow* window = nullptr; QQuickItem* mContentItem = nullptr; bool reloadComplete = false; + bool ranLints = false; private: void polishItems(); From eb5a5b8b6795cda71d4a06921b64ba300c185473 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Tue, 19 Nov 2024 15:58:55 -0800 Subject: [PATCH 22/52] debug: run lints after window expose Ensures items are at their final sizes before checking them, fixing some false positives. --- src/wayland/wlr_layershell.cpp | 6 +++--- src/wayland/wlr_layershell.hpp | 4 ++-- src/window/proxywindow.cpp | 15 ++++++++++++--- src/window/proxywindow.hpp | 25 +++++++++++++++++++++---- 4 files changed, 38 insertions(+), 12 deletions(-) diff --git a/src/wayland/wlr_layershell.cpp b/src/wayland/wlr_layershell.cpp index 1ce7b7f..9b4f32f 100644 --- a/src/wayland/wlr_layershell.cpp +++ b/src/wayland/wlr_layershell.cpp @@ -18,7 +18,7 @@ WlrLayershell::WlrLayershell(QObject* parent) : ProxyWindowBase(parent) , ext(new LayershellWindowExtension(this)) {} -QQuickWindow* WlrLayershell::retrieveWindow(QObject* oldInstance) { +ProxiedWindow* WlrLayershell::retrieveWindow(QObject* oldInstance) { auto* old = qobject_cast(oldInstance); auto* window = old == nullptr ? nullptr : old->disownWindow(); @@ -33,8 +33,8 @@ QQuickWindow* WlrLayershell::retrieveWindow(QObject* oldInstance) { return this->createQQuickWindow(); } -QQuickWindow* WlrLayershell::createQQuickWindow() { - auto* window = new QQuickWindow(); +ProxiedWindow* WlrLayershell::createQQuickWindow() { + auto* window = this->ProxyWindowBase::createQQuickWindow(); if (!this->ext->attach(window)) { qWarning() << "Could not attach Layershell extension to new QQuickWindow. Layer will not " diff --git a/src/wayland/wlr_layershell.hpp b/src/wayland/wlr_layershell.hpp index e7a1a07..f6f6988 100644 --- a/src/wayland/wlr_layershell.hpp +++ b/src/wayland/wlr_layershell.hpp @@ -64,8 +64,8 @@ class WlrLayershell: public ProxyWindowBase { public: explicit WlrLayershell(QObject* parent = nullptr); - QQuickWindow* retrieveWindow(QObject* oldInstance) override; - QQuickWindow* createQQuickWindow() override; + ProxiedWindow* retrieveWindow(QObject* oldInstance) override; + ProxiedWindow* createQQuickWindow() override; void connectWindow() override; [[nodiscard]] bool deleteOnInvisible() const override; diff --git a/src/window/proxywindow.cpp b/src/window/proxywindow.cpp index 426b405..2a1f51d 100644 --- a/src/window/proxywindow.cpp +++ b/src/window/proxywindow.cpp @@ -1,6 +1,7 @@ #include "proxywindow.hpp" #include +#include #include #include #include @@ -80,7 +81,7 @@ void ProxyWindowBase::onReload(QObject* oldInstance) { void ProxyWindowBase::postCompleteWindow() { this->setVisible(this->mVisible); } -QQuickWindow* ProxyWindowBase::createQQuickWindow() { return new QQuickWindow(); } +ProxiedWindow* ProxyWindowBase::createQQuickWindow() { return new ProxiedWindow(); } void ProxyWindowBase::createWindow() { if (this->window != nullptr) return; @@ -102,7 +103,7 @@ void ProxyWindowBase::deleteWindow(bool keepItemOwnership) { } } -QQuickWindow* ProxyWindowBase::disownWindow(bool keepItemOwnership) { +ProxiedWindow* ProxyWindowBase::disownWindow(bool keepItemOwnership) { if (this->window == nullptr) return nullptr; QObject::disconnect(this->window, nullptr, this, nullptr); @@ -116,7 +117,7 @@ QQuickWindow* ProxyWindowBase::disownWindow(bool keepItemOwnership) { return window; } -QQuickWindow* ProxyWindowBase::retrieveWindow(QObject* oldInstance) { +ProxiedWindow* ProxyWindowBase::retrieveWindow(QObject* oldInstance) { auto* old = qobject_cast(oldInstance); return old == nullptr ? nullptr : old->disownWindow(); } @@ -136,6 +137,7 @@ void ProxyWindowBase::connectWindow() { QObject::connect(this->window, &QWindow::heightChanged, this, &ProxyWindowBase::heightChanged); QObject::connect(this->window, &QWindow::screenChanged, this, &ProxyWindowBase::screenChanged); QObject::connect(this->window, &QQuickWindow::colorChanged, this, &ProxyWindowBase::colorChanged); + QObject::connect(this->window, &ProxiedWindow::exposed, this, &ProxyWindowBase::onWindowExposeEvent); // clang-format on } @@ -215,7 +217,9 @@ void ProxyWindowBase::polishItems() { // This hack manually polishes the item tree right before showing the window so it will // always be created with the correct size. QQuickWindowPrivate::get(this->window)->polishItems(); +} +void ProxyWindowBase::onWindowExposeEvent() { if (!this->ranLints) { qs::debug::lintItemTree(this->mContentItem); this->ranLints = true; @@ -368,3 +372,8 @@ void ProxyWindowBase::onHeightChanged() { this->mContentItem->setHeight(this->he QObject* ProxyWindowAttached::window() const { return this->mWindow; } QQuickItem* ProxyWindowAttached::contentItem() const { return this->mWindow->contentItem(); } + +void ProxiedWindow::exposeEvent(QExposeEvent* event) { + this->QQuickWindow::exposeEvent(event); + emit this->exposed(); +} diff --git a/src/window/proxywindow.hpp b/src/window/proxywindow.hpp index 8ab8bfd..769dad0 100644 --- a/src/window/proxywindow.hpp +++ b/src/window/proxywindow.hpp @@ -11,12 +11,15 @@ #include #include #include +#include #include "../core/qmlscreen.hpp" #include "../core/region.hpp" #include "../core/reload.hpp" #include "windowinterface.hpp" +class ProxiedWindow; + // Proxy to an actual window exposing a limited property set with the ability to // transfer it to a new window. @@ -60,10 +63,10 @@ class ProxyWindowBase: public Reloadable { void deleteWindow(bool keepItemOwnership = false); // Disown the backing window and delete all its children. - virtual QQuickWindow* disownWindow(bool keepItemOwnership = false); + virtual ProxiedWindow* disownWindow(bool keepItemOwnership = false); - virtual QQuickWindow* retrieveWindow(QObject* oldInstance); - virtual QQuickWindow* createQQuickWindow(); + virtual ProxiedWindow* retrieveWindow(QObject* oldInstance); + virtual ProxiedWindow* createQQuickWindow(); virtual void connectWindow(); virtual void completeWindow(); virtual void postCompleteWindow(); @@ -119,6 +122,7 @@ protected slots: void onMaskChanged(); void onMaskDestroyed(); void onScreenDestroyed(); + void onWindowExposeEvent(); protected: bool mVisible = true; @@ -127,7 +131,7 @@ protected slots: QScreen* mScreen = nullptr; QColor mColor = Qt::white; PendingRegion* mMask = nullptr; - QQuickWindow* window = nullptr; + ProxiedWindow* window = nullptr; QQuickItem* mContentItem = nullptr; bool reloadComplete = false; bool ranLints = false; @@ -151,3 +155,16 @@ class ProxyWindowAttached: public QsWindowAttached { private: ProxyWindowBase* mWindow; }; + +class ProxiedWindow: public QQuickWindow { + Q_OBJECT; + +public: + explicit ProxiedWindow(QWindow* parent = nullptr): QQuickWindow(parent) {} + +signals: + void exposed(); + +protected: + void exposeEvent(QExposeEvent* event) override; +}; From dbaaf55eb62bc8cea6c7cf327486fd1476a56931 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Tue, 19 Nov 2024 17:20:53 -0800 Subject: [PATCH 23/52] core/popupwindow: remove parentWindow deprecation message Was being falsely triggered by lints. --- src/debug/lint.cpp | 3 --- src/wayland/popupanchor.cpp | 1 + src/window/popupwindow.cpp | 15 +++++++-------- src/window/popupwindow.hpp | 6 +++--- 4 files changed, 11 insertions(+), 14 deletions(-) diff --git a/src/debug/lint.cpp b/src/debug/lint.cpp index 1ec0086..60d8224 100644 --- a/src/debug/lint.cpp +++ b/src/debug/lint.cpp @@ -7,10 +7,7 @@ #include #include #include -#include #include -#include -#include #include #include diff --git a/src/wayland/popupanchor.cpp b/src/wayland/popupanchor.cpp index b13fb48..81c1cb1 100644 --- a/src/wayland/popupanchor.cpp +++ b/src/wayland/popupanchor.cpp @@ -6,6 +6,7 @@ #include #include #include +#include #include "../core/popupanchor.hpp" #include "../core/types.hpp" diff --git a/src/window/popupwindow.cpp b/src/window/popupwindow.cpp index b355238..df68474 100644 --- a/src/window/popupwindow.cpp +++ b/src/window/popupwindow.cpp @@ -3,7 +3,7 @@ #include #include #include -#include +#include #include #include @@ -38,12 +38,11 @@ void ProxyPopupWindow::completeWindow() { void ProxyPopupWindow::postCompleteWindow() { this->updateTransientParent(); } void ProxyPopupWindow::setParentWindow(QObject* parent) { - qWarning() << "PopupWindow.parentWindow is deprecated. Use PopupWindow.anchor.window."; + qmlWarning(this) << "PopupWindow.parentWindow is deprecated. Use PopupWindow.anchor.window."; this->mAnchor.setWindow(parent); } QObject* ProxyPopupWindow::parentWindow() const { - qWarning() << "PopupWindow.parentWindow is deprecated. Use PopupWindow.anchor.window."; return this->mAnchor.window(); } @@ -71,7 +70,7 @@ void ProxyPopupWindow::updateTransientParent() { void ProxyPopupWindow::onParentUpdated() { this->updateTransientParent(); } void ProxyPopupWindow::setScreen(QuickshellScreenInfo* /*unused*/) { - qWarning() << "Cannot set screen of popup window, as that is controlled by the parent window"; + qmlWarning(this) << "Cannot set screen of popup window, as that is controlled by the parent window"; } void ProxyPopupWindow::setVisible(bool visible) { @@ -101,7 +100,7 @@ void ProxyPopupWindow::onVisibleChanged() { } void ProxyPopupWindow::setRelativeX(qint32 x) { - qWarning() << "PopupWindow.relativeX is deprecated. Use PopupWindow.anchor.rect.x."; + qmlWarning(this) << "PopupWindow.relativeX is deprecated. Use PopupWindow.anchor.rect.x."; auto rect = this->mAnchor.rect(); if (x == rect.x) return; rect.x = x; @@ -109,12 +108,12 @@ void ProxyPopupWindow::setRelativeX(qint32 x) { } qint32 ProxyPopupWindow::relativeX() const { - qWarning() << "PopupWindow.relativeX is deprecated. Use PopupWindow.anchor.rect.x."; + qmlWarning(this) << "PopupWindow.relativeX is deprecated. Use PopupWindow.anchor.rect.x."; return this->mAnchor.rect().x; } void ProxyPopupWindow::setRelativeY(qint32 y) { - qWarning() << "PopupWindow.relativeY is deprecated. Use PopupWindow.anchor.rect.y."; + qmlWarning(this) << "PopupWindow.relativeY is deprecated. Use PopupWindow.anchor.rect.y."; auto rect = this->mAnchor.rect(); if (y == rect.y) return; rect.y = y; @@ -122,7 +121,7 @@ void ProxyPopupWindow::setRelativeY(qint32 y) { } qint32 ProxyPopupWindow::relativeY() const { - qWarning() << "PopupWindow.relativeY is deprecated. Use PopupWindow.anchor.rect.y."; + qmlWarning(this) << "PopupWindow.relativeY is deprecated. Use PopupWindow.anchor.rect.y."; return this->mAnchor.rect().y; } diff --git a/src/window/popupwindow.hpp b/src/window/popupwindow.hpp index bb245eb..a1e535f 100644 --- a/src/window/popupwindow.hpp +++ b/src/window/popupwindow.hpp @@ -30,9 +30,9 @@ /// } /// /// PopupWindow { -/// parentWindow: toplevel -/// relativeX: parentWindow.width / 2 - width / 2 -/// relativeY: parentWindow.height +/// anchor.window: toplevel +/// anchor.rect.x: parentWindow.width / 2 - width / 2 +/// anchor.rect.y: parentWindow.height /// width: 500 /// height: 500 /// visible: true From 8450543e09125fa4a5797bc03146da29e004692e Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Tue, 19 Nov 2024 18:28:19 -0800 Subject: [PATCH 24/52] service/mpris!: convert trackArtists from list to string Most people treat it as a string already, which breaks in Qt 6.8, and I have not seen a meaningful multi-artist response. --- src/services/mpris/player.cpp | 2 +- src/services/mpris/player.hpp | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/services/mpris/player.cpp b/src/services/mpris/player.cpp index b2d4af6..8629d59 100644 --- a/src/services/mpris/player.cpp +++ b/src/services/mpris/player.cpp @@ -301,7 +301,7 @@ void MprisPlayer::onMetadataChanged() { auto trackTitleChanged = this->setTrackTitle(trackTitle.isNull() ? "Unknown Track" : trackTitle); auto trackArtists = this->pMetadata.get().value("xesam:artist").value>(); - auto trackArtistsChanged = this->setTrackArtists(trackArtists); + auto trackArtistsChanged = this->setTrackArtists(trackArtists.join(", ")); auto trackAlbum = this->pMetadata.get().value("xesam:album").toString(); auto trackAlbumChanged = this->setTrackAlbum(trackAlbum.isNull() ? "Unknown Album" : trackAlbum); diff --git a/src/services/mpris/player.hpp b/src/services/mpris/player.hpp index 47ea792..9e63355 100644 --- a/src/services/mpris/player.hpp +++ b/src/services/mpris/player.hpp @@ -141,7 +141,7 @@ class MprisPlayer: public QObject { /// The current track's album artist, or "Unknown Artist" if none was provided. Q_PROPERTY(QString trackAlbumArtist READ trackAlbumArtist NOTIFY trackAlbumArtistChanged); /// The current track's artists, or an empty list if none were provided. - Q_PROPERTY(QVector trackArtists READ trackArtists NOTIFY trackArtistsChanged); + Q_PROPERTY(QString trackArtists READ trackArtists NOTIFY trackArtistsChanged); /// The current track's art url, or `""` if none was provided. Q_PROPERTY(QString trackArtUrl READ trackArtUrl NOTIFY trackArtUrlChanged); /// The playback state of the media player. @@ -373,7 +373,7 @@ private slots: QString mTrackId; QString mTrackUrl; QString mTrackTitle; - QVector mTrackArtists; + QString mTrackArtists; QString mTrackAlbum; QString mTrackAlbumArtist; QString mTrackArtUrl; From dca75b7d6a8e60a9c5c93f8c0ee84326aa9f5322 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Wed, 20 Nov 2024 00:52:47 -0800 Subject: [PATCH 25/52] service/mpris: clarify trackinfo emit order and use QBindings --- src/core/util.hpp | 10 +++++ src/services/mpris/player.cpp | 55 ++++++++++++--------------- src/services/mpris/player.hpp | 71 +++++++++++++++++++++-------------- src/wayland/popupanchor.cpp | 2 +- src/window/popupwindow.cpp | 8 ++-- 5 files changed, 79 insertions(+), 67 deletions(-) diff --git a/src/core/util.hpp b/src/core/util.hpp index 1ff9b22..5dae122 100644 --- a/src/core/util.hpp +++ b/src/core/util.hpp @@ -4,6 +4,7 @@ #include #include +#include #include #include @@ -258,3 +259,12 @@ template bool setSimpleObjectHandle(auto* parent, auto* value) { return SimpleObjectHandleOps::setObject(parent, value); } + +// NOLINTBEGIN +#define QS_TRIVIAL_GETTER(Type, member, getter) \ + [[nodiscard]] Type getter() { return this->member; } + +#define QS_BINDABLE_GETTER(Type, member, getter, bindable) \ + [[nodiscard]] Type getter() { return this->member.value(); } \ + [[nodiscard]] QBindable bindable() { return &this->member; } +// NOLINTEND diff --git a/src/services/mpris/player.cpp b/src/services/mpris/player.cpp index 8629d59..a97020b 100644 --- a/src/services/mpris/player.cpp +++ b/src/services/mpris/player.cpp @@ -7,6 +7,7 @@ #include #include #include +#include #include #include #include @@ -256,19 +257,13 @@ void MprisPlayer::setVolume(qreal volume) { this->pVolume.write(); } -const QVariantMap& MprisPlayer::metadata() const { return this->pMetadata.get(); } - void MprisPlayer::onMetadataChanged() { - emit this->metadataChanged(); - auto lengthVariant = this->pMetadata.get().value("mpris:length"); qlonglong length = -1; if (lengthVariant.isValid() && lengthVariant.canConvert()) { length = lengthVariant.value(); } - auto lengthChanged = this->setLength(length); - auto trackChanged = false; QString trackId; @@ -297,47 +292,43 @@ void MprisPlayer::onMetadataChanged() { } } + if (trackChanged) { + emit this->trackChanged(); + } + + Qt::beginPropertyUpdateGroup(); + + this->bMetadata = this->pMetadata.get(); + auto trackTitle = this->pMetadata.get().value("xesam:title").toString(); - auto trackTitleChanged = this->setTrackTitle(trackTitle.isNull() ? "Unknown Track" : trackTitle); + this->bTrackTitle = trackTitle.isNull() ? "Unknown Track" : trackTitle; - auto trackArtists = this->pMetadata.get().value("xesam:artist").value>(); - auto trackArtistsChanged = this->setTrackArtists(trackArtists.join(", ")); + auto trackArtist = this->pMetadata.get().value("xesam:artist").value>(); + this->bTrackArtist = trackArtist.join(", "); auto trackAlbum = this->pMetadata.get().value("xesam:album").toString(); - auto trackAlbumChanged = this->setTrackAlbum(trackAlbum.isNull() ? "Unknown Album" : trackAlbum); + this->bTrackAlbum = trackAlbum.isNull() ? "Unknown Album" : trackAlbum; - auto trackAlbumArtist = this->pMetadata.get().value("xesam:albumArtist").toString(); - auto trackAlbumArtistChanged = this->setTrackAlbumArtist(trackAlbumArtist); - - auto trackArtUrl = this->pMetadata.get().value("mpris:artUrl").toString(); - auto trackArtUrlChanged = this->setTrackArtUrl(trackArtUrl); + this->bTrackAlbumArtist = this->pMetadata.get().value("xesam:albumArtist").toString(); + this->bTrackArtUrl = this->pMetadata.get().value("mpris:artUrl").toString(); if (trackChanged) { - this->mUniqueId++; + emit this->trackChanged(); + this->bUniqueId = this->bUniqueId + 1; + // Some players don't seem to send position updates or seeks on track change. this->pPosition.update(); - emit this->trackChanged(); } - DropEmitter::call( - trackTitleChanged, - trackArtistsChanged, - trackAlbumChanged, - trackAlbumArtistChanged, - trackArtUrlChanged, - lengthChanged - ); + Qt::endPropertyUpdateGroup(); + + this->setLength(length); + + emit this->postTrackChanged(); } -DEFINE_MEMBER_GET(MprisPlayer, uniqueId); DEFINE_MEMBER_SET(MprisPlayer, length, setLength); -DEFINE_MEMBER_GETSET(MprisPlayer, trackTitle, setTrackTitle); -DEFINE_MEMBER_GETSET(MprisPlayer, trackArtists, setTrackArtists); -DEFINE_MEMBER_GETSET(MprisPlayer, trackAlbum, setTrackAlbum); -DEFINE_MEMBER_GETSET(MprisPlayer, trackAlbumArtist, setTrackAlbumArtist); -DEFINE_MEMBER_GETSET(MprisPlayer, trackArtUrl, setTrackArtUrl); - MprisPlaybackState::Enum MprisPlayer::playbackState() const { return this->mPlaybackState; } void MprisPlayer::setPlaybackState(MprisPlaybackState::Enum playbackState) { diff --git a/src/services/mpris/player.hpp b/src/services/mpris/player.hpp index 9e63355..b509980 100644 --- a/src/services/mpris/player.hpp +++ b/src/services/mpris/player.hpp @@ -2,6 +2,7 @@ #include #include +#include #include #include #include @@ -125,25 +126,27 @@ class MprisPlayer: public QObject { /// A map of common properties is available [here](https://www.freedesktop.org/wiki/Specifications/mpris-spec/metadata). /// Do not count on any of them actually being present. /// - /// Note that the @@trackTitle, @@trackAlbum, @@trackAlbumArtist, @@trackArtists and @@trackArtUrl + /// Note that the @@trackTitle, @@trackAlbum, @@trackAlbumArtist, @@trackArtist and @@trackArtUrl /// properties have extra logic to guard against bad players sending weird metadata, and should /// be used over grabbing the properties directly from the metadata. - Q_PROPERTY(QVariantMap metadata READ metadata NOTIFY metadataChanged); + Q_PROPERTY(QVariantMap metadata READ metadata NOTIFY metadataChanged BINDABLE bindableMetadata); /// An opaque identifier for the current track unique within the current player. /// /// > [!WARNING] This is NOT `mpris:trackid` as that is sometimes missing or nonunique /// > in some players. - Q_PROPERTY(quint32 uniqueId READ uniqueId NOTIFY trackChanged); + Q_PROPERTY(quint32 uniqueId READ uniqueId NOTIFY trackChanged BINDABLE bindableUniqueId); /// The title of the current track, or "Unknown Track" if none was provided. - Q_PROPERTY(QString trackTitle READ trackTitle NOTIFY trackTitleChanged); + Q_PROPERTY(QString trackTitle READ trackTitle NOTIFY trackTitleChanged BINDABLE bindableTrackTitle); + /// The current track's artist, or an empty string if none was provided. + Q_PROPERTY(QString trackArtist READ trackArtist NOTIFY trackArtistChanged BINDABLE bindableTrackArtist); + /// > [!ERROR] deprecated in favor of @@trackArtist. + Q_PROPERTY(QString trackArtists READ trackArtist NOTIFY trackArtistChanged BINDABLE bindableTrackArtist); /// The current track's album, or "Unknown Album" if none was provided. - Q_PROPERTY(QString trackAlbum READ trackAlbum NOTIFY trackAlbumChanged); + Q_PROPERTY(QString trackAlbum READ trackAlbum NOTIFY trackAlbumChanged BINDABLE bindableTrackAlbum); /// The current track's album artist, or "Unknown Artist" if none was provided. - Q_PROPERTY(QString trackAlbumArtist READ trackAlbumArtist NOTIFY trackAlbumArtistChanged); - /// The current track's artists, or an empty list if none were provided. - Q_PROPERTY(QString trackArtists READ trackArtists NOTIFY trackArtistsChanged); + Q_PROPERTY(QString trackAlbumArtist READ trackAlbumArtist NOTIFY trackAlbumArtistChanged BINDABLE bindableTrackAlbumArtist); /// The current track's art url, or `""` if none was provided. - Q_PROPERTY(QString trackArtUrl READ trackArtUrl NOTIFY trackArtUrlChanged); + Q_PROPERTY(QString trackArtUrl READ trackArtUrl NOTIFY trackArtUrlChanged BINDABLE bindableTrackArtUrl); /// The playback state of the media player. /// /// - If @@canPlay is false, you cannot assign the `Playing` state. @@ -254,7 +257,13 @@ class MprisPlayer: public QObject { [[nodiscard]] bool volumeSupported() const; void setVolume(qreal volume); - [[nodiscard]] const QVariantMap& metadata() const; + QS_BINDABLE_GETTER(quint32, bUniqueId, uniqueId, bindableUniqueId); + QS_BINDABLE_GETTER(QVariantMap, bMetadata, metadata, bindableMetadata); + QS_BINDABLE_GETTER(QString, bTrackTitle, trackTitle, bindableTrackTitle); + QS_BINDABLE_GETTER(QString, bTrackAlbum, trackAlbum, bindableTrackAlbum); + QS_BINDABLE_GETTER(QString, bTrackAlbumArtist, trackAlbumArtist, bindableTrackAlbumArtist); + QS_BINDABLE_GETTER(QString, bTrackArtist, trackArtist, bindableTrackArtist); + QS_BINDABLE_GETTER(QString, bTrackArtUrl, trackArtUrl, bindableTrackArtUrl); [[nodiscard]] MprisPlaybackState::Enum playbackState() const; void setPlaybackState(MprisPlaybackState::Enum playbackState); @@ -281,9 +290,22 @@ class MprisPlayer: public QObject { signals: /// The track has changed. /// - /// All track info change signalss will fire immediately after if applicable, - /// but their values will be updated before the signal fires. + /// All track information properties that were sent by the player + /// will be updated immediately following this signal. @@postTrackChanged + /// will be sent after they update. + /// + /// Track information properties: @@uniqueId, @@metadata, @@trackTitle, + /// @@trackArtist, @@trackAlbum, @@trackAlbumArtist, @@trackArtUrl + /// + /// > [!WARNING] Some particularly poorly behaved players will update metadata + /// > *before* indicating the track has changed. void trackChanged(); + /// Sent after track info related properties have been updated, following @@trackChanged. + /// + /// > [!WARNING] It is not safe to assume all track information is up to date after + /// > this signal is emitted. A large number of players will update track information, + /// > particularly @@trackArtUrl, slightly after this signal. + void postTrackChanged(); QSDOC_HIDE void ready(); void canControlChanged(); @@ -306,9 +328,9 @@ class MprisPlayer: public QObject { void volumeSupportedChanged(); void metadataChanged(); void trackTitleChanged(); + void trackArtistChanged(); void trackAlbumChanged(); void trackAlbumArtistChanged(); - void trackArtistsChanged(); void trackArtUrlChanged(); void playbackStateChanged(); void loopStateChanged(); @@ -369,30 +391,21 @@ private slots: DBusMprisPlayerApp* app = nullptr; DBusMprisPlayer* player = nullptr; - quint32 mUniqueId = 0; QString mTrackId; QString mTrackUrl; - QString mTrackTitle; - QString mTrackArtists; - QString mTrackAlbum; - QString mTrackAlbumArtist; - QString mTrackArtUrl; - - DECLARE_MEMBER_NS(MprisPlayer, uniqueId, mUniqueId); DECLARE_MEMBER(MprisPlayer, length, mLength, lengthChanged); DECLARE_MEMBER_SET(length, setLength); // clang-format off - DECLARE_PRIVATE_MEMBER(MprisPlayer, trackTitle, setTrackTitle, mTrackTitle, trackTitleChanged); - DECLARE_PRIVATE_MEMBER(MprisPlayer, trackArtists, setTrackArtists, mTrackArtists, trackArtistsChanged); - DECLARE_PRIVATE_MEMBER(MprisPlayer, trackAlbum, setTrackAlbum, mTrackAlbum, trackAlbumChanged); - DECLARE_PRIVATE_MEMBER(MprisPlayer, trackAlbumArtist, setTrackAlbumArtist, mTrackAlbumArtist, trackAlbumArtistChanged); - DECLARE_PRIVATE_MEMBER(MprisPlayer, trackArtUrl, setTrackArtUrl, mTrackArtUrl, trackArtUrlChanged); + Q_OBJECT_BINDABLE_PROPERTY(MprisPlayer, quint32, bUniqueId); + Q_OBJECT_BINDABLE_PROPERTY(MprisPlayer, QVariantMap, bMetadata, &MprisPlayer::metadataChanged); + Q_OBJECT_BINDABLE_PROPERTY(MprisPlayer, QString, bTrackArtist, &MprisPlayer::trackArtistChanged); + Q_OBJECT_BINDABLE_PROPERTY(MprisPlayer, QString, bTrackTitle, &MprisPlayer::trackTitleChanged); + Q_OBJECT_BINDABLE_PROPERTY(MprisPlayer, QString, bTrackAlbum, &MprisPlayer::trackAlbumChanged); + Q_OBJECT_BINDABLE_PROPERTY(MprisPlayer, QString, bTrackAlbumArtist, &MprisPlayer::trackAlbumArtistChanged); + Q_OBJECT_BINDABLE_PROPERTY(MprisPlayer, QString, bTrackArtUrl, &MprisPlayer::trackArtUrlChanged); // clang-format on - -public: - DECLARE_MEMBER_GET(uniqueId); }; } // namespace qs::service::mpris diff --git a/src/wayland/popupanchor.cpp b/src/wayland/popupanchor.cpp index 81c1cb1..c6a6412 100644 --- a/src/wayland/popupanchor.cpp +++ b/src/wayland/popupanchor.cpp @@ -4,9 +4,9 @@ #include #include #include +#include #include #include -#include #include "../core/popupanchor.hpp" #include "../core/types.hpp" diff --git a/src/window/popupwindow.cpp b/src/window/popupwindow.cpp index df68474..7454d36 100644 --- a/src/window/popupwindow.cpp +++ b/src/window/popupwindow.cpp @@ -1,6 +1,5 @@ #include "popupwindow.hpp" -#include #include #include #include @@ -42,9 +41,7 @@ void ProxyPopupWindow::setParentWindow(QObject* parent) { this->mAnchor.setWindow(parent); } -QObject* ProxyPopupWindow::parentWindow() const { - return this->mAnchor.window(); -} +QObject* ProxyPopupWindow::parentWindow() const { return this->mAnchor.window(); } void ProxyPopupWindow::updateTransientParent() { auto* bw = this->mAnchor.backingWindow(); @@ -70,7 +67,8 @@ void ProxyPopupWindow::updateTransientParent() { void ProxyPopupWindow::onParentUpdated() { this->updateTransientParent(); } void ProxyPopupWindow::setScreen(QuickshellScreenInfo* /*unused*/) { - qmlWarning(this) << "Cannot set screen of popup window, as that is controlled by the parent window"; + qmlWarning(this + ) << "Cannot set screen of popup window, as that is controlled by the parent window"; } void ProxyPopupWindow::setVisible(bool visible) { From 4163713bc4af4c79008b61d0f3c28580e6edae03 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Wed, 20 Nov 2024 03:15:09 -0800 Subject: [PATCH 26/52] dbus/properties: decouple properties from AbstractDBusProperty Importantly, this decouples properties from having to be QObjects, allowing future property types to be much lighter. --- src/dbus/properties.cpp | 180 +++++++++++++++++++++------------------- src/dbus/properties.hpp | 70 ++++++++++------ 2 files changed, 137 insertions(+), 113 deletions(-) diff --git a/src/dbus/properties.cpp b/src/dbus/properties.cpp index 6156b2a..a1cd7e6 100644 --- a/src/dbus/properties.cpp +++ b/src/dbus/properties.cpp @@ -110,100 +110,25 @@ void asyncReadPropertyInternal( QObject::connect(call, &QDBusPendingCallWatcher::finished, &interface, responseCallback); } -void AbstractDBusProperty::tryUpdate(const QVariant& variant) { - this->mExists = true; - - auto error = this->read(variant); - if (error.isValid()) { - qCWarning(logDbusProperties).noquote() - << "Error demarshalling property update for" << this->toString(); - qCWarning(logDbusProperties) << error; - } else { - qCDebug(logDbusProperties).noquote() - << "Updated property" << this->toString() << "to" << this->valueString(); - } -} - void AbstractDBusProperty::update() { if (this->group == nullptr) { - qFatal(logDbusProperties) << "Tried to update dbus property" << this->name + qFatal(logDbusProperties) << "Tried to update dbus property" << this->nameRef() << "which is not attached to a group"; } else { - const QString propStr = this->toString(); - - if (this->group->interface == nullptr) { - qFatal(logDbusProperties).noquote() - << "Tried to update property" << propStr << "of a disconnected interface"; - } - - qCDebug(logDbusProperties).noquote() << "Updating property" << propStr; - - auto pendingCall = - this->group->propertyInterface->Get(this->group->interface->interface(), this->name); - - auto* call = new QDBusPendingCallWatcher(pendingCall, this); - - auto responseCallback = [this, propStr](QDBusPendingCallWatcher* call) { - const QDBusPendingReply reply = *call; - - if (reply.isError()) { - qCWarning(logDbusProperties).noquote() << "Error updating property" << propStr; - qCWarning(logDbusProperties) << reply.error(); - } else { - this->tryUpdate(reply.value().variant()); - } - - delete call; - }; - - QObject::connect(call, &QDBusPendingCallWatcher::finished, this, responseCallback); + this->group->requestPropertyUpdate(this); } } void AbstractDBusProperty::write() { if (this->group == nullptr) { - qFatal(logDbusProperties) << "Tried to write dbus property" << this->name + qFatal(logDbusProperties) << "Tried to write dbus property" << this->nameRef() << "which is not attached to a group"; } else { - const QString propStr = this->toString(); - - if (this->group->interface == nullptr) { - qFatal(logDbusProperties).noquote() - << "Tried to write property" << propStr << "of a disconnected interface"; - } - - qCDebug(logDbusProperties).noquote() << "Writing property" << propStr; - - auto pendingCall = this->group->propertyInterface->Set( - this->group->interface->interface(), - this->name, - QDBusVariant(this->serialize()) - ); - - auto* call = new QDBusPendingCallWatcher(pendingCall, this); - - auto responseCallback = [propStr](QDBusPendingCallWatcher* call) { - const QDBusPendingReply<> reply = *call; - - if (reply.isError()) { - qCWarning(logDbusProperties).noquote() << "Error writing property" << propStr; - qCWarning(logDbusProperties) << reply.error(); - } - delete call; - }; - - QObject::connect(call, &QDBusPendingCallWatcher::finished, this, responseCallback); + this->group->pushPropertyUpdate(this); } } -bool AbstractDBusProperty::exists() const { return this->mExists; } - -QString AbstractDBusProperty::toString() const { - const QString group = this->group == nullptr ? "{ NO GROUP }" : this->group->toString(); - return group + ':' + this->name; -} - -DBusPropertyGroup::DBusPropertyGroup(QVector properties, QObject* parent) +DBusPropertyGroup::DBusPropertyGroup(QVector properties, QObject* parent) : QObject(parent) , properties(std::move(properties)) {} @@ -246,7 +171,7 @@ void DBusPropertyGroup::updateAllDirect() { } for (auto* property: this->properties) { - property->update(); + this->requestPropertyUpdate(property); } } @@ -287,27 +212,102 @@ void DBusPropertyGroup::updatePropertySet(const QVariantMap& properties, bool co auto prop = std::find_if( this->properties.begin(), this->properties.end(), - [&name](AbstractDBusProperty* prop) { return prop->name == name; } + [&name](DBusPropertyCore* prop) { return prop->nameRef() == name; } ); if (prop == this->properties.end()) { qCDebug(logDbusProperties) << "Ignoring untracked property update" << name << "for" << this->toString(); } else { - (*prop)->tryUpdate(value); + this->tryUpdateProperty(*prop, value); } } if (complainMissing) { for (const auto* prop: this->properties) { - if (prop->required && !properties.contains(prop->name)) { + if (prop->isRequired() && !properties.contains(prop->name())) { qCWarning(logDbusProperties) - << prop->name << "missing from property set for" << this->toString(); + << prop->nameRef() << "missing from property set for" << this->toString(); } } } } +void DBusPropertyGroup::tryUpdateProperty(DBusPropertyCore* property, const QVariant& variant) + const { + property->mExists = true; + + auto error = property->store(variant); + if (error.isValid()) { + qCWarning(logDbusProperties).noquote() + << "Error demarshalling property update for" << this->propertyString(property); + qCWarning(logDbusProperties) << error; + } else { + qCDebug(logDbusProperties).noquote() + << "Updated property" << this->propertyString(property) << "to" << property->valueString(); + } +} + +void DBusPropertyGroup::requestPropertyUpdate(DBusPropertyCore* property) { + const QString propStr = this->propertyString(property); + + if (this->interface == nullptr) { + qFatal(logDbusProperties).noquote() + << "Tried to update property" << propStr << "of a disconnected interface"; + } + + qCDebug(logDbusProperties).noquote() << "Updating property" << propStr; + + auto pendingCall = this->propertyInterface->Get(this->interface->interface(), property->name()); + auto* call = new QDBusPendingCallWatcher(pendingCall, this); + + auto responseCallback = [this, propStr, property](QDBusPendingCallWatcher* call) { + const QDBusPendingReply reply = *call; + + if (reply.isError()) { + qCWarning(logDbusProperties).noquote() << "Error updating property" << propStr; + qCWarning(logDbusProperties) << reply.error(); + } else { + this->tryUpdateProperty(property, reply.value().variant()); + } + + delete call; + }; + + QObject::connect(call, &QDBusPendingCallWatcher::finished, this, responseCallback); +} + +void DBusPropertyGroup::pushPropertyUpdate(DBusPropertyCore* property) { + const QString propStr = this->propertyString(property); + + if (this->interface == nullptr) { + qFatal(logDbusProperties).noquote() + << "Tried to write property" << propStr << "of a disconnected interface"; + } + + qCDebug(logDbusProperties).noquote() << "Writing property" << propStr; + + auto pendingCall = this->propertyInterface->Set( + this->interface->interface(), + property->name(), + QDBusVariant(property->serialize()) + ); + + auto* call = new QDBusPendingCallWatcher(pendingCall, this); + + auto responseCallback = [propStr](QDBusPendingCallWatcher* call) { + const QDBusPendingReply<> reply = *call; + + if (reply.isError()) { + qCWarning(logDbusProperties).noquote() << "Error writing property" << propStr; + qCWarning(logDbusProperties) << reply.error(); + } + delete call; + }; + + QObject::connect(call, &QDBusPendingCallWatcher::finished, this, responseCallback); +} + QString DBusPropertyGroup::toString() const { if (this->interface == nullptr) { return "{ DISCONNECTED }"; @@ -317,6 +317,12 @@ QString DBusPropertyGroup::toString() const { } } +QString DBusPropertyGroup::propertyString(const DBusPropertyCore* property) const { + return this->toString() % ':' % property->nameRef(); +} + +QString AbstractDBusProperty::toString() const { return this->group->propertyString(this); } + void DBusPropertyGroup::onPropertiesChanged( const QString& interfaceName, const QVariantMap& changedProperties, @@ -330,14 +336,14 @@ void DBusPropertyGroup::onPropertiesChanged( auto prop = std::find_if( this->properties.begin(), this->properties.end(), - [&name](AbstractDBusProperty* prop) { return prop->name == name; } + [&name](DBusPropertyCore* prop) { return prop->nameRef() == name; } ); if (prop == this->properties.end()) { qCDebug(logDbusProperties) << "Ignoring untracked property invalidation" << name << "for" << this; } else { - (*prop)->update(); + this->requestPropertyUpdate(*prop); } } diff --git a/src/dbus/properties.hpp b/src/dbus/properties.hpp index 65f51af..3008d35 100644 --- a/src/dbus/properties.hpp +++ b/src/dbus/properties.hpp @@ -14,6 +14,7 @@ #include #include #include +#include #include #include @@ -75,24 +76,44 @@ void asyncReadProperty( class DBusPropertyGroup; -class AbstractDBusProperty: public QObject { +class DBusPropertyCore { +public: + DBusPropertyCore() = default; + virtual ~DBusPropertyCore() = default; + Q_DISABLE_COPY_MOVE(DBusPropertyCore); + + [[nodiscard]] virtual QString name() const = 0; + [[nodiscard]] virtual QStringView nameRef() const = 0; + [[nodiscard]] virtual QString valueString() = 0; + [[nodiscard]] virtual bool isRequired() const = 0; + [[nodiscard]] bool exists() const { return this->mExists; } + +protected: + virtual QDBusError store(const QVariant& variant) = 0; + [[nodiscard]] virtual QVariant serialize() = 0; + +private: + bool mExists : 1 = false; + + friend class DBusPropertyGroup; +}; + +class AbstractDBusProperty + : public QObject + , public DBusPropertyCore { Q_OBJECT; public: - explicit AbstractDBusProperty( - QString name, - const QMetaType& type, - bool required, - QObject* parent = nullptr - ) + explicit AbstractDBusProperty(QString name, bool required, QObject* parent = nullptr) : QObject(parent) - , name(std::move(name)) - , type(type) - , required(required) {} + , required(required) + , mName(std::move(name)) {} + + [[nodiscard]] QString name() const override { return this->mName; }; + [[nodiscard]] QStringView nameRef() const override { return this->mName; }; + [[nodiscard]] bool isRequired() const override { return this->required; }; - [[nodiscard]] bool exists() const; [[nodiscard]] QString toString() const; - [[nodiscard]] virtual QString valueString() = 0; public slots: void update(); @@ -101,19 +122,12 @@ public slots: signals: void changed(); -protected: - virtual QDBusError read(const QVariant& variant) = 0; - virtual QVariant serialize() = 0; - private: - void tryUpdate(const QVariant& variant); + bool required : 1; + bool mExists : 1 = false; DBusPropertyGroup* group = nullptr; - - QString name; - QMetaType type; - bool required; - bool mExists = false; + QString mName; friend class DBusPropertyGroup; }; @@ -123,7 +137,7 @@ class DBusPropertyGroup: public QObject { public: explicit DBusPropertyGroup( - QVector properties = QVector(), + QVector properties = QVector(), QObject* parent = nullptr ); @@ -146,10 +160,14 @@ private slots: private: void updatePropertySet(const QVariantMap& properties, bool complainMissing); + void requestPropertyUpdate(DBusPropertyCore* property); + void pushPropertyUpdate(DBusPropertyCore* property); + void tryUpdateProperty(DBusPropertyCore* property, const QVariant& variant) const; + [[nodiscard]] QString propertyString(const DBusPropertyCore* property) const; DBusPropertiesInterface* propertyInterface = nullptr; QDBusAbstractInterface* interface = nullptr; - QVector properties; + QVector properties; friend class AbstractDBusProperty; }; @@ -163,7 +181,7 @@ class DBusProperty: public AbstractDBusProperty { bool required = true, QObject* parent = nullptr ) - : AbstractDBusProperty(std::move(name), QMetaType::fromType(), required, parent) + : AbstractDBusProperty(std::move(name), required, parent) , value(std::move(value)) {} explicit DBusProperty( @@ -191,7 +209,7 @@ class DBusProperty: public AbstractDBusProperty { } protected: - QDBusError read(const QVariant& variant) override { + QDBusError store(const QVariant& variant) override { auto result = demarshallVariant(variant); if (result.isValid()) { From 1955deee74c31ce05cdc288df65b517fd225e2fc Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Wed, 20 Nov 2024 19:22:23 -0800 Subject: [PATCH 27/52] dbus/properties: add QObjectBindableProperty based dbus property Many times more lightweight than the original QObject-based one. --- src/core/util.hpp | 38 +++++++++--- src/dbus/properties.cpp | 4 ++ src/dbus/properties.hpp | 124 +++++++++++++++++++++++++++++++++++++--- 3 files changed, 151 insertions(+), 15 deletions(-) diff --git a/src/core/util.hpp b/src/core/util.hpp index 5dae122..62264b9 100644 --- a/src/core/util.hpp +++ b/src/core/util.hpp @@ -2,22 +2,45 @@ #include #include +#include #include #include #include +#include #include #include -template +template struct StringLiteral { - constexpr StringLiteral(const char (&str)[Length]) { // NOLINT - std::copy_n(str, Length, this->value); + constexpr StringLiteral(const char (&str)[length]) { // NOLINT + std::copy_n(str, length, this->value); } constexpr operator const char*() const noexcept { return this->value; } - operator QLatin1StringView() const { return QLatin1String(this->value, Length); } + operator QLatin1StringView() const { return QLatin1String(this->value, length); } - char value[Length]; // NOLINT + char value[length]; // NOLINT +}; + +template +struct StringLiteral16 { + constexpr StringLiteral16(const char16_t (&str)[length]) { // NOLINT + std::copy_n(str, length, this->value); + } + + [[nodiscard]] constexpr const QChar* qCharPtr() const noexcept { + return std::bit_cast(&this->value); + } + + [[nodiscard]] Q_ALWAYS_INLINE operator QString() const noexcept { + return QString::fromRawData(this->qCharPtr(), static_cast(length - 1)); + } + + [[nodiscard]] Q_ALWAYS_INLINE operator QStringView() const noexcept { + return QStringView(this->qCharPtr(), static_cast(length - 1)); + } + + char16_t value[length]; // NOLINT }; // NOLINTBEGIN @@ -149,11 +172,10 @@ public: // NOLINTEND template -class MemberPointerTraits; +struct MemberPointerTraits; template -class MemberPointerTraits { -public: +struct MemberPointerTraits { using Class = C; using Type = T; }; diff --git a/src/dbus/properties.cpp b/src/dbus/properties.cpp index a1cd7e6..0814663 100644 --- a/src/dbus/properties.cpp +++ b/src/dbus/properties.cpp @@ -162,6 +162,10 @@ void DBusPropertyGroup::attachProperty(AbstractDBusProperty* property) { property->group = this; } +void DBusPropertyGroup::attachProperty(DBusPropertyCore* property) { + this->properties.append(property); +} + void DBusPropertyGroup::updateAllDirect() { qCDebug(logDbusProperties).noquote() << "Updating all properties of" << this->toString() << "via individual queries"; diff --git a/src/dbus/properties.hpp b/src/dbus/properties.hpp index 3008d35..11e7e06 100644 --- a/src/dbus/properties.hpp +++ b/src/dbus/properties.hpp @@ -3,6 +3,7 @@ #include #include +#include #include #include #include @@ -14,10 +15,14 @@ #include #include #include +#include +#include #include #include #include +#include "../core/util.hpp" + class DBusPropertiesInterface; Q_DECLARE_LOGGING_CATEGORY(logDbusProperties); @@ -132,21 +137,94 @@ public slots: friend class DBusPropertyGroup; }; +namespace bindable_p { + +template +struct BindableParams; + +template