From 47bcf8ee610b9868e0b670b0b9439b770232cbae Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Thu, 2 Jan 2025 21:54:36 -0800 Subject: [PATCH 01/31] service/upower: add power-profiles support --- src/dbus/properties.hpp | 4 +- src/services/upower/CMakeLists.txt | 1 + src/services/upower/core.hpp | 8 + src/services/upower/module.md | 1 + src/services/upower/powerprofiles.cpp | 213 +++++++++++++++++++++++ src/services/upower/powerprofiles.hpp | 237 ++++++++++++++++++++++++++ 6 files changed, 462 insertions(+), 2 deletions(-) create mode 100644 src/services/upower/powerprofiles.cpp create mode 100644 src/services/upower/powerprofiles.hpp diff --git a/src/dbus/properties.hpp b/src/dbus/properties.hpp index 846f70f2..f800ef3e 100644 --- a/src/dbus/properties.hpp +++ b/src/dbus/properties.hpp @@ -36,8 +36,8 @@ template class DBusResult { public: explicit DBusResult() = default; - explicit DBusResult(T value): value(std::move(value)) {} - explicit DBusResult(QDBusError error): error(std::move(error)) {} + DBusResult(T value): value(std::move(value)) {} + DBusResult(QDBusError error): error(std::move(error)) {} explicit DBusResult(T value, QDBusError error) : value(std::move(value)) , error(std::move(error)) {} diff --git a/src/services/upower/CMakeLists.txt b/src/services/upower/CMakeLists.txt index ca87f6ae..b18ec155 100644 --- a/src/services/upower/CMakeLists.txt +++ b/src/services/upower/CMakeLists.txt @@ -21,6 +21,7 @@ qt_add_dbus_interface(DBUS_INTERFACES qt_add_library(quickshell-service-upower STATIC core.cpp device.cpp + powerprofiles.cpp ${DBUS_INTERFACES} ) diff --git a/src/services/upower/core.hpp b/src/services/upower/core.hpp index 9ade8121..c3878150 100644 --- a/src/services/upower/core.hpp +++ b/src/services/upower/core.hpp @@ -54,6 +54,14 @@ private: DBusUPowerService* service = nullptr; }; +///! Provides access to the UPower service. +/// An interface to the [UPower daemon], which can be used to +/// view battery and power statistics for your computer and +/// connected devices. +/// +/// > [!NOTE] The UPower daemon must be installed to use this service. +/// +/// [UPower daemon]: https://upower.freedesktop.org class UPowerQml: public QObject { Q_OBJECT; QML_NAMED_ELEMENT(UPower); diff --git a/src/services/upower/module.md b/src/services/upower/module.md index 99c7ece4..e1d697fe 100644 --- a/src/services/upower/module.md +++ b/src/services/upower/module.md @@ -3,5 +3,6 @@ description = "UPower Service" headers = [ "core.hpp", "device.hpp", + "powerprofiles.hpp", ] ----- diff --git a/src/services/upower/powerprofiles.cpp b/src/services/upower/powerprofiles.cpp new file mode 100644 index 00000000..b4b477b0 --- /dev/null +++ b/src/services/upower/powerprofiles.cpp @@ -0,0 +1,213 @@ +#include "powerprofiles.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "../../dbus/bus.hpp" +#include "../../dbus/properties.hpp" + +namespace qs::service::upower { + +namespace { +Q_LOGGING_CATEGORY(logPowerProfiles, "quickshell.service.powerprofiles", QtWarningMsg); +} + +QString PowerProfile::toString(PowerProfile::Enum profile) { + switch (profile) { + case PowerProfile::PowerSaver: return QStringLiteral("PowerSaver"); + case PowerProfile::Balanced: return QStringLiteral("Balanced"); + case PowerProfile::Performance: return QStringLiteral("Performance"); + default: return QStringLiteral("Invalid"); + } +} + +QString PerformanceDegradationReason::toString(PerformanceDegradationReason::Enum reason) { + switch (reason) { + case PerformanceDegradationReason::LapDetected: return QStringLiteral("LapDetected"); + case PerformanceDegradationReason::HighTemperature: return QStringLiteral("HighTemperature"); + default: return QStringLiteral("Invalid"); + } +} + +bool PowerProfileHold::operator==(const PowerProfileHold& other) const { + return other.profile == this->profile && other.applicationId == this->applicationId + && other.reason == this->reason; +} + +QDebug& operator<<(QDebug& debug, const PowerProfileHold& hold) { + auto saver = QDebugStateSaver(debug); + + debug.nospace(); + debug << "PowerProfileHold(profile=" << hold.profile << ", applicationId=" << hold.applicationId + << ", reason=" << hold.reason << ')'; + + return debug; +} + +PowerProfiles::PowerProfiles() { + qDBusRegisterMetaType>(); + + this->bHasPerformanceProfile.setBinding([this]() { + return this->bProfiles.value().contains(PowerProfile::Performance); + }); + + qCDebug(logPowerProfiles) << "Starting PowerProfiles Service."; + + auto bus = QDBusConnection::systemBus(); + + if (!bus.isConnected()) { + qCWarning(logPowerProfiles + ) << "Could not connect to DBus. PowerProfiles services will not work."; + } + + this->service = new QDBusInterface( + "org.freedesktop.UPower.PowerProfiles", + "/org/freedesktop/UPower/PowerProfiles", + "org.freedesktop.UPower.PowerProfiles", + bus, + this + ); + + if (!this->service->isValid()) { + qCDebug(logPowerProfiles + ) << "PowerProfilesDaemon is not currently running, attempting to start it."; + + dbus::tryLaunchService(this, bus, "org.freedesktop.UPower.PowerProfiles", [this](bool success) { + if (success) { + qCDebug(logPowerProfiles) << "Successfully launched PowerProfiles service."; + this->init(); + } else { + qCWarning(logPowerProfiles) + << "Could not start PowerProfilesDaemon. The PowerProfiles service will not work."; + } + }); + } else { + this->init(); + } +} + +void PowerProfiles::init() { + this->properties.setInterface(this->service); + this->properties.updateAllViaGetAll(); +} + +void PowerProfiles::setProfile(PowerProfile::Enum profile) { + if (profile == PowerProfile::Performance && !this->bHasPerformanceProfile) { + qCCritical(logPowerProfiles + ) << "Cannot request performance profile as it is not present for this device."; + return; + } else if (profile < PowerProfile::PowerSaver || profile > PowerProfile::Performance) { + qCCritical(logPowerProfiles) << "Tried to request invalid power profile" << profile; + return; + } + + this->bProfile = profile; + this->pProfile.write(); +} + +PowerProfiles* PowerProfiles::instance() { + static auto* instance = new PowerProfiles(); // NOLINT + return instance; +} + +PowerProfilesQml::PowerProfilesQml(QObject* parent): QObject(parent) { + auto* instance = PowerProfiles::instance(); + + this->bProfile.setBinding([instance]() { return instance->bProfile.value(); }); + + this->bHasPerformanceProfile.setBinding([instance]() { + return instance->bHasPerformanceProfile.value(); + }); + + this->bDegradationReason.setBinding([instance]() { return instance->bDegradationReason.value(); } + ); + + this->bHolds.setBinding([instance]() { return instance->bHolds.value(); }); +} + +} // namespace qs::service::upower + +namespace qs::dbus { + +using namespace qs::service::upower; + +DBusResult DBusDataTransform::fromWire(const Wire& wire) { + if (wire == QStringLiteral("power-saver")) { + return PowerProfile::PowerSaver; + } else if (wire == QStringLiteral("balanced")) { + return PowerProfile::Balanced; + } else if (wire == QStringLiteral("performance")) { + return PowerProfile::Performance; + } else { + return QDBusError(QDBusError::InvalidArgs, QString("Invalid PowerProfile: %1").arg(wire)); + } +} + +QString DBusDataTransform::toWire(Data data) { + switch (data) { + case PowerProfile::PowerSaver: return QStringLiteral("power-saver"); + case PowerProfile::Balanced: return QStringLiteral("balanced"); + case PowerProfile::Performance: return QStringLiteral("performance"); + } +} + +DBusResult> +DBusDataTransform>::fromWire(const Wire& wire) { + QList profiles; + + for (const auto& entry: wire) { + auto profile = + DBusDataTransform::fromWire(entry.value("Profile").value()); + + if (!profile.isValid()) return profile.error; + profiles.append(profile.value); + } + + return profiles; +} + +DBusResult +DBusDataTransform::fromWire(const Wire& wire) { + if (wire.isEmpty()) { + return PerformanceDegradationReason::None; + } else if (wire == QStringLiteral("lap-detected")) { + return PerformanceDegradationReason::LapDetected; + } else if (wire == QStringLiteral("high-operating-temperature")) { + return PerformanceDegradationReason::HighTemperature; + } else { + return QDBusError( + QDBusError::InvalidArgs, + QString("Invalid PerformanceDegradationReason: %1").arg(wire) + ); + } +} + +DBusResult> +DBusDataTransform>::fromWire(const Wire& wire) { + QList holds; + + for (const auto& entry: wire) { + auto profile = + DBusDataTransform::fromWire(entry.value("Profile").value()); + + if (!profile.isValid()) return profile.error; + + auto applicationId = entry.value("ApplicationId").value(); + auto reason = entry.value("Reason").value(); + + holds.append(PowerProfileHold(profile.value, applicationId, reason)); + } + + return holds; +} + +} // namespace qs::dbus diff --git a/src/services/upower/powerprofiles.hpp b/src/services/upower/powerprofiles.hpp new file mode 100644 index 00000000..b7340328 --- /dev/null +++ b/src/services/upower/powerprofiles.hpp @@ -0,0 +1,237 @@ +#pragma once + +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "../../dbus/properties.hpp" + +namespace qs::service::upower { + +///! Power profile exposed by the PowerProfiles service. +/// See @@PowerProfiles. +class PowerProfile: public QObject { + Q_OBJECT; + QML_ELEMENT; + QML_SINGLETON; + +public: + enum Enum : quint8 { + /// This profile will limit system performance in order to save power. + PowerSaver = 0, + /// This profile is the default, and will attempt to strike a balance + /// between performance and power consumption. + Balanced = 1, + /// This profile will maximize performance at the cost of power consumption. + Performance = 2, + }; + Q_ENUM(Enum); + + Q_INVOKABLE static QString toString(qs::service::upower::PowerProfile::Enum profile); +}; + +///! Reason for performance degradation exposed by the PowerProfiles service. +/// See @@PowerProfiles.degradationReason for more information. +class PerformanceDegradationReason: public QObject { + Q_OBJECT; + QML_ELEMENT; + QML_SINGLETON; + +public: + enum Enum : quint8 { + /// Performance has not been degraded in a way power-profiles-daemon can detect. + None = 0, + /// Performance has been reduced due to the computer's lap detection function, + /// which attempts to keep the computer from getting too hot while on your lap. + LapDetected = 1, + /// Performance has been reduced due to high system temperatures. + HighTemperature = 2, + }; + Q_ENUM(Enum); + + // clang-format off + Q_INVOKABLE static QString toString(qs::service::upower::PerformanceDegradationReason::Enum reason); + // clang-format on +}; + +class PowerProfileHold; + +} // namespace qs::service::upower + +namespace qs::dbus { + +template <> +struct DBusDataTransform { + using Wire = QString; + using Data = qs::service::upower::PowerProfile::Enum; + static DBusResult fromWire(const Wire& wire); + static Wire toWire(Data data); +}; + +template <> +struct DBusDataTransform> { + using Wire = QList; + using Data = QList; + static DBusResult fromWire(const Wire& wire); +}; + +template <> +struct DBusDataTransform { + using Wire = QString; + using Data = qs::service::upower::PerformanceDegradationReason::Enum; + static DBusResult fromWire(const Wire& wire); +}; + +template <> +struct DBusDataTransform> { + using Wire = QList; + using Data = QList; + static DBusResult fromWire(const Wire& wire); +}; + +} // namespace qs::dbus + +namespace qs::service::upower { + +// docgen can't hit gadgets yet +class PowerProfileHold { + Q_GADGET; + QML_VALUE_TYPE(powerProfileHold); + Q_PROPERTY(qs::service::upower::PowerProfile::Enum profile MEMBER profile CONSTANT); + Q_PROPERTY(QString applicationId MEMBER applicationId CONSTANT); + Q_PROPERTY(QString reason MEMBER reason CONSTANT); + +public: + explicit PowerProfileHold() = default; + explicit PowerProfileHold(PowerProfile::Enum profile, QString applicationId, QString reason) + : profile(profile) + , applicationId(std::move(applicationId)) + , reason(std::move(reason)) {} + + PowerProfile::Enum profile = PowerProfile::Balanced; + QString applicationId; + QString reason; + + [[nodiscard]] bool operator==(const PowerProfileHold& other) const; +}; + +QDebug& operator<<(QDebug& debug, const PowerProfileHold& hold); + +class PowerProfiles: public QObject { + Q_OBJECT; + +public: + void setProfile(PowerProfile::Enum profile); + + // clang-format off + Q_OBJECT_BINDABLE_PROPERTY_WITH_ARGS(PowerProfiles, PowerProfile::Enum, bProfile, PowerProfile::Balanced); + Q_OBJECT_BINDABLE_PROPERTY(PowerProfiles, bool, bHasPerformanceProfile); + Q_OBJECT_BINDABLE_PROPERTY(PowerProfiles, PerformanceDegradationReason::Enum, bDegradationReason); + Q_OBJECT_BINDABLE_PROPERTY(PowerProfiles, QList, bHolds); + // clang-format on + + static PowerProfiles* instance(); + +private: + explicit PowerProfiles(); + void init(); + + // clang-format off + Q_OBJECT_BINDABLE_PROPERTY(PowerProfiles, QList, bProfiles); + + QS_DBUS_BINDABLE_PROPERTY_GROUP(PowerProfiles, properties); + QS_DBUS_PROPERTY_BINDING(PowerProfiles, pProfile, bProfile, properties, "ActiveProfile"); + QS_DBUS_PROPERTY_BINDING(PowerProfiles, pProfiles, bProfiles, properties, "Profiles"); + QS_DBUS_PROPERTY_BINDING(PowerProfiles, pPerformanceDegraded, bDegradationReason, properties, "PerformanceDegraded"); + QS_DBUS_PROPERTY_BINDING(PowerProfiles, pHolds, bHolds, properties, "ActiveProfileHolds"); + // clang-format on + + QDBusInterface* service = nullptr; +}; + +///! Provides access to the Power Profiles service. +/// An interface to the UPower [power profiles daemon], which can be +/// used to view and manage power profiles. +/// +/// > [!NOTE] The power profiles daemon must be installed to use this service. +/// > Installing UPower does not necessarily install the power profiles daemon. +/// +/// [power profiles daemon]: https://gitlab.freedesktop.org/upower/power-profiles-daemon +class PowerProfilesQml: public QObject { + Q_OBJECT; + QML_NAMED_ELEMENT(PowerProfiles); + QML_SINGLETON; + // clang-format off + /// The current power profile. + /// + /// This property may be set to change the system's power profile, however + /// it cannot be set to `Performance` unless @@hasPerformanceProfile is true. + Q_PROPERTY(qs::service::upower::PowerProfile::Enum profile READ default WRITE setProfile NOTIFY profileChanged BINDABLE bindableProfile); + /// If the system has a performance profile. + /// + /// If this property is false, your system does not have a performance + /// profile known to power-profiles-daemon. + Q_PROPERTY(bool hasPerformanceProfile READ default NOTIFY hasPerformanceProfileChanged BINDABLE bindableHasPerformanceProfile); + /// If power-profiles-daemon detects degraded system performance, the reason + /// for the degradation will be present here. + Q_PROPERTY(qs::service::upower::PerformanceDegradationReason::Enum degradationReason READ default NOTIFY degradationReasonChanged BINDABLE bindableDegradationReason); + /// Power profile holds created by other applications. + /// + /// This property returns a `powerProfileHold` object, which has the following properties. + /// - `profile` - The @@PowerProfile held by the application. + /// - `applicationId` - A string identifying the application + /// - `reason` - The reason the application has given for holding the profile. + /// + /// Applications may "hold" a power profile in place for their lifetime, such + /// as a game holding Performance mode or a system daemon holding Power Saver mode + /// when reaching a battery threshold. If the user selects a different profile explicitly + /// (e.g. by setting @@profile$) all holds will be removed. + /// + /// Multiple applications may hold a power profile, however if multiple applications request + /// profiles than `PowerSaver` will win over `Performance`. Only `Performance` and `PowerSaver` + /// profiles may be held. + Q_PROPERTY(QList holds READ default NOTIFY holdsChanged BINDABLE bindableHolds); + // clang-format on + +signals: + void profileChanged(); + void hasPerformanceProfileChanged(); + void degradationReasonChanged(); + void holdsChanged(); + +public: + explicit PowerProfilesQml(QObject* parent = nullptr); + + [[nodiscard]] QBindable bindableProfile() const { return &this->bProfile; } + + static void setProfile(PowerProfile::Enum profile) { + PowerProfiles::instance()->setProfile(profile); + } + + [[nodiscard]] QBindable bindableHasPerformanceProfile() const { + return &this->bHasPerformanceProfile; + } + + [[nodiscard]] QBindable bindableDegradationReason() const { + return &this->bDegradationReason; + } + + [[nodiscard]] QBindable> bindableHolds() const { return &this->bHolds; } + +private: + // clang-format off + Q_OBJECT_BINDABLE_PROPERTY(PowerProfilesQml, PowerProfile::Enum, bProfile, &PowerProfilesQml::profileChanged); + Q_OBJECT_BINDABLE_PROPERTY(PowerProfilesQml, bool, bHasPerformanceProfile, &PowerProfilesQml::hasPerformanceProfileChanged); + Q_OBJECT_BINDABLE_PROPERTY(PowerProfilesQml, PerformanceDegradationReason::Enum, bDegradationReason, &PowerProfilesQml::degradationReasonChanged); + Q_OBJECT_BINDABLE_PROPERTY(PowerProfilesQml, QList, bHolds, &PowerProfilesQml::holdsChanged); + // clang-format on +}; + +} // namespace qs::service::upower From dc3a79600dfd2a7677ba41be66359cdd1e8c5d2f Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Fri, 3 Jan 2025 02:42:32 -0800 Subject: [PATCH 02/31] core/command: avoid running when cli11 forces returning 0 Fixes running when --help is passed. --- src/launch/command.cpp | 2 +- src/launch/parsecommand.cpp | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/launch/command.cpp b/src/launch/command.cpp index 93801113..9e8ac27c 100644 --- a/src/launch/command.cpp +++ b/src/launch/command.cpp @@ -43,7 +43,7 @@ int locateConfigFile(CommandState& cmd, QString& path); int runCommand(int argc, char** argv, QCoreApplication* coreApplication) { auto state = CommandState(); - if (auto ret = parseCommand(argc, argv, state); ret != 0) return ret; + if (auto ret = parseCommand(argc, argv, state); ret != 65535) return ret; if (state.misc.checkCompat) { if (strcmp(qVersion(), QT_VERSION_STR) != 0) { diff --git a/src/launch/parsecommand.cpp b/src/launch/parsecommand.cpp index 91f7dc04..bee9dd00 100644 --- a/src/launch/parsecommand.cpp +++ b/src/launch/parsecommand.cpp @@ -192,7 +192,7 @@ int parseCommand(int argc, char** argv, CommandState& state) { CLI11_PARSE(*cli, argc, argv); - return 0; + return 65535; } } // namespace qs::launch From f3b7171b25ffd993347c1b6f484e4c5dd176d7c3 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Fri, 3 Jan 2025 21:01:17 -0800 Subject: [PATCH 03/31] core/window: allow explicit surface format selection --- src/wayland/wlr_layershell.cpp | 2 + src/wayland/wlr_layershell.hpp | 3 ++ src/window/floatingwindow.cpp | 2 + src/window/floatingwindow.hpp | 4 ++ src/window/panelinterface.hpp | 6 ++- src/window/proxywindow.cpp | 69 ++++++++++++++++++++++++++++++++-- src/window/proxywindow.hpp | 11 ++++++ src/window/windowinterface.hpp | 38 +++++++++++++++++++ src/x11/panel_window.cpp | 2 + src/x11/panel_window.hpp | 3 ++ 10 files changed, 134 insertions(+), 6 deletions(-) diff --git a/src/wayland/wlr_layershell.cpp b/src/wayland/wlr_layershell.cpp index 9b4f32f2..010d0f77 100644 --- a/src/wayland/wlr_layershell.cpp +++ b/src/wayland/wlr_layershell.cpp @@ -196,6 +196,7 @@ WaylandPanelInterface::WaylandPanelInterface(QObject* parent) QObject::connect(this->layer, &ProxyWindowBase::windowTransformChanged, this, &WaylandPanelInterface::windowTransformChanged); QObject::connect(this->layer, &ProxyWindowBase::colorChanged, this, &WaylandPanelInterface::colorChanged); QObject::connect(this->layer, &ProxyWindowBase::maskChanged, this, &WaylandPanelInterface::maskChanged); + QObject::connect(this->layer, &ProxyWindowBase::surfaceFormatChanged, this, &WaylandPanelInterface::surfaceFormatChanged); // panel specific QObject::connect(this->layer, &WlrLayershell::anchorsChanged, this, &WaylandPanelInterface::anchorsChanged); @@ -232,6 +233,7 @@ proxyPair(qint32, height, setHeight); proxyPair(QuickshellScreenInfo*, screen, setScreen); proxyPair(QColor, color, setColor); proxyPair(PendingRegion*, mask, setMask); +proxyPair(QsSurfaceFormat, surfaceFormat, setSurfaceFormat); // panel specific proxyPair(Anchors, anchors, setAnchors); diff --git a/src/wayland/wlr_layershell.hpp b/src/wayland/wlr_layershell.hpp index f6f6988a..32aeecdb 100644 --- a/src/wayland/wlr_layershell.hpp +++ b/src/wayland/wlr_layershell.hpp @@ -155,6 +155,9 @@ public: [[nodiscard]] PendingRegion* mask() const override; void setMask(PendingRegion* mask) override; + [[nodiscard]] QsSurfaceFormat surfaceFormat() const override; + void setSurfaceFormat(QsSurfaceFormat mask) override; + [[nodiscard]] QQmlListProperty data() override; // panel specific diff --git a/src/window/floatingwindow.cpp b/src/window/floatingwindow.cpp index a1d23f54..918b186d 100644 --- a/src/window/floatingwindow.cpp +++ b/src/window/floatingwindow.cpp @@ -36,6 +36,7 @@ FloatingWindowInterface::FloatingWindowInterface(QObject* parent) QObject::connect(this->window, &ProxyWindowBase::windowTransformChanged, this, &FloatingWindowInterface::windowTransformChanged); QObject::connect(this->window, &ProxyWindowBase::colorChanged, this, &FloatingWindowInterface::colorChanged); QObject::connect(this->window, &ProxyWindowBase::maskChanged, this, &FloatingWindowInterface::maskChanged); + QObject::connect(this->window, &ProxyWindowBase::surfaceFormatChanged, this, &FloatingWindowInterface::surfaceFormatChanged); // clang-format on } @@ -64,6 +65,7 @@ proxyPair(qint32, height, setHeight); proxyPair(QuickshellScreenInfo*, screen, setScreen); proxyPair(QColor, color, setColor); proxyPair(PendingRegion*, mask, setMask); +proxyPair(QsSurfaceFormat, surfaceFormat, setSurfaceFormat); #undef proxyPair // NOLINTEND diff --git a/src/window/floatingwindow.hpp b/src/window/floatingwindow.hpp index def1183a..36f933b4 100644 --- a/src/window/floatingwindow.hpp +++ b/src/window/floatingwindow.hpp @@ -4,6 +4,7 @@ #include #include "proxywindow.hpp" +#include "windowinterface.hpp" class ProxyFloatingWindow: public ProxyWindowBase { Q_OBJECT; @@ -50,6 +51,9 @@ public: [[nodiscard]] PendingRegion* mask() const override; void setMask(PendingRegion* mask) override; + [[nodiscard]] QsSurfaceFormat surfaceFormat() const override; + void setSurfaceFormat(QsSurfaceFormat mask) override; + [[nodiscard]] QQmlListProperty data() override; // NOLINTEND diff --git a/src/window/panelinterface.hpp b/src/window/panelinterface.hpp index 6f8d1ec1..b9664ff9 100644 --- a/src/window/panelinterface.hpp +++ b/src/window/panelinterface.hpp @@ -13,7 +13,8 @@ class Anchors { Q_PROPERTY(bool right MEMBER mRight); Q_PROPERTY(bool top MEMBER mTop); Q_PROPERTY(bool bottom MEMBER mBottom); - QML_VALUE_TYPE(anchors); + QML_VALUE_TYPE(panelAnchors); + QML_STRUCTURED_VALUE; public: [[nodiscard]] bool horizontalConstraint() const noexcept { return this->mLeft && this->mRight; } @@ -40,7 +41,8 @@ class Margins { Q_PROPERTY(qint32 right MEMBER mRight); Q_PROPERTY(qint32 top MEMBER mTop); Q_PROPERTY(qint32 bottom MEMBER mBottom); - QML_VALUE_TYPE(margins); + QML_VALUE_TYPE(panelMargins); + QML_STRUCTURED_VALUE; public: [[nodiscard]] bool operator==(const Margins& other) const noexcept { diff --git a/src/window/proxywindow.cpp b/src/window/proxywindow.cpp index 6af1dcc3..608883ae 100644 --- a/src/window/proxywindow.cpp +++ b/src/window/proxywindow.cpp @@ -6,10 +6,13 @@ #include #include #include +#include #include #include #include #include +#include +#include #include #include #include @@ -50,7 +53,7 @@ ProxyWindowBase::~ProxyWindowBase() { this->deleteWindow(true); } void ProxyWindowBase::onReload(QObject* oldInstance) { this->window = this->retrieveWindow(oldInstance); auto wasVisible = this->window != nullptr && this->window->isVisible(); - if (this->window == nullptr) this->window = this->createQQuickWindow(); + this->ensureQWindow(); // The qml engine will leave the WindowInterface as owner of everything // nested in an item, so we have to make sure the interface's children @@ -85,10 +88,55 @@ void ProxyWindowBase::postCompleteWindow() { this->setVisible(this->mVisible); } ProxiedWindow* ProxyWindowBase::createQQuickWindow() { return new ProxiedWindow(this); } -void ProxyWindowBase::createWindow() { - if (this->window != nullptr) return; - this->window = this->createQQuickWindow(); +void ProxyWindowBase::ensureQWindow() { + auto format = QSurfaceFormat::defaultFormat(); + { + // match QtQuick's default format, including env var controls + static const auto useDepth = qEnvironmentVariableIsEmpty("QSG_NO_DEPTH_BUFFER"); + static const auto useStencil = qEnvironmentVariableIsEmpty("QSG_NO_STENCIL_BUFFER"); + static const auto enableDebug = qEnvironmentVariableIsSet("QSG_OPENGL_DEBUG"); + static const auto disableVSync = qEnvironmentVariableIsSet("QSG_NO_VSYNC"); + + if (useDepth && format.depthBufferSize() == -1) format.setDepthBufferSize(24); + else if (!useDepth) format.setDepthBufferSize(0); + + if (useStencil && format.stencilBufferSize() == -1) format.setStencilBufferSize(8); + else if (!useStencil) format.setStencilBufferSize(0); + + auto opaque = this->qsSurfaceFormat.opaqueModified ? this->qsSurfaceFormat.opaque + : this->mColor.alpha() >= 255; + + if (opaque) format.setAlphaBufferSize(0); + else format.setAlphaBufferSize(8); + + if (enableDebug) format.setOption(QSurfaceFormat::DebugContext); + if (disableVSync) format.setSwapInterval(0); + + format.setSwapBehavior(QSurfaceFormat::DoubleBuffer); + format.setRedBufferSize(8); + format.setGreenBufferSize(8); + format.setBlueBufferSize(8); + } + + this->mSurfaceFormat = format; + + auto useOldWindow = this->window != nullptr; + + if (useOldWindow) { + if (this->window->requestedFormat() != format) { + useOldWindow = false; + } + } + + if (useOldWindow) return; + delete this->window; + this->window = this->createQQuickWindow(); + this->window->setFormat(format); +} + +void ProxyWindowBase::createWindow() { + this->ensureQWindow(); this->connectWindow(); this->completeWindow(); emit this->windowConnected(); @@ -320,6 +368,8 @@ void ProxyWindowBase::setColor(QColor color) { ); this->window->setColor(premultiplied); + // setColor also modifies the alpha buffer size of the surface format + this->window->setFormat(this->mSurfaceFormat); } } @@ -343,6 +393,17 @@ void ProxyWindowBase::setMask(PendingRegion* mask) { emit this->maskChanged(); } +void ProxyWindowBase::setSurfaceFormat(QsSurfaceFormat format) { + if (format == this->qsSurfaceFormat) return; + if (this->window != nullptr) { + qmlWarning(this) << "Cannot set window surface format."; + return; + } + + this->qsSurfaceFormat = format; + emit this->surfaceFormatChanged(); +} + void ProxyWindowBase::onMaskChanged() { if (this->window != nullptr) this->updateMask(); } diff --git a/src/window/proxywindow.hpp b/src/window/proxywindow.hpp index 14c90339..2ed4bcd3 100644 --- a/src/window/proxywindow.hpp +++ b/src/window/proxywindow.hpp @@ -9,6 +9,7 @@ #include #include #include +#include #include #include #include @@ -29,6 +30,7 @@ class ProxiedWindow; /// [FloatingWindow]: ../floatingwindow class ProxyWindowBase: public Reloadable { Q_OBJECT; + // clang-format off /// The QtQuick window backing this window. /// /// > [!WARNING] Do not expect values set via this property to work correctly. @@ -46,7 +48,9 @@ class ProxyWindowBase: public Reloadable { Q_PROPERTY(PendingRegion* mask READ mask WRITE setMask NOTIFY maskChanged); Q_PROPERTY(QObject* windowTransform READ windowTransform NOTIFY windowTransformChanged); Q_PROPERTY(bool backingWindowVisible READ isVisibleDirect NOTIFY backerVisibilityChanged); + Q_PROPERTY(QsSurfaceFormat surfaceFormat READ surfaceFormat WRITE setSurfaceFormat NOTIFY surfaceFormatChanged); Q_PROPERTY(QQmlListProperty data READ data); + // clang-format on Q_CLASSINFO("DefaultProperty", "data"); public: @@ -59,6 +63,7 @@ public: void operator=(ProxyWindowBase&&) = delete; void onReload(QObject* oldInstance) override; + void ensureQWindow(); void createWindow(); void deleteWindow(bool keepItemOwnership = false); @@ -98,6 +103,9 @@ public: [[nodiscard]] PendingRegion* mask() const; virtual void setMask(PendingRegion* mask); + [[nodiscard]] QsSurfaceFormat surfaceFormat() const { return this->qsSurfaceFormat; } + void setSurfaceFormat(QsSurfaceFormat format); + [[nodiscard]] QObject* windowTransform() const { return nullptr; } // NOLINT [[nodiscard]] QQmlListProperty data(); @@ -115,6 +123,7 @@ signals: void screenChanged(); void colorChanged(); void maskChanged(); + void surfaceFormatChanged(); protected slots: virtual void onWidthChanged(); @@ -135,6 +144,8 @@ protected: QQuickItem* mContentItem = nullptr; bool reloadComplete = false; bool ranLints = false; + QsSurfaceFormat qsSurfaceFormat; + QSurfaceFormat mSurfaceFormat; private: void polishItems(); diff --git a/src/window/windowinterface.hpp b/src/window/windowinterface.hpp index c969a21d..8603de31 100644 --- a/src/window/windowinterface.hpp +++ b/src/window/windowinterface.hpp @@ -15,6 +15,26 @@ class ProxyWindowBase; class QsWindowAttached; +class QsSurfaceFormat { + Q_GADGET; + QML_VALUE_TYPE(surfaceFormat); + QML_STRUCTURED_VALUE; + Q_PROPERTY(bool opaque MEMBER opaque WRITE setOpaque); + +public: + bool opaque = false; + bool opaqueModified = false; + + void setOpaque(bool opaque) { + this->opaque = opaque; + this->opaqueModified = true; + } + + [[nodiscard]] bool operator==(const QsSurfaceFormat& other) const { + return other.opaqueModified == this->opaqueModified && other.opaque == this->opaque; + } +}; + ///! Base class of Quickshell windows /// Base class of Quickshell windows /// ### Attached properties @@ -46,6 +66,10 @@ class WindowInterface: public Reloadable { /// along with map[To|From]Item (which is not reactive). Q_PROPERTY(QObject* windowTransform READ windowTransform NOTIFY windowTransformChanged); /// The background color of the window. Defaults to white. + /// + /// > [!WARNING] If the window color is opaque before it is made visible, + /// > it will not be able to become transparent later unless @@surfaceFormat$.opaque + /// > is false. Q_PROPERTY(QColor color READ color WRITE setColor NOTIFY colorChanged); /// The clickthrough mask. Defaults to null. /// @@ -90,6 +114,16 @@ class WindowInterface: public Reloadable { /// } /// ``` Q_PROPERTY(PendingRegion* mask READ mask WRITE setMask NOTIFY maskChanged); + /// Set the surface format to request from the system. + /// + /// - `opaque` - If the requested surface should be opaque. Opaque windows allow + /// the operating system to avoid drawing things behind them, or blending the window + /// with those behind it, saving power and GPU load. If unset, this property defaults to + /// true if @@color is opaque, or false if not. *You should not need to modify this + /// property unless you create a surface that starts opaque and later becomes transparent.* + /// + /// > [!NOTE] The surface format cannot be changed after the window is created. + Q_PROPERTY(QsSurfaceFormat surfaceFormat READ surfaceFormat WRITE setSurfaceFormat NOTIFY surfaceFormatChanged); Q_PROPERTY(QQmlListProperty data READ data); // clang-format on Q_CLASSINFO("DefaultProperty", "data"); @@ -124,6 +158,9 @@ public: [[nodiscard]] virtual PendingRegion* mask() const = 0; virtual void setMask(PendingRegion* mask) = 0; + [[nodiscard]] virtual QsSurfaceFormat surfaceFormat() const = 0; + virtual void setSurfaceFormat(QsSurfaceFormat format) = 0; + [[nodiscard]] virtual QQmlListProperty data() = 0; static QsWindowAttached* qmlAttachedProperties(QObject* object); @@ -138,6 +175,7 @@ signals: void windowTransformChanged(); void colorChanged(); void maskChanged(); + void surfaceFormatChanged(); }; class QsWindowAttached: public QObject { diff --git a/src/x11/panel_window.cpp b/src/x11/panel_window.cpp index c133abd3..bda4aa54 100644 --- a/src/x11/panel_window.cpp +++ b/src/x11/panel_window.cpp @@ -482,6 +482,7 @@ XPanelInterface::XPanelInterface(QObject* parent) QObject::connect(this->panel, &ProxyWindowBase::windowTransformChanged, this, &XPanelInterface::windowTransformChanged); QObject::connect(this->panel, &ProxyWindowBase::colorChanged, this, &XPanelInterface::colorChanged); QObject::connect(this->panel, &ProxyWindowBase::maskChanged, this, &XPanelInterface::maskChanged); + QObject::connect(this->panel, &ProxyWindowBase::surfaceFormatChanged, this, &XPanelInterface::surfaceFormatChanged); // panel specific QObject::connect(this->panel, &XPanelWindow::anchorsChanged, this, &XPanelInterface::anchorsChanged); @@ -516,6 +517,7 @@ proxyPair(qint32, height, setHeight); proxyPair(QuickshellScreenInfo*, screen, setScreen); proxyPair(QColor, color, setColor); proxyPair(PendingRegion*, mask, setMask); +proxyPair(QsSurfaceFormat, surfaceFormat, setSurfaceFormat); // panel specific proxyPair(Anchors, anchors, setAnchors); diff --git a/src/x11/panel_window.hpp b/src/x11/panel_window.hpp index b37c9c50..12645589 100644 --- a/src/x11/panel_window.hpp +++ b/src/x11/panel_window.hpp @@ -136,6 +136,9 @@ public: [[nodiscard]] PendingRegion* mask() const override; void setMask(PendingRegion* mask) override; + [[nodiscard]] QsSurfaceFormat surfaceFormat() const override; + void setSurfaceFormat(QsSurfaceFormat mask) override; + [[nodiscard]] QQmlListProperty data() override; // panel specific From eaf854935b0dfc51f8e7870d8d563a9a0c00573d Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Sat, 4 Jan 2025 03:37:53 -0800 Subject: [PATCH 04/31] service/upower: correctly deserialize UPowerDeviceState::Discharging ??? --- src/services/upower/device.cpp | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/services/upower/device.cpp b/src/services/upower/device.cpp index d9c6268d..dd9fbaa2 100644 --- a/src/services/upower/device.cpp +++ b/src/services/upower/device.cpp @@ -114,9 +114,7 @@ DBusResult DBusDataTransform::fromWire(qreal wire) { DBusResult DBusDataTransform::fromWire(quint32 wire) { - if (wire != UPowerDeviceType::Battery && wire >= UPowerDeviceState::Unknown - && wire <= UPowerDeviceState::PendingDischarge) - { + if (wire >= UPowerDeviceState::Unknown && wire <= UPowerDeviceState::PendingDischarge) { return DBusResult(static_cast(wire)); } From fca058e66cfc01d0a45805701234961a16573646 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Sat, 4 Jan 2025 04:38:03 -0800 Subject: [PATCH 05/31] service/upower: add device model property --- src/services/upower/device.hpp | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/services/upower/device.hpp b/src/services/upower/device.hpp index 8d1ac709..899ead93 100644 --- a/src/services/upower/device.hpp +++ b/src/services/upower/device.hpp @@ -154,6 +154,8 @@ class UPowerDevice: public QObject { Q_PROPERTY(bool isLaptopBattery READ isLaptopBattery NOTIFY isLaptopBatteryChanged BINDABLE bindableIsLaptopBattery); /// Native path of the device specific to your OS. Q_PROPERTY(QString nativePath READ nativePath NOTIFY nativePathChanged BINDABLE bindableNativePath); + /// Model name of the device. Unlikely to be useful for internal devices. + Q_PROPERTY(QString model READ model NOTIFY modelChanged BINDABLE bindableModel); /// If device statistics have been queried for this device yet. /// This will be true for all devices returned from @@UPower.devices, but not the default /// device, which may be returned before it is ready to avoid returning null. @@ -186,6 +188,7 @@ public: QS_BINDABLE_GETTER(QString, bIconName, iconName, bindableIconName); QS_BINDABLE_GETTER(bool, bIsLaptopBattery, isLaptopBattery, bindableIsLaptopBattery); QS_BINDABLE_GETTER(QString, bNativePath, nativePath, bindableNativePath); + QS_BINDABLE_GETTER(QString, bModel, model, bindableModel); QS_BINDABLE_GETTER(bool, bReady, ready, bindableReady); signals: @@ -206,6 +209,7 @@ signals: void iconNameChanged(); void isLaptopBatteryChanged(); void nativePathChanged(); + void modelChanged(); private slots: void onGetAllFinished(); @@ -227,6 +231,7 @@ private: Q_OBJECT_BINDABLE_PROPERTY(UPowerDevice, QString, bIconName, &UPowerDevice::iconNameChanged); Q_OBJECT_BINDABLE_PROPERTY(UPowerDevice, bool, bIsLaptopBattery, &UPowerDevice::isLaptopBatteryChanged); Q_OBJECT_BINDABLE_PROPERTY(UPowerDevice, QString, bNativePath, &UPowerDevice::nativePathChanged); + Q_OBJECT_BINDABLE_PROPERTY(UPowerDevice, QString, bModel, &UPowerDevice::modelChanged); Q_OBJECT_BINDABLE_PROPERTY(UPowerDevice, bool, bReady, &UPowerDevice::readyChanged); QS_DBUS_BINDABLE_PROPERTY_GROUP(UPowerDevice, deviceProperties); @@ -243,6 +248,7 @@ private: QS_DBUS_PROPERTY_BINDING(UPowerDevice, pHealthPercentage, bHealthPercentage, deviceProperties, "Capacity"); QS_DBUS_PROPERTY_BINDING(UPowerDevice, pIconName, bIconName, deviceProperties, "IconName"); QS_DBUS_PROPERTY_BINDING(UPowerDevice, pNativePath, bNativePath, deviceProperties, "NativePath"); + QS_DBUS_PROPERTY_BINDING(UPowerDevice, pModel, bModel, deviceProperties, "Model"); // clang-format on DBusUPowerDevice* device = nullptr; From 761d99d644d42bc6de7a510bbaf8c5ab970605d8 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Sun, 5 Jan 2025 01:55:33 -0800 Subject: [PATCH 06/31] service/mpris: reset position timestamps on seek Moving the onPositionUpdated callback to a bpPosition binding caused it not to fire when Position was changed to the same value, which can happen when quickly changing tracks before the player has sent a new position. This reverts the above change while still updating position on seek. --- src/services/mpris/player.cpp | 9 +++++++-- src/services/mpris/player.hpp | 6 +++--- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/src/services/mpris/player.cpp b/src/services/mpris/player.cpp index 642ca3bd..c4fe2ac9 100644 --- a/src/services/mpris/player.cpp +++ b/src/services/mpris/player.cpp @@ -237,7 +237,7 @@ void MprisPlayer::setPosition(qreal position) { this->player->Seek(target - pos); } - this->bpPosition = target; + this->setPosition(target); } void MprisPlayer::onPositionUpdated() { @@ -248,11 +248,16 @@ void MprisPlayer::onPositionUpdated() { if (firstChange) emit this->positionSupportedChanged(); } +void MprisPlayer::setPosition(qlonglong position) { + this->bpPosition = position; + this->onPositionUpdated(); +} + void MprisPlayer::onExportedPositionChanged() { if (!this->lengthSupported()) emit this->lengthChanged(); } -void MprisPlayer::onSeek(qlonglong time) { this->bpPosition = time; } +void MprisPlayer::onSeek(qlonglong time) { this->setPosition(time); } qreal MprisPlayer::length() const { if (this->bInternalLength == -1) { diff --git a/src/services/mpris/player.hpp b/src/services/mpris/player.hpp index 7637c10a..2eda305d 100644 --- a/src/services/mpris/player.hpp +++ b/src/services/mpris/player.hpp @@ -391,6 +391,8 @@ private slots: private: void onMetadataChanged(); void onPositionUpdated(); + // call instead of setting bpPosition + void setPosition(qlonglong position); void requestPositionUpdate() { this->pPosition.requestUpdate(); }; // clang-format off @@ -457,7 +459,7 @@ private: QS_DBUS_PROPERTY_BINDING(MprisPlayer, pCanSeek, bpCanSeek, playerProperties, "CanSeek"); QS_DBUS_PROPERTY_BINDING(MprisPlayer, pCanGoNext, bpCanGoNext, playerProperties, "CanGoNext"); QS_DBUS_PROPERTY_BINDING(MprisPlayer, pCanGoPrevious, bpCanGoPrevious, playerProperties, "CanGoPrevious"); - QS_DBUS_PROPERTY_BINDING(MprisPlayer, qlonglong, pPosition, bpPosition, playerProperties, "Position", false); + QS_DBUS_PROPERTY_BINDING(MprisPlayer, qlonglong, pPosition, bpPosition, onPositionUpdated, playerProperties, "Position", false); QS_DBUS_PROPERTY_BINDING(MprisPlayer, pVolume, bVolume, playerProperties, "Volume", false); QS_DBUS_PROPERTY_BINDING(MprisPlayer, pMetadata, bpMetadata, playerProperties, "Metadata"); QS_DBUS_PROPERTY_BINDING(MprisPlayer, pPlaybackStatus, bpPlaybackStatus, playerProperties, "PlaybackStatus"); @@ -466,8 +468,6 @@ private: QS_DBUS_PROPERTY_BINDING(MprisPlayer, pMinRate, bMinRate, playerProperties, "MinimumRate", false); QS_DBUS_PROPERTY_BINDING(MprisPlayer, pMaxRate, bMaxRate, playerProperties, "MaximumRate", false); QS_DBUS_PROPERTY_BINDING(MprisPlayer, pShuffle, bShuffle, playerProperties, "Shuffle", false); - - QS_BINDING_SUBSCRIBE_METHOD(MprisPlayer, bpPosition, onPositionUpdated, onValueChanged); // clang-format on QDateTime lastPositionTimestamp; From af86d5fd19fea627b484e825402629a5aae5747f Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Sun, 5 Jan 2025 23:53:03 -0800 Subject: [PATCH 07/31] hyprland/surface: remove debug print --- src/wayland/hyprland/surface/qml.cpp | 1 - 1 file changed, 1 deletion(-) diff --git a/src/wayland/hyprland/surface/qml.cpp b/src/wayland/hyprland/surface/qml.cpp index 8477de55..ab8aace8 100644 --- a/src/wayland/hyprland/surface/qml.cpp +++ b/src/wayland/hyprland/surface/qml.cpp @@ -28,7 +28,6 @@ HyprlandWindow* HyprlandWindow::qmlAttachedProperties(QObject* object) { } } - qDebug() << "hlwindow for" << proxyWindow; if (!proxyWindow) return nullptr; return new HyprlandWindow(proxyWindow); } From 26d443aa5041bb836aa454be24feb3b08ad7b945 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Mon, 6 Jan 2025 22:08:00 -0800 Subject: [PATCH 08/31] ci: add 6.8.1 --- .github/workflows/build.yml | 2 +- ci/nix-checkouts.nix | 9 +++++++-- flake.lock | 6 +++--- 3 files changed, 11 insertions(+), 6 deletions(-) diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index b176e982..2ec6d8cf 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -6,7 +6,7 @@ jobs: name: Nix strategy: matrix: - qtver: [qt6.8.0, qt6.7.3, qt6.7.2, qt6.7.1, qt6.7.0, qt6.6.3, qt6.6.2, qt6.6.1, qt6.6.0] + qtver: [qt6.8.1, qt6.8.0, qt6.7.3, qt6.7.2, qt6.7.1, qt6.7.0, qt6.6.3, qt6.6.2, qt6.6.1, qt6.6.0] compiler: [clang, gcc] runs-on: ubuntu-latest steps: diff --git a/ci/nix-checkouts.nix b/ci/nix-checkouts.nix index 3c99ab11..f38d5fa4 100644 --- a/ci/nix-checkouts.nix +++ b/ci/nix-checkouts.nix @@ -11,9 +11,14 @@ in { # For old qt versions, grab the commit before the version bump that has all the patches # instead of the bumped version. + qt6_8_1 = byCommit { + commit = "3df3c47c19dc90fec35359e89ffb52b34d2b0e94"; + sha256 = "1lhlm7czhwwys5ak6ngb5li6bxddilb9479k9nkss502kw8hwjyz"; + }; + qt6_8_0 = byCommit { - commit = "23e89b7da85c3640bbc2173fe04f4bd114342367"; - sha256 = "1b2v6y3bja4br5ribh9lj6xzz2k81dggz708b2mib83rwb509wyb"; + commit = "352f462ad9d2aa2cde75fdd8f1734e86402a3ff6"; + sha256 = "02zfgkr9fpd6iwfh6dcr3m6fnx61jppm3v081f3brvkqwmmz7zq1"; }; qt6_7_3 = byCommit { diff --git a/flake.lock b/flake.lock index ed928826..df5aa3f9 100644 --- a/flake.lock +++ b/flake.lock @@ -2,11 +2,11 @@ "nodes": { "nixpkgs": { "locked": { - "lastModified": 1732014248, - "narHash": "sha256-y/MEyuJ5oBWrWAic/14LaIr/u5E0wRVzyYsouYY3W6w=", + "lastModified": 1736012469, + "narHash": "sha256-/qlNWm/IEVVH7GfgAIyP6EsVZI6zjAx1cV5zNyrs+rI=", "owner": "NixOS", "repo": "nixpkgs", - "rev": "23e89b7da85c3640bbc2173fe04f4bd114342367", + "rev": "8f3e1f807051e32d8c95cd12b9b421623850a34d", "type": "github" }, "original": { From 2c411fce5a7d7e0708be1cee875bb3547f40887d Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Tue, 7 Jan 2025 03:11:19 -0800 Subject: [PATCH 09/31] all: fix new lints --- .clang-tidy | 2 + Justfile | 6 +- src/core/desktopentry.cpp | 3 + src/core/iconimageprovider.cpp | 5 +- src/core/imageprovider.cpp | 26 ++- src/core/module.md | 1 + src/core/paths.cpp | 2 + src/core/plugin.cpp | 11 +- src/core/popupanchor.cpp | 9 +- src/core/test/scriptmodel.cpp | 2 + src/core/variants.cpp | 2 +- src/core/variants.hpp | 4 +- src/crash/handler.cpp | 2 + src/crash/main.cpp | 84 ++++---- src/dbus/bus.cpp | 2 + src/dbus/dbusmenu/dbusmenu.cpp | 8 +- src/dbus/properties.cpp | 16 +- src/dbus/properties.hpp | 2 +- src/debug/lint.cpp | 6 + src/io/fileview.cpp | 2 + src/launch/command.cpp | 222 ++++++++++---------- src/launch/launch.cpp | 4 + src/launch/main.cpp | 62 +++--- src/services/greetd/connection.cpp | 2 + src/services/mpris/player.cpp | 2 + src/services/mpris/watcher.cpp | 2 + src/services/notifications/dbusimage.cpp | 1 + src/services/notifications/notification.cpp | 1 + src/services/notifications/server.cpp | 1 + src/services/pipewire/core.cpp | 2 + src/services/pipewire/defaults.cpp | 2 + src/services/pipewire/device.cpp | 2 + src/services/pipewire/link.cpp | 2 + src/services/pipewire/metadata.cpp | 2 + src/services/pipewire/node.cpp | 2 + src/services/status_notifier/item.cpp | 1 - src/services/upower/core.cpp | 2 + src/services/upower/device.cpp | 2 + src/wayland/hyprland/ipc/connection.cpp | 31 ++- src/wayland/init.cpp | 4 +- src/wayland/platformmenu.cpp | 4 + src/wayland/wlr_layershell/surface.cpp | 91 ++++---- src/x11/i3/ipc/connection.cpp | 28 ++- 43 files changed, 351 insertions(+), 316 deletions(-) diff --git a/.clang-tidy b/.clang-tidy index ca6c9549..002c444d 100644 --- a/.clang-tidy +++ b/.clang-tidy @@ -7,6 +7,7 @@ Checks: > -bugprone-easily-swappable-parameters, -bugprone-forward-declararion-namespace, -bugprone-forward-declararion-namespace, + -bugprone-return-const-ref-from-parameter, concurrency-*, cppcoreguidelines-*, -cppcoreguidelines-owning-memory, @@ -44,6 +45,7 @@ Checks: > -readability-container-data-pointer, -readability-implicit-bool-conversion, -readability-avoid-nested-conditional-operator, + -readability-math-missing-parentheses, tidyfox-*, CheckOptions: performance-for-range-copy.WarnOnAllAutoCopies: true diff --git a/Justfile b/Justfile index b4fe87ec..f60771aa 100644 --- a/Justfile +++ b/Justfile @@ -4,13 +4,13 @@ fmt: find src -type f \( -name "*.cpp" -o -name "*.hpp" \) -print0 | xargs -0 clang-format -i lint: - find src -type f -name "*.cpp" -print0 | parallel -q0 --no-notice --will-cite --tty --bar clang-tidy --load={{ env_var("TIDYFOX") }} + find src -type f -name "*.cpp" -print0 | parallel -j$(nproc) -q0 --no-notice --will-cite --tty --bar clang-tidy --load={{ env_var("TIDYFOX") }} lint-ci: - find src -type f -name "*.cpp" -print0 | parallel -q0 --no-notice --will-cite --tty clang-tidy --load={{ env_var("TIDYFOX") }} + find src -type f -name "*.cpp" -print0 | parallel -j$(nproc) -q0 --no-notice --will-cite --tty clang-tidy --load={{ env_var("TIDYFOX") }} lint-changed: - git diff --name-only HEAD | grep "^.*\.cpp\$" | parallel --no-notice --will-cite --tty --bar clang-tidy --load={{ env_var("TIDYFOX") }} + git diff --name-only HEAD | grep "^.*\.cpp\$" | parallel -j$(nproc) --no-notice --will-cite --tty --bar clang-tidy --load={{ env_var("TIDYFOX") }} configure target='debug' *FLAGS='': cmake -GNinja -B {{builddir}} \ diff --git a/src/core/desktopentry.cpp b/src/core/desktopentry.cpp index 3714df01..975db3b4 100644 --- a/src/core/desktopentry.cpp +++ b/src/core/desktopentry.cpp @@ -18,7 +18,9 @@ #include "model.hpp" +namespace { Q_LOGGING_CATEGORY(logDesktopEntry, "quickshell.desktopentry", QtWarningMsg); +} struct Locale { explicit Locale() = default; @@ -78,6 +80,7 @@ struct Locale { QString modifier; }; +// NOLINTNEXTLINE(misc-use-internal-linkage) QDebug operator<<(QDebug debug, const Locale& locale) { auto saver = QDebugStateSaver(debug); debug.nospace() << "Locale(language=" << locale.language << ", territory=" << locale.territory diff --git a/src/core/iconimageprovider.cpp b/src/core/iconimageprovider.cpp index cf24d37d..43e00fd8 100644 --- a/src/core/iconimageprovider.cpp +++ b/src/core/iconimageprovider.cpp @@ -1,4 +1,5 @@ #include "iconimageprovider.hpp" +#include #include #include @@ -49,8 +50,8 @@ IconImageProvider::requestPixmap(const QString& id, QSize* size, const QSize& re QPixmap IconImageProvider::missingPixmap(const QSize& size) { auto width = size.width() % 2 == 0 ? size.width() : size.width() + 1; auto height = size.height() % 2 == 0 ? size.height() : size.height() + 1; - if (width < 2) width = 2; - if (height < 2) height = 2; + width = std::max(width, 2); + height = std::max(height, 2); auto pixmap = QPixmap(width, height); pixmap.fill(QColorConstants::Black); diff --git a/src/core/imageprovider.cpp b/src/core/imageprovider.cpp index cc81c47f..256faaed 100644 --- a/src/core/imageprovider.cpp +++ b/src/core/imageprovider.cpp @@ -8,7 +8,21 @@ #include #include -static QMap liveImages; // NOLINT +namespace { + +QMap liveImages; // NOLINT + +void parseReq(const QString& req, QString& target, QString& param) { + auto splitIdx = req.indexOf('/'); + if (splitIdx != -1) { + target = req.sliced(0, splitIdx); + param = req.sliced(splitIdx + 1); + } else { + target = req; + } +} + +} // namespace QsImageHandle::QsImageHandle(QQmlImageProviderBase::ImageType type, QObject* parent) : QObject(parent) @@ -43,16 +57,6 @@ QPixmap QsImageHandle:: return QPixmap(); } -void parseReq(const QString& req, QString& target, QString& param) { - auto splitIdx = req.indexOf('/'); - if (splitIdx != -1) { - target = req.sliced(0, splitIdx); - param = req.sliced(splitIdx + 1); - } else { - target = req; - } -} - QImage QsImageProvider::requestImage(const QString& id, QSize* size, const QSize& requestedSize) { QString target; QString param; diff --git a/src/core/module.md b/src/core/module.md index 831f561b..c8b17ab9 100644 --- a/src/core/module.md +++ b/src/core/module.md @@ -28,5 +28,6 @@ headers = [ "types.hpp", "qsmenuanchor.hpp", "clock.hpp", + "scriptmodel.hpp", ] ----- diff --git a/src/core/paths.cpp b/src/core/paths.cpp index e108da03..e49a9d41 100644 --- a/src/core/paths.cpp +++ b/src/core/paths.cpp @@ -15,7 +15,9 @@ #include "instanceinfo.hpp" +namespace { Q_LOGGING_CATEGORY(logPaths, "quickshell.paths", QtWarningMsg); +} QsPaths* QsPaths::instance() { static auto* instance = new QsPaths(); // NOLINT diff --git a/src/core/plugin.cpp b/src/core/plugin.cpp index c6ceb13e..0eb9a067 100644 --- a/src/core/plugin.cpp +++ b/src/core/plugin.cpp @@ -10,16 +10,9 @@ static QVector plugins; // NOLINT void QsEnginePlugin::registerPlugin(QsEnginePlugin& plugin) { plugins.push_back(&plugin); } void QsEnginePlugin::initPlugins() { - plugins.erase( - std::remove_if( - plugins.begin(), - plugins.end(), - [](QsEnginePlugin* plugin) { return !plugin->applies(); } - ), - plugins.end() - ); + plugins.removeIf([](QsEnginePlugin* plugin) { return !plugin->applies(); }); - std::sort(plugins.begin(), plugins.end(), [](QsEnginePlugin* a, QsEnginePlugin* b) { + std::ranges::sort(plugins, [](QsEnginePlugin* a, QsEnginePlugin* b) { return b->dependencies().contains(a->name()); }); diff --git a/src/core/popupanchor.cpp b/src/core/popupanchor.cpp index 2b5b1bae..f72d5c59 100644 --- a/src/core/popupanchor.cpp +++ b/src/core/popupanchor.cpp @@ -1,4 +1,5 @@ #include "popupanchor.hpp" +#include #include #include @@ -276,9 +277,7 @@ void PopupPositioner::reposition(PopupAnchor* anchor, QWindow* window, bool only effectiveX = screenGeometry.right() - windowGeometry.width() + 1; } - if (effectiveX < screenGeometry.left()) { - effectiveX = screenGeometry.left(); - } + effectiveX = std::max(effectiveX, screenGeometry.left()); } if (adjustment.testFlag(PopupAdjustment::SlideY)) { @@ -286,9 +285,7 @@ void PopupPositioner::reposition(PopupAnchor* anchor, QWindow* window, bool only effectiveY = screenGeometry.bottom() - windowGeometry.height() + 1; } - if (effectiveY < screenGeometry.top()) { - effectiveY = screenGeometry.top(); - } + effectiveY = std::max(effectiveY, screenGeometry.top()); } if (adjustment.testFlag(PopupAdjustment::ResizeX)) { diff --git a/src/core/test/scriptmodel.cpp b/src/core/test/scriptmodel.cpp index bdf9c709..66746832 100644 --- a/src/core/test/scriptmodel.cpp +++ b/src/core/test/scriptmodel.cpp @@ -22,6 +22,7 @@ bool ModelOperation::operator==(const ModelOperation& other) const { && other.length == this->length && other.destIndex == this->destIndex; } +// NOLINTNEXTLINE(misc-use-internal-linkage) QDebug& operator<<(QDebug& debug, const ModelOperation& op) { auto saver = QDebugStateSaver(debug); debug.nospace(); @@ -43,6 +44,7 @@ QDebug& operator<<(QDebug& debug, const ModelOperation& op) { return debug; } +// NOLINTNEXTLINE(misc-use-internal-linkage) QDebug& operator<<(QDebug& debug, const QVariantList& list) { auto str = QString(); diff --git a/src/core/variants.cpp b/src/core/variants.cpp index 6c8713b6..a190e36d 100644 --- a/src/core/variants.cpp +++ b/src/core/variants.cpp @@ -196,7 +196,7 @@ V* AwfulMap::get(const K& key) { } template -void AwfulMap::insert(K key, V value) { +void AwfulMap::insert(const K& key, V value) { this->values.push_back(QPair(key, value)); } diff --git a/src/core/variants.hpp b/src/core/variants.hpp index ebf87ae1..fa0333d3 100644 --- a/src/core/variants.hpp +++ b/src/core/variants.hpp @@ -20,8 +20,8 @@ class AwfulMap { public: [[nodiscard]] bool contains(const K& key) const; [[nodiscard]] V* get(const K& key); - void insert(K key, V value); // assumes no duplicates - bool remove(const K& key); // returns true if anything was removed + void insert(const K& key, V value); // assumes no duplicates + bool remove(const K& key); // returns true if anything was removed QList> values; }; diff --git a/src/crash/handler.cpp b/src/crash/handler.cpp index 1f300cc9..8d9a8a71 100644 --- a/src/crash/handler.cpp +++ b/src/crash/handler.cpp @@ -22,7 +22,9 @@ using namespace google_breakpad; namespace qs::crash { +namespace { Q_LOGGING_CATEGORY(logCrashHandler, "quickshell.crashhandler", QtWarningMsg); +} struct CrashHandlerPrivate { ExceptionHandler* exceptionHandler = nullptr; diff --git a/src/crash/main.cpp b/src/crash/main.cpp index 1beb6749..7c3bad73 100644 --- a/src/crash/main.cpp +++ b/src/crash/main.cpp @@ -22,49 +22,10 @@ #include "build.hpp" #include "interface.hpp" +namespace { + Q_LOGGING_CATEGORY(logCrashReporter, "quickshell.crashreporter", QtWarningMsg); -void recordCrashInfo(const QDir& crashDir, const InstanceInfo& instance); - -void qsCheckCrash(int argc, char** argv) { - auto fd = qEnvironmentVariable("__QUICKSHELL_CRASH_DUMP_FD"); - if (fd.isEmpty()) return; - auto app = QApplication(argc, argv); - - RelaunchInfo info; - - auto crashProc = qEnvironmentVariable("__QUICKSHELL_CRASH_DUMP_PID").toInt(); - - { - auto infoFd = qEnvironmentVariable("__QUICKSHELL_CRASH_INFO_FD").toInt(); - - QFile file; - file.open(infoFd, QFile::ReadOnly, QFile::AutoCloseHandle); - file.seek(0); - - auto ds = QDataStream(&file); - ds >> info; - } - - LogManager::init( - !info.noColor, - info.timestamp, - info.sparseLogsOnly, - info.defaultLogLevel, - info.logRules - ); - - auto crashDir = QsPaths::crashDir(info.instance.instanceId); - - qCInfo(logCrashReporter) << "Starting crash reporter..."; - - recordCrashInfo(crashDir, info.instance); - - auto gui = CrashReporterGui(crashDir.path(), crashProc); - gui.show(); - exit(QApplication::exec()); // NOLINT -} - int tryDup(int fd, const QString& path) { QFile sourceFile; if (!sourceFile.open(fd, QFile::ReadOnly, QFile::AutoCloseHandle)) { @@ -184,3 +145,44 @@ void recordCrashInfo(const QDir& crashDir, const InstanceInfo& instance) { qCDebug(logCrashReporter) << "Recorded crash information."; } + +} // namespace + +void qsCheckCrash(int argc, char** argv) { + auto fd = qEnvironmentVariable("__QUICKSHELL_CRASH_DUMP_FD"); + if (fd.isEmpty()) return; + auto app = QApplication(argc, argv); + + RelaunchInfo info; + + auto crashProc = qEnvironmentVariable("__QUICKSHELL_CRASH_DUMP_PID").toInt(); + + { + auto infoFd = qEnvironmentVariable("__QUICKSHELL_CRASH_INFO_FD").toInt(); + + QFile file; + file.open(infoFd, QFile::ReadOnly, QFile::AutoCloseHandle); + file.seek(0); + + auto ds = QDataStream(&file); + ds >> info; + } + + LogManager::init( + !info.noColor, + info.timestamp, + info.sparseLogsOnly, + info.defaultLogLevel, + info.logRules + ); + + auto crashDir = QsPaths::crashDir(info.instance.instanceId); + + qCInfo(logCrashReporter) << "Starting crash reporter..."; + + recordCrashInfo(crashDir, info.instance); + + auto gui = CrashReporterGui(crashDir.path(), crashProc); + gui.show(); + exit(QApplication::exec()); // NOLINT +} diff --git a/src/dbus/bus.cpp b/src/dbus/bus.cpp index 6f560e9e..dc6d21bf 100644 --- a/src/dbus/bus.cpp +++ b/src/dbus/bus.cpp @@ -14,7 +14,9 @@ namespace qs::dbus { +namespace { Q_LOGGING_CATEGORY(logDbus, "quickshell.dbus", QtWarningMsg); +} void tryLaunchService( QObject* parent, diff --git a/src/dbus/dbusmenu/dbusmenu.cpp b/src/dbus/dbusmenu/dbusmenu.cpp index e86b580d..0267af8e 100644 --- a/src/dbus/dbusmenu/dbusmenu.cpp +++ b/src/dbus/dbusmenu/dbusmenu.cpp @@ -368,11 +368,9 @@ void DBusMenu::updateLayoutRecursive( auto childrenChanged = false; auto iter = item->mChildren.begin(); while (iter != item->mChildren.end()) { - auto existing = std::find_if( - layout.children.begin(), - layout.children.end(), - [&](const DBusMenuLayout& layout) { return layout.id == *iter; } - ); + auto existing = std::ranges::find_if(layout.children, [&](const DBusMenuLayout& layout) { + return layout.id == *iter; + }); if (!item->mShowChildren || existing == layout.children.end()) { qCDebug(logDbusMenu) << "Removing missing layout item" << this->items.value(*iter) << "from" diff --git a/src/dbus/properties.cpp b/src/dbus/properties.cpp index a9ef2d27..52f50060 100644 --- a/src/dbus/properties.cpp +++ b/src/dbus/properties.cpp @@ -190,11 +190,9 @@ void DBusPropertyGroup::updateAllViaGetAll() { void DBusPropertyGroup::updatePropertySet(const QVariantMap& properties, bool complainMissing) { for (const auto [name, value]: properties.asKeyValueRange()) { - auto prop = std::find_if( - this->properties.begin(), - this->properties.end(), - [&name](DBusPropertyCore* prop) { return prop->nameRef() == name; } - ); + auto prop = std::ranges::find_if(this->properties, [&name](DBusPropertyCore* prop) { + return prop->nameRef() == name; + }); if (prop == this->properties.end()) { qCDebug(logDbusProperties) << "Ignoring untracked property update" << name << "for" @@ -312,11 +310,9 @@ void DBusPropertyGroup::onPropertiesChanged( << "Received property change set and invalidations for" << this->toString(); for (const auto& name: invalidatedProperties) { - auto prop = std::find_if( - this->properties.begin(), - this->properties.end(), - [&name](DBusPropertyCore* prop) { return prop->nameRef() == name; } - ); + auto prop = std::ranges::find_if(this->properties, [&name](DBusPropertyCore* prop) { + return prop->nameRef() == name; + }); if (prop == this->properties.end()) { qCDebug(logDbusProperties) << "Ignoring untracked property invalidation" << name << "for" diff --git a/src/dbus/properties.hpp b/src/dbus/properties.hpp index f800ef3e..6feaa43d 100644 --- a/src/dbus/properties.hpp +++ b/src/dbus/properties.hpp @@ -66,7 +66,7 @@ template void asyncReadProperty( QDBusAbstractInterface& interface, const QString& property, - std::function callback + const std::function& callback ) { asyncReadPropertyInternal( QMetaType::fromType(), diff --git a/src/debug/lint.cpp b/src/debug/lint.cpp index f9727968..eb0450f8 100644 --- a/src/debug/lint.cpp +++ b/src/debug/lint.cpp @@ -13,10 +13,12 @@ namespace qs::debug { +namespace { Q_LOGGING_CATEGORY(logLint, "quickshell.linter", QtWarningMsg); void lintZeroSized(QQuickItem* item); bool isRenderable(QQuickItem* item); +} // namespace void lintObjectTree(QObject* object) { if (!logLint().isWarningEnabled()) return; @@ -41,6 +43,8 @@ void lintItemTree(QQuickItem* item) { } } +namespace { + void lintZeroSized(QQuickItem* item) { if (!item->isEnabled() || !item->isVisible()) return; if (item->childItems().isEmpty()) return; @@ -71,4 +75,6 @@ bool isRenderable(QQuickItem* item) { return std::ranges::any_of(item->childItems(), [](auto* item) { return isRenderable(item); }); } +} // namespace + } // namespace qs::debug diff --git a/src/io/fileview.cpp b/src/io/fileview.cpp index 29e6971b..23656d34 100644 --- a/src/io/fileview.cpp +++ b/src/io/fileview.cpp @@ -24,7 +24,9 @@ namespace qs::io { +namespace { Q_LOGGING_CATEGORY(logFileView, "quickshell.io.fileview", QtWarningMsg); +} QString FileViewError::toString(FileViewError::Enum value) { switch (value) { diff --git a/src/launch/command.cpp b/src/launch/command.cpp index 9e8ac27c..eb27df74 100644 --- a/src/launch/command.cpp +++ b/src/launch/command.cpp @@ -34,113 +34,7 @@ namespace qs::launch { using qs::ipc::IpcClient; -int readLogFile(CommandState& cmd); -int listInstances(CommandState& cmd); -int killInstances(CommandState& cmd); -int msgInstance(CommandState& cmd); -int launchFromCommand(CommandState& cmd, QCoreApplication* coreApplication); -int locateConfigFile(CommandState& cmd, QString& path); - -int runCommand(int argc, char** argv, QCoreApplication* coreApplication) { - auto state = CommandState(); - if (auto ret = parseCommand(argc, argv, state); ret != 65535) return ret; - - if (state.misc.checkCompat) { - if (strcmp(qVersion(), QT_VERSION_STR) != 0) { - QTextStream(stdout) << "\033[31mCOMPATIBILITY WARNING: Quickshell was built against Qt " - << QT_VERSION_STR << " but the system has updated to Qt " << qVersion() - << " without rebuilding the package. This is likely to cause crashes, so " - "you must rebuild the quickshell package.\n"; - return 1; - } - - return 0; - } - - // Has to happen before extra threads are spawned. - if (state.misc.daemonize) { - auto closepipes = std::array(); - if (pipe(closepipes.data()) == -1) { - qFatal().nospace() << "Failed to create messaging pipes for daemon with error " << errno - << ": " << qt_error_string(); - } - - pid_t pid = fork(); // NOLINT (include) - - if (pid == -1) { - qFatal().nospace() << "Failed to fork daemon with error " << errno << ": " - << qt_error_string(); - } else if (pid == 0) { - DAEMON_PIPE = closepipes[1]; - close(closepipes[0]); - - if (setsid() == -1) { - qFatal().nospace() << "Failed to setsid with error " << errno << ": " << qt_error_string(); - } - } else { - close(closepipes[1]); - - int ret = 0; - if (read(closepipes[0], &ret, sizeof(int)) == -1) { - qFatal() << "Failed to wait for daemon launch (it may have crashed)"; - } - - return ret; - } - } - - { - auto level = state.log.verbosity == 0 ? QtWarningMsg - : state.log.verbosity == 1 ? QtInfoMsg - : QtDebugMsg; - - LogManager::init( - !state.log.noColor, - state.log.timestamp, - state.log.sparse, - level, - *state.log.rules, - *state.subcommand.log ? "READER" : "" - ); - } - - if (state.misc.printVersion) { - qCInfo(logBare).noquote().nospace() << "quickshell pre-release, revision " << GIT_REVISION - << ", distributed by: " << DISTRIBUTOR; - - if (state.log.verbosity > 1) { - qCInfo(logBare).noquote() << "\nBuildtime Qt Version:" << QT_VERSION_STR; - qCInfo(logBare).noquote() << "Runtime Qt Version:" << qVersion(); - qCInfo(logBare).noquote() << "Compiler:" << COMPILER; - qCInfo(logBare).noquote() << "Compile Flags:" << COMPILE_FLAGS; - } - - if (state.log.verbosity > 0) { - qCInfo(logBare).noquote() << "\nBuild Type:" << BUILD_TYPE; - qCInfo(logBare).noquote() << "Build configuration:"; - qCInfo(logBare).noquote().nospace() << BUILD_CONFIGURATION; - } - } else if (*state.subcommand.log) { - return readLogFile(state); - } else if (*state.subcommand.list) { - return listInstances(state); - } else if (*state.subcommand.kill) { - return killInstances(state); - } else if (*state.subcommand.msg) { - return msgInstance(state); - } else { - if (strcmp(qVersion(), QT_VERSION_STR) != 0) { - qWarning() << "\033[31mQuickshell was built against Qt" << QT_VERSION_STR - << "but the system has updated to Qt" << qVersion() - << "without rebuilding the package. This is likely to cause crashes, so " - "the quickshell package must be rebuilt.\n"; - } - - return launchFromCommand(state, coreApplication); - } - - return 0; -} +namespace { int locateConfigFile(CommandState& cmd, QString& path) { if (!cmd.config.path->isEmpty()) { @@ -209,7 +103,7 @@ int locateConfigFile(CommandState& cmd, QString& path) { } void sortInstances(QVector& list) { - std::sort(list.begin(), list.end(), [](const InstanceLockInfo& a, const InstanceLockInfo& b) { + std::ranges::sort(list, [](const InstanceLockInfo& a, const InstanceLockInfo& b) { return a.instance.launchTime < b.instance.launchTime; }); }; @@ -230,12 +124,9 @@ int selectInstance(CommandState& cmd, InstanceLockInfo* instance) { path = basePath->filePath("by-pid"); auto instances = QsPaths::collectInstances(path); - auto itr = - std::remove_if(instances.begin(), instances.end(), [&](const InstanceLockInfo& info) { - return !info.instance.instanceId.startsWith(*cmd.instance.id); - }); - - instances.erase(itr, instances.end()); + instances.removeIf([&](const InstanceLockInfo& info) { + return !info.instance.instanceId.startsWith(*cmd.instance.id); + }); if (instances.isEmpty()) { qCInfo(logBare) << "No running instances start with" << *cmd.instance.id; @@ -444,4 +335,107 @@ int launchFromCommand(CommandState& cmd, QCoreApplication* coreApplication) { ); } +} // namespace + +int runCommand(int argc, char** argv, QCoreApplication* coreApplication) { + auto state = CommandState(); + if (auto ret = parseCommand(argc, argv, state); ret != 65535) return ret; + + if (state.misc.checkCompat) { + if (strcmp(qVersion(), QT_VERSION_STR) != 0) { + QTextStream(stdout) << "\033[31mCOMPATIBILITY WARNING: Quickshell was built against Qt " + << QT_VERSION_STR << " but the system has updated to Qt " << qVersion() + << " without rebuilding the package. This is likely to cause crashes, so " + "you must rebuild the quickshell package.\n"; + return 1; + } + + return 0; + } + + // Has to happen before extra threads are spawned. + if (state.misc.daemonize) { + auto closepipes = std::array(); + if (pipe(closepipes.data()) == -1) { + qFatal().nospace() << "Failed to create messaging pipes for daemon with error " << errno + << ": " << qt_error_string(); + } + + pid_t pid = fork(); // NOLINT (include) + + if (pid == -1) { + qFatal().nospace() << "Failed to fork daemon with error " << errno << ": " + << qt_error_string(); + } else if (pid == 0) { + DAEMON_PIPE = closepipes[1]; + close(closepipes[0]); + + if (setsid() == -1) { + qFatal().nospace() << "Failed to setsid with error " << errno << ": " << qt_error_string(); + } + } else { + close(closepipes[1]); + + int ret = 0; + if (read(closepipes[0], &ret, sizeof(int)) == -1) { + qFatal() << "Failed to wait for daemon launch (it may have crashed)"; + } + + return ret; + } + } + + { + auto level = state.log.verbosity == 0 ? QtWarningMsg + : state.log.verbosity == 1 ? QtInfoMsg + : QtDebugMsg; + + LogManager::init( + !state.log.noColor, + state.log.timestamp, + state.log.sparse, + level, + *state.log.rules, + *state.subcommand.log ? "READER" : "" + ); + } + + if (state.misc.printVersion) { + qCInfo(logBare).noquote().nospace() << "quickshell pre-release, revision " << GIT_REVISION + << ", distributed by: " << DISTRIBUTOR; + + if (state.log.verbosity > 1) { + qCInfo(logBare).noquote() << "\nBuildtime Qt Version:" << QT_VERSION_STR; + qCInfo(logBare).noquote() << "Runtime Qt Version:" << qVersion(); + qCInfo(logBare).noquote() << "Compiler:" << COMPILER; + qCInfo(logBare).noquote() << "Compile Flags:" << COMPILE_FLAGS; + } + + if (state.log.verbosity > 0) { + qCInfo(logBare).noquote() << "\nBuild Type:" << BUILD_TYPE; + qCInfo(logBare).noquote() << "Build configuration:"; + qCInfo(logBare).noquote().nospace() << BUILD_CONFIGURATION; + } + } else if (*state.subcommand.log) { + return readLogFile(state); + } else if (*state.subcommand.list) { + return listInstances(state); + } else if (*state.subcommand.kill) { + return killInstances(state); + } else if (*state.subcommand.msg) { + return msgInstance(state); + } else { + if (strcmp(qVersion(), QT_VERSION_STR) != 0) { + qWarning() << "\033[31mQuickshell was built against Qt" << QT_VERSION_STR + << "but the system has updated to Qt" << qVersion() + << "without rebuilding the package. This is likely to cause crashes, so " + "the quickshell package must be rebuilt.\n"; + } + + return launchFromCommand(state, coreApplication); + } + + return 0; +} + } // namespace qs::launch diff --git a/src/launch/launch.cpp b/src/launch/launch.cpp index ec5863e9..848da499 100644 --- a/src/launch/launch.cpp +++ b/src/launch/launch.cpp @@ -32,6 +32,8 @@ namespace qs::launch { +namespace { + template QString base36Encode(T number) { const QString digits = "0123456789abcdefghijklmnopqrstuvwxyz"; @@ -52,6 +54,8 @@ QString base36Encode(T number) { return result; } +} // namespace + int launch(const LaunchArgs& args, char** argv, QCoreApplication* coreApplication) { auto pathId = QCryptographicHash::hash(args.configPath.toUtf8(), QCryptographicHash::Md5).toHex(); auto shellId = QString(pathId); diff --git a/src/launch/main.cpp b/src/launch/main.cpp index 3a2b5822..2bcbebd3 100644 --- a/src/launch/main.cpp +++ b/src/launch/main.cpp @@ -22,36 +22,7 @@ namespace qs::launch { -void checkCrashRelaunch(char** argv, QCoreApplication* coreApplication); - -int DAEMON_PIPE = -1; // NOLINT - -void exitDaemon(int code) { - if (DAEMON_PIPE == -1) return; - - if (write(DAEMON_PIPE, &code, sizeof(int)) == -1) { - qCritical().nospace() << "Failed to write daemon exit command with error code " << errno << ": " - << qt_error_string(); - } - - close(DAEMON_PIPE); - - close(STDIN_FILENO); - close(STDOUT_FILENO); - close(STDERR_FILENO); - - if (open("/dev/null", O_RDONLY) != STDIN_FILENO) { // NOLINT - qFatal() << "Failed to open /dev/null on stdin"; - } - - if (open("/dev/null", O_WRONLY) != STDOUT_FILENO) { // NOLINT - qFatal() << "Failed to open /dev/null on stdout"; - } - - if (open("/dev/null", O_WRONLY) != STDERR_FILENO) { // NOLINT - qFatal() << "Failed to open /dev/null on stderr"; - } -} +namespace { void checkCrashRelaunch(char** argv, QCoreApplication* coreApplication) { #if CRASH_REPORTER @@ -96,6 +67,37 @@ void checkCrashRelaunch(char** argv, QCoreApplication* coreApplication) { #endif } +} // namespace + +int DAEMON_PIPE = -1; // NOLINT + +void exitDaemon(int code) { + if (DAEMON_PIPE == -1) return; + + if (write(DAEMON_PIPE, &code, sizeof(int)) == -1) { + qCritical().nospace() << "Failed to write daemon exit command with error code " << errno << ": " + << qt_error_string(); + } + + close(DAEMON_PIPE); + + close(STDIN_FILENO); + close(STDOUT_FILENO); + close(STDERR_FILENO); + + if (open("/dev/null", O_RDONLY) != STDIN_FILENO) { // NOLINT + qFatal() << "Failed to open /dev/null on stdin"; + } + + if (open("/dev/null", O_WRONLY) != STDOUT_FILENO) { // NOLINT + qFatal() << "Failed to open /dev/null on stdout"; + } + + if (open("/dev/null", O_WRONLY) != STDERR_FILENO) { // NOLINT + qFatal() << "Failed to open /dev/null on stderr"; + } +} + int main(int argc, char** argv) { QCoreApplication::setApplicationName("quickshell"); diff --git a/src/services/greetd/connection.cpp b/src/services/greetd/connection.cpp index 822df5fb..ecfd9a59 100644 --- a/src/services/greetd/connection.cpp +++ b/src/services/greetd/connection.cpp @@ -15,7 +15,9 @@ #include "../../core/generation.hpp" +namespace { Q_LOGGING_CATEGORY(logGreetd, "quickshell.service.greetd"); +} QString GreetdState::toString(GreetdState::Enum value) { switch (value) { diff --git a/src/services/mpris/player.cpp b/src/services/mpris/player.cpp index c4fe2ac9..4aac2826 100644 --- a/src/services/mpris/player.cpp +++ b/src/services/mpris/player.cpp @@ -21,7 +21,9 @@ using namespace qs::dbus; namespace qs::service::mpris { +namespace { Q_LOGGING_CATEGORY(logMprisPlayer, "quickshell.service.mp.player", QtWarningMsg); +} QString MprisPlaybackState::toString(MprisPlaybackState::Enum status) { switch (status) { diff --git a/src/services/mpris/watcher.cpp b/src/services/mpris/watcher.cpp index 8a788933..94619074 100644 --- a/src/services/mpris/watcher.cpp +++ b/src/services/mpris/watcher.cpp @@ -14,7 +14,9 @@ namespace qs::service::mpris { +namespace { Q_LOGGING_CATEGORY(logMprisWatcher, "quickshell.service.mpris.watcher", QtWarningMsg); +} MprisWatcher::MprisWatcher() { qCDebug(logMprisWatcher) << "Starting MprisWatcher"; diff --git a/src/services/notifications/dbusimage.cpp b/src/services/notifications/dbusimage.cpp index 46a72a78..9c10e222 100644 --- a/src/services/notifications/dbusimage.cpp +++ b/src/services/notifications/dbusimage.cpp @@ -9,6 +9,7 @@ namespace qs::service::notifications { +// NOLINTNEXTLINE(misc-use-internal-linkage) Q_DECLARE_LOGGING_CATEGORY(logNotifications); // server.cpp QImage DBusNotificationImage::createImage() const { diff --git a/src/services/notifications/notification.cpp b/src/services/notifications/notification.cpp index b1bbdf3a..51a64154 100644 --- a/src/services/notifications/notification.cpp +++ b/src/services/notifications/notification.cpp @@ -18,6 +18,7 @@ namespace qs::service::notifications { +// NOLINTNEXTLINE(misc-use-internal-linkage) Q_DECLARE_LOGGING_CATEGORY(logNotifications); // server.cpp QString NotificationUrgency::toString(NotificationUrgency::Enum value) { diff --git a/src/services/notifications/server.cpp b/src/services/notifications/server.cpp index 9a866f7c..0c41fb84 100644 --- a/src/services/notifications/server.cpp +++ b/src/services/notifications/server.cpp @@ -19,6 +19,7 @@ namespace qs::service::notifications { +// NOLINTNEXTLINE(misc-use-internal-linkage) Q_LOGGING_CATEGORY(logNotifications, "quickshell.service.notifications"); NotificationServer::NotificationServer() { diff --git a/src/services/pipewire/core.cpp b/src/services/pipewire/core.cpp index c4b31ab5..c325bb33 100644 --- a/src/services/pipewire/core.cpp +++ b/src/services/pipewire/core.cpp @@ -15,7 +15,9 @@ namespace qs::service::pipewire { +namespace { Q_LOGGING_CATEGORY(logLoop, "quickshell.service.pipewire.loop", QtWarningMsg); +} PwCore::PwCore(QObject* parent): QObject(parent), notifier(QSocketNotifier::Read) { qCInfo(logLoop) << "Creating pipewire event loop."; diff --git a/src/services/pipewire/defaults.cpp b/src/services/pipewire/defaults.cpp index cd018f9f..0333c87f 100644 --- a/src/services/pipewire/defaults.cpp +++ b/src/services/pipewire/defaults.cpp @@ -18,7 +18,9 @@ namespace qs::service::pipewire { +namespace { Q_LOGGING_CATEGORY(logDefaults, "quickshell.service.pipewire.defaults", QtWarningMsg); +} PwDefaultTracker::PwDefaultTracker(PwRegistry* registry): registry(registry) { QObject::connect(registry, &PwRegistry::metadataAdded, this, &PwDefaultTracker::onMetadataAdded); diff --git a/src/services/pipewire/device.cpp b/src/services/pipewire/device.cpp index 06ed102a..0a1e1b6a 100644 --- a/src/services/pipewire/device.cpp +++ b/src/services/pipewire/device.cpp @@ -23,7 +23,9 @@ namespace qs::service::pipewire { +namespace { Q_LOGGING_CATEGORY(logDevice, "quickshell.service.pipewire.device", QtWarningMsg); +} // https://github.com/PipeWire/wireplumber/blob/895c1c7286e8809fad869059179e53ab39c807e9/modules/module-mixer-api.c#L397 // https://github.com/PipeWire/pipewire/blob/48c2e9516585ccc791335bc7baf4af6952ec54a0/src/modules/module-protocol-pulse/pulse-server.c#L2743-L2743 diff --git a/src/services/pipewire/link.cpp b/src/services/pipewire/link.cpp index 8370446b..c6421af6 100644 --- a/src/services/pipewire/link.cpp +++ b/src/services/pipewire/link.cpp @@ -14,7 +14,9 @@ namespace qs::service::pipewire { +namespace { Q_LOGGING_CATEGORY(logLink, "quickshell.service.pipewire.link", QtWarningMsg); +} QString PwLinkState::toString(Enum value) { return QString(pw_link_state_as_string(static_cast(value))); diff --git a/src/services/pipewire/metadata.cpp b/src/services/pipewire/metadata.cpp index 930725c3..ea79611a 100644 --- a/src/services/pipewire/metadata.cpp +++ b/src/services/pipewire/metadata.cpp @@ -15,7 +15,9 @@ namespace qs::service::pipewire { +namespace { Q_LOGGING_CATEGORY(logMeta, "quickshell.service.pipewire.metadata", QtWarningMsg); +} void PwMetadata::bindHooks() { pw_metadata_add_listener(this->proxy(), &this->listener.hook, &PwMetadata::EVENTS, this); diff --git a/src/services/pipewire/node.cpp b/src/services/pipewire/node.cpp index b89bbff0..ffb8c164 100644 --- a/src/services/pipewire/node.cpp +++ b/src/services/pipewire/node.cpp @@ -28,7 +28,9 @@ namespace qs::service::pipewire { +namespace { Q_LOGGING_CATEGORY(logNode, "quickshell.service.pipewire.node", QtWarningMsg); +} QString PwAudioChannel::toString(Enum value) { switch (value) { diff --git a/src/services/status_notifier/item.cpp b/src/services/status_notifier/item.cpp index 347edbbd..d145c451 100644 --- a/src/services/status_notifier/item.cpp +++ b/src/services/status_notifier/item.cpp @@ -35,7 +35,6 @@ using namespace qs::dbus::dbusmenu; using namespace qs::menu::platform; Q_LOGGING_CATEGORY(logStatusNotifierItem, "quickshell.service.sni.item", QtWarningMsg); -Q_LOGGING_CATEGORY(logSniMenu, "quickshell.service.sni.item.menu", QtWarningMsg); namespace qs::service::sni { diff --git a/src/services/upower/core.cpp b/src/services/upower/core.cpp index 750da5c9..9fe0e60c 100644 --- a/src/services/upower/core.cpp +++ b/src/services/upower/core.cpp @@ -20,7 +20,9 @@ namespace qs::service::upower { +namespace { Q_LOGGING_CATEGORY(logUPower, "quickshell.service.upower", QtWarningMsg); +} UPower::UPower() { qCDebug(logUPower) << "Starting UPower Service"; diff --git a/src/services/upower/device.cpp b/src/services/upower/device.cpp index dd9fbaa2..b7c61e12 100644 --- a/src/services/upower/device.cpp +++ b/src/services/upower/device.cpp @@ -15,7 +15,9 @@ using namespace qs::dbus; namespace qs::service::upower { +namespace { Q_LOGGING_CATEGORY(logUPowerDevice, "quickshell.service.upower.device", QtWarningMsg); +} QString UPowerDeviceState::toString(UPowerDeviceState::Enum status) { switch (status) { diff --git a/src/wayland/hyprland/ipc/connection.cpp b/src/wayland/hyprland/ipc/connection.cpp index c33ebd60..1e1af05a 100644 --- a/src/wayland/hyprland/ipc/connection.cpp +++ b/src/wayland/hyprland/ipc/connection.cpp @@ -26,8 +26,10 @@ namespace qs::hyprland::ipc { +namespace { Q_LOGGING_CATEGORY(logHyprlandIpc, "quickshell.hyprland.ipc", QtWarningMsg); Q_LOGGING_CATEGORY(logHyprlandIpcEvents, "quickshell.hyprland.ipc.events", QtWarningMsg); +} // namespace HyprlandIpc::HyprlandIpc() { auto his = qEnvironmentVariable("HYPRLAND_INSTANCE_SIGNATURE"); @@ -241,9 +243,8 @@ void HyprlandIpc::onEvent(HyprlandIpcEvent* event) { const auto& mList = this->mMonitors.valueList(); auto name = QString::fromUtf8(event->data); - auto monitorIter = std::find_if(mList.begin(), mList.end(), [name](const HyprlandMonitor* m) { - return m->name() == name; - }); + auto monitorIter = + std::ranges::find_if(mList, [name](const HyprlandMonitor* m) { return m->name() == name; }); if (monitorIter == mList.end()) { qCWarning(logHyprlandIpc) << "Got removal for monitor" << name @@ -292,9 +293,8 @@ void HyprlandIpc::onEvent(HyprlandIpcEvent* event) { const auto& mList = this->mWorkspaces.valueList(); - auto workspaceIter = std::find_if(mList.begin(), mList.end(), [id](const HyprlandWorkspace* m) { - return m->id() == id; - }); + auto workspaceIter = + std::ranges::find_if(mList, [id](const HyprlandWorkspace* m) { return m->id() == id; }); if (workspaceIter == mList.end()) { qCWarning(logHyprlandIpc) << "Got removal for workspace id" << id << "name" << name @@ -359,9 +359,8 @@ HyprlandWorkspace* HyprlandIpc::findWorkspaceByName(const QString& name, bool createIfMissing, qint32 id) { const auto& mList = this->mWorkspaces.valueList(); - auto workspaceIter = std::find_if(mList.begin(), mList.end(), [name](const HyprlandWorkspace* m) { - return m->name() == name; - }); + auto workspaceIter = + std::ranges::find_if(mList, [name](const HyprlandWorkspace* m) { return m->name() == name; }); if (workspaceIter != mList.end()) { return *workspaceIter; @@ -395,10 +394,9 @@ void HyprlandIpc::refreshWorkspaces(bool canCreate) { auto object = entry.toObject().toVariantMap(); auto name = object.value("name").toString(); - auto workspaceIter = - std::find_if(mList.begin(), mList.end(), [name](const HyprlandWorkspace* m) { - return m->name() == name; - }); + auto workspaceIter = std::ranges::find_if(mList, [name](const HyprlandWorkspace* m) { + return m->name() == name; + }); auto* workspace = workspaceIter == mList.end() ? nullptr : *workspaceIter; auto existed = workspace != nullptr; @@ -436,9 +434,8 @@ HyprlandMonitor* HyprlandIpc::findMonitorByName(const QString& name, bool createIfMissing, qint32 id) { const auto& mList = this->mMonitors.valueList(); - auto monitorIter = std::find_if(mList.begin(), mList.end(), [name](const HyprlandMonitor* m) { - return m->name() == name; - }); + auto monitorIter = + std::ranges::find_if(mList, [name](const HyprlandMonitor* m) { return m->name() == name; }); if (monitorIter != mList.end()) { return *monitorIter; @@ -506,7 +503,7 @@ void HyprlandIpc::refreshMonitors(bool canCreate) { auto object = entry.toObject().toVariantMap(); auto name = object.value("name").toString(); - auto monitorIter = std::find_if(mList.begin(), mList.end(), [name](const HyprlandMonitor* m) { + auto monitorIter = std::ranges::find_if(mList, [name](const HyprlandMonitor* m) { return m->name() == name; }); diff --git a/src/wayland/init.cpp b/src/wayland/init.cpp index 7c024c42..3f2a18b4 100644 --- a/src/wayland/init.cpp +++ b/src/wayland/init.cpp @@ -10,8 +10,8 @@ #include "wlr_layershell.hpp" #endif -void installPlatformMenuHook(); -void installPopupPositioner(); +void installPlatformMenuHook(); // NOLINT(misc-use-internal-linkage) +void installPopupPositioner(); // NOLINT(misc-use-internal-linkage) namespace { diff --git a/src/wayland/platformmenu.cpp b/src/wayland/platformmenu.cpp index e64e8880..117d028d 100644 --- a/src/wayland/platformmenu.cpp +++ b/src/wayland/platformmenu.cpp @@ -12,6 +12,8 @@ using namespace qs::menu::platform; +namespace { + // fixes positioning of submenus when hitting screen edges void platformMenuHook(PlatformMenuQMenu* menu) { auto* window = menu->windowHandle(); @@ -62,4 +64,6 @@ void platformMenuHook(PlatformMenuQMenu* menu) { window->setProperty("_q_waylandPopupConstraintAdjustment", constraintAdjustment); } +} // namespace + void installPlatformMenuHook() { PlatformMenuEntry::registerCreationHook(&platformMenuHook); } diff --git a/src/wayland/wlr_layershell/surface.cpp b/src/wayland/wlr_layershell/surface.cpp index 25b58ff8..3c0338c7 100644 --- a/src/wayland/wlr_layershell/surface.cpp +++ b/src/wayland/wlr_layershell/surface.cpp @@ -22,12 +22,51 @@ #include #endif -// clang-format off -[[nodiscard]] QtWayland::zwlr_layer_shell_v1::layer toWaylandLayer(const WlrLayer::Enum& layer) noexcept; -[[nodiscard]] QtWayland::zwlr_layer_surface_v1::anchor toWaylandAnchors(const Anchors& anchors) noexcept; -[[nodiscard]] QtWayland::zwlr_layer_surface_v1::keyboard_interactivity toWaylandKeyboardFocus(const WlrKeyboardFocus::Enum& focus) noexcept; -[[nodiscard]] QSize constrainedSize(const Anchors& anchors, const QSize& size) noexcept; -// clang-format on +namespace { + +[[nodiscard]] QtWayland::zwlr_layer_shell_v1::layer toWaylandLayer(const WlrLayer::Enum& layer +) noexcept { + switch (layer) { + case WlrLayer::Background: return QtWayland::zwlr_layer_shell_v1::layer_background; + case WlrLayer::Bottom: return QtWayland::zwlr_layer_shell_v1::layer_bottom; + case WlrLayer::Top: return QtWayland::zwlr_layer_shell_v1::layer_top; + case WlrLayer::Overlay: return QtWayland::zwlr_layer_shell_v1::layer_overlay; + } + + return QtWayland::zwlr_layer_shell_v1::layer_top; +} + +[[nodiscard]] QtWayland::zwlr_layer_surface_v1::anchor toWaylandAnchors(const Anchors& anchors +) noexcept { + quint32 wl = 0; + if (anchors.mLeft) wl |= QtWayland::zwlr_layer_surface_v1::anchor_left; + if (anchors.mRight) wl |= QtWayland::zwlr_layer_surface_v1::anchor_right; + if (anchors.mTop) wl |= QtWayland::zwlr_layer_surface_v1::anchor_top; + if (anchors.mBottom) wl |= QtWayland::zwlr_layer_surface_v1::anchor_bottom; + return static_cast(wl); +} + +[[nodiscard]] QtWayland::zwlr_layer_surface_v1::keyboard_interactivity +toWaylandKeyboardFocus(const WlrKeyboardFocus::Enum& focus) noexcept { + switch (focus) { + case WlrKeyboardFocus::None: return QtWayland::zwlr_layer_surface_v1::keyboard_interactivity_none; + case WlrKeyboardFocus::Exclusive: + return QtWayland::zwlr_layer_surface_v1::keyboard_interactivity_exclusive; + case WlrKeyboardFocus::OnDemand: + return QtWayland::zwlr_layer_surface_v1::keyboard_interactivity_on_demand; + } + + return QtWayland::zwlr_layer_surface_v1::keyboard_interactivity_none; +} + +[[nodiscard]] QSize constrainedSize(const Anchors& anchors, const QSize& size) noexcept { + return QSize( + anchors.horizontalConstraint() ? 0 : size.width(), + anchors.verticalConstraint() ? 0 : size.height() + ); +} + +} // namespace QSWaylandLayerSurface::QSWaylandLayerSurface( QSWaylandLayerShellIntegration* shell, @@ -148,46 +187,6 @@ void QSWaylandLayerSurface::updateKeyboardFocus() { this->window()->waylandSurface()->commit(); } -QtWayland::zwlr_layer_shell_v1::layer toWaylandLayer(const WlrLayer::Enum& layer) noexcept { - switch (layer) { - case WlrLayer::Background: return QtWayland::zwlr_layer_shell_v1::layer_background; - case WlrLayer::Bottom: return QtWayland::zwlr_layer_shell_v1::layer_bottom; - case WlrLayer::Top: return QtWayland::zwlr_layer_shell_v1::layer_top; - case WlrLayer::Overlay: return QtWayland::zwlr_layer_shell_v1::layer_overlay; - } - - return QtWayland::zwlr_layer_shell_v1::layer_top; -} - -QtWayland::zwlr_layer_surface_v1::anchor toWaylandAnchors(const Anchors& anchors) noexcept { - quint32 wl = 0; - if (anchors.mLeft) wl |= QtWayland::zwlr_layer_surface_v1::anchor_left; - if (anchors.mRight) wl |= QtWayland::zwlr_layer_surface_v1::anchor_right; - if (anchors.mTop) wl |= QtWayland::zwlr_layer_surface_v1::anchor_top; - if (anchors.mBottom) wl |= QtWayland::zwlr_layer_surface_v1::anchor_bottom; - return static_cast(wl); -} - -QtWayland::zwlr_layer_surface_v1::keyboard_interactivity -toWaylandKeyboardFocus(const WlrKeyboardFocus::Enum& focus) noexcept { - switch (focus) { - case WlrKeyboardFocus::None: return QtWayland::zwlr_layer_surface_v1::keyboard_interactivity_none; - case WlrKeyboardFocus::Exclusive: - return QtWayland::zwlr_layer_surface_v1::keyboard_interactivity_exclusive; - case WlrKeyboardFocus::OnDemand: - return QtWayland::zwlr_layer_surface_v1::keyboard_interactivity_on_demand; - } - - return QtWayland::zwlr_layer_surface_v1::keyboard_interactivity_none; -} - -QSize constrainedSize(const Anchors& anchors, const QSize& size) noexcept { - return QSize( - anchors.horizontalConstraint() ? 0 : size.width(), - anchors.verticalConstraint() ? 0 : size.height() - ); -} - void QSWaylandLayerSurface::attachPopup(QtWaylandClient::QWaylandShellSurface* popup) { std::any role = popup->surfaceRole(); diff --git a/src/x11/i3/ipc/connection.cpp b/src/x11/i3/ipc/connection.cpp index 8ce78228..b6aa1513 100644 --- a/src/x11/i3/ipc/connection.cpp +++ b/src/x11/i3/ipc/connection.cpp @@ -28,10 +28,12 @@ #include "monitor.hpp" #include "workspace.hpp" +namespace qs::i3::ipc { + +namespace { Q_LOGGING_CATEGORY(logI3Ipc, "quickshell.I3.ipc", QtWarningMsg); Q_LOGGING_CATEGORY(logI3IpcEvents, "quickshell.I3.ipc.events", QtWarningMsg); - -namespace qs::i3::ipc { +} // namespace void I3Ipc::makeRequest(const QByteArray& request) { if (!this->valid) { @@ -262,9 +264,8 @@ void I3Ipc::handleGetWorkspacesEvent(I3IpcEvent* event) { auto object = entry.toObject().toVariantMap(); auto name = object["name"].toString(); - auto workspaceIter = std::find_if(mList.begin(), mList.end(), [name](const I3Workspace* m) { - return m->name() == name; - }); + auto workspaceIter = + std::ranges::find_if(mList, [name](const I3Workspace* m) { return m->name() == name; }); auto* workspace = workspaceIter == mList.end() ? nullptr : *workspaceIter; auto existed = workspace != nullptr; @@ -319,9 +320,8 @@ void I3Ipc::handleGetOutputsEvent(I3IpcEvent* event) { auto object = elem.toObject().toVariantMap(); auto name = object["name"].toString(); - auto monitorIter = std::find_if(mList.begin(), mList.end(), [name](const I3Monitor* m) { - return m->name() == name; - }); + auto monitorIter = + std::ranges::find_if(mList, [name](const I3Monitor* m) { return m->name() == name; }); auto* monitor = monitorIter == mList.end() ? nullptr : *monitorIter; auto existed = monitor != nullptr; @@ -477,25 +477,23 @@ I3Monitor* I3Ipc::monitorFor(QuickshellScreenInfo* screen) { I3Workspace* I3Ipc::findWorkspaceByID(qint32 id) { auto list = this->mWorkspaces.valueList(); auto workspaceIter = - std::find_if(list.begin(), list.end(), [id](const I3Workspace* m) { return m->id() == id; }); + std::ranges::find_if(list, [id](const I3Workspace* m) { return m->id() == id; }); return workspaceIter == list.end() ? nullptr : *workspaceIter; } I3Workspace* I3Ipc::findWorkspaceByName(const QString& name) { auto list = this->mWorkspaces.valueList(); - auto workspaceIter = std::find_if(list.begin(), list.end(), [name](const I3Workspace* m) { - return m->name() == name; - }); + auto workspaceIter = + std::ranges::find_if(list, [name](const I3Workspace* m) { return m->name() == name; }); return workspaceIter == list.end() ? nullptr : *workspaceIter; } I3Monitor* I3Ipc::findMonitorByName(const QString& name) { auto list = this->mMonitors.valueList(); - auto monitorIter = std::find_if(list.begin(), list.end(), [name](const I3Monitor* m) { - return m->name() == name; - }); + auto monitorIter = + std::ranges::find_if(list, [name](const I3Monitor* m) { return m->name() == name; }); return monitorIter == list.end() ? nullptr : *monitorIter; } From 918dd2392d21ee7bbbaecd5adf5b40f8dd8f2fb7 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Sat, 11 Jan 2025 23:59:19 -0800 Subject: [PATCH 10/31] build/wayland: do not link to a target in wl_proto --- src/wayland/CMakeLists.txt | 29 +++++++++---------- .../hyprland/focus_grab/CMakeLists.txt | 6 ++-- .../hyprland/global_shortcuts/CMakeLists.txt | 6 ++-- src/wayland/hyprland/surface/CMakeLists.txt | 6 ++-- src/wayland/session_lock/CMakeLists.txt | 3 +- .../toplevel_management/CMakeLists.txt | 6 ++-- src/wayland/wlr_layershell/CMakeLists.txt | 5 ++-- 7 files changed, 27 insertions(+), 34 deletions(-) diff --git a/src/wayland/CMakeLists.txt b/src/wayland/CMakeLists.txt index db03cf16..54bb59bc 100644 --- a/src/wayland/CMakeLists.txt +++ b/src/wayland/CMakeLists.txt @@ -28,7 +28,7 @@ qs_add_pchset(wayland-protocol ) -function (wl_proto target name path) +function (wl_proto target name dir) set(PROTO_BUILD_PATH ${CMAKE_CURRENT_BINARY_DIR}/wl-proto/${name}) make_directory(${PROTO_BUILD_PATH}) @@ -36,39 +36,38 @@ function (wl_proto target name path) set(WS_CLIENT_CODE "${PROTO_BUILD_PATH}/wayland-${name}.c") set(QWS_CLIENT_HEADER "${PROTO_BUILD_PATH}/qwayland-${name}.h") set(QWS_CLIENT_CODE "${PROTO_BUILD_PATH}/qwayland-${name}.cpp") + set(PATH "${dir}/${name}.xml") add_custom_command( OUTPUT "${WS_CLIENT_HEADER}" - COMMAND Wayland::Scanner client-header "${path}" "${WS_CLIENT_HEADER}" - DEPENDS Wayland::Scanner "${path}" + COMMAND Wayland::Scanner client-header "${PATH}" "${WS_CLIENT_HEADER}" + DEPENDS Wayland::Scanner "${PATH}" ) add_custom_command( OUTPUT "${WS_CLIENT_CODE}" - COMMAND Wayland::Scanner private-code "${path}" "${WS_CLIENT_CODE}" - DEPENDS Wayland::Scanner "${path}" + COMMAND Wayland::Scanner private-code "${PATH}" "${WS_CLIENT_CODE}" + DEPENDS Wayland::Scanner "${PATH}" ) add_custom_command( OUTPUT "${QWS_CLIENT_HEADER}" - COMMAND Qt6::qtwaylandscanner client-header "${path}" > "${QWS_CLIENT_HEADER}" - DEPENDS Qt6::qtwaylandscanner "${path}" + COMMAND Qt6::qtwaylandscanner client-header "${PATH}" > "${QWS_CLIENT_HEADER}" + DEPENDS Qt6::qtwaylandscanner "${PATH}" ) add_custom_command( OUTPUT "${QWS_CLIENT_CODE}" - COMMAND Qt6::qtwaylandscanner client-code "${path}" > "${QWS_CLIENT_CODE}" - DEPENDS Qt6::qtwaylandscanner "${path}" + COMMAND Qt6::qtwaylandscanner client-code "${PATH}" > "${QWS_CLIENT_CODE}" + DEPENDS Qt6::qtwaylandscanner "${PATH}" ) add_library(wl-proto-${name}-wl STATIC ${WS_CLIENT_HEADER} ${WS_CLIENT_CODE}) - add_library(wl-proto-${name} STATIC ${QWS_CLIENT_HEADER} ${QWS_CLIENT_CODE}) + add_library(${target} STATIC ${QWS_CLIENT_HEADER} ${QWS_CLIENT_CODE}) - target_include_directories(wl-proto-${name} INTERFACE ${PROTO_BUILD_PATH}) - target_link_libraries(wl-proto-${name} wl-proto-${name}-wl Qt6::WaylandClient Qt6::WaylandClientPrivate) - qs_pch(wl-proto-${name} SET wayland-protocol) - - target_link_libraries(${target} PRIVATE wl-proto-${name}) + target_include_directories(${target} INTERFACE ${PROTO_BUILD_PATH}) + target_link_libraries(${target} wl-proto-${name}-wl Qt6::WaylandClient Qt6::WaylandClientPrivate) + qs_pch(${target} SET wayland-protocol) endfunction() # ----- diff --git a/src/wayland/hyprland/focus_grab/CMakeLists.txt b/src/wayland/hyprland/focus_grab/CMakeLists.txt index 04b6e0a9..95825628 100644 --- a/src/wayland/hyprland/focus_grab/CMakeLists.txt +++ b/src/wayland/hyprland/focus_grab/CMakeLists.txt @@ -14,13 +14,11 @@ qs_add_module_deps_light(quickshell-hyprland-focus-grab Quickshell) install_qml_module(quickshell-hyprland-focus-grab) -wl_proto(quickshell-hyprland-focus-grab - hyprland-focus-grab-v1 - "${CMAKE_CURRENT_SOURCE_DIR}/hyprland-focus-grab-v1.xml" -) +wl_proto(wlp-hyprland-focus-grab hyprland-focus-grab-v1 "${CMAKE_CURRENT_SOURCE_DIR}") target_link_libraries(quickshell-hyprland-focus-grab PRIVATE Qt::Quick Qt::WaylandClient Qt::WaylandClientPrivate wayland-client + wlp-hyprland-focus-grab ) qs_module_pch(quickshell-hyprland-focus-grab SET large) diff --git a/src/wayland/hyprland/global_shortcuts/CMakeLists.txt b/src/wayland/hyprland/global_shortcuts/CMakeLists.txt index 986f2d8e..ece0037e 100644 --- a/src/wayland/hyprland/global_shortcuts/CMakeLists.txt +++ b/src/wayland/hyprland/global_shortcuts/CMakeLists.txt @@ -12,14 +12,12 @@ qt_add_qml_module(quickshell-hyprland-global-shortcuts install_qml_module(quickshell-hyprland-global-shortcuts) -wl_proto(quickshell-hyprland-global-shortcuts - hyprland-global-shortcuts-v1 - "${CMAKE_CURRENT_SOURCE_DIR}/hyprland-global-shortcuts-v1.xml" -) +wl_proto(wlp-hyprland-shortcuts hyprland-global-shortcuts-v1 "${CMAKE_CURRENT_SOURCE_DIR}") target_link_libraries(quickshell-hyprland-global-shortcuts PRIVATE Qt::Qml Qt::WaylandClient Qt::WaylandClientPrivate wayland-client Qt::Quick # pch + wlp-hyprland-shortcuts ) qs_module_pch(quickshell-hyprland-global-shortcuts) diff --git a/src/wayland/hyprland/surface/CMakeLists.txt b/src/wayland/hyprland/surface/CMakeLists.txt index 04fa5c58..7f889c96 100644 --- a/src/wayland/hyprland/surface/CMakeLists.txt +++ b/src/wayland/hyprland/surface/CMakeLists.txt @@ -12,13 +12,11 @@ qt_add_qml_module(quickshell-hyprland-surface-extensions install_qml_module(quickshell-hyprland-surface-extensions) -wl_proto(quickshell-hyprland-surface-extensions - hyprland-surface-v1 - "${CMAKE_CURRENT_SOURCE_DIR}/hyprland-surface-v1.xml" -) +wl_proto(wlp-hyprland-surface hyprland-surface-v1 "${CMAKE_CURRENT_SOURCE_DIR}") target_link_libraries(quickshell-hyprland-surface-extensions PRIVATE Qt::Quick Qt::WaylandClient Qt::WaylandClientPrivate wayland-client + wlp-hyprland-surface ) qs_module_pch(quickshell-hyprland-surface-extensions) diff --git a/src/wayland/session_lock/CMakeLists.txt b/src/wayland/session_lock/CMakeLists.txt index 245d1f25..d157fc13 100644 --- a/src/wayland/session_lock/CMakeLists.txt +++ b/src/wayland/session_lock/CMakeLists.txt @@ -6,10 +6,11 @@ qt_add_library(quickshell-wayland-sessionlock STATIC session_lock.cpp ) -wl_proto(quickshell-wayland-sessionlock ext-session-lock-v1 "${WAYLAND_PROTOCOLS}/staging/ext-session-lock/ext-session-lock-v1.xml") +wl_proto(wlp-session-lock ext-session-lock-v1 "${WAYLAND_PROTOCOLS}/staging/ext-session-lock") target_link_libraries(quickshell-wayland-sessionlock PRIVATE Qt::Quick Qt::WaylandClient Qt::WaylandClientPrivate wayland-client + wlp-session-lock ) qs_pch(quickshell-wayland-sessionlock SET large) diff --git a/src/wayland/toplevel_management/CMakeLists.txt b/src/wayland/toplevel_management/CMakeLists.txt index 0db82aae..97f8c7a4 100644 --- a/src/wayland/toplevel_management/CMakeLists.txt +++ b/src/wayland/toplevel_management/CMakeLists.txt @@ -16,13 +16,11 @@ qs_add_module_deps_light(quickshell-wayland-toplevel-management install_qml_module(quickshell-wayland-toplevel-management) -wl_proto(quickshell-wayland-toplevel-management - wlr-foreign-toplevel-management-unstable-v1 - "${CMAKE_CURRENT_SOURCE_DIR}/wlr-foreign-toplevel-management-unstable-v1.xml" -) +wl_proto(wlp-foreign-toplevel wlr-foreign-toplevel-management-unstable-v1 "${CMAKE_CURRENT_SOURCE_DIR}") target_link_libraries(quickshell-wayland-toplevel-management PRIVATE Qt::Quick Qt::WaylandClient Qt::WaylandClientPrivate wayland-client + wlp-foreign-toplevel ) qs_module_pch(quickshell-wayland-toplevel-management SET large) diff --git a/src/wayland/wlr_layershell/CMakeLists.txt b/src/wayland/wlr_layershell/CMakeLists.txt index f22ee0ba..69b24e92 100644 --- a/src/wayland/wlr_layershell/CMakeLists.txt +++ b/src/wayland/wlr_layershell/CMakeLists.txt @@ -14,13 +14,14 @@ qs_add_module_deps_light(quickshell-wayland-layershell Quickshell Quickshell.Way install_qml_module(quickshell-wayland-layershell) -wl_proto(quickshell-wayland-layershell wlr-layer-shell-unstable-v1 "${CMAKE_CURRENT_SOURCE_DIR}/wlr-layer-shell-unstable-v1.xml") +wl_proto(wlp-layer-shell wlr-layer-shell-unstable-v1 "${CMAKE_CURRENT_SOURCE_DIR}") # link dependency of wlr-layer-shell's codegen -wl_proto(quickshell-wayland-layershell xdg-shell "${WAYLAND_PROTOCOLS}/stable/xdg-shell/xdg-shell.xml") +wl_proto(wlp-xdg-shell xdg-shell "${WAYLAND_PROTOCOLS}/stable/xdg-shell") target_link_libraries(quickshell-wayland-layershell PRIVATE Qt::Quick Qt::WaylandClient Qt::WaylandClientPrivate wayland-client + wlp-layer-shell wlp-xdg-shell ) qs_module_pch(quickshell-wayland-layershell SET large) From cd429142a4806ab6ebce382e7a7f710d4f378b1a Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Tue, 14 Jan 2025 04:43:05 -0800 Subject: [PATCH 11/31] wayland/screencopy: add screencopy --- .github/workflows/build.yml | 1 + BUILD.md | 18 + CMakeLists.txt | 6 +- default.nix | 5 +- src/core/stacklist.hpp | 159 +++++ src/wayland/CMakeLists.txt | 7 + src/wayland/buffer/CMakeLists.txt | 18 + src/wayland/buffer/dmabuf.cpp | 659 ++++++++++++++++++ src/wayland/buffer/dmabuf.hpp | 195 ++++++ src/wayland/buffer/manager.cpp | 114 +++ src/wayland/buffer/manager.hpp | 134 ++++ src/wayland/buffer/manager_p.hpp | 20 + src/wayland/buffer/qsg.hpp | 45 ++ src/wayland/buffer/shm.cpp | 91 +++ src/wayland/buffer/shm.hpp | 63 ++ src/wayland/module.md | 1 + src/wayland/screencopy/CMakeLists.txt | 42 ++ src/wayland/screencopy/build.hpp.in | 6 + .../hyprland_screencopy/CMakeLists.txt | 16 + .../hyprland-toplevel-export-v1.xml | 228 ++++++ .../hyprland_screencopy.cpp | 122 ++++ .../hyprland_screencopy.hpp | 26 + .../hyprland_screencopy_p.hpp | 50 ++ .../image_copy_capture/CMakeLists.txt | 19 + .../image_copy_capture/image_copy_capture.cpp | 225 ++++++ .../image_copy_capture/image_copy_capture.hpp | 36 + .../image_copy_capture_p.hpp | 53 ++ src/wayland/screencopy/manager.cpp | 56 ++ src/wayland/screencopy/manager.hpp | 33 + src/wayland/screencopy/view.cpp | 149 ++++ src/wayland/screencopy/view.hpp | 96 +++ .../screencopy/wlr_screencopy/CMakeLists.txt | 14 + .../wlr-screencopy-unstable-v1.xml | 232 ++++++ .../wlr_screencopy/wlr_screencopy.cpp | 133 ++++ .../wlr_screencopy/wlr_screencopy.hpp | 25 + .../wlr_screencopy/wlr_screencopy_p.hpp | 51 ++ src/wayland/toplevel_management/qml.hpp | 4 +- 37 files changed, 3149 insertions(+), 3 deletions(-) create mode 100644 src/core/stacklist.hpp create mode 100644 src/wayland/buffer/CMakeLists.txt create mode 100644 src/wayland/buffer/dmabuf.cpp create mode 100644 src/wayland/buffer/dmabuf.hpp create mode 100644 src/wayland/buffer/manager.cpp create mode 100644 src/wayland/buffer/manager.hpp create mode 100644 src/wayland/buffer/manager_p.hpp create mode 100644 src/wayland/buffer/qsg.hpp create mode 100644 src/wayland/buffer/shm.cpp create mode 100644 src/wayland/buffer/shm.hpp create mode 100644 src/wayland/screencopy/CMakeLists.txt create mode 100644 src/wayland/screencopy/build.hpp.in create mode 100644 src/wayland/screencopy/hyprland_screencopy/CMakeLists.txt create mode 100644 src/wayland/screencopy/hyprland_screencopy/hyprland-toplevel-export-v1.xml create mode 100644 src/wayland/screencopy/hyprland_screencopy/hyprland_screencopy.cpp create mode 100644 src/wayland/screencopy/hyprland_screencopy/hyprland_screencopy.hpp create mode 100644 src/wayland/screencopy/hyprland_screencopy/hyprland_screencopy_p.hpp create mode 100644 src/wayland/screencopy/image_copy_capture/CMakeLists.txt create mode 100644 src/wayland/screencopy/image_copy_capture/image_copy_capture.cpp create mode 100644 src/wayland/screencopy/image_copy_capture/image_copy_capture.hpp create mode 100644 src/wayland/screencopy/image_copy_capture/image_copy_capture_p.hpp create mode 100644 src/wayland/screencopy/manager.cpp create mode 100644 src/wayland/screencopy/manager.hpp create mode 100644 src/wayland/screencopy/view.cpp create mode 100644 src/wayland/screencopy/view.hpp create mode 100644 src/wayland/screencopy/wlr_screencopy/CMakeLists.txt create mode 100644 src/wayland/screencopy/wlr_screencopy/wlr-screencopy-unstable-v1.xml create mode 100644 src/wayland/screencopy/wlr_screencopy/wlr_screencopy.cpp create mode 100644 src/wayland/screencopy/wlr_screencopy/wlr_screencopy.hpp create mode 100644 src/wayland/screencopy/wlr_screencopy/wlr_screencopy_p.hpp diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 2ec6d8cf..a67e5f43 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -43,6 +43,7 @@ jobs: qt6-shadertools \ wayland-protocols \ wayland \ + libdrm \ libxcb \ libpipewire \ cli11 \ diff --git a/BUILD.md b/BUILD.md index cf6b3a03..3172dbe3 100644 --- a/BUILD.md +++ b/BUILD.md @@ -130,6 +130,24 @@ which allows quickshell to be used as a session lock under compatible wayland co To disable: `-DWAYLAND_TOPLEVEL_MANAGEMENT=OFF` +#### Screencopy +Enables streaming video from monitors and toplevel windows through various protocols. + +To disable: `-DSCREENCOPY=OFF` + +Dependencies: +- `libdrm` +- `libgbm` + +Specific protocols can also be disabled: +- `DSCREENCOPY_ICC=OFF` - Disable screencopy via [ext-image-copy-capture-v1] +- `DSCREENCOPY_WLR=OFF` - Disable screencopy via [zwlr-screencopy-v1] +- `DSCREENCOPY_HYPRLAND_TOPLEVEL=OFF` - Disable screencopy via [hyprland-toplevel-export-v1] + +[ext-image-copy-capture-v1]:https://wayland.app/protocols/ext-image-copy-capture-v1 +[zwlr-screencopy-v1]: https://wayland.app/protocols/wlr-screencopy-unstable-v1 +[hyprland-toplevel-export-v1]: https://wayland.app/protocols/hyprland-toplevel-export-v1 + ### X11 This feature enables x11 support. Currently this implements panel windows for X11 similarly to the wlroots layershell above. diff --git a/CMakeLists.txt b/CMakeLists.txt index a4919952..846a280c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -56,6 +56,10 @@ boption(HYPRLAND_IPC " Hyprland IPC" ON REQUIRES HYPRLAND) boption(HYPRLAND_GLOBAL_SHORTCUTS " Hyprland Global Shortcuts" ON REQUIRES HYPRLAND) boption(HYPRLAND_FOCUS_GRAB " Hyprland Focus Grabbing" ON REQUIRES HYPRLAND) boption(HYPRLAND_SURFACE_EXTENSIONS " Hyprland Surface Extensions" ON REQUIRES HYPRLAND) +boption(SCREENCOPY " Screencopy" ON REQUIRES WAYLAND) +boption(SCREENCOPY_ICC " Image Copy Capture" ON REQUIRES WAYLAND) +boption(SCREENCOPY_WLR " Wlroots Screencopy" ON REQUIRES WAYLAND) +boption(SCREENCOPY_HYPRLAND_TOPLEVEL " Hyprland Toplevel Export" ON REQUIRES WAYLAND) boption(X11 "X11" ON) boption(I3 "I3/Sway" ON) boption(I3_IPC " I3/Sway IPC" ON REQUIRES I3) @@ -70,7 +74,7 @@ boption(SERVICE_NOTIFICATIONS "Notifications" ON) include(cmake/install-qml-module.cmake) include(cmake/util.cmake) -add_compile_options(-Wall -Wextra) +add_compile_options(-Wall -Wextra -Wno-vla-cxx-extension) # pipewire defines this, breaking PCH add_compile_definitions(_REENTRANT) diff --git a/default.nix b/default.nix index fab038a7..79c9b7a4 100644 --- a/default.nix +++ b/default.nix @@ -14,6 +14,8 @@ jemalloc, wayland, wayland-protocols, + libdrm, + libgbm ? null, xorg, pipewire, pam, @@ -64,7 +66,7 @@ ++ lib.optional withCrashReporter breakpad ++ lib.optional withJemalloc jemalloc ++ lib.optional withQtSvg qt6.qtsvg - ++ lib.optionals withWayland [ qt6.qtwayland wayland ] + ++ lib.optionals withWayland ([ qt6.qtwayland wayland ] ++ (if libgbm != null then [ libdrm libgbm ] else [])) ++ lib.optional withX11 xorg.libxcb ++ lib.optional withPam pam ++ lib.optional withPipewire pipewire; @@ -79,6 +81,7 @@ (lib.cmakeBool "CRASH_REPORTER" withCrashReporter) (lib.cmakeBool "USE_JEMALLOC" withJemalloc) (lib.cmakeBool "WAYLAND" withWayland) + (lib.cmakeBool "SCREENCOPY" (libgbm != null)) (lib.cmakeBool "SERVICE_PIPEWIRE" withPipewire) (lib.cmakeBool "SERVICE_PAM" withPam) (lib.cmakeBool "HYPRLAND" withHyprland) diff --git a/src/core/stacklist.hpp b/src/core/stacklist.hpp new file mode 100644 index 00000000..7e9ee788 --- /dev/null +++ b/src/core/stacklist.hpp @@ -0,0 +1,159 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +#include + +template +class StackList { +public: + T& operator[](size_t i) { + if (i < N) { + return this->array[i]; + } else { + return this->vec[i - N]; + } + } + + const T& operator[](size_t i) const { + return const_cast*>(this)->operator[](i); // NOLINT + } + + void push(const T& value) { + if (this->size < N) { + this->array[this->size] = value; + } else { + this->vec.push_back(value); + } + + ++this->size; + } + + [[nodiscard]] size_t length() const { return this->size; } + [[nodiscard]] bool isEmpty() const { return this->size == 0; } + + [[nodiscard]] bool operator==(const StackList& other) const { + if (other.size != this->size) return false; + + for (size_t i = 0; i < this->size; ++i) { + if (this->operator[](i) != other[i]) return false; + } + + return true; + } + + template + struct BaseIterator { + using iterator_category = std::bidirectional_iterator_tag; + using difference_type = int64_t; + using value_type = IT; + using pointer = IT*; + using reference = IT&; + + BaseIterator() = default; + explicit BaseIterator(ListPtr list, size_t i): list(list), i(i) {} + + reference operator*() const { return this->list->operator[](this->i); } + pointer operator->() const { return &**this; } + + Self& operator++() { + ++this->i; + return *static_cast(this); + } + Self& operator--() { + --this->i; + return *static_cast(this); + } + + Self operator++(int) { + auto v = *this; + this->operator++(); + return v; + } + Self operator--(int) { + auto v = *this; + this->operator--(); + return v; + } + + difference_type operator-(const Self& other) { + return static_cast(this->i) - static_cast(other.i); + } + + Self& operator+(difference_type offset) { + return Self(this->list, static_cast(this->i) + offset); + } + + [[nodiscard]] bool operator==(const Self& other) const { + return this->list == other.list && this->i == other.i; + } + + [[nodiscard]] bool operator!=(const Self& other) const { return !(*this == other); } + + private: + ListPtr list = nullptr; + size_t i = 0; + }; + + struct Iterator: public BaseIterator*, T> { + Iterator() = default; + Iterator(StackList* list, size_t i) + : BaseIterator*, T>(list, i) {} + }; + + struct ConstIterator: public BaseIterator*, const T> { + ConstIterator() = default; + ConstIterator(const StackList* list, size_t i) + : BaseIterator*, const T>(list, i) {} + }; + + [[nodiscard]] Iterator begin() { return Iterator(this, 0); } + [[nodiscard]] Iterator end() { return Iterator(this, this->size); } + + [[nodiscard]] ConstIterator begin() const { return ConstIterator(this, 0); } + [[nodiscard]] ConstIterator end() const { return ConstIterator(this, this->size); } + + [[nodiscard]] bool isContiguous() const { return this->vec.empty(); } + [[nodiscard]] const T* pArray() const { return this->array.data(); } + [[nodiscard]] size_t dataLength() const { return this->size * sizeof(T); } + + const T* populateAlloc(void* alloc) const { + auto arraylen = std::min(this->size, N) * sizeof(T); + memcpy(alloc, this->array.data(), arraylen); + + if (!this->vec.empty()) { + memcpy( + static_cast(alloc) + arraylen, // NOLINT + this->vec.data(), + this->vec.size() * sizeof(T) + ); + } + + return static_cast(alloc); + } + +private: + std::array array {}; + std::vector vec; + size_t size = 0; +}; + +// might be incorrectly aligned depending on type +// #define STACKLIST_ALLOCA_VIEW(list) ((list).isContiguous() ? (list).pArray() : (list).populateAlloc(alloca((list).dataLength()))) + +// NOLINTBEGIN +#define STACKLIST_VLA_VIEW(type, list, var) \ + const type* var; \ + type var##Data[(list).length()]; \ + if ((list).isContiguous()) { \ + (var) = (list).pArray(); \ + } else { \ + (list).populateAlloc(var##Data); \ + (var) = var##Data; \ + } +// NOLINTEND diff --git a/src/wayland/CMakeLists.txt b/src/wayland/CMakeLists.txt index 54bb59bc..3b3d08ae 100644 --- a/src/wayland/CMakeLists.txt +++ b/src/wayland/CMakeLists.txt @@ -103,6 +103,13 @@ if (WAYLAND_TOPLEVEL_MANAGEMENT) list(APPEND WAYLAND_MODULES Quickshell.Wayland._ToplevelManagement) endif() +if (SCREENCOPY) + add_subdirectory(buffer) + add_subdirectory(screencopy) + list(APPEND WAYLAND_MODULES Quickshell.Wayland._Screencopy) +endif() + + if (HYPRLAND) add_subdirectory(hyprland) endif() diff --git a/src/wayland/buffer/CMakeLists.txt b/src/wayland/buffer/CMakeLists.txt new file mode 100644 index 00000000..f80c53a3 --- /dev/null +++ b/src/wayland/buffer/CMakeLists.txt @@ -0,0 +1,18 @@ +find_package(PkgConfig REQUIRED) +pkg_check_modules(dmabuf-deps REQUIRED IMPORTED_TARGET libdrm gbm egl) + +qt_add_library(quickshell-wayland-buffer STATIC + manager.cpp + dmabuf.cpp + shm.cpp +) + +wl_proto(wlp-linux-dmabuf linux-dmabuf-v1 "${WAYLAND_PROTOCOLS}/stable/linux-dmabuf") + +target_link_libraries(quickshell-wayland-buffer PRIVATE + Qt::Quick Qt::WaylandClient Qt::WaylandClientPrivate wayland-client + PkgConfig::dmabuf-deps + wlp-linux-dmabuf +) + +qs_pch(quickshell-wayland-buffer SET large) diff --git a/src/wayland/buffer/dmabuf.cpp b/src/wayland/buffer/dmabuf.cpp new file mode 100644 index 00000000..47167020 --- /dev/null +++ b/src/wayland/buffer/dmabuf.cpp @@ -0,0 +1,659 @@ +#include "dmabuf.hpp" +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "../../core/stacklist.hpp" +#include "manager.hpp" +#include "manager_p.hpp" + +namespace qs::wayland::buffer::dmabuf { + +namespace { + +Q_LOGGING_CATEGORY(logDmabuf, "quickshell.wayland.buffer.dmabuf", QtWarningMsg); + +LinuxDmabufManager* MANAGER = nullptr; // NOLINT + +class FourCCStr { +public: + explicit FourCCStr(uint32_t code) + : chars( + {static_cast(code >> 0 & 0xff), + static_cast(code >> 8 & 0xff), + static_cast(code >> 16 & 0xff), + static_cast(code >> 24 & 0xff), + '\0'} + ) { + for (auto i = 3; i != 0; i--) { + if (chars[i] == ' ') chars[i] = '\0'; + else break; + } + } + + [[nodiscard]] const char* cStr() const { return this->chars.data(); } + +private: + std::array chars {}; +}; + +class FourCCModStr { +public: + explicit FourCCModStr(uint64_t code): drmStr(drmGetFormatModifierName(code)) {} + ~FourCCModStr() { + if (this->drmStr) drmFree(this->drmStr); + } + + Q_DISABLE_COPY_MOVE(FourCCModStr); + + [[nodiscard]] const char* cStr() const { return this->drmStr; } + +private: + char* drmStr; +}; + +QDebug& operator<<(QDebug& debug, const FourCCStr& fourcc) { + debug << fourcc.cStr(); + return debug; +} + +QDebug& operator<<(QDebug& debug, const FourCCModStr& fourcc) { + debug << fourcc.cStr(); + return debug; +} + +} // namespace + +QDebug& operator<<(QDebug& debug, const WlDmaBuffer* buffer) { + auto saver = QDebugStateSaver(debug); + debug.nospace(); + + if (buffer) { + debug << "WlDmaBuffer(" << static_cast(buffer) << ", size=" << buffer->width << 'x' + << buffer->height << ", format=" << FourCCStr(buffer->format) << ", modifier=`" + << FourCCModStr(buffer->modifier) << "`)"; + } else { + debug << "WlDmaBuffer(0x0)"; + } + + return debug; +} + +GbmDeviceHandle::~GbmDeviceHandle() { + if (device) { + MANAGER->unrefGbmDevice(this->device); + } +} + +// This will definitely backfire later +void LinuxDmabufFormatSelection::ensureSorted() { + if (this->sorted) return; + auto beginIter = this->formats.begin(); + + auto xrgbIter = std::ranges::find_if(this->formats, [](const auto& format) { + return format.first == DRM_FORMAT_XRGB8888; + }); + + if (xrgbIter != this->formats.end()) { + std::swap(*beginIter, *xrgbIter); + ++beginIter; + } + + auto argbIter = std::ranges::find_if(this->formats, [](const auto& format) { + return format.first == DRM_FORMAT_ARGB8888; + }); + + if (argbIter != this->formats.end()) std::swap(*beginIter, *argbIter); + + this->sorted = true; +} + +LinuxDmabufFeedback::LinuxDmabufFeedback(::zwp_linux_dmabuf_feedback_v1* feedback) + : zwp_linux_dmabuf_feedback_v1(feedback) {} + +LinuxDmabufFeedback::~LinuxDmabufFeedback() { this->destroy(); } + +void LinuxDmabufFeedback::zwp_linux_dmabuf_feedback_v1_format_table(int32_t fd, uint32_t size) { + this->formatTableSize = size; + + this->formatTable = mmap(nullptr, size, PROT_READ, MAP_PRIVATE, fd, 0); + + if (this->formatTable == MAP_FAILED) { + this->formatTable = nullptr; + qCFatal(logDmabuf) << "Failed to mmap format table."; + } + + qCDebug(logDmabuf) << "Got format table"; +} + +void LinuxDmabufFeedback::zwp_linux_dmabuf_feedback_v1_main_device(wl_array* device) { + if (device->size != sizeof(dev_t)) { + qCFatal(logDmabuf) << "The size of dev_t used by the compositor and quickshell is mismatched. " + "Try recompiling both."; + } + + this->mainDevice = *reinterpret_cast(device->data); + qCDebug(logDmabuf) << "Got main device id" << this->mainDevice; +} + +void LinuxDmabufFeedback::zwp_linux_dmabuf_feedback_v1_tranche_target_device(wl_array* device) { + if (device->size != sizeof(dev_t)) { + qCFatal(logDmabuf) << "The size of dev_t used by the compositor and quickshell is mismatched. " + "Try recompiling both."; + } + + auto& tranche = this->tranches.emplaceBack(); + tranche.device = *reinterpret_cast(device->data); + qCDebug(logDmabuf) << "Got target device id" << this->mainDevice; +} + +void LinuxDmabufFeedback::zwp_linux_dmabuf_feedback_v1_tranche_flags(uint32_t flags) { + this->tranches.back().flags = flags; + qCDebug(logDmabuf) << "Got target device flags" << flags; +} + +void LinuxDmabufFeedback::zwp_linux_dmabuf_feedback_v1_tranche_formats(wl_array* indices) { + struct FormatTableEntry { + uint32_t format; + uint32_t padding; + uint64_t modifier; + }; + + static_assert(sizeof(FormatTableEntry) == 16, "Format table entry was not packed to 16 bytes."); + + if (this->formatTable == nullptr) { + qCFatal(logDmabuf) << "Received tranche formats before format table."; + } + + auto& tranche = this->tranches.back(); + + auto* table = reinterpret_cast(this->formatTable); + auto* indexTable = reinterpret_cast(indices->data); + auto indexTableLength = indices->size / sizeof(uint16_t); + + uint32_t lastFormat = 0; + LinuxDmabufModifiers* lastModifiers = nullptr; + LinuxDmabufModifiers* modifiers = nullptr; + + for (uint16_t ti = 0; ti != indexTableLength; ++ti) { + auto i = indexTable[ti]; // NOLINT + const auto& entry = table[i]; // NOLINT + + // Compositors usually send a single format's modifiers as a block. + if (!modifiers || entry.format != lastFormat) { + // We can often share modifier lists between formats + if (lastModifiers && modifiers->modifiers == lastModifiers->modifiers) { + // avoids storing a second list + modifiers->modifiers = lastModifiers->modifiers; + } + + lastFormat = entry.format; + lastModifiers = modifiers; + + auto modifiersIter = std::ranges::find_if(tranche.formats.formats, [&](const auto& pair) { + return pair.first == entry.format; + }); + + if (modifiersIter == tranche.formats.formats.end()) { + tranche.formats.formats.push(qMakePair(entry.format, LinuxDmabufModifiers())); + modifiers = &(--tranche.formats.formats.end())->second; + } else { + modifiers = &modifiersIter->second; + } + } + + if (entry.modifier == DRM_FORMAT_MOD_INVALID) { + modifiers->implicit = true; + } else { + modifiers->modifiers.push(entry.modifier); + } + } + + if (lastModifiers && modifiers && modifiers->modifiers == lastModifiers->modifiers) { + modifiers->modifiers = lastModifiers->modifiers; + } +} + +void LinuxDmabufFeedback::zwp_linux_dmabuf_feedback_v1_tranche_done() { + qCDebug(logDmabuf) << "Got tranche end."; +} + +void LinuxDmabufFeedback::zwp_linux_dmabuf_feedback_v1_done() { + qCDebug(logDmabuf) << "Got feedback done."; + + if (this->formatTable) { + munmap(this->formatTable, this->formatTableSize); + this->formatTable = nullptr; + } + + if (logDmabuf().isDebugEnabled()) { + qCDebug(logDmabuf) << "Dmabuf tranches:"; + + for (auto& tranche: this->tranches) { + qCDebug(logDmabuf) << " Tranche on device" << tranche.device; + + // will be sorted on first use otherwise + tranche.formats.ensureSorted(); + + for (auto& [format, modifiers]: tranche.formats.formats) { + qCDebug(logDmabuf) << " Format" << FourCCStr(format); + + if (modifiers.implicit) { + qCDebug(logDmabuf) << " Implicit Modifier"; + } + + for (const auto& modifier: modifiers.modifiers) { + qCDebug(logDmabuf) << " Explicit Modifier" << FourCCModStr(modifier); + } + } + } + } + + // Copy tranches to the manager. If the compositor ever updates + // our tranches, we'll start from a clean slate. + MANAGER->tranches = this->tranches; + this->tranches.clear(); + + MANAGER->feedbackDone(); +} + +LinuxDmabufManager::LinuxDmabufManager(WlBufferManagerPrivate* manager) + : QWaylandClientExtensionTemplate(5) + , manager(manager) { + MANAGER = this; + this->initialize(); + + if (this->isActive()) { + qCDebug(logDmabuf) << "Requesting default dmabuf feedback..."; + new LinuxDmabufFeedback(this->get_default_feedback()); + } +} + +void LinuxDmabufManager::feedbackDone() { this->manager->dmabufReady(); } + +GbmDeviceHandle LinuxDmabufManager::getGbmDevice(dev_t handle) { + struct DrmFree { + static void cleanup(drmDevice* d) { drmFreeDevice(&d); } + }; + + std::string renderNodeStorage; + std::string* renderNode = nullptr; + + auto sharedDevice = std::ranges::find_if(this->gbmDevices, [&](const SharedGbmDevice& d) { + return d.handle == handle; + }); + + if (sharedDevice != this->gbmDevices.end()) { + renderNode = &sharedDevice->renderNode; + } else { + drmDevice* drmDevPtr = nullptr; + if (auto error = drmGetDeviceFromDevId(handle, 0, &drmDevPtr); error != 0) { + qCWarning(logDmabuf) << "Failed to get drm device information from handle:" + << qt_error_string(error); + return nullptr; + } + + auto drmDev = QScopedPointer(drmDevPtr); + + if (!(drmDev->available_nodes & (1 << DRM_NODE_RENDER))) { + qCDebug(logDmabuf) << "Cannot create GBM device: DRM device does not have render node."; + return nullptr; + } + + renderNodeStorage = drmDev->nodes[DRM_NODE_RENDER]; // NOLINT + renderNode = &renderNodeStorage; + sharedDevice = std::ranges::find_if(this->gbmDevices, [&](const SharedGbmDevice& d) { + return d.renderNode == renderNodeStorage; + }); + } + + if (sharedDevice != this->gbmDevices.end()) { + qCDebug(logDmabuf) << "Used existing GBM device on render node" << *renderNode; + ++sharedDevice->refcount; + return sharedDevice->device; + } else { + auto fd = open(renderNode->c_str(), O_RDWR | O_CLOEXEC); + if (fd < 0) { + qCDebug(logDmabuf) << "Could not open render node" << *renderNode << ":" + << qt_error_string(fd); + return nullptr; + } + + auto* device = gbm_create_device(fd); + + if (!device) { + qCDebug(logDmabuf) << "Failed to create GBM device from render node" << *renderNode; + close(fd); + return nullptr; + } + + qCDebug(logDmabuf) << "Created GBM device on render node" << *renderNode; + + this->gbmDevices.push_back({ + .handle = handle, + .renderNode = std::move(renderNodeStorage), + .device = device, + .refcount = 1, + }); + + return device; + } +} + +void LinuxDmabufManager::unrefGbmDevice(gbm_device* device) { + auto iter = std::ranges::find_if(this->gbmDevices, [device](const SharedGbmDevice& d) { + return d.device == device; + }); + if (iter == this->gbmDevices.end()) return; + + qCDebug(logDmabuf) << "Lost reference to GBM device" << device; + + if (--iter->refcount == 0) { + auto fd = gbm_device_get_fd(iter->device); + gbm_device_destroy(iter->device); + close(fd); + + this->gbmDevices.erase(iter); + qCDebug(logDmabuf) << "Destroyed GBM device" << device; + } +} + +GbmDeviceHandle LinuxDmabufManager::dupHandle(const GbmDeviceHandle& handle) { + if (!handle) return GbmDeviceHandle(); + + auto iter = std::ranges::find_if(this->gbmDevices, [&handle](const SharedGbmDevice& d) { + return d.device == *handle; + }); + if (iter == this->gbmDevices.end()) return GbmDeviceHandle(); + + qCDebug(logDmabuf) << "Duplicated GBM device handle" << *handle; + ++iter->refcount; + return GbmDeviceHandle(*handle); +} + +WlBuffer* LinuxDmabufManager::createDmabuf(const WlBufferRequest& request) { + for (auto& tranche: this->tranches) { + if (request.dmabuf.device != 0 && tranche.device != request.dmabuf.device) { + continue; + } + + LinuxDmabufFormatSelection formats; + for (const auto& format: request.dmabuf.formats) { + if (!format.modifiers.isEmpty()) { + formats.formats.push( + qMakePair(format.format, LinuxDmabufModifiers {.modifiers = format.modifiers}) + ); + } else { + for (const auto& trancheFormat: tranche.formats.formats) { + if (trancheFormat.first == format.format) { + formats.formats.push(trancheFormat); + } + } + } + } + + if (formats.formats.isEmpty()) continue; + formats.ensureSorted(); + + auto gbmDevice = this->getGbmDevice(tranche.device); + + if (!gbmDevice) { + qCWarning(logDmabuf) << "Hit unusable tranche device while trying to create dmabuf."; + continue; + } + + for (const auto& [format, modifiers]: formats.formats) { + if (auto* buf = + this->createDmabuf(gbmDevice, format, modifiers, request.width, request.height)) + { + return buf; + } + } + } + + qCWarning(logDmabuf) << "Unable to create dmabuf for request: No matching formats."; + return nullptr; +} + +WlBuffer* LinuxDmabufManager::createDmabuf( + GbmDeviceHandle& device, + uint32_t format, + const LinuxDmabufModifiers& modifiers, + uint32_t width, + uint32_t height +) { + auto buffer = std::unique_ptr(new WlDmaBuffer()); + auto& bo = buffer->bo; + + const uint32_t flags = GBM_BO_USE_RENDERING; + + if (modifiers.modifiers.isEmpty()) { + if (!modifiers.implicit) { + qCritical(logDmabuf + ) << "Failed to create gbm_bo: format supports no implicit OR explicit modifiers."; + return nullptr; + } + + qCDebug(logDmabuf) << "Creating gbm_bo without modifiers..."; + bo = gbm_bo_create(*device, width, height, format, flags); + } else { + qCDebug(logDmabuf) << "Creating gbm_bo with modifiers..."; + + STACKLIST_VLA_VIEW(uint64_t, modifiers.modifiers, modifiersData); + + bo = gbm_bo_create_with_modifiers2( + *device, + width, + height, + format, + modifiersData, + modifiers.modifiers.length(), + flags + ); + } + + if (!bo) { + qCritical(logDmabuf) << "Failed to create gbm_bo."; + return nullptr; + } + + buffer->planeCount = gbm_bo_get_plane_count(bo); + buffer->planes = new WlDmaBuffer::Plane[buffer->planeCount](); + buffer->modifier = gbm_bo_get_modifier(bo); + + auto params = QtWayland::zwp_linux_buffer_params_v1(this->create_params()); + + for (auto i = 0; i < buffer->planeCount; ++i) { + auto& plane = buffer->planes[i]; // NOLINT + plane.fd = gbm_bo_get_fd_for_plane(bo, i); + + if (plane.fd < 0) { + qCritical(logDmabuf) << "Failed to get gbm_bo fd for plane" << i << qt_error_string(plane.fd); + params.destroy(); + gbm_bo_destroy(bo); + return nullptr; + } + + plane.stride = gbm_bo_get_stride_for_plane(bo, i); + plane.offset = gbm_bo_get_offset(bo, i); + + params.add( + plane.fd, + i, + plane.offset, + plane.stride, + buffer->modifier >> 32, + buffer->modifier & 0xffffffff + ); + } + + buffer->mBuffer = + params.create_immed(static_cast(width), static_cast(height), format, 0); + params.destroy(); + + buffer->device = this->dupHandle(device); + buffer->width = width; + buffer->height = height; + buffer->format = format; + + qCDebug(logDmabuf) << "Created dmabuf" << buffer.get(); + return buffer.release(); +} + +WlDmaBuffer::WlDmaBuffer(WlDmaBuffer&& other) noexcept + : device(std::move(other.device)) + , bo(other.bo) + , mBuffer(other.mBuffer) + , planes(other.planes) { + other.mBuffer = nullptr; + other.bo = nullptr; + other.planeCount = 0; +} + +WlDmaBuffer& WlDmaBuffer::operator=(WlDmaBuffer&& other) noexcept { + this->~WlDmaBuffer(); + new (this) WlDmaBuffer(std::move(other)); + return *this; +} + +WlDmaBuffer::~WlDmaBuffer() { + if (this->mBuffer) { + wl_buffer_destroy(this->mBuffer); + } + + if (this->bo) { + gbm_bo_destroy(this->bo); + qCDebug(logDmabuf) << "Destroyed" << this << "freeing bo" << this->bo; + } + + for (auto i = 0; i < this->planeCount; ++i) { + const auto& plane = this->planes[i]; // NOLINT + if (plane.fd) close(plane.fd); + } + + delete[] this->planes; +} + +bool WlDmaBuffer::isCompatible(const WlBufferRequest& request) const { + if (request.width != this->width || request.height != this->height) return false; + + auto matchingFormat = std::ranges::find_if(request.dmabuf.formats, [&](const auto& format) { + return format.format == this->format + && (format.modifiers.isEmpty() + || std::ranges::find(format.modifiers, this->modifier) != format.modifiers.end()); + }); + + return matchingFormat != request.dmabuf.formats.end(); +} + +WlBufferQSGTexture* WlDmaBuffer::createQsgTexture(QQuickWindow* window) const { + static auto* glEGLImageTargetTexture2DOES = []() { + auto* fn = reinterpret_cast( + eglGetProcAddress("glEGLImageTargetTexture2DOES") + ); + + if (!fn) { + qCFatal(logDmabuf) << "Failed to create QSG texture from WlDmaBuffer: " + "glEGLImageTargetTexture2DOES is missing."; + } + + return fn; + }(); + + auto* context = QOpenGLContext::currentContext(); + if (!context) { + qCFatal(logDmabuf) << "Failed to create QSG texture from WlDmaBuffer: No GL context."; + } + + auto* qEglContext = context->nativeInterface(); + if (!qEglContext) { + qCFatal(logDmabuf) << "Failed to create QSG texture from WlDmaBuffer: No EGL context."; + } + + auto* display = qEglContext->display(); + + // clang-format off + auto attribs = std::array { + EGL_WIDTH, this->width, + EGL_HEIGHT, this->height, + EGL_LINUX_DRM_FOURCC_EXT, this->format, + EGL_DMA_BUF_PLANE0_FD_EXT, this->planes[0].fd, // NOLINT + EGL_DMA_BUF_PLANE0_OFFSET_EXT, this->planes[0].offset, // NOLINT + EGL_DMA_BUF_PLANE0_PITCH_EXT, this->planes[0].stride, // NOLINT + EGL_NONE + }; + // clang-format on + + auto* eglImage = + eglCreateImage(display, EGL_NO_CONTEXT, EGL_LINUX_DMA_BUF_EXT, nullptr, attribs.data()); + + if (eglImage == EGL_NO_IMAGE) { + qFatal() << "failed to make egl image" << eglGetError(); + return nullptr; + } + + window->beginExternalCommands(); + GLuint glTexture = 0; + glGenTextures(1, &glTexture); + + glBindTexture(GL_TEXTURE_2D, glTexture); + glEGLImageTargetTexture2DOES(GL_TEXTURE_2D, eglImage); + glBindTexture(GL_TEXTURE_2D, 0); + window->endExternalCommands(); + + auto* qsgTexture = QNativeInterface::QSGOpenGLTexture::fromNative( + glTexture, + window, + QSize(static_cast(this->width), static_cast(this->height)) + ); + + auto* tex = new WlDmaBufferQSGTexture(eglImage, glTexture, qsgTexture); + qCDebug(logDmabuf) << "Created WlDmaBufferQSGTexture" << tex << "from" << this; + return tex; +} + +WlDmaBufferQSGTexture::~WlDmaBufferQSGTexture() { + auto* context = QOpenGLContext::currentContext(); + auto* display = context->nativeInterface()->display(); + + if (this->glTexture) glDeleteTextures(1, &this->glTexture); + if (this->eglImage) eglDestroyImage(display, this->eglImage); + delete this->qsgTexture; + + qCDebug(logDmabuf) << "WlDmaBufferQSGTexture" << this << "destroyed."; +} + +} // namespace qs::wayland::buffer::dmabuf diff --git a/src/wayland/buffer/dmabuf.hpp b/src/wayland/buffer/dmabuf.hpp new file mode 100644 index 00000000..97b5576f --- /dev/null +++ b/src/wayland/buffer/dmabuf.hpp @@ -0,0 +1,195 @@ +#pragma once + +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "manager.hpp" +#include "qsg.hpp" + +namespace qs::wayland::buffer { +class WlBufferManagerPrivate; +} + +namespace qs::wayland::buffer::dmabuf { + +class LinuxDmabufManager; + +class GbmDeviceHandle { +public: + GbmDeviceHandle() = default; + GbmDeviceHandle(gbm_device* device): device(device) {} + + GbmDeviceHandle(GbmDeviceHandle&& other) noexcept: device(other.device) { + other.device = nullptr; + } + + ~GbmDeviceHandle(); + Q_DISABLE_COPY(GbmDeviceHandle); + + GbmDeviceHandle& operator=(GbmDeviceHandle&& other) noexcept { + this->device = other.device; + other.device = nullptr; + return *this; + } + + [[nodiscard]] gbm_device* operator*() const { return this->device; } + [[nodiscard]] operator bool() const { return this->device; } + +private: + gbm_device* device = nullptr; +}; + +class WlDmaBufferQSGTexture: public WlBufferQSGTexture { +public: + ~WlDmaBufferQSGTexture() override; + Q_DISABLE_COPY_MOVE(WlDmaBufferQSGTexture); + + [[nodiscard]] QSGTexture* texture() const override { return this->qsgTexture; } + +private: + WlDmaBufferQSGTexture(EGLImage eglImage, GLuint glTexture, QSGTexture* qsgTexture) + : eglImage(eglImage) + , glTexture(glTexture) + , qsgTexture(qsgTexture) {} + + EGLImage eglImage; + GLuint glTexture; + QSGTexture* qsgTexture; + + friend class WlDmaBuffer; +}; + +class WlDmaBuffer: public WlBuffer { +public: + ~WlDmaBuffer() override; + Q_DISABLE_COPY(WlDmaBuffer); + WlDmaBuffer(WlDmaBuffer&& other) noexcept; + WlDmaBuffer& operator=(WlDmaBuffer&& other) noexcept; + + [[nodiscard]] wl_buffer* buffer() const override { return this->mBuffer; } + + [[nodiscard]] QSize size() const override { + return QSize(static_cast(this->width), static_cast(this->height)); + } + + [[nodiscard]] bool isCompatible(const WlBufferRequest& request) const override; + [[nodiscard]] WlBufferQSGTexture* createQsgTexture(QQuickWindow* window) const override; + +private: + WlDmaBuffer() noexcept = default; + + struct Plane { + int fd = 0; + uint32_t offset = 0; + uint32_t stride = 0; + }; + + GbmDeviceHandle device; + gbm_bo* bo = nullptr; + wl_buffer* mBuffer = nullptr; + int planeCount = 0; + Plane* planes = nullptr; + uint32_t format = 0; + uint64_t modifier = 0; + uint32_t width = 0; + uint32_t height = 0; + + friend class LinuxDmabufManager; + friend QDebug& operator<<(QDebug& debug, const WlDmaBuffer* buffer); +}; + +QDebug& operator<<(QDebug& debug, const WlDmaBuffer* buffer); + +struct LinuxDmabufModifiers { + StackList modifiers; + bool implicit = false; +}; + +struct LinuxDmabufFormatSelection { + bool sorted = false; + StackList, 2> formats; + void ensureSorted(); +}; + +struct LinuxDmabufTranche { + dev_t device = 0; + uint32_t flags = 0; + LinuxDmabufFormatSelection formats; +}; + +class LinuxDmabufFeedback: public QtWayland::zwp_linux_dmabuf_feedback_v1 { +public: + explicit LinuxDmabufFeedback(::zwp_linux_dmabuf_feedback_v1* feedback); + ~LinuxDmabufFeedback() override; + Q_DISABLE_COPY_MOVE(LinuxDmabufFeedback); + +protected: + void zwp_linux_dmabuf_feedback_v1_main_device(wl_array* device) override; + void zwp_linux_dmabuf_feedback_v1_format_table(int32_t fd, uint32_t size) override; + void zwp_linux_dmabuf_feedback_v1_tranche_target_device(wl_array* device) override; + void zwp_linux_dmabuf_feedback_v1_tranche_flags(uint32_t flags) override; + void zwp_linux_dmabuf_feedback_v1_tranche_formats(wl_array* indices) override; + void zwp_linux_dmabuf_feedback_v1_tranche_done() override; + void zwp_linux_dmabuf_feedback_v1_done() override; + +private: + dev_t mainDevice = 0; + QList tranches; + void* formatTable = nullptr; + uint32_t formatTableSize = 0; +}; + +class LinuxDmabufManager + : public QWaylandClientExtensionTemplate + , public QtWayland::zwp_linux_dmabuf_v1 { +public: + explicit LinuxDmabufManager(WlBufferManagerPrivate* manager); + + [[nodiscard]] WlBuffer* createDmabuf(const WlBufferRequest& request); + + [[nodiscard]] WlBuffer* createDmabuf( + GbmDeviceHandle& device, + uint32_t format, + const LinuxDmabufModifiers& modifiers, + uint32_t width, + uint32_t height + ); + +private: + struct SharedGbmDevice { + dev_t handle = 0; + std::string renderNode; + gbm_device* device = nullptr; + qsizetype refcount = 0; + }; + + void feedbackDone(); + + GbmDeviceHandle getGbmDevice(dev_t handle); + void unrefGbmDevice(gbm_device* device); + GbmDeviceHandle dupHandle(const GbmDeviceHandle& handle); + + QList tranches; + QList gbmDevices; + WlBufferManagerPrivate* manager; + + friend class LinuxDmabufFeedback; + friend class GbmDeviceHandle; +}; + +} // namespace qs::wayland::buffer::dmabuf diff --git a/src/wayland/buffer/manager.cpp b/src/wayland/buffer/manager.cpp new file mode 100644 index 00000000..dde71a88 --- /dev/null +++ b/src/wayland/buffer/manager.cpp @@ -0,0 +1,114 @@ +#include "manager.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "dmabuf.hpp" +#include "manager_p.hpp" +#include "qsg.hpp" +#include "shm.hpp" + +namespace qs::wayland::buffer { + +WlBuffer* WlBufferSwapchain::createBackbuffer(const WlBufferRequest& request, bool* newBuffer) { + auto& buffer = this->presentSecondBuffer ? this->buffer1 : this->buffer2; + + if (!buffer || !buffer->isCompatible(request)) { + buffer.reset(WlBufferManager::instance()->createBuffer(request)); + if (newBuffer) *newBuffer = true; + } + + return buffer.get(); +} + +WlBufferManager::WlBufferManager(): p(new WlBufferManagerPrivate(this)) {} + +WlBufferManager::~WlBufferManager() { delete this->p; } + +WlBufferManager* WlBufferManager::instance() { + static auto* instance = new WlBufferManager(); + return instance; +} + +bool WlBufferManager::isReady() const { return this->p->mReady; } + +[[nodiscard]] WlBuffer* WlBufferManager::createBuffer(const WlBufferRequest& request) { + static const bool dmabufDisabled = qEnvironmentVariableIsSet("QS_DISABLE_DMABUF"); + + if (!dmabufDisabled) { + if (auto* buf = this->p->dmabuf.createDmabuf(request)) return buf; + qCWarning(shm::logShm) << "DMA buffer creation failed, falling back to SHM."; + } + + return shm::ShmbufManager::createShmbuf(request); +} + +WlBufferManagerPrivate::WlBufferManagerPrivate(WlBufferManager* manager) + : manager(manager) + , dmabuf(this) {} + +void WlBufferManagerPrivate::dmabufReady() { + this->mReady = true; + emit this->manager->ready(); +} + +WlBufferQSGDisplayNode::WlBufferQSGDisplayNode(QQuickWindow* window) + : window(window) + , imageNode(window->createImageNode()) { + this->appendChildNode(this->imageNode); +} + +void WlBufferQSGDisplayNode::setRect(const QRectF& rect) { + const auto* buffer = (this->presentSecondBuffer ? this->buffer2 : this->buffer1).first; + if (!buffer) return; + + auto matrix = QMatrix4x4(); + auto center = rect.center(); + auto centerX = static_cast(center.x()); + auto centerY = static_cast(center.y()); + matrix.translate(centerX, centerY); + buffer->transform.apply(matrix); + matrix.translate(-centerX, -centerY); + + auto viewRect = matrix.mapRect(rect); + auto bufferSize = buffer->size().toSizeF(); + + bufferSize.scale(viewRect.width(), viewRect.height(), Qt::KeepAspectRatio); + this->imageNode->setRect( + viewRect.x() + viewRect.width() / 2 - bufferSize.width() / 2, + viewRect.y() + viewRect.height() / 2 - bufferSize.height() / 2, + bufferSize.width(), + bufferSize.height() + ); + + this->setMatrix(matrix); +} + +void WlBufferQSGDisplayNode::syncSwapchain(const WlBufferSwapchain& swapchain) { + auto* buffer = swapchain.frontbuffer(); + auto& texture = swapchain.presentSecondBuffer ? this->buffer2 : this->buffer1; + + if (swapchain.presentSecondBuffer == this->presentSecondBuffer && texture.first == buffer) { + return; + } + + this->presentSecondBuffer = swapchain.presentSecondBuffer; + + if (texture.first == buffer) { + texture.second->sync(texture.first, this->window); + } else { + texture.first = buffer; + texture.second.reset(buffer->createQsgTexture(this->window)); + } + + this->imageNode->setTexture(texture.second->texture()); +} + +} // namespace qs::wayland::buffer diff --git a/src/wayland/buffer/manager.hpp b/src/wayland/buffer/manager.hpp new file mode 100644 index 00000000..c3f62a0d --- /dev/null +++ b/src/wayland/buffer/manager.hpp @@ -0,0 +1,134 @@ +#pragma once + +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "../../core/stacklist.hpp" + +class QQuickWindow; + +namespace qs::wayland::buffer { + +class WlBufferManagerPrivate; +class WlBufferQSGTexture; + +struct WlBufferTransform { + enum Transform : uint8_t { + Normal0 = 0, + Normal90 = 1, + Normal180 = 2, + Normal270 = 3, + Flipped0 = 4, + Flipped90 = 5, + Flipped180 = 6, + Flipped270 = 7, + } transform = Normal0; + + WlBufferTransform() = default; + WlBufferTransform(uint8_t transform): transform(static_cast(transform)) {} + + [[nodiscard]] int degrees() const { return 90 * (this->transform & 0b11111011); } + [[nodiscard]] bool flip() const { return this->transform & 0b00000100; } + + void apply(QMatrix4x4& matrix) const { + matrix.rotate(this->flip() ? 180 : 0, 0, 1, 0); + matrix.rotate(static_cast(this->degrees()), 0, 0, 1); + } +}; + +struct WlBufferRequest { + uint32_t width = 0; + uint32_t height = 0; + + struct DmaFormat { + DmaFormat() = default; + DmaFormat(uint32_t format): format(format) {} + + uint32_t format = 0; + StackList modifiers; + }; + + struct { + StackList formats; + } shm; + + struct { + dev_t device = 0; + StackList formats; + } dmabuf; +}; + +class WlBuffer { +public: + virtual ~WlBuffer() = default; + Q_DISABLE_COPY_MOVE(WlBuffer); + + [[nodiscard]] virtual wl_buffer* buffer() const = 0; + [[nodiscard]] virtual QSize size() const = 0; + [[nodiscard]] virtual bool isCompatible(const WlBufferRequest& request) const = 0; + [[nodiscard]] operator bool() const { return this->buffer(); } + + // Must be called from render thread. + [[nodiscard]] virtual WlBufferQSGTexture* createQsgTexture(QQuickWindow* window) const = 0; + + WlBufferTransform transform; + +protected: + explicit WlBuffer() = default; +}; + +class WlBufferSwapchain { +public: + [[nodiscard]] WlBuffer* + createBackbuffer(const WlBufferRequest& request, bool* newBuffer = nullptr); + + void swapBuffers() { this->presentSecondBuffer = !this->presentSecondBuffer; } + + [[nodiscard]] WlBuffer* backbuffer() const { + return this->presentSecondBuffer ? this->buffer1.get() : this->buffer2.get(); + } + + [[nodiscard]] WlBuffer* frontbuffer() const { + return this->presentSecondBuffer ? this->buffer2.get() : this->buffer1.get(); + } + +private: + std::unique_ptr buffer1; + std::unique_ptr buffer2; + bool presentSecondBuffer = false; + + friend class WlBufferQSGDisplayNode; +}; + +class WlBufferManager: public QObject { + Q_OBJECT; + +public: + ~WlBufferManager() override; + Q_DISABLE_COPY_MOVE(WlBufferManager); + + static WlBufferManager* instance(); + + [[nodiscard]] bool isReady() const; + [[nodiscard]] WlBuffer* createBuffer(const WlBufferRequest& request); + +signals: + void ready(); + +private: + explicit WlBufferManager(); + + WlBufferManagerPrivate* p; +}; + +} // namespace qs::wayland::buffer diff --git a/src/wayland/buffer/manager_p.hpp b/src/wayland/buffer/manager_p.hpp new file mode 100644 index 00000000..55f5e667 --- /dev/null +++ b/src/wayland/buffer/manager_p.hpp @@ -0,0 +1,20 @@ +#pragma once + +#include "dmabuf.hpp" +#include "manager.hpp" + +namespace qs::wayland::buffer { + +class WlBufferManagerPrivate { +public: + explicit WlBufferManagerPrivate(WlBufferManager* manager); + + void dmabufReady(); + + WlBufferManager* manager; + dmabuf::LinuxDmabufManager dmabuf; + + bool mReady = false; +}; + +} // namespace qs::wayland::buffer diff --git a/src/wayland/buffer/qsg.hpp b/src/wayland/buffer/qsg.hpp new file mode 100644 index 00000000..c230cfee --- /dev/null +++ b/src/wayland/buffer/qsg.hpp @@ -0,0 +1,45 @@ +#pragma once + +#include + +#include +#include +#include +#include +#include +#include + +#include "manager.hpp" + +namespace qs::wayland::buffer { + +// Interact only from QSG thread. +class WlBufferQSGTexture { +public: + virtual ~WlBufferQSGTexture() = default; + Q_DISABLE_COPY_MOVE(WlBufferQSGTexture); + + [[nodiscard]] virtual QSGTexture* texture() const = 0; + virtual void sync(const WlBuffer* /*buffer*/, QQuickWindow* /*window*/) {} + +protected: + WlBufferQSGTexture() = default; +}; + +// Interact only from QSG thread. +class WlBufferQSGDisplayNode: public QSGTransformNode { +public: + explicit WlBufferQSGDisplayNode(QQuickWindow* window); + + void syncSwapchain(const WlBufferSwapchain& swapchain); + void setRect(const QRectF& rect); + +private: + QQuickWindow* window; + QSGImageNode* imageNode; + QPair> buffer1; + QPair> buffer2; + bool presentSecondBuffer = false; +}; + +} // namespace qs::wayland::buffer diff --git a/src/wayland/buffer/shm.cpp b/src/wayland/buffer/shm.cpp new file mode 100644 index 00000000..8973cdfb --- /dev/null +++ b/src/wayland/buffer/shm.cpp @@ -0,0 +1,91 @@ +#include "shm.hpp" +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "manager.hpp" + +namespace qs::wayland::buffer::shm { + +Q_LOGGING_CATEGORY(logShm, "quickshell.wayland.buffer.shm", QtWarningMsg); + +bool WlShmBuffer::isCompatible(const WlBufferRequest& request) const { + if (QSize(static_cast(request.width), static_cast(request.height)) != this->size()) { + return false; + } + + auto matchingFormat = std::ranges::find(request.shm.formats, this->format); + return matchingFormat != request.shm.formats.end(); +} + +QDebug& operator<<(QDebug& debug, const WlShmBuffer* buffer) { + auto saver = QDebugStateSaver(debug); + debug.nospace(); + + if (buffer) { + auto fmt = QtWaylandClient::QWaylandShm::formatFrom( + static_cast<::wl_shm_format>(buffer->format) // NOLINT + ); + + debug << "WlShmBuffer(" << static_cast(buffer) << ", size=" << buffer->size() + << ", format=" << fmt << ')'; + } else { + debug << "WlShmBuffer(0x0)"; + } + + return debug; +} + +WlShmBuffer::~WlShmBuffer() { qCDebug(logShm) << "Destroyed" << this; } + +WlBufferQSGTexture* WlShmBuffer::createQsgTexture(QQuickWindow* window) const { + auto* texture = new WlShmBufferQSGTexture(); + + // If the QWaylandShmBuffer is destroyed before the QSGTexture, we'll hit a UAF + // in the render thread. + texture->shmBuffer = this->shmBuffer; + + texture->qsgTexture.reset(window->createTextureFromImage(*this->shmBuffer->image())); + texture->sync(this, window); + return texture; +} + +void WlShmBufferQSGTexture::sync(const WlBuffer* /*unused*/, QQuickWindow* window) { + // This is both dumb and expensive. We should use an RHI texture and render images into + // it more intelligently, but shm buffers are already a horribly slow fallback path, + // to the point where it barely matters. + this->qsgTexture.reset(window->createTextureFromImage(*this->shmBuffer->image())); +} + +WlBuffer* ShmbufManager::createShmbuf(const WlBufferRequest& request) { + if (request.shm.formats.isEmpty()) return nullptr; + + static const auto* waylandIntegration = QtWaylandClient::QWaylandIntegration::instance(); + auto* display = waylandIntegration->display(); + + // Its probably fine... + auto format = request.shm.formats[0]; + + auto* buffer = new WlShmBuffer( + new QtWaylandClient::QWaylandShmBuffer( + display, + QSize(static_cast(request.width), static_cast(request.height)), + QtWaylandClient::QWaylandShm::formatFrom(static_cast<::wl_shm_format>(format)) // NOLINT + ), + format + ); + + qCDebug(logShm) << "Created shmbuf" << buffer; + return buffer; +} +} // namespace qs::wayland::buffer::shm diff --git a/src/wayland/buffer/shm.hpp b/src/wayland/buffer/shm.hpp new file mode 100644 index 00000000..12af26e3 --- /dev/null +++ b/src/wayland/buffer/shm.hpp @@ -0,0 +1,63 @@ +#pragma once + +#include + +#include +#include +#include +#include +#include +#include + +#include "manager.hpp" +#include "qsg.hpp" + +namespace qs::wayland::buffer::shm { + +Q_DECLARE_LOGGING_CATEGORY(logShm); + +class WlShmBuffer: public WlBuffer { +public: + ~WlShmBuffer() override; + Q_DISABLE_COPY_MOVE(WlShmBuffer); + + [[nodiscard]] wl_buffer* buffer() const override { return this->shmBuffer->buffer(); } + [[nodiscard]] QSize size() const override { return this->shmBuffer->size(); } + [[nodiscard]] bool isCompatible(const WlBufferRequest& request) const override; + [[nodiscard]] WlBufferQSGTexture* createQsgTexture(QQuickWindow* window) const override; + +private: + WlShmBuffer(QtWaylandClient::QWaylandShmBuffer* shmBuffer, uint32_t format) + : shmBuffer(shmBuffer) + , format(format) {} + + std::shared_ptr shmBuffer; + uint32_t format; + + friend class WlShmBufferQSGTexture; + friend class ShmbufManager; + friend QDebug& operator<<(QDebug& debug, const WlShmBuffer* buffer); +}; + +QDebug& operator<<(QDebug& debug, const WlShmBuffer* buffer); + +class WlShmBufferQSGTexture: public WlBufferQSGTexture { +public: + [[nodiscard]] QSGTexture* texture() const override { return this->qsgTexture.get(); } + void sync(const WlBuffer* buffer, QQuickWindow* window) override; + +private: + WlShmBufferQSGTexture() = default; + + std::shared_ptr shmBuffer; + std::unique_ptr qsgTexture; + + friend class WlShmBuffer; +}; + +class ShmbufManager { +public: + [[nodiscard]] static WlBuffer* createShmbuf(const WlBufferRequest& request); +}; + +} // namespace qs::wayland::buffer::shm diff --git a/src/wayland/module.md b/src/wayland/module.md index d6376e39..db9bfb5a 100644 --- a/src/wayland/module.md +++ b/src/wayland/module.md @@ -5,5 +5,6 @@ headers = [ "wlr_layershell.hpp", "session_lock.hpp", "toplevel_management/qml.hpp", + "screencopy/view.hpp", ] ----- diff --git a/src/wayland/screencopy/CMakeLists.txt b/src/wayland/screencopy/CMakeLists.txt new file mode 100644 index 00000000..97c4209e --- /dev/null +++ b/src/wayland/screencopy/CMakeLists.txt @@ -0,0 +1,42 @@ +qt_add_library(quickshell-wayland-screencopy STATIC + manager.cpp + view.cpp +) + +qt_add_qml_module(quickshell-wayland-screencopy + URI Quickshell.Wayland._Screencopy + VERSION 0.1 + DEPENDENCIES QtQuick +) + +install_qml_module(quickshell-wayland-screencopy) + +set(SCREENCOPY_MODULES) + +if (SCREENCOPY_ICC) + add_subdirectory(image_copy_capture) + list(APPEND SCREENCOPY_MODULES quickshell-wayland-screencopy-icc) +endif() + +if (SCREENCOPY_WLR) + add_subdirectory(wlr_screencopy) + list(APPEND SCREENCOPY_MODULES quickshell-wayland-screencopy-wlr) +endif() + +if (SCREENCOPY_HYPRLAND_TOPLEVEL) + add_subdirectory(hyprland_screencopy) + list(APPEND SCREENCOPY_MODULES quickshell-wayland-screencopy-hyprland) +endif() + +configure_file(build.hpp.in build.hpp @ONLY) +target_include_directories(quickshell-wayland-screencopy PRIVATE ${CMAKE_CURRENT_BINARY_DIR}) + +target_link_libraries(quickshell-wayland-screencopy PRIVATE + Qt::Quick Qt::WaylandClient Qt::WaylandClientPrivate wayland-client + quickshell-wayland-buffer + ${SCREENCOPY_MODULES} +) + +qs_module_pch(quickshell-wayland-screencopy SET large) + +target_link_libraries(quickshell PRIVATE quickshell-wayland-screencopyplugin) diff --git a/src/wayland/screencopy/build.hpp.in b/src/wayland/screencopy/build.hpp.in new file mode 100644 index 00000000..9276daaa --- /dev/null +++ b/src/wayland/screencopy/build.hpp.in @@ -0,0 +1,6 @@ +#pragma once +// NOLINTBEGIN +#cmakedefine01 SCREENCOPY_ICC +#cmakedefine01 SCREENCOPY_WLR +#cmakedefine01 SCREENCOPY_HYPRLAND_TOPLEVEL +// NOLINTEND diff --git a/src/wayland/screencopy/hyprland_screencopy/CMakeLists.txt b/src/wayland/screencopy/hyprland_screencopy/CMakeLists.txt new file mode 100644 index 00000000..d06a91e5 --- /dev/null +++ b/src/wayland/screencopy/hyprland_screencopy/CMakeLists.txt @@ -0,0 +1,16 @@ +qt_add_library(quickshell-wayland-screencopy-hyprland STATIC + hyprland_screencopy.cpp +) + +wl_proto(wlp-hyprland-screencopy hyprland-toplevel-export-v1 "${CMAKE_CURRENT_SOURCE_DIR}") + +target_link_libraries(quickshell-wayland-screencopy-hyprland PRIVATE + Qt::WaylandClient Qt::WaylandClientPrivate wayland-client + Qt::Quick # for pch +) + +target_link_libraries(quickshell-wayland-screencopy-hyprland PUBLIC + wlp-hyprland-screencopy wlp-foreign-toplevel +) + +qs_pch(quickshell-wayland-screencopy-hyprland SET large) diff --git a/src/wayland/screencopy/hyprland_screencopy/hyprland-toplevel-export-v1.xml b/src/wayland/screencopy/hyprland_screencopy/hyprland-toplevel-export-v1.xml new file mode 100644 index 00000000..b1185aa5 --- /dev/null +++ b/src/wayland/screencopy/hyprland_screencopy/hyprland-toplevel-export-v1.xml @@ -0,0 +1,228 @@ + + + + Copyright © 2022 Vaxry + All rights reserved. + + Redistribution and use in source and binary forms, with or without + modification, are permitted provided that the following conditions are met: + + 1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + + 2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + + 3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + + THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + + + + This protocol allows clients to ask for exporting another toplevel's + surface(s) to a buffer. + + Particularly useful for sharing a single window. + + + + + This object is a manager which offers requests to start capturing from a + source. + + + + + Capture the next frame of a toplevel. (window) + + The captured frame will not contain any server-side decorations and will + ignore the compositor-set geometry, like e.g. rounded corners. + + It will contain all the subsurfaces and popups, however the latter will be clipped + to the geometry of the base surface. + + The handle parameter refers to the address of the window as seen in `hyprctl clients`. + For example, for d161e7b0 it would be 3512854448. + + + + + + + + + All objects created by the manager will still remain valid, until their + appropriate destroy request has been called. + + + + + + + Same as capture_toplevel, but with a zwlr_foreign_toplevel_handle_v1 handle. + + + + + + + + + + + This object represents a single frame. + + When created, a series of buffer events will be sent, each representing a + supported buffer type. The "buffer_done" event is sent afterwards to + indicate that all supported buffer types have been enumerated. The client + will then be able to send a "copy" request. If the capture is successful, + the compositor will send a "flags" followed by a "ready" event. + + wl_shm buffers are always supported, ie. the "buffer" event is guaranteed to be sent. + + If the capture failed, the "failed" event is sent. This can happen anytime + before the "ready" event. + + Once either a "ready" or a "failed" event is received, the client should + destroy the frame. + + + + + Provides information about wl_shm buffer parameters that need to be + used for this frame. This event is sent once after the frame is created + if wl_shm buffers are supported. + + + + + + + + + + Copy the frame to the supplied buffer. The buffer must have the + correct size, see hyprland_toplevel_export_frame_v1.buffer and + hyprland_toplevel_export_frame_v1.linux_dmabuf. The buffer needs to have a + supported format. + + If the frame is successfully copied, a "flags" and a "ready" event is + sent. Otherwise, a "failed" event is sent. + + This event will wait for appropriate damage to be copied, unless the ignore_damage + arg is set to a non-zero value. + + + + + + + + This event is sent right before the ready event when ignore_damage was + not set. It may be generated multiple times for each copy + request. + + The arguments describe a box around an area that has changed since the + last copy request that was derived from the current screencopy manager + instance. + + The union of all regions received between the call to copy + and a ready event is the total damage since the prior ready event. + + + + + + + + + + + + + + + + + + + Provides flags about the frame. This event is sent once before the + "ready" event. + + + + + + + Called as soon as the frame is copied, indicating it is available + for reading. This event includes the time at which presentation happened + at. + + The timestamp is expressed as tv_sec_hi, tv_sec_lo, tv_nsec triples, + each component being an unsigned 32-bit value. Whole seconds are in + tv_sec which is a 64-bit value combined from tv_sec_hi and tv_sec_lo, + and the additional fractional part in tv_nsec as nanoseconds. Hence, + for valid timestamps tv_nsec must be in [0, 999999999]. The seconds part + may have an arbitrary offset at start. + + After receiving this event, the client should destroy the object. + + + + + + + + + This event indicates that the attempted frame copy has failed. + + After receiving this event, the client should destroy the object. + + + + + + Destroys the frame. This request can be sent at any time by the client. + + + + + + Provides information about linux-dmabuf buffer parameters that need to + be used for this frame. This event is sent once after the frame is + created if linux-dmabuf buffers are supported. + + + + + + + + + This event is sent once after all buffer events have been sent. + + The client should proceed to create a buffer of one of the supported + types, and send a "copy" request. + + + + diff --git a/src/wayland/screencopy/hyprland_screencopy/hyprland_screencopy.cpp b/src/wayland/screencopy/hyprland_screencopy/hyprland_screencopy.cpp new file mode 100644 index 00000000..457f1055 --- /dev/null +++ b/src/wayland/screencopy/hyprland_screencopy/hyprland_screencopy.cpp @@ -0,0 +1,122 @@ +#include "hyprland_screencopy.hpp" +#include + +#include +#include +#include +#include +#include +#include + +#include "../../toplevel_management/handle.hpp" +#include "../manager.hpp" +#include "hyprland_screencopy_p.hpp" + +namespace qs::wayland::screencopy::hyprland { + +namespace { +Q_LOGGING_CATEGORY(logScreencopy, "quickshell.wayland.screencopy.hyprland", QtWarningMsg); +} + +HyprlandScreencopyManager::HyprlandScreencopyManager(): QWaylandClientExtensionTemplate(2) { + this->initialize(); +} + +HyprlandScreencopyManager* HyprlandScreencopyManager::instance() { + static auto* instance = new HyprlandScreencopyManager(); + return instance; +} + +ScreencopyContext* HyprlandScreencopyManager::captureToplevel( + toplevel_management::impl::ToplevelHandle* handle, + bool paintCursors +) { + return new HyprlandScreencopyContext(this, handle, paintCursors); +} + +HyprlandScreencopyContext::HyprlandScreencopyContext( + HyprlandScreencopyManager* manager, + toplevel_management::impl::ToplevelHandle* handle, + bool paintCursors +) + : manager(manager) + , handle(handle) + , paintCursors(paintCursors) { + QObject::connect( + handle, + &QObject::destroyed, + this, + &HyprlandScreencopyContext::onToplevelDestroyed + ); +} + +HyprlandScreencopyContext::~HyprlandScreencopyContext() { + if (this->object()) this->destroy(); +} + +void HyprlandScreencopyContext::onToplevelDestroyed() { + qCWarning(logScreencopy) << "Toplevel destroyed while recording. Stopping" << this; + if (this->object()) this->destroy(); + emit this->stopped(); +} + +void HyprlandScreencopyContext::captureFrame() { + if (this->object()) return; + + this->init(this->manager->capture_toplevel_with_wlr_toplevel_handle( + this->paintCursors ? 1 : 0, + this->handle->object() + )); +} + +void HyprlandScreencopyContext::hyprland_toplevel_export_frame_v1_buffer( + uint32_t format, + uint32_t width, + uint32_t height, + uint32_t /*stride*/ +) { + // While different sizes can technically be requested, that would be insane. + this->request.width = width; + this->request.height = height; + this->request.shm.formats.push(format); +} + +void HyprlandScreencopyContext::hyprland_toplevel_export_frame_v1_linux_dmabuf( + uint32_t format, + uint32_t width, + uint32_t height +) { + // While different sizes can technically be requested, that would be insane. + this->request.width = width; + this->request.height = height; + this->request.dmabuf.formats.push(format); +} + +void HyprlandScreencopyContext::hyprland_toplevel_export_frame_v1_flags(uint32_t flags) { + if (flags & HYPRLAND_TOPLEVEL_EXPORT_FRAME_V1_FLAGS_Y_INVERT) { + this->mSwapchain.backbuffer()->transform = buffer::WlBufferTransform::Flipped180; + } +} + +void HyprlandScreencopyContext::hyprland_toplevel_export_frame_v1_buffer_done() { + auto* backbuffer = this->mSwapchain.createBackbuffer(this->request); + this->copy(backbuffer->buffer(), this->copiedFirstFrame ? 0 : 1); +} + +void HyprlandScreencopyContext::hyprland_toplevel_export_frame_v1_ready( + uint32_t /*tvSecHi*/, + uint32_t /*tvSecLo*/, + uint32_t /*tvNsec*/ +) { + this->destroy(); + this->copiedFirstFrame = true; + this->mSwapchain.swapBuffers(); + emit this->frameCaptured(); +} + +void HyprlandScreencopyContext::hyprland_toplevel_export_frame_v1_failed() { + qCWarning(logScreencopy) << "Ending recording due to screencopy failure for" << this; + emit this->stopped(); +} + +} // namespace qs::wayland::screencopy::hyprland diff --git a/src/wayland/screencopy/hyprland_screencopy/hyprland_screencopy.hpp b/src/wayland/screencopy/hyprland_screencopy/hyprland_screencopy.hpp new file mode 100644 index 00000000..fbd08c54 --- /dev/null +++ b/src/wayland/screencopy/hyprland_screencopy/hyprland_screencopy.hpp @@ -0,0 +1,26 @@ +#pragma once + +#include +#include + +#include "../../toplevel_management/handle.hpp" +#include "../manager.hpp" + +namespace qs::wayland::screencopy::hyprland { + +class HyprlandScreencopyManager + : public QWaylandClientExtensionTemplate + , public QtWayland::hyprland_toplevel_export_manager_v1 { +public: + ScreencopyContext* + captureToplevel(toplevel_management::impl::ToplevelHandle* handle, bool paintCursors); + + static HyprlandScreencopyManager* instance(); + +private: + explicit HyprlandScreencopyManager(); + + friend class HyprlandScreencopyContext; +}; + +} // namespace qs::wayland::screencopy::hyprland diff --git a/src/wayland/screencopy/hyprland_screencopy/hyprland_screencopy_p.hpp b/src/wayland/screencopy/hyprland_screencopy/hyprland_screencopy_p.hpp new file mode 100644 index 00000000..199390ec --- /dev/null +++ b/src/wayland/screencopy/hyprland_screencopy/hyprland_screencopy_p.hpp @@ -0,0 +1,50 @@ +#pragma once + +#include +#include + +#include "../../toplevel_management/handle.hpp" +#include "../manager.hpp" + +namespace qs::wayland::screencopy::hyprland { + +class HyprlandScreencopyManager; + +class HyprlandScreencopyContext + : public ScreencopyContext + , public QtWayland::hyprland_toplevel_export_frame_v1 { +public: + explicit HyprlandScreencopyContext( + HyprlandScreencopyManager* manager, + toplevel_management::impl::ToplevelHandle* handle, + bool paintCursors + ); + + ~HyprlandScreencopyContext() override; + Q_DISABLE_COPY_MOVE(HyprlandScreencopyContext); + + void captureFrame() override; + +protected: + // clang-format off + void hyprland_toplevel_export_frame_v1_buffer(uint32_t format, uint32_t width, uint32_t height, uint32_t stride) override; + void hyprland_toplevel_export_frame_v1_linux_dmabuf(uint32_t format, uint32_t width, uint32_t height) override; + void hyprland_toplevel_export_frame_v1_flags(uint32_t flags) override; + void hyprland_toplevel_export_frame_v1_buffer_done() override; + void hyprland_toplevel_export_frame_v1_ready(uint32_t tvSecHi, uint32_t tvSecLo, uint32_t tvNsec) override; + void hyprland_toplevel_export_frame_v1_failed() override; + // clang-format on + +private slots: + void onToplevelDestroyed(); + +private: + HyprlandScreencopyManager* manager; + buffer::WlBufferRequest request; + bool copiedFirstFrame = false; + + toplevel_management::impl::ToplevelHandle* handle; + bool paintCursors; +}; + +} // namespace qs::wayland::screencopy::hyprland diff --git a/src/wayland/screencopy/image_copy_capture/CMakeLists.txt b/src/wayland/screencopy/image_copy_capture/CMakeLists.txt new file mode 100644 index 00000000..954fdda3 --- /dev/null +++ b/src/wayland/screencopy/image_copy_capture/CMakeLists.txt @@ -0,0 +1,19 @@ +qt_add_library(quickshell-wayland-screencopy-icc STATIC + image_copy_capture.cpp +) + +wl_proto(wlp-ext-foreign-toplevel ext-foreign-toplevel-list-v1 "${WAYLAND_PROTOCOLS}/staging/ext-foreign-toplevel-list") +wl_proto(wlp-image-copy-capture ext-image-copy-capture-v1 "${WAYLAND_PROTOCOLS}/staging/ext-image-copy-capture") +wl_proto(wlp-image-capture-source ext-image-capture-source-v1 "${WAYLAND_PROTOCOLS}/staging/ext-image-capture-source") + +target_link_libraries(quickshell-wayland-screencopy-icc PRIVATE + Qt::WaylandClient Qt::WaylandClientPrivate wayland-client + Qt::Quick # for pch +) + +target_link_libraries(quickshell-wayland-screencopy-icc PUBLIC + wlp-image-copy-capture wlp-image-capture-source + wlp-ext-foreign-toplevel # required for capture source to build +) + +qs_pch(quickshell-wayland-screencopy-icc SET large) diff --git a/src/wayland/screencopy/image_copy_capture/image_copy_capture.cpp b/src/wayland/screencopy/image_copy_capture/image_copy_capture.cpp new file mode 100644 index 00000000..649b111b --- /dev/null +++ b/src/wayland/screencopy/image_copy_capture/image_copy_capture.cpp @@ -0,0 +1,225 @@ +#include "image_copy_capture.hpp" +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "../manager.hpp" +#include "image_copy_capture_p.hpp" + +namespace qs::wayland::screencopy::icc { + +namespace { +Q_LOGGING_CATEGORY(logIcc, "quickshell.wayland.screencopy.icc", QtWarningMsg); +} + +using IccCaptureSession = QtWayland::ext_image_copy_capture_session_v1; +using IccCaptureFrame = QtWayland::ext_image_copy_capture_frame_v1; + +IccScreencopyContext::IccScreencopyContext(::ext_image_copy_capture_session_v1* session) + : IccCaptureSession(session) {} + +IccScreencopyContext::~IccScreencopyContext() { + if (this->IccCaptureSession::object()) { + this->IccCaptureSession::destroy(); + } + + if (this->IccCaptureFrame::object()) { + this->IccCaptureFrame::destroy(); + } +} + +void IccScreencopyContext::captureFrame() { + if (this->IccCaptureFrame::object() || this->capturePending) return; + + if (this->statePending) this->capturePending = true; + else this->doCapture(); +} + +void IccScreencopyContext::ext_image_copy_capture_session_v1_buffer_size( + uint32_t width, + uint32_t height +) { + this->clearOldState(); + + this->request.width = width; + this->request.height = height; +} + +void IccScreencopyContext::ext_image_copy_capture_session_v1_shm_format(uint32_t format) { + this->clearOldState(); + + this->request.shm.formats.push(format); +} + +void IccScreencopyContext::ext_image_copy_capture_session_v1_dmabuf_device(wl_array* device) { + this->clearOldState(); + + if (device->size != sizeof(dev_t)) { + qCFatal(logIcc) << "The size of dev_t used by the compositor and quickshell is mismatched. Try " + "recompiling both."; + } + + this->request.dmabuf.device = *reinterpret_cast(device->data); +} + +void IccScreencopyContext::ext_image_copy_capture_session_v1_dmabuf_format( + uint32_t format, + wl_array* modifiers +) { + this->clearOldState(); + + auto* modifierArray = reinterpret_cast(modifiers->data); + auto modifierCount = modifiers->size / sizeof(uint64_t); + + auto reqFormat = buffer::WlBufferRequest::DmaFormat(format); + + for (uint16_t i = 0; i != modifierCount; i++) { + reqFormat.modifiers.push(modifierArray[i]); // NOLINT + } + + this->request.dmabuf.formats.push(reqFormat); +} + +void IccScreencopyContext::ext_image_copy_capture_session_v1_done() { + this->statePending = false; + + if (this->capturePending) { + this->doCapture(); + } +} + +void IccScreencopyContext::ext_image_copy_capture_session_v1_stopped() { + qCInfo(logIcc) << "Ending recording due to screencopy stop for" << this; + emit this->stopped(); +} + +void IccScreencopyContext::clearOldState() { + if (!this->statePending) { + this->request = buffer::WlBufferRequest(); + this->statePending = true; + } +} + +void IccScreencopyContext::doCapture() { + this->capturePending = false; + + auto newBuffer = false; + auto* backbuffer = this->mSwapchain.createBackbuffer(this->request, &newBuffer); + + this->IccCaptureFrame::init(this->IccCaptureSession::create_frame()); + this->IccCaptureFrame::attach_buffer(backbuffer->buffer()); + + if (newBuffer) { + // If the buffer was replaced, it will be blank and the compositor needs + // to repaint the whole thing. + this->IccCaptureFrame::damage_buffer( + 0, + 0, + static_cast(this->request.width), + static_cast(this->request.height) + ); + + // We don't care about partial damage if the whole buffer was replaced. + this->lastDamage = QRect(); + } else if (!this->lastDamage.isEmpty()) { + // If buffers were swapped between the last frame and the current one, request a repaint + // of the backbuffer in the same places that changes to the frontbuffer were recorded. + this->IccCaptureFrame::damage_buffer( + this->lastDamage.x(), + this->lastDamage.y(), + this->lastDamage.width(), + this->lastDamage.height() + ); + + // We don't need to do this more than once per buffer swap. + this->lastDamage = QRect(); + } + + this->IccCaptureFrame::capture(); +} + +void IccScreencopyContext::ext_image_copy_capture_frame_v1_transform(uint32_t transform) { + this->mSwapchain.backbuffer()->transform = transform; +} + +void IccScreencopyContext::ext_image_copy_capture_frame_v1_damage( + int32_t x, + int32_t y, + int32_t width, + int32_t height +) { + this->damage = this->damage.united(QRect(x, y, width, height)); +} + +void IccScreencopyContext::ext_image_copy_capture_frame_v1_ready() { + this->IccCaptureFrame::destroy(); + + this->mSwapchain.swapBuffers(); + this->lastDamage = this->damage; + this->damage = QRect(); + + emit this->frameCaptured(); +} + +void IccScreencopyContext::ext_image_copy_capture_frame_v1_failed(uint32_t reason) { + switch (static_cast(reason)) { + case IccCaptureFrame::failure_reason_buffer_constraints: + qFatal(logIcc) << "Got a buffer_constraints failure, however the buffer matches the last sent " + "size. There is a bug in quickshell or your compositor."; + break; + case IccCaptureFrame::failure_reason_stopped: + // Handled in the ExtCaptureSession handler. + break; + case IccCaptureFrame::failure_reason_unknown: + qCWarning(logIcc) << "Ending recording due to screencopy failure for" << this; + emit this->stopped(); + break; + } +} + +IccManager::IccManager(): QWaylandClientExtensionTemplate(1) { this->initialize(); } + +IccManager* IccManager::instance() { + static auto* instance = new IccManager(); + return instance; +} + +ScreencopyContext* +IccManager::createSession(::ext_image_capture_source_v1* source, bool paintCursors) { + auto* session = this->create_session( + source, + paintCursors ? QtWayland::ext_image_copy_capture_manager_v1::options_paint_cursors : 0 + ); + return new IccScreencopyContext(session); +} + +IccOutputSourceManager::IccOutputSourceManager(): QWaylandClientExtensionTemplate(1) { + this->initialize(); +} + +IccOutputSourceManager* IccOutputSourceManager::instance() { + static auto* instance = new IccOutputSourceManager(); + return instance; +} + +ScreencopyContext* IccOutputSourceManager::captureOutput(QScreen* screen, bool paintCursors) { + auto* waylandScreen = dynamic_cast(screen->handle()); + if (!waylandScreen) return nullptr; + + return IccManager::instance()->createSession( + this->create_source(waylandScreen->output()), + paintCursors + ); +} + +} // namespace qs::wayland::screencopy::icc diff --git a/src/wayland/screencopy/image_copy_capture/image_copy_capture.hpp b/src/wayland/screencopy/image_copy_capture/image_copy_capture.hpp new file mode 100644 index 00000000..93ba36c3 --- /dev/null +++ b/src/wayland/screencopy/image_copy_capture/image_copy_capture.hpp @@ -0,0 +1,36 @@ +#pragma once + +#include +#include +#include +#include + +#include "../manager.hpp" + +namespace qs::wayland::screencopy::icc { + +class IccManager + : public QWaylandClientExtensionTemplate + , public QtWayland::ext_image_copy_capture_manager_v1 { +public: + ScreencopyContext* createSession(::ext_image_capture_source_v1* source, bool paintCursors); + + static IccManager* instance(); + +private: + explicit IccManager(); +}; + +class IccOutputSourceManager + : public QWaylandClientExtensionTemplate + , public QtWayland::ext_output_image_capture_source_manager_v1 { +public: + ScreencopyContext* captureOutput(QScreen* screen, bool paintCursors); + + static IccOutputSourceManager* instance(); + +private: + explicit IccOutputSourceManager(); +}; + +} // namespace qs::wayland::screencopy::icc diff --git a/src/wayland/screencopy/image_copy_capture/image_copy_capture_p.hpp b/src/wayland/screencopy/image_copy_capture/image_copy_capture_p.hpp new file mode 100644 index 00000000..14f20675 --- /dev/null +++ b/src/wayland/screencopy/image_copy_capture/image_copy_capture_p.hpp @@ -0,0 +1,53 @@ +#pragma once + +#include + +#include +#include +#include + +#include "../manager.hpp" + +namespace qs::wayland::screencopy::icc { + +class IccScreencopyContext + : public ScreencopyContext + , public QtWayland::ext_image_copy_capture_session_v1 + , public QtWayland::ext_image_copy_capture_frame_v1 { + +public: + IccScreencopyContext(::ext_image_copy_capture_session_v1* session); + ~IccScreencopyContext() override; + Q_DISABLE_COPY_MOVE(IccScreencopyContext); + + void captureFrame() override; + +protected: + // clang-formt off + void ext_image_copy_capture_session_v1_buffer_size(uint32_t width, uint32_t height) override; + void ext_image_copy_capture_session_v1_shm_format(uint32_t format) override; + void ext_image_copy_capture_session_v1_dmabuf_device(wl_array* device) override; + void + ext_image_copy_capture_session_v1_dmabuf_format(uint32_t format, wl_array* modifiers) override; + void ext_image_copy_capture_session_v1_done() override; + void ext_image_copy_capture_session_v1_stopped() override; + + void ext_image_copy_capture_frame_v1_transform(uint32_t transform) override; + void ext_image_copy_capture_frame_v1_damage(int32_t x, int32_t y, int32_t width, int32_t height) + override; + void ext_image_copy_capture_frame_v1_ready() override; + void ext_image_copy_capture_frame_v1_failed(uint32_t reason) override; + // clang-formt on + +private: + void clearOldState(); + void doCapture(); + + buffer::WlBufferRequest request; + bool statePending = true; + bool capturePending = false; + QRect damage; + QRect lastDamage; +}; + +} // namespace qs::wayland::screencopy::icc diff --git a/src/wayland/screencopy/manager.cpp b/src/wayland/screencopy/manager.cpp new file mode 100644 index 00000000..8345e314 --- /dev/null +++ b/src/wayland/screencopy/manager.cpp @@ -0,0 +1,56 @@ +#include "manager.hpp" + +#include + +#include "build.hpp" + +#if SCREENCOPY_ICC || SCREENCOPY_WLR +#include "../../core/qmlscreen.hpp" +#endif + +#if SCREENCOPY_ICC +#include "image_copy_capture/image_copy_capture.hpp" +#endif + +#if SCREENCOPY_WLR +#include "wlr_screencopy/wlr_screencopy.hpp" +#endif + +#if SCREENCOPY_HYPRLAND_TOPLEVEL +#include "../toplevel_management/qml.hpp" +#include "hyprland_screencopy/hyprland_screencopy.hpp" +#endif + +namespace qs::wayland::screencopy { + +ScreencopyContext* ScreencopyManager::createContext(QObject* object, bool paintCursors) { + if (auto* screen = qobject_cast(object)) { +#if SCREENCOPY_ICC + { + auto* manager = icc::IccOutputSourceManager::instance(); + if (manager->isActive()) { + return manager->captureOutput(screen->screen, paintCursors); + } + } +#endif +#if SCREENCOPY_WLR + { + auto* manager = wlr::WlrScreencopyManager::instance(); + if (manager->isActive()) { + return manager->captureOutput(screen->screen, paintCursors); + } + } +#endif +#if SCREENCOPY_HYPRLAND_TOPLEVEL + } else if (auto* toplevel = qobject_cast(object)) { + auto* manager = hyprland::HyprlandScreencopyManager::instance(); + if (manager->isActive()) { + return manager->captureToplevel(toplevel->implHandle(), paintCursors); + } +#endif + } + + return nullptr; +} + +} // namespace qs::wayland::screencopy diff --git a/src/wayland/screencopy/manager.hpp b/src/wayland/screencopy/manager.hpp new file mode 100644 index 00000000..f58e0052 --- /dev/null +++ b/src/wayland/screencopy/manager.hpp @@ -0,0 +1,33 @@ +#pragma once + +#include +#include +#include + +#include "../buffer/manager.hpp" + +namespace qs::wayland::screencopy { + +class ScreencopyContext: public QObject { + Q_OBJECT; + +public: + [[nodiscard]] buffer::WlBufferSwapchain& swapchain() { return this->mSwapchain; } + virtual void captureFrame() = 0; + +signals: + void frameCaptured(); + void stopped(); + +protected: + ScreencopyContext() = default; + + buffer::WlBufferSwapchain mSwapchain; +}; + +class ScreencopyManager { +public: + static ScreencopyContext* createContext(QObject* object, bool paintCursors); +}; + +} // namespace qs::wayland::screencopy diff --git a/src/wayland/screencopy/view.cpp b/src/wayland/screencopy/view.cpp new file mode 100644 index 00000000..fe517352 --- /dev/null +++ b/src/wayland/screencopy/view.cpp @@ -0,0 +1,149 @@ +#include "view.hpp" + +#include +#include +#include +#include +#include + +#include "../buffer/manager.hpp" +#include "../buffer/qsg.hpp" +#include "manager.hpp" + +namespace qs::wayland::screencopy { + +void ScreencopyView::setCaptureSource(QObject* captureSource) { + if (captureSource == this->mCaptureSource) return; + auto hadContext = this->context != nullptr; + this->destroyContext(false); + + this->mCaptureSource = captureSource; + + if (captureSource) { + QObject::connect( + captureSource, + &QObject::destroyed, + this, + &ScreencopyView::onCaptureSourceDestroyed + ); + + if (this->completed) this->createContext(); + } + + if (!this->context && hadContext) this->update(); + emit this->captureSourceChanged(); +} + +void ScreencopyView::onCaptureSourceDestroyed() { + this->mCaptureSource = nullptr; + this->destroyContext(); +} + +void ScreencopyView::setPaintCursors(bool paintCursors) { + if (paintCursors == this->mPaintCursors) return; + this->mPaintCursors = paintCursors; + if (this->completed && this->context) this->createContext(); + emit this->paintCursorsChanged(); +} + +void ScreencopyView::setLive(bool live) { + if (live == this->mLive) return; + + if (live && !this->mLive && this->context) { + this->context->captureFrame(); + } + + this->mLive = live; + emit this->liveChanged(); +} + +void ScreencopyView::createContext() { + this->destroyContext(false); + this->context = ScreencopyManager::createContext(this->mCaptureSource, this->mPaintCursors); + + if (!this->context) { + qmlWarning(this) << "Capture source set to non captureable object."; + return; + } + + QObject::connect( + this->context, + &ScreencopyContext::stopped, + this, + &ScreencopyView::destroyContextWithUpdate + ); + + QObject::connect( + this->context, + &ScreencopyContext::frameCaptured, + this, + &ScreencopyView::onFrameCaptured + ); + + this->context->captureFrame(); +} + +void ScreencopyView::destroyContext(bool update) { + auto hadContext = this->context != nullptr; + delete this->context; + this->context = nullptr; + this->bHasContent = false; + this->bSourceSize = QSize(); + if (hadContext && update) this->update(); +} + +void ScreencopyView::captureFrame() { + if (this->context) this->context->captureFrame(); + else qmlWarning(this) << "Cannot capture frame, as no recording context is ready."; +} + +void ScreencopyView::onFrameCaptured() { + this->setFlag(QQuickItem::ItemHasContents); + this->update(); + this->bHasContent = true; + this->bSourceSize = this->context->swapchain().frontbuffer()->size(); +} + +void ScreencopyView::componentComplete() { + this->QQuickItem::componentComplete(); + + auto* bufManager = buffer::WlBufferManager::instance(); + if (!bufManager->isReady()) { + QObject::connect( + bufManager, + &buffer::WlBufferManager::ready, + this, + &ScreencopyView::onBuffersReady + ); + } else { + this->onBuffersReady(); + } +} + +void ScreencopyView::onBuffersReady() { + this->completed = true; + if (this->mCaptureSource) this->createContext(); +} + +QSGNode* ScreencopyView::updatePaintNode(QSGNode* oldNode, UpdatePaintNodeData* /*unused*/) { + if (!this->context || !this->bHasContent) { + delete oldNode; + this->setFlag(QQuickItem::ItemHasContents, false); + return nullptr; + } + + auto* node = static_cast(oldNode); // NOLINT + + if (!node) { + node = new buffer::WlBufferQSGDisplayNode(this->window()); + } + + auto& swapchain = this->context->swapchain(); + node->syncSwapchain(swapchain); + node->setRect(this->boundingRect()); + + if (this->mLive) this->context->captureFrame(); + return node; +} + +} // namespace qs::wayland::screencopy diff --git a/src/wayland/screencopy/view.hpp b/src/wayland/screencopy/view.hpp new file mode 100644 index 00000000..53f42398 --- /dev/null +++ b/src/wayland/screencopy/view.hpp @@ -0,0 +1,96 @@ +#pragma once + +#include +#include +#include +#include +#include +#include + +#include "manager.hpp" + +namespace qs::wayland::screencopy { + +///! Displays a video stream from other windows or a monitor. +/// ScreencopyView displays live video streams or single captured frames from valid +/// capture sources. See @@captureSource for details on which objects are accepted. +class ScreencopyView: public QQuickItem { + Q_OBJECT; + QML_ELEMENT; + // clang-format off + /// The object to capture from. Accepts any of the following: + /// - `null` - Clears the displayed image. + /// - @@Quickshell.ShellScreen - A monitor. + /// Requires a compositor that supports `wlr-screencopy-unstable` + /// or both `ext-image-copy-capture-v1` and `ext-capture-source-v1`. + /// - @@Quickshell.Wayland.Toplevel - A toplevel window. + /// Requires a compositor that supports `hyprland-toplevel-export-v1`. + Q_PROPERTY(QObject* captureSource READ captureSource WRITE setCaptureSource NOTIFY captureSourceChanged); + /// If true, the system cursor will be painted on the image. Defaults to false. + Q_PROPERTY(bool paintCursor READ paintCursors WRITE setPaintCursors NOTIFY paintCursorsChanged); + /// If true, a live video feed from the capture source will be displayed instead of a still image. + /// Defaults to false. + Q_PROPERTY(bool live READ live WRITE setLive NOTIFY liveChanged); + /// If true, the view has content ready to display. Content is not always immediately available, + /// and this property can be used to avoid displaying it until ready. + Q_PROPERTY(bool hasContent READ default NOTIFY hasContentChanged BINDABLE bindableHasContent); + /// The size of the source image. Valid when @@hasContent is true. + Q_PROPERTY(QSize sourceSize READ default NOTIFY sourceSizeChanged BINDABLE bindableSourceSize); + // clang-format on + +public: + explicit ScreencopyView(QQuickItem* parent = nullptr): QQuickItem(parent) {} + + void componentComplete() override; + + /// Capture a single frame. Has no effect if @@live is true. + Q_INVOKABLE void captureFrame(); + + [[nodiscard]] QObject* captureSource() const { return this->mCaptureSource; } + void setCaptureSource(QObject* captureSource); + + [[nodiscard]] bool paintCursors() const { return this->mPaintCursors; } + void setPaintCursors(bool paintCursors); + + [[nodiscard]] bool live() const { return this->mLive; } + void setLive(bool live); + + [[nodiscard]] QBindable bindableHasContent() { return &this->bHasContent; } + [[nodiscard]] QBindable bindableSourceSize() { return &this->bSourceSize; } + +signals: + /// The compositor has ended the video stream. Attempting to restart it may or may not work. + void stopped(); + + void captureSourceChanged(); + void paintCursorsChanged(); + void liveChanged(); + void hasContentChanged(); + void sourceSizeChanged(); + +protected: + QSGNode* updatePaintNode(QSGNode* oldNode, UpdatePaintNodeData* data) override; + +private slots: + void onCaptureSourceDestroyed(); + void onFrameCaptured(); + void destroyContextWithUpdate() { this->destroyContext(); } + void onBuffersReady(); + +private: + void destroyContext(bool update = true); + void createContext(); + + // clang-format off + Q_OBJECT_BINDABLE_PROPERTY(ScreencopyView, bool, bHasContent, &ScreencopyView::hasContentChanged); + Q_OBJECT_BINDABLE_PROPERTY(ScreencopyView, QSize, bSourceSize, &ScreencopyView::sourceSizeChanged); + // clang-format on + + QObject* mCaptureSource = nullptr; + bool mPaintCursors = false; + bool mLive = false; + ScreencopyContext* context = nullptr; + bool completed = false; +}; + +} // namespace qs::wayland::screencopy diff --git a/src/wayland/screencopy/wlr_screencopy/CMakeLists.txt b/src/wayland/screencopy/wlr_screencopy/CMakeLists.txt new file mode 100644 index 00000000..5829d915 --- /dev/null +++ b/src/wayland/screencopy/wlr_screencopy/CMakeLists.txt @@ -0,0 +1,14 @@ +qt_add_library(quickshell-wayland-screencopy-wlr STATIC + wlr_screencopy.cpp +) + +wl_proto(wlp-wlr-screencopy wlr-screencopy-unstable-v1 "${CMAKE_CURRENT_SOURCE_DIR}") + +target_link_libraries(quickshell-wayland-screencopy-wlr PRIVATE + Qt::WaylandClient Qt::WaylandClientPrivate wayland-client + Qt::Quick # for pch +) + +target_link_libraries(quickshell-wayland-screencopy-wlr PUBLIC wlp-wlr-screencopy) + +qs_pch(quickshell-wayland-screencopy-wlr SET large) diff --git a/src/wayland/screencopy/wlr_screencopy/wlr-screencopy-unstable-v1.xml b/src/wayland/screencopy/wlr_screencopy/wlr-screencopy-unstable-v1.xml new file mode 100644 index 00000000..50b1b7d2 --- /dev/null +++ b/src/wayland/screencopy/wlr_screencopy/wlr-screencopy-unstable-v1.xml @@ -0,0 +1,232 @@ + + + + Copyright © 2018 Simon Ser + Copyright © 2019 Andri Yngvason + + Permission is hereby granted, free of charge, to any person obtaining a + copy of this software and associated documentation files (the "Software"), + to deal in the Software without restriction, including without limitation + the rights to use, copy, modify, merge, publish, distribute, sublicense, + and/or sell copies of the Software, and to permit persons to whom the + Software is furnished to do so, subject to the following conditions: + + The above copyright notice and this permission notice (including the next + paragraph) shall be included in all copies or substantial portions of the + Software. + + THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL + THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING + FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER + DEALINGS IN THE SOFTWARE. + + + + This protocol allows clients to ask the compositor to copy part of the + screen content to a client buffer. + + Warning! The protocol described in this file is experimental and + backward incompatible changes may be made. Backward compatible changes + may be added together with the corresponding interface version bump. + Backward incompatible changes are done by bumping the version number in + the protocol and interface names and resetting the interface version. + Once the protocol is to be declared stable, the 'z' prefix and the + version number in the protocol and interface names are removed and the + interface version number is reset. + + + + + This object is a manager which offers requests to start capturing from a + source. + + + + + Capture the next frame of an entire output. + + + + + + + + + Capture the next frame of an output's region. + + The region is given in output logical coordinates, see + xdg_output.logical_size. The region will be clipped to the output's + extents. + + + + + + + + + + + + + All objects created by the manager will still remain valid, until their + appropriate destroy request has been called. + + + + + + + This object represents a single frame. + + When created, a series of buffer events will be sent, each representing a + supported buffer type. The "buffer_done" event is sent afterwards to + indicate that all supported buffer types have been enumerated. The client + will then be able to send a "copy" request. If the capture is successful, + the compositor will send a "flags" followed by a "ready" event. + + For objects version 2 or lower, wl_shm buffers are always supported, ie. + the "buffer" event is guaranteed to be sent. + + If the capture failed, the "failed" event is sent. This can happen anytime + before the "ready" event. + + Once either a "ready" or a "failed" event is received, the client should + destroy the frame. + + + + + Provides information about wl_shm buffer parameters that need to be + used for this frame. This event is sent once after the frame is created + if wl_shm buffers are supported. + + + + + + + + + + Copy the frame to the supplied buffer. The buffer must have a the + correct size, see zwlr_screencopy_frame_v1.buffer and + zwlr_screencopy_frame_v1.linux_dmabuf. The buffer needs to have a + supported format. + + If the frame is successfully copied, a "flags" and a "ready" events are + sent. Otherwise, a "failed" event is sent. + + + + + + + + + + + + + + + + Provides flags about the frame. This event is sent once before the + "ready" event. + + + + + + + Called as soon as the frame is copied, indicating it is available + for reading. This event includes the time at which presentation happened + at. + + The timestamp is expressed as tv_sec_hi, tv_sec_lo, tv_nsec triples, + each component being an unsigned 32-bit value. Whole seconds are in + tv_sec which is a 64-bit value combined from tv_sec_hi and tv_sec_lo, + and the additional fractional part in tv_nsec as nanoseconds. Hence, + for valid timestamps tv_nsec must be in [0, 999999999]. The seconds part + may have an arbitrary offset at start. + + After receiving this event, the client should destroy the object. + + + + + + + + + This event indicates that the attempted frame copy has failed. + + After receiving this event, the client should destroy the object. + + + + + + Destroys the frame. This request can be sent at any time by the client. + + + + + + + Same as copy, except it waits until there is damage to copy. + + + + + + + This event is sent right before the ready event when copy_with_damage is + requested. It may be generated multiple times for each copy_with_damage + request. + + The arguments describe a box around an area that has changed since the + last copy request that was derived from the current screencopy manager + instance. + + The union of all regions received between the call to copy_with_damage + and a ready event is the total damage since the prior ready event. + + + + + + + + + + + Provides information about linux-dmabuf buffer parameters that need to + be used for this frame. This event is sent once after the frame is + created if linux-dmabuf buffers are supported. + + + + + + + + + This event is sent once after all buffer events have been sent. + + The client should proceed to create a buffer of one of the supported + types, and send a "copy" request. + + + + diff --git a/src/wayland/screencopy/wlr_screencopy/wlr_screencopy.cpp b/src/wayland/screencopy/wlr_screencopy/wlr_screencopy.cpp new file mode 100644 index 00000000..8cc89bca --- /dev/null +++ b/src/wayland/screencopy/wlr_screencopy/wlr_screencopy.cpp @@ -0,0 +1,133 @@ +#include "wlr_screencopy.hpp" +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +#include "../../buffer/manager.hpp" +#include "../manager.hpp" +#include "wlr_screencopy_p.hpp" + +namespace qs::wayland::screencopy::wlr { + +namespace { +Q_LOGGING_CATEGORY(logScreencopy, "quickshell.wayland.screencopy.wlr", QtWarningMsg); +} + +WlrScreencopyManager::WlrScreencopyManager(): QWaylandClientExtensionTemplate(3) { + this->initialize(); +} + +WlrScreencopyManager* WlrScreencopyManager::instance() { + static auto* instance = new WlrScreencopyManager(); + return instance; +} + +ScreencopyContext* +WlrScreencopyManager::captureOutput(QScreen* screen, bool paintCursors, QRect region) { + if (!dynamic_cast(screen->handle())) return nullptr; + return new WlrScreencopyContext(this, screen, paintCursors, region); +} + +WlrScreencopyContext::WlrScreencopyContext( + WlrScreencopyManager* manager, + QScreen* screen, + bool paintCursors, + QRect region +) + : manager(manager) + , screen(dynamic_cast(screen->handle())) + , paintCursors(paintCursors) + , region(region) { + QObject::connect(screen, &QObject::destroyed, this, &WlrScreencopyContext::onScreenDestroyed); +} + +WlrScreencopyContext::~WlrScreencopyContext() { + if (this->object()) this->destroy(); +} + +void WlrScreencopyContext::onScreenDestroyed() { + qCWarning(logScreencopy) << "Screen destroyed while recording. Stopping" << this; + if (this->object()) this->destroy(); + emit this->stopped(); +} + +void WlrScreencopyContext::captureFrame() { + if (this->object()) return; + + if (this->region.isEmpty()) { + this->init(manager->capture_output(this->paintCursors ? 1 : 0, screen->output())); + } else { + this->init(manager->capture_output_region( + this->paintCursors ? 1 : 0, + screen->output(), + this->region.x(), + this->region.y(), + this->region.width(), + this->region.height() + )); + } +} + +void WlrScreencopyContext::zwlr_screencopy_frame_v1_buffer( + uint32_t format, + uint32_t width, + uint32_t height, + uint32_t /*stride*/ +) { + // While different sizes can technically be requested, that would be insane. + this->request.width = width; + this->request.height = height; + this->request.shm.formats.push(format); +} + +void WlrScreencopyContext::zwlr_screencopy_frame_v1_linux_dmabuf( + uint32_t format, + uint32_t width, + uint32_t height +) { + // While different sizes can technically be requested, that would be insane. + this->request.width = width; + this->request.height = height; + this->request.dmabuf.formats.push(format); +} + +void WlrScreencopyContext::zwlr_screencopy_frame_v1_flags(uint32_t flags) { + if (flags & ZWLR_SCREENCOPY_FRAME_V1_FLAGS_Y_INVERT) { + this->mSwapchain.backbuffer()->transform = buffer::WlBufferTransform::Flipped180; + } +} + +void WlrScreencopyContext::zwlr_screencopy_frame_v1_buffer_done() { + auto* backbuffer = this->mSwapchain.createBackbuffer(this->request); + + if (this->copiedFirstFrame) { + this->copy_with_damage(backbuffer->buffer()); + } else { + this->copy(backbuffer->buffer()); + } +} + +void WlrScreencopyContext::zwlr_screencopy_frame_v1_ready( + uint32_t /*tvSecHi*/, + uint32_t /*tvSecLo*/, + uint32_t /*tvNsec*/ +) { + this->destroy(); + this->copiedFirstFrame = true; + this->mSwapchain.swapBuffers(); + emit this->frameCaptured(); +} + +void WlrScreencopyContext::zwlr_screencopy_frame_v1_failed() { + qCWarning(logScreencopy) << "Ending recording due to screencopy failure for" << this; + emit this->stopped(); +} + +} // namespace qs::wayland::screencopy::wlr diff --git a/src/wayland/screencopy/wlr_screencopy/wlr_screencopy.hpp b/src/wayland/screencopy/wlr_screencopy/wlr_screencopy.hpp new file mode 100644 index 00000000..bea17332 --- /dev/null +++ b/src/wayland/screencopy/wlr_screencopy/wlr_screencopy.hpp @@ -0,0 +1,25 @@ +#pragma once + +#include +#include +#include + +#include "../manager.hpp" + +namespace qs::wayland::screencopy::wlr { + +class WlrScreencopyManager + : public QWaylandClientExtensionTemplate + , public QtWayland::zwlr_screencopy_manager_v1 { +public: + ScreencopyContext* captureOutput(QScreen* screen, bool paintCursors, QRect region = QRect()); + + static WlrScreencopyManager* instance(); + +private: + explicit WlrScreencopyManager(); + + friend class WlrScreencopyContext; +}; + +} // namespace qs::wayland::screencopy::wlr diff --git a/src/wayland/screencopy/wlr_screencopy/wlr_screencopy_p.hpp b/src/wayland/screencopy/wlr_screencopy/wlr_screencopy_p.hpp new file mode 100644 index 00000000..7bdbafb7 --- /dev/null +++ b/src/wayland/screencopy/wlr_screencopy/wlr_screencopy_p.hpp @@ -0,0 +1,51 @@ +#pragma once + +#include +#include +#include + +#include "../manager.hpp" + +namespace qs::wayland::screencopy::wlr { + +class WlrScreencopyManager; + +class WlrScreencopyContext + : public ScreencopyContext + , public QtWayland::zwlr_screencopy_frame_v1 { +public: + explicit WlrScreencopyContext( + WlrScreencopyManager* manager, + QScreen* screen, + bool paintCursors, + QRect region + ); + ~WlrScreencopyContext() override; + Q_DISABLE_COPY_MOVE(WlrScreencopyContext); + + void captureFrame() override; + +protected: + // clang-format off + void zwlr_screencopy_frame_v1_buffer(uint32_t format, uint32_t width, uint32_t height, uint32_t stride) override; + void zwlr_screencopy_frame_v1_linux_dmabuf(uint32_t format, uint32_t width, uint32_t height) override; + void zwlr_screencopy_frame_v1_flags(uint32_t flags) override; + void zwlr_screencopy_frame_v1_buffer_done() override; + void zwlr_screencopy_frame_v1_ready(uint32_t tvSecHi, uint32_t tvSecLo, uint32_t tvNsec) override; + void zwlr_screencopy_frame_v1_failed() override; + // clang-format on + +private slots: + void onScreenDestroyed(); + +private: + WlrScreencopyManager* manager; + buffer::WlBufferRequest request; + bool copiedFirstFrame = false; + + QtWaylandClient::QWaylandScreen* screen; + bool paintCursors; + QRect region; +}; + +} // namespace qs::wayland::screencopy::wlr diff --git a/src/wayland/toplevel_management/qml.hpp b/src/wayland/toplevel_management/qml.hpp index 127b4c8f..20347343 100644 --- a/src/wayland/toplevel_management/qml.hpp +++ b/src/wayland/toplevel_management/qml.hpp @@ -13,7 +13,7 @@ namespace qs::wayland::toplevel_management { namespace impl { -class ToplevelManager; +class ToplevelManager; // NOLINT class ToplevelHandle; } // namespace impl @@ -80,6 +80,8 @@ public: [[nodiscard]] bool fullscreen() const; void setFullscreen(bool fullscreen); + [[nodiscard]] impl::ToplevelHandle* implHandle() const { return this->handle; } + signals: void closed(); void appIdChanged(); From 8b6aa624a294248c352f95e159ebe664e9f282a6 Mon Sep 17 00:00:00 2001 From: Richard Bainesly Date: Thu, 9 Jan 2025 16:06:40 -0500 Subject: [PATCH 12/31] fix fd leaks in scanPath use auto --- src/core/desktopentry.cpp | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/core/desktopentry.cpp b/src/core/desktopentry.cpp index 975db3b4..063aacd6 100644 --- a/src/core/desktopentry.cpp +++ b/src/core/desktopentry.cpp @@ -309,7 +309,7 @@ void DesktopEntryManager::scanPath(const QDir& dir, const QString& prefix) { auto entries = dir.entryInfoList(QDir::Dirs | QDir::Files | QDir::NoDotAndDotDot); for (auto& entry: entries) { - if (entry.isDir()) this->scanPath(entry.path(), prefix + dir.dirName() + "-"); + if (entry.isDir()) this->scanPath(entry.absoluteFilePath(), prefix + dir.dirName() + "-"); else if (entry.isFile()) { auto path = entry.filePath(); if (!path.endsWith(".desktop")) { @@ -317,9 +317,8 @@ void DesktopEntryManager::scanPath(const QDir& dir, const QString& prefix) { continue; } - auto* file = new QFile(path); - - if (!file->open(QFile::ReadOnly)) { + auto file = QFile(path); + if (!file.open(QFile::ReadOnly)) { qCDebug(logDesktopEntry) << "Could not open file" << path; continue; } @@ -327,7 +326,7 @@ void DesktopEntryManager::scanPath(const QDir& dir, const QString& prefix) { auto id = prefix + entry.fileName().sliced(0, entry.fileName().length() - 8); auto lowerId = id.toLower(); - auto text = QString::fromUtf8(file->readAll()); + auto text = QString::fromUtf8(file.readAll()); auto* dentry = new DesktopEntry(id, this); dentry->parseEntry(text); From 6d8022b709ac74f27bbdcb652bc803daf510e9e6 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Tue, 14 Jan 2025 15:08:29 -0800 Subject: [PATCH 13/31] service/pipewire: add registry and node ready properties --- src/services/pipewire/core.cpp | 26 ++++++++++++++++++++++++++ src/services/pipewire/core.hpp | 23 +++++++++++++++-------- src/services/pipewire/node.cpp | 22 ++++++++++++++++++++++ src/services/pipewire/node.hpp | 6 ++++++ src/services/pipewire/qml.cpp | 16 ++++++++++++++++ src/services/pipewire/qml.hpp | 18 ++++++++++++++++++ src/services/pipewire/registry.cpp | 23 +++++++++++++++++++++++ src/services/pipewire/registry.hpp | 11 +++++++++++ 8 files changed, 137 insertions(+), 8 deletions(-) diff --git a/src/services/pipewire/core.cpp b/src/services/pipewire/core.cpp index c325bb33..9c2a3dbf 100644 --- a/src/services/pipewire/core.cpp +++ b/src/services/pipewire/core.cpp @@ -10,6 +10,7 @@ #include #include #include +#include #include #include @@ -19,6 +20,19 @@ namespace { Q_LOGGING_CATEGORY(logLoop, "quickshell.service.pipewire.loop", QtWarningMsg); } +const pw_core_events PwCore::EVENTS = { + .version = PW_VERSION_CORE_EVENTS, + .info = nullptr, + .done = &PwCore::onSync, + .ping = nullptr, + .error = nullptr, + .remove_id = nullptr, + .bound_id = nullptr, + .add_mem = nullptr, + .remove_mem = nullptr, + .bound_props = nullptr, +}; + PwCore::PwCore(QObject* parent): QObject(parent), notifier(QSocketNotifier::Read) { qCInfo(logLoop) << "Creating pipewire event loop."; pw_init(nullptr, nullptr); @@ -42,6 +56,8 @@ PwCore::PwCore(QObject* parent): QObject(parent), notifier(QSocketNotifier::Read return; } + pw_core_add_listener(this->core, &this->listener.hook, &PwCore::EVENTS, this); + qCInfo(logLoop) << "Linking pipewire event loop."; // Tie the pw event loop into qt. auto fd = pw_loop_get_fd(this->loop); @@ -79,6 +95,16 @@ void PwCore::poll() { emit this->polled(); } +qint32 PwCore::sync(quint32 id) const { + // Seq param doesn't seem to do anything. Seq is instead the returned value. + return pw_core_sync(this->core, id, 0); +} + +void PwCore::onSync(void* data, quint32 id, qint32 seq) { + auto* self = static_cast(data); + emit self->synced(id, seq); +} + SpaHook::SpaHook() { // NOLINT spa_zero(this->hook); } diff --git a/src/services/pipewire/core.hpp b/src/services/pipewire/core.hpp index 49728eeb..262e2d31 100644 --- a/src/services/pipewire/core.hpp +++ b/src/services/pipewire/core.hpp @@ -14,6 +14,14 @@ namespace qs::service::pipewire { +class SpaHook { +public: + explicit SpaHook(); + + void remove(); + spa_hook hook; +}; + class PwCore: public QObject { Q_OBJECT; @@ -23,6 +31,7 @@ public: Q_DISABLE_COPY_MOVE(PwCore); [[nodiscard]] bool isValid() const; + [[nodiscard]] qint32 sync(quint32 id) const; pw_loop* loop = nullptr; pw_context* context = nullptr; @@ -30,12 +39,18 @@ public: signals: void polled(); + void synced(quint32 id, qint32 seq); private slots: void poll(); private: + static const pw_core_events EVENTS; + + static void onSync(void* data, quint32 id, qint32 seq); + QSocketNotifier notifier; + SpaHook listener; }; template @@ -49,12 +64,4 @@ public: T* object; }; -class SpaHook { -public: - explicit SpaHook(); - - void remove(); - spa_hook hook; -}; - } // namespace qs::service::pipewire diff --git a/src/services/pipewire/node.cpp b/src/services/pipewire/node.cpp index ffb8c164..21815b82 100644 --- a/src/services/pipewire/node.cpp +++ b/src/services/pipewire/node.cpp @@ -24,6 +24,8 @@ #include #include +#include "connection.hpp" +#include "core.hpp" #include "device.hpp" namespace qs::service::pipewire { @@ -92,6 +94,12 @@ void PwNode::bindHooks() { } void PwNode::unbindHooks() { + if (this->ready) { + this->ready = false; + emit this->readyChanged(); + } + + this->syncSeq = 0; this->listener.remove(); this->routeDevice = -1; this->properties.clear(); @@ -201,6 +209,20 @@ void PwNode::onInfo(void* data, const pw_node_info* info) { if (self->boundData != nullptr) { self->boundData->onInfo(info); } + + if (!self->ready && !self->syncSeq) { + auto* core = PwConnection::instance()->registry.core; + QObject::connect(core, &PwCore::synced, self, &PwNode::onCoreSync); + self->syncSeq = core->sync(self->id); + } +} + +void PwNode::onCoreSync(quint32 id, qint32 seq) { + if (id != this->id || seq != this->syncSeq) return; + qCInfo(logNode) << "Completed initial sync for" << this; + this->ready = true; + this->syncSeq = 0; + emit this->readyChanged(); } void PwNode::onParam( diff --git a/src/services/pipewire/node.hpp b/src/services/pipewire/node.hpp index 697a54e0..a18abccf 100644 --- a/src/services/pipewire/node.hpp +++ b/src/services/pipewire/node.hpp @@ -172,6 +172,7 @@ public: PwNodeType type = PwNodeType::Untracked; bool isSink = false; bool isStream = false; + bool ready = false; PwNodeBoundData* boundData = nullptr; @@ -180,6 +181,10 @@ public: signals: void propertiesChanged(); + void readyChanged(); + +private slots: + void onCoreSync(quint32 id, qint32 seq); private: static const pw_node_events EVENTS; @@ -187,6 +192,7 @@ private: static void onParam(void* data, qint32 seq, quint32 id, quint32 index, quint32 next, const spa_pod* param); + qint32 syncSeq = 0; SpaHook listener; }; diff --git a/src/services/pipewire/qml.cpp b/src/services/pipewire/qml.cpp index be50ec6e..6eef238b 100644 --- a/src/services/pipewire/qml.cpp +++ b/src/services/pipewire/qml.cpp @@ -2,6 +2,7 @@ #include #include +#include #include #include #include @@ -87,6 +88,16 @@ Pipewire::Pipewire(QObject* parent): QObject(parent) { this, &Pipewire::defaultConfiguredAudioSourceChanged ); + + if (!connection->registry.isInitialized()) { + QObject::connect( + &connection->registry, + &PwRegistry::initialized, + this, + &Pipewire::readyChanged, + Qt::SingleShotConnection + ); + } } ObjectModel* Pipewire::nodes() { return &this->mNodes; } @@ -156,6 +167,8 @@ void Pipewire::setDefaultConfiguredAudioSource(PwNodeIface* node) { PwConnection::instance()->defaults.changeConfiguredSource(node ? node->node() : nullptr); } +bool Pipewire::isReady() { return PwConnection::instance()->registry.isInitialized(); } + PwNodeIface* PwNodeLinkTracker::node() const { return this->mNode; } void PwNodeLinkTracker::setNode(PwNodeIface* node) { @@ -298,6 +311,7 @@ void PwNodeAudioIface::setVolumes(const QVector& volumes) { PwNodeIface::PwNodeIface(PwNode* node): PwObjectIface(node), mNode(node) { QObject::connect(node, &PwNode::propertiesChanged, this, &PwNodeIface::propertiesChanged); + QObject::connect(node, &PwNode::readyChanged, this, &PwNodeIface::readyChanged); if (auto* audioBoundData = dynamic_cast(node->boundData)) { this->audioIface = new PwNodeAudioIface(audioBoundData, this); @@ -318,6 +332,8 @@ bool PwNodeIface::isSink() const { return this->mNode->isSink; } bool PwNodeIface::isStream() const { return this->mNode->isStream; } +bool PwNodeIface::isReady() const { return this->mNode->ready; } + QVariantMap PwNodeIface::properties() const { auto map = QVariantMap(); for (auto [k, v]: this->mNode->properties.asKeyValueRange()) { diff --git a/src/services/pipewire/qml.hpp b/src/services/pipewire/qml.hpp index 675b923b..6313a42b 100644 --- a/src/services/pipewire/qml.hpp +++ b/src/services/pipewire/qml.hpp @@ -116,6 +116,13 @@ class Pipewire: public QObject { /// /// See @@defaultAudioSource for the current default source, regardless of preference. Q_PROPERTY(qs::service::pipewire::PwNodeIface* preferredDefaultAudioSource READ defaultConfiguredAudioSource WRITE setDefaultConfiguredAudioSource NOTIFY defaultConfiguredAudioSourceChanged); + /// This property is true if quickshell has completed its initial sync with + /// the pipewire server. If true, nodes, links and sync/source preferences will be + /// in a good state. + /// + /// > [!NOTE] You can use the pipewire object before it is ready, but some nodes/links + /// > may be missing, and preference metadata may be null. + Q_PROPERTY(bool ready READ isReady NOTIFY readyChanged); // clang-format on QML_ELEMENT; QML_SINGLETON; @@ -136,6 +143,8 @@ public: [[nodiscard]] PwNodeIface* defaultConfiguredAudioSource() const; static void setDefaultConfiguredAudioSource(PwNodeIface* node); + [[nodiscard]] static bool isReady(); + signals: void defaultAudioSinkChanged(); void defaultAudioSourceChanged(); @@ -143,6 +152,8 @@ signals: void defaultConfiguredAudioSinkChanged(); void defaultConfiguredAudioSourceChanged(); + void readyChanged(); + private slots: void onNodeAdded(PwNode* node); void onNodeRemoved(QObject* object); @@ -294,6 +305,11 @@ class PwNodeIface: public PwObjectIface { /// The presence or absence of this property can be used to determine if a node /// manages audio, regardless of if it is bound. If non null, the node is an audio node. Q_PROPERTY(qs::service::pipewire::PwNodeAudioIface* audio READ audio CONSTANT); + /// True if the node is fully bound and ready to use. + /// + /// > [!NOTE] The node may be used before it is fully bound, but some data + /// > may be missing or incorrect. + Q_PROPERTY(bool ready READ isReady NOTIFY readyChanged); QML_NAMED_ELEMENT(PwNode); QML_UNCREATABLE("PwNodes cannot be created directly"); @@ -307,6 +323,7 @@ public: [[nodiscard]] QString nickname() const; [[nodiscard]] bool isSink() const; [[nodiscard]] bool isStream() const; + [[nodiscard]] bool isReady() const; [[nodiscard]] QVariantMap properties() const; [[nodiscard]] PwNodeAudioIface* audio() const; @@ -314,6 +331,7 @@ public: signals: void propertiesChanged(); + void readyChanged(); private: PwNode* mNode; diff --git a/src/services/pipewire/registry.cpp b/src/services/pipewire/registry.cpp index 04bd9ace..d2967d03 100644 --- a/src/services/pipewire/registry.cpp +++ b/src/services/pipewire/registry.cpp @@ -126,6 +126,29 @@ void PwRegistry::init(PwCore& core) { this->core = &core; this->object = pw_core_get_registry(core.core, PW_VERSION_REGISTRY, 0); pw_registry_add_listener(this->object, &this->listener.hook, &PwRegistry::EVENTS, this); + + QObject::connect(this->core, &PwCore::synced, this, &PwRegistry::onCoreSync); + + qCDebug(logRegistry) << "Registry created. Sending core sync for initial object tracking."; + this->coreSyncSeq = this->core->sync(PW_ID_CORE); +} + +void PwRegistry::onCoreSync(quint32 id, qint32 seq) { + if (id != PW_ID_CORE || seq != this->coreSyncSeq) return; + + switch (this->initState) { + case InitState::SendingObjects: + qCDebug(logRegistry) << "Initial sync for objects received. Syncing for metadata binding."; + this->coreSyncSeq = this->core->sync(PW_ID_CORE); + this->initState = InitState::Binding; + break; + case InitState::Binding: + qCInfo(logRegistry) << "Initial state sync complete."; + this->initState = InitState::Done; + emit this->initialized(); + break; + default: break; + } } const pw_registry_events PwRegistry::EVENTS = { diff --git a/src/services/pipewire/registry.hpp b/src/services/pipewire/registry.hpp index be282a20..f1ba9610 100644 --- a/src/services/pipewire/registry.hpp +++ b/src/services/pipewire/registry.hpp @@ -116,6 +116,8 @@ class PwRegistry public: void init(PwCore& core); + [[nodiscard]] bool isInitialized() const { return this->initState == InitState::Done; } + //QHash clients; QHash metadata; QHash nodes; @@ -132,9 +134,11 @@ signals: void linkAdded(PwLink* link); void linkGroupAdded(PwLinkGroup* group); void metadataAdded(PwMetadata* metadata); + void initialized(); private slots: void onLinkGroupDestroyed(QObject* object); + void onCoreSync(quint32 id, qint32 seq); private: static const pw_registry_events EVENTS; @@ -152,6 +156,13 @@ private: void addLinkToGroup(PwLink* link); + enum class InitState : quint8 { + SendingObjects, + Binding, + Done + } initState = InitState::SendingObjects; + + qint32 coreSyncSeq = 0; SpaHook listener; }; From 6024c374928aa2679f7a21b0410358ff8dd19e28 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Tue, 14 Jan 2025 15:21:22 -0800 Subject: [PATCH 14/31] core/scriptmodel: improve docs --- src/core/scriptmodel.hpp | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/src/core/scriptmodel.hpp b/src/core/scriptmodel.hpp index b57456b3..10a42d6b 100644 --- a/src/core/scriptmodel.hpp +++ b/src/core/scriptmodel.hpp @@ -36,6 +36,8 @@ /// delegate: // ... /// } /// ``` +/// [QAbstractItemModel]: https://doc.qt.io/qt-6/qabstractitemmodel.html +/// [Data Model]: https://doc.qt.io/qt-6/qtquick-modelviewsdata-modelview.html#qml-data-models class ScriptModel: public QAbstractListModel { Q_OBJECT; /// The list of values to reflect in the model. @@ -51,8 +53,19 @@ class ScriptModel: public QAbstractListModel { /// > } /// > ``` /// > - /// > Note that we are using @@DesktopEntries.values because it will cause @@ScriptModel.values + /// > Note that we are using @@ObjectModel.values because it will cause @@ScriptModel.values /// > to receive an update on change. + /// + /// > [!TIP] Most lists exposed by Quickshell are read-only. Some operations like `sort()` + /// > act on a list in-place and cannot be used directly on a list exposed by Quickshell. + /// > You can copy a list using spread syntax: `[...variable]` instead of `variable`. + /// > + /// > For example: + /// > ```qml + /// > ScriptModel { + /// > values: [...DesktopEntries.applications.values].sort(...) + /// > } + /// > ``` Q_PROPERTY(QVariantList values READ values WRITE setValues NOTIFY valuesChanged); QML_ELEMENT; From c2ed5bf559fcde7a634b2dfaf3981335a4e24499 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Wed, 15 Jan 2025 02:47:14 -0800 Subject: [PATCH 15/31] core/stacklist: add tests --- src/core/stacklist.hpp | 16 ++++++- src/core/test/CMakeLists.txt | 1 + src/core/test/stacklist.cpp | 92 ++++++++++++++++++++++++++++++++++++ src/core/test/stacklist.hpp | 15 ++++++ 4 files changed, 123 insertions(+), 1 deletion(-) create mode 100644 src/core/test/stacklist.cpp create mode 100644 src/core/test/stacklist.hpp diff --git a/src/core/stacklist.hpp b/src/core/stacklist.hpp index 7e9ee788..41dc58ee 100644 --- a/src/core/stacklist.hpp +++ b/src/core/stacklist.hpp @@ -7,6 +7,7 @@ #include #include +#include #include template @@ -40,13 +41,24 @@ public: [[nodiscard]] bool operator==(const StackList& other) const { if (other.size != this->size) return false; - for (size_t i = 0; i < this->size; ++i) { + for (size_t i = 0; i != this->size; ++i) { if (this->operator[](i) != other[i]) return false; } return true; } + [[nodiscard]] QList toList() const { + QList list; + list.reserve(this->size); + + for (const auto& entry: *this) { + list.push_back(entry); + } + + return list; + } + template struct BaseIterator { using iterator_category = std::bidirectional_iterator_tag; @@ -65,6 +77,7 @@ public: ++this->i; return *static_cast(this); } + Self& operator--() { --this->i; return *static_cast(this); @@ -75,6 +88,7 @@ public: this->operator++(); return v; } + Self operator--(int) { auto v = *this; this->operator--(); diff --git a/src/core/test/CMakeLists.txt b/src/core/test/CMakeLists.txt index d38c2868..bb49192d 100644 --- a/src/core/test/CMakeLists.txt +++ b/src/core/test/CMakeLists.txt @@ -7,3 +7,4 @@ endfunction() qs_test(transformwatcher transformwatcher.cpp) qs_test(ringbuffer ringbuf.cpp) qs_test(scriptmodel scriptmodel.cpp) +qs_test(stacklist stacklist.cpp) diff --git a/src/core/test/stacklist.cpp b/src/core/test/stacklist.cpp new file mode 100644 index 00000000..9b981729 --- /dev/null +++ b/src/core/test/stacklist.cpp @@ -0,0 +1,92 @@ +#include "stacklist.hpp" +#include + +#include +#include +#include + +#include "../stacklist.hpp" + +void TestStackList::push() { + StackList list; + + list.push(1); + list.push(2); + + QCOMPARE_EQ(list.toList(), QList({1, 2})); + QCOMPARE_EQ(list.length(), 2); +} + +void TestStackList::pushAndGrow() { + StackList list; + + list.push(1); + list.push(2); + list.push(3); + list.push(4); + + QCOMPARE_EQ(list.toList(), QList({1, 2, 3, 4})); + QCOMPARE_EQ(list.length(), 4); +} + +void TestStackList::copy() { + StackList list; + + list.push(1); + list.push(2); + list.push(3); + list.push(4); + + QCOMPARE_EQ(list.toList(), QList({1, 2, 3, 4})); + QCOMPARE_EQ(list.length(), 4); + + auto list2 = list; + + QCOMPARE_EQ(list2.toList(), QList({1, 2, 3, 4})); + QCOMPARE_EQ(list2.length(), 4); + QCOMPARE_EQ(list2, list); +} + +void TestStackList::viewVla() { + StackList list; + + list.push(1); + list.push(2); + + QCOMPARE_EQ(list.toList(), QList({1, 2})); + QCOMPARE_EQ(list.length(), 2); + + STACKLIST_VLA_VIEW(int, list, listView); + + QList ql; + + for (size_t i = 0; i != list.length(); ++i) { + ql.push_back(listView[i]); // NOLINT + } + + QCOMPARE_EQ(ql, list.toList()); +} + +void TestStackList::viewVlaGrown() { + StackList list; + + list.push(1); + list.push(2); + list.push(3); + list.push(4); + + QCOMPARE_EQ(list.toList(), QList({1, 2, 3, 4})); + QCOMPARE_EQ(list.length(), 4); + + STACKLIST_VLA_VIEW(int, list, listView); + + QList ql; + + for (size_t i = 0; i != list.length(); ++i) { + ql.push_back(listView[i]); // NOLINT + } + + QCOMPARE_EQ(ql, list.toList()); +} + +QTEST_MAIN(TestStackList); diff --git a/src/core/test/stacklist.hpp b/src/core/test/stacklist.hpp new file mode 100644 index 00000000..f582761d --- /dev/null +++ b/src/core/test/stacklist.hpp @@ -0,0 +1,15 @@ +#pragma once + +#include +#include + +class TestStackList: public QObject { + Q_OBJECT; + +private slots: + static void push(); + static void pushAndGrow(); + static void copy(); + static void viewVla(); + static void viewVlaGrown(); +}; From ca79715ccee0d5cd06c4459ce76e889f6631c929 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Wed, 15 Jan 2025 02:52:08 -0800 Subject: [PATCH 16/31] wayland/screencopy: log more information during buffer creation --- src/wayland/buffer/dmabuf.cpp | 40 +--------------------------------- src/wayland/buffer/dmabuf.hpp | 40 ++++++++++++++++++++++++++++++++++ src/wayland/buffer/manager.cpp | 25 ++++++++++++++++++++- src/wayland/buffer/shm.cpp | 2 ++ src/wayland/buffer/shm.hpp | 2 -- 5 files changed, 67 insertions(+), 42 deletions(-) diff --git a/src/wayland/buffer/dmabuf.cpp b/src/wayland/buffer/dmabuf.cpp index 47167020..c6c7724f 100644 --- a/src/wayland/buffer/dmabuf.cpp +++ b/src/wayland/buffer/dmabuf.cpp @@ -25,7 +25,6 @@ #include #include #include -#include #include #include #include @@ -48,42 +47,7 @@ Q_LOGGING_CATEGORY(logDmabuf, "quickshell.wayland.buffer.dmabuf", QtWarningMsg); LinuxDmabufManager* MANAGER = nullptr; // NOLINT -class FourCCStr { -public: - explicit FourCCStr(uint32_t code) - : chars( - {static_cast(code >> 0 & 0xff), - static_cast(code >> 8 & 0xff), - static_cast(code >> 16 & 0xff), - static_cast(code >> 24 & 0xff), - '\0'} - ) { - for (auto i = 3; i != 0; i--) { - if (chars[i] == ' ') chars[i] = '\0'; - else break; - } - } - - [[nodiscard]] const char* cStr() const { return this->chars.data(); } - -private: - std::array chars {}; -}; - -class FourCCModStr { -public: - explicit FourCCModStr(uint64_t code): drmStr(drmGetFormatModifierName(code)) {} - ~FourCCModStr() { - if (this->drmStr) drmFree(this->drmStr); - } - - Q_DISABLE_COPY_MOVE(FourCCModStr); - - [[nodiscard]] const char* cStr() const { return this->drmStr; } - -private: - char* drmStr; -}; +} // namespace QDebug& operator<<(QDebug& debug, const FourCCStr& fourcc) { debug << fourcc.cStr(); @@ -95,8 +59,6 @@ QDebug& operator<<(QDebug& debug, const FourCCModStr& fourcc) { return debug; } -} // namespace - QDebug& operator<<(QDebug& debug, const WlDmaBuffer* buffer) { auto saver = QDebugStateSaver(debug); debug.nospace(); diff --git a/src/wayland/buffer/dmabuf.hpp b/src/wayland/buffer/dmabuf.hpp index 97b5576f..a05e82a1 100644 --- a/src/wayland/buffer/dmabuf.hpp +++ b/src/wayland/buffer/dmabuf.hpp @@ -17,6 +17,7 @@ #include #include #include +#include #include "manager.hpp" #include "qsg.hpp" @@ -28,6 +29,45 @@ class WlBufferManagerPrivate; namespace qs::wayland::buffer::dmabuf { class LinuxDmabufManager; +class FourCCStr { +public: + explicit FourCCStr(uint32_t code) + : chars( + {static_cast(code >> 0 & 0xff), + static_cast(code >> 8 & 0xff), + static_cast(code >> 16 & 0xff), + static_cast(code >> 24 & 0xff), + '\0'} + ) { + for (auto i = 3; i != 0; i--) { + if (chars[i] == ' ') chars[i] = '\0'; + else break; + } + } + + [[nodiscard]] const char* cStr() const { return this->chars.data(); } + +private: + std::array chars {}; +}; + +class FourCCModStr { +public: + explicit FourCCModStr(uint64_t code): drmStr(drmGetFormatModifierName(code)) {} + ~FourCCModStr() { + if (this->drmStr) drmFree(this->drmStr); + } + + Q_DISABLE_COPY_MOVE(FourCCModStr); + + [[nodiscard]] const char* cStr() const { return this->drmStr; } + +private: + char* drmStr; +}; + +QDebug& operator<<(QDebug& debug, const FourCCStr& fourcc); +QDebug& operator<<(QDebug& debug, const FourCCModStr& fourcc); class GbmDeviceHandle { public: diff --git a/src/wayland/buffer/manager.cpp b/src/wayland/buffer/manager.cpp index dde71a88..4c2e267e 100644 --- a/src/wayland/buffer/manager.cpp +++ b/src/wayland/buffer/manager.cpp @@ -17,6 +17,10 @@ namespace qs::wayland::buffer { +namespace { +Q_LOGGING_CATEGORY(logBuffer, "quickshell.wayland.buffer", QtWarningMsg); +} + WlBuffer* WlBufferSwapchain::createBackbuffer(const WlBufferRequest& request, bool* newBuffer) { auto& buffer = this->presentSecondBuffer ? this->buffer1 : this->buffer2; @@ -42,9 +46,28 @@ bool WlBufferManager::isReady() const { return this->p->mReady; } [[nodiscard]] WlBuffer* WlBufferManager::createBuffer(const WlBufferRequest& request) { static const bool dmabufDisabled = qEnvironmentVariableIsSet("QS_DISABLE_DMABUF"); + qCDebug(logBuffer).nospace() << "Creating buffer from request at " << request.width << 'x' + << request.height; + qCDebug(logBuffer).nospace() << " Dmabuf requests on device " << request.dmabuf.device + << " (disabled: " << dmabufDisabled << ')'; + + for (const auto& [format, modifiers]: request.dmabuf.formats) { + qCDebug(logBuffer) << " Format" << dmabuf::FourCCStr(format); + + for (const auto& modifier: modifiers) { + qCDebug(logBuffer) << " Explicit Modifier" << dmabuf::FourCCModStr(modifier); + } + } + + qCDebug(logBuffer).nospace() << " Shm requests"; + + for (const auto& format: request.shm.formats) { + qCDebug(logBuffer) << " Format" << format; + } + if (!dmabufDisabled) { if (auto* buf = this->p->dmabuf.createDmabuf(request)) return buf; - qCWarning(shm::logShm) << "DMA buffer creation failed, falling back to SHM."; + qCWarning(logBuffer) << "DMA buffer creation failed, falling back to SHM."; } return shm::ShmbufManager::createShmbuf(request); diff --git a/src/wayland/buffer/shm.cpp b/src/wayland/buffer/shm.cpp index 8973cdfb..59a8e914 100644 --- a/src/wayland/buffer/shm.cpp +++ b/src/wayland/buffer/shm.cpp @@ -17,7 +17,9 @@ namespace qs::wayland::buffer::shm { +namespace { Q_LOGGING_CATEGORY(logShm, "quickshell.wayland.buffer.shm", QtWarningMsg); +} bool WlShmBuffer::isCompatible(const WlBufferRequest& request) const { if (QSize(static_cast(request.width), static_cast(request.height)) != this->size()) { diff --git a/src/wayland/buffer/shm.hpp b/src/wayland/buffer/shm.hpp index 12af26e3..f3597cbc 100644 --- a/src/wayland/buffer/shm.hpp +++ b/src/wayland/buffer/shm.hpp @@ -14,8 +14,6 @@ namespace qs::wayland::buffer::shm { -Q_DECLARE_LOGGING_CATEGORY(logShm); - class WlShmBuffer: public WlBuffer { public: ~WlShmBuffer() override; From d195ca76806bae53cf5ac6d5dda95b49f069373c Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Wed, 15 Jan 2025 03:24:19 -0800 Subject: [PATCH 17/31] wayland/screencopy: fix UAF in dmabuf modifier collection The QList optimization the code was for no longer exists. --- src/wayland/buffer/dmabuf.cpp | 12 ------------ 1 file changed, 12 deletions(-) diff --git a/src/wayland/buffer/dmabuf.cpp b/src/wayland/buffer/dmabuf.cpp index c6c7724f..09abc15f 100644 --- a/src/wayland/buffer/dmabuf.cpp +++ b/src/wayland/buffer/dmabuf.cpp @@ -167,7 +167,6 @@ void LinuxDmabufFeedback::zwp_linux_dmabuf_feedback_v1_tranche_formats(wl_array* auto indexTableLength = indices->size / sizeof(uint16_t); uint32_t lastFormat = 0; - LinuxDmabufModifiers* lastModifiers = nullptr; LinuxDmabufModifiers* modifiers = nullptr; for (uint16_t ti = 0; ti != indexTableLength; ++ti) { @@ -176,14 +175,7 @@ void LinuxDmabufFeedback::zwp_linux_dmabuf_feedback_v1_tranche_formats(wl_array* // Compositors usually send a single format's modifiers as a block. if (!modifiers || entry.format != lastFormat) { - // We can often share modifier lists between formats - if (lastModifiers && modifiers->modifiers == lastModifiers->modifiers) { - // avoids storing a second list - modifiers->modifiers = lastModifiers->modifiers; - } - lastFormat = entry.format; - lastModifiers = modifiers; auto modifiersIter = std::ranges::find_if(tranche.formats.formats, [&](const auto& pair) { return pair.first == entry.format; @@ -203,10 +195,6 @@ void LinuxDmabufFeedback::zwp_linux_dmabuf_feedback_v1_tranche_formats(wl_array* modifiers->modifiers.push(entry.modifier); } } - - if (lastModifiers && modifiers && modifiers->modifiers == lastModifiers->modifiers) { - modifiers->modifiers = lastModifiers->modifiers; - } } void LinuxDmabufFeedback::zwp_linux_dmabuf_feedback_v1_tranche_done() { From d6b58521e992ff9f751af6c991890e17d9a96df4 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Sun, 19 Jan 2025 01:00:03 -0800 Subject: [PATCH 18/31] core!: fix typo in ShellScreen.primaryOrientation --- src/core/qmlscreen.hpp | 2 +- .../image_copy_capture/image_copy_capture_p.hpp | 10 ++++------ 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/src/core/qmlscreen.hpp b/src/core/qmlscreen.hpp index 69c0762d..c74d3b2c 100644 --- a/src/core/qmlscreen.hpp +++ b/src/core/qmlscreen.hpp @@ -40,7 +40,7 @@ class QuickshellScreenInfo: public QObject { /// The ratio between physical pixels and device-independent (scaled) pixels. Q_PROPERTY(qreal devicePixelRatio READ devicePixelRatio NOTIFY physicalPixelDensityChanged); Q_PROPERTY(Qt::ScreenOrientation orientation READ orientation NOTIFY orientationChanged); - Q_PROPERTY(Qt::ScreenOrientation primatyOrientation READ primaryOrientation NOTIFY primaryOrientationChanged); + Q_PROPERTY(Qt::ScreenOrientation primaryOrientation READ primaryOrientation NOTIFY primaryOrientationChanged); // clang-format on public: diff --git a/src/wayland/screencopy/image_copy_capture/image_copy_capture_p.hpp b/src/wayland/screencopy/image_copy_capture/image_copy_capture_p.hpp index 14f20675..e89af4a5 100644 --- a/src/wayland/screencopy/image_copy_capture/image_copy_capture_p.hpp +++ b/src/wayland/screencopy/image_copy_capture/image_copy_capture_p.hpp @@ -23,21 +23,19 @@ public: void captureFrame() override; protected: - // clang-formt off + // clang-format off void ext_image_copy_capture_session_v1_buffer_size(uint32_t width, uint32_t height) override; void ext_image_copy_capture_session_v1_shm_format(uint32_t format) override; void ext_image_copy_capture_session_v1_dmabuf_device(wl_array* device) override; - void - ext_image_copy_capture_session_v1_dmabuf_format(uint32_t format, wl_array* modifiers) override; + void ext_image_copy_capture_session_v1_dmabuf_format(uint32_t format, wl_array* modifiers) override; void ext_image_copy_capture_session_v1_done() override; void ext_image_copy_capture_session_v1_stopped() override; void ext_image_copy_capture_frame_v1_transform(uint32_t transform) override; - void ext_image_copy_capture_frame_v1_damage(int32_t x, int32_t y, int32_t width, int32_t height) - override; + void ext_image_copy_capture_frame_v1_damage(int32_t x, int32_t y, int32_t width, int32_t height) override; void ext_image_copy_capture_frame_v1_ready() override; void ext_image_copy_capture_frame_v1_failed(uint32_t reason) override; - // clang-formt on + // clang-format on private: void clearOldState(); From 6464ead0f120da20ecd12122d6181e294813f40a Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Mon, 20 Jan 2025 00:51:56 -0800 Subject: [PATCH 19/31] core/window: move input mask handling + commit scheduling to polish --- src/core/region.cpp | 12 +++++++ src/core/region.hpp | 6 ++++ src/wayland/hyprland/surface/qml.cpp | 4 +-- src/wayland/util.cpp | 12 ++----- src/wayland/util.hpp | 4 +-- src/window/proxywindow.cpp | 49 ++++++++++++++++++---------- src/window/proxywindow.hpp | 21 +++++++++++- 7 files changed, 76 insertions(+), 32 deletions(-) diff --git a/src/core/region.cpp b/src/core/region.cpp index 47f15d4a..439cfbd2 100644 --- a/src/core/region.cpp +++ b/src/core/region.cpp @@ -8,6 +8,7 @@ #include #include #include +#include PendingRegion::PendingRegion(QObject* parent): QObject(parent) { QObject::connect(this, &PendingRegion::shapeChanged, this, &PendingRegion::changed); @@ -105,8 +106,19 @@ QRegion PendingRegion::applyTo(QRegion& region) const { return region; } +QRegion PendingRegion::applyTo(const QRect& rect) const { + // if left as the default, dont combine it with the whole rect area, leave it as is. + if (this->mIntersection == Intersection::Combine) { + return this->build(); + } else { + auto baseRegion = QRegion(rect); + return this->applyTo(baseRegion); + } +} + void PendingRegion::regionsAppend(QQmlListProperty* prop, PendingRegion* region) { auto* self = static_cast(prop->object); // NOLINT + if (!region) return; QObject::connect(region, &QObject::destroyed, self, &PendingRegion::onChildDestroyed); QObject::connect(region, &PendingRegion::changed, self, &PendingRegion::childrenChanged); diff --git a/src/core/region.hpp b/src/core/region.hpp index 02d7a26b..6637d7bd 100644 --- a/src/core/region.hpp +++ b/src/core/region.hpp @@ -96,6 +96,7 @@ public: [[nodiscard]] bool empty() const; [[nodiscard]] QRegion build() const; [[nodiscard]] QRegion applyTo(QRegion& region) const; + [[nodiscard]] QRegion applyTo(const QRect& rect) const; RegionShape::Enum mShape = RegionShape::Rect; Intersection::Enum mIntersection = Intersection::Combine; @@ -109,6 +110,11 @@ signals: void widthChanged(); void heightChanged(); void childrenChanged(); + + /// Triggered when the region's geometry changes. + /// + /// In some cases the region does not update automatically. + /// In those cases you can emit this signal manually. void changed(); private slots: diff --git a/src/wayland/hyprland/surface/qml.cpp b/src/wayland/hyprland/surface/qml.cpp index ab8aace8..5150487f 100644 --- a/src/wayland/hyprland/surface/qml.cpp +++ b/src/wayland/hyprland/surface/qml.cpp @@ -62,7 +62,7 @@ void HyprlandWindow::setOpacity(qreal opacity) { if (this->surface) { this->surface->setOpacity(opacity); - qs::wayland::util::scheduleCommit(this->mWaylandWindow); + qs::wayland::util::scheduleCommit(this->proxyWindow); } emit this->opacityChanged(); @@ -127,7 +127,7 @@ void HyprlandWindow::onWaylandSurfaceCreated() { if (this->mOpacity != 1.0) { this->surface->setOpacity(this->mOpacity); - qs::wayland::util::scheduleCommit(this->mWaylandWindow); + qs::wayland::util::scheduleCommit(this->proxyWindow); } } diff --git a/src/wayland/util.cpp b/src/wayland/util.cpp index 6bce2621..abcf8a4d 100644 --- a/src/wayland/util.cpp +++ b/src/wayland/util.cpp @@ -1,17 +1,9 @@ #include "util.hpp" -#include -#include +#include "../window/proxywindow.hpp" namespace qs::wayland::util { -void scheduleCommit(QtWaylandClient::QWaylandWindow* window) { - // This seems to be one of the less offensive ways to force Qt to send a wl_surface.commit on its own terms. - // Ideally we would trigger the commit more directly. - QWindowSystemInterface::handleExposeEvent( - window->window(), - QRect(QPoint(), window->geometry().size()) - ); -} +void scheduleCommit(ProxyWindowBase* window) { window->schedulePolish(); } } // namespace qs::wayland::util diff --git a/src/wayland/util.hpp b/src/wayland/util.hpp index 7967fadc..aadf44f1 100644 --- a/src/wayland/util.hpp +++ b/src/wayland/util.hpp @@ -1,9 +1,9 @@ #pragma once -#include +#include "../window/proxywindow.hpp" namespace qs::wayland::util { -void scheduleCommit(QtWaylandClient::QWaylandWindow* window); +void scheduleCommit(ProxyWindowBase* window); } diff --git a/src/window/proxywindow.cpp b/src/window/proxywindow.cpp index 608883ae..d89a6344 100644 --- a/src/window/proxywindow.cpp +++ b/src/window/proxywindow.cpp @@ -28,15 +28,16 @@ ProxyWindowBase::ProxyWindowBase(QObject* parent) : Reloadable(parent) - , mContentItem(new QQuickItem()) { + , mContentItem(new ProxyWindowContentItem()) { QQmlEngine::setObjectOwnership(this->mContentItem, QQmlEngine::CppOwnership); this->mContentItem->setParent(this); // clang-format off + QObject::connect(this->mContentItem, &ProxyWindowContentItem::polished, this, &ProxyWindowBase::onPolished); + QObject::connect(this, &ProxyWindowBase::widthChanged, this, &ProxyWindowBase::onWidthChanged); QObject::connect(this, &ProxyWindowBase::heightChanged, this, &ProxyWindowBase::onHeightChanged); - QObject::connect(this, &ProxyWindowBase::maskChanged, this, &ProxyWindowBase::onMaskChanged); QObject::connect(this, &ProxyWindowBase::widthChanged, this, &ProxyWindowBase::onMaskChanged); QObject::connect(this, &ProxyWindowBase::heightChanged, this, &ProxyWindowBase::onMaskChanged); @@ -264,6 +265,12 @@ void ProxyWindowBase::setVisibleDirect(bool visible) { } } +void ProxyWindowBase::schedulePolish() { + if (this->isVisibleDirect()) { + this->mContentItem->polish(); + } +} + void ProxyWindowBase::polishItems() { // Due to QTBUG-126704, layouts in invisible windows don't update their dimensions. // Usually this isn't an issue, but it is when the size of a window is based on the size @@ -385,11 +392,11 @@ void ProxyWindowBase::setMask(PendingRegion* mask) { this->mMask = mask; if (mask != nullptr) { - mask->setParent(this); QObject::connect(mask, &QObject::destroyed, this, &ProxyWindowBase::onMaskDestroyed); - QObject::connect(mask, &PendingRegion::changed, this, &ProxyWindowBase::maskChanged); + QObject::connect(mask, &PendingRegion::changed, this, &ProxyWindowBase::onMaskChanged); } + this->onMaskChanged(); emit this->maskChanged(); } @@ -410,23 +417,13 @@ void ProxyWindowBase::onMaskChanged() { void ProxyWindowBase::onMaskDestroyed() { this->mMask = nullptr; + this->onMaskChanged(); emit this->maskChanged(); } void ProxyWindowBase::updateMask() { - QRegion mask; - if (this->mMask != nullptr) { - // if left as the default, dont combine it with the whole window area, leave it as is. - if (this->mMask->mIntersection == Intersection::Combine) { - mask = this->mMask->build(); - } else { - auto windowRegion = QRegion(QRect(0, 0, this->width(), this->height())); - mask = this->mMask->applyTo(windowRegion); - } - } - - this->window->setFlag(Qt::WindowTransparentForInput, this->mMask != nullptr && mask.isEmpty()); - this->window->setMask(mask); + this->pendingPolish.inputMask = true; + this->schedulePolish(); } QQmlListProperty ProxyWindowBase::data() { @@ -463,3 +460,21 @@ void ProxiedWindow::exposeEvent(QExposeEvent* event) { this->QQuickWindow::exposeEvent(event); emit this->exposed(); } + +void ProxyWindowContentItem::updatePolish() { emit this->polished(); } + +void ProxyWindowBase::onPolished() { + if (this->pendingPolish.inputMask) { + QRegion mask; + if (this->mMask != nullptr) { + mask = this->mMask->applyTo(QRect(0, 0, this->width(), this->height())); + } + + this->window->setFlag(Qt::WindowTransparentForInput, this->mMask != nullptr && mask.isEmpty()); + this->window->setMask(mask); + + this->pendingPolish.inputMask = false; + } + + emit this->polished(); +} diff --git a/src/window/proxywindow.hpp b/src/window/proxywindow.hpp index 2ed4bcd3..6c7946b1 100644 --- a/src/window/proxywindow.hpp +++ b/src/window/proxywindow.hpp @@ -20,6 +20,7 @@ #include "windowinterface.hpp" class ProxiedWindow; +class ProxyWindowContentItem; // Proxy to an actual window exposing a limited property set with the ability to // transfer it to a new window. @@ -85,6 +86,8 @@ public: virtual void setVisible(bool visible); virtual void setVisibleDirect(bool visible); + void schedulePolish(); + [[nodiscard]] virtual qint32 x() const; [[nodiscard]] virtual qint32 y() const; @@ -124,6 +127,7 @@ signals: void colorChanged(); void maskChanged(); void surfaceFormatChanged(); + void polished(); protected slots: virtual void onWidthChanged(); @@ -131,6 +135,7 @@ protected slots: void onMaskChanged(); void onMaskDestroyed(); void onScreenDestroyed(); + void onPolished(); void runLints(); protected: @@ -141,12 +146,16 @@ protected: QColor mColor = Qt::white; PendingRegion* mMask = nullptr; ProxiedWindow* window = nullptr; - QQuickItem* mContentItem = nullptr; + ProxyWindowContentItem* mContentItem = nullptr; bool reloadComplete = false; bool ranLints = false; QsSurfaceFormat qsSurfaceFormat; QSurfaceFormat mSurfaceFormat; + struct { + bool inputMask : 1 = false; + } pendingPolish; + private: void polishItems(); void updateMask(); @@ -190,3 +199,13 @@ protected: private: ProxyWindowBase* mProxy; }; + +class ProxyWindowContentItem: public QQuickItem { + Q_OBJECT; + +signals: + void polished(); + +protected: + void updatePolish() override; +}; From bc73d35d03b00627d41616addf310e32af324597 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Mon, 20 Jan 2025 15:53:04 -0800 Subject: [PATCH 20/31] wayland/screencopy: fix ScreencopyContext leak in ScreencopyView Also caused an FD leak. --- src/wayland/screencopy/view.cpp | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/wayland/screencopy/view.cpp b/src/wayland/screencopy/view.cpp index fe517352..fe744fd8 100644 --- a/src/wayland/screencopy/view.cpp +++ b/src/wayland/screencopy/view.cpp @@ -66,6 +66,8 @@ void ScreencopyView::createContext() { return; } + this->context->setParent(this); + QObject::connect( this->context, &ScreencopyContext::stopped, From b336129c34991968e4c741085a4160826e23c62e Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Wed, 22 Jan 2025 03:33:46 -0800 Subject: [PATCH 21/31] core/window: add QsWindow.devicePixelRatio --- src/wayland/wlr_layershell.cpp | 4 ++++ src/wayland/wlr_layershell.hpp | 2 ++ src/window/floatingwindow.cpp | 4 ++++ src/window/floatingwindow.hpp | 2 ++ src/window/proxywindow.cpp | 17 +++++++++++++++++ src/window/proxywindow.hpp | 6 ++++++ src/window/windowinterface.hpp | 9 +++++++++ src/x11/panel_window.cpp | 2 ++ src/x11/panel_window.hpp | 2 ++ 9 files changed, 48 insertions(+) diff --git a/src/wayland/wlr_layershell.cpp b/src/wayland/wlr_layershell.cpp index 010d0f77..1a338522 100644 --- a/src/wayland/wlr_layershell.cpp +++ b/src/wayland/wlr_layershell.cpp @@ -192,6 +192,7 @@ WaylandPanelInterface::WaylandPanelInterface(QObject* parent) QObject::connect(this->layer, &ProxyWindowBase::backerVisibilityChanged, this, &WaylandPanelInterface::backingWindowVisibleChanged); QObject::connect(this->layer, &ProxyWindowBase::heightChanged, this, &WaylandPanelInterface::heightChanged); QObject::connect(this->layer, &ProxyWindowBase::widthChanged, this, &WaylandPanelInterface::widthChanged); + QObject::connect(this->layer, &ProxyWindowBase::devicePixelRatioChanged, this, &WaylandPanelInterface::devicePixelRatioChanged); QObject::connect(this->layer, &ProxyWindowBase::screenChanged, this, &WaylandPanelInterface::screenChanged); QObject::connect(this->layer, &ProxyWindowBase::windowTransformChanged, this, &WaylandPanelInterface::windowTransformChanged); QObject::connect(this->layer, &ProxyWindowBase::colorChanged, this, &WaylandPanelInterface::colorChanged); @@ -218,10 +219,13 @@ void WaylandPanelInterface::onReload(QObject* oldInstance) { QQmlListProperty WaylandPanelInterface::data() { return this->layer->data(); } ProxyWindowBase* WaylandPanelInterface::proxyWindow() const { return this->layer; } QQuickItem* WaylandPanelInterface::contentItem() const { return this->layer->contentItem(); } + bool WaylandPanelInterface::isBackingWindowVisible() const { return this->layer->isVisibleDirect(); } +qreal WaylandPanelInterface::devicePixelRatio() const { return this->layer->devicePixelRatio(); } + // NOLINTBEGIN #define proxyPair(type, get, set) \ type WaylandPanelInterface::get() const { return this->layer->get(); } \ diff --git a/src/wayland/wlr_layershell.hpp b/src/wayland/wlr_layershell.hpp index 32aeecdb..c3448706 100644 --- a/src/wayland/wlr_layershell.hpp +++ b/src/wayland/wlr_layershell.hpp @@ -146,6 +146,8 @@ public: [[nodiscard]] qint32 height() const override; void setHeight(qint32 height) override; + [[nodiscard]] virtual qreal devicePixelRatio() const override; + [[nodiscard]] QuickshellScreenInfo* screen() const override; void setScreen(QuickshellScreenInfo* screen) override; diff --git a/src/window/floatingwindow.cpp b/src/window/floatingwindow.cpp index 918b186d..761bc2d4 100644 --- a/src/window/floatingwindow.cpp +++ b/src/window/floatingwindow.cpp @@ -32,6 +32,7 @@ FloatingWindowInterface::FloatingWindowInterface(QObject* parent) QObject::connect(this->window, &ProxyWindowBase::backerVisibilityChanged, this, &FloatingWindowInterface::backingWindowVisibleChanged); QObject::connect(this->window, &ProxyWindowBase::heightChanged, this, &FloatingWindowInterface::heightChanged); QObject::connect(this->window, &ProxyWindowBase::widthChanged, this, &FloatingWindowInterface::widthChanged); + QObject::connect(this->window, &ProxyWindowBase::devicePixelRatioChanged, this, &FloatingWindowInterface::devicePixelRatioChanged); QObject::connect(this->window, &ProxyWindowBase::screenChanged, this, &FloatingWindowInterface::screenChanged); QObject::connect(this->window, &ProxyWindowBase::windowTransformChanged, this, &FloatingWindowInterface::windowTransformChanged); QObject::connect(this->window, &ProxyWindowBase::colorChanged, this, &FloatingWindowInterface::colorChanged); @@ -50,10 +51,13 @@ void FloatingWindowInterface::onReload(QObject* oldInstance) { QQmlListProperty FloatingWindowInterface::data() { return this->window->data(); } ProxyWindowBase* FloatingWindowInterface::proxyWindow() const { return this->window; } QQuickItem* FloatingWindowInterface::contentItem() const { return this->window->contentItem(); } + bool FloatingWindowInterface::isBackingWindowVisible() const { return this->window->isVisibleDirect(); } +qreal FloatingWindowInterface::devicePixelRatio() const { return this->window->devicePixelRatio(); } + // NOLINTBEGIN #define proxyPair(type, get, set) \ type FloatingWindowInterface::get() const { return this->window->get(); } \ diff --git a/src/window/floatingwindow.hpp b/src/window/floatingwindow.hpp index 36f933b4..7dd0d4ed 100644 --- a/src/window/floatingwindow.hpp +++ b/src/window/floatingwindow.hpp @@ -42,6 +42,8 @@ public: [[nodiscard]] qint32 height() const override; void setHeight(qint32 height) override; + [[nodiscard]] virtual qreal devicePixelRatio() const override; + [[nodiscard]] QuickshellScreenInfo* screen() const override; void setScreen(QuickshellScreenInfo* screen) override; diff --git a/src/window/proxywindow.cpp b/src/window/proxywindow.cpp index d89a6344..f75fc641 100644 --- a/src/window/proxywindow.cpp +++ b/src/window/proxywindow.cpp @@ -1,6 +1,7 @@ #include "proxywindow.hpp" #include +#include #include #include #include @@ -191,6 +192,7 @@ void ProxyWindowBase::connectWindow() { 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::runLints); + QObject::connect(this->window, &ProxiedWindow::devicePixelRatioChanged, this, &ProxyWindowBase::devicePixelRatioChanged); // clang-format on } @@ -213,6 +215,7 @@ void ProxyWindowBase::completeWindow() { emit this->yChanged(); emit this->widthChanged(); emit this->heightChanged(); + emit this->devicePixelRatioChanged(); this->mContentItem->setParentItem(this->window->contentItem()); this->mContentItem->setWidth(this->width()); @@ -411,6 +414,12 @@ void ProxyWindowBase::setSurfaceFormat(QsSurfaceFormat format) { emit this->surfaceFormatChanged(); } +qreal ProxyWindowBase::devicePixelRatio() const { + if (this->window != nullptr) return this->window->devicePixelRatio(); + if (this->mScreen != nullptr) return this->mScreen->devicePixelRatio(); + return 1.0; +} + void ProxyWindowBase::onMaskChanged() { if (this->window != nullptr) this->updateMask(); } @@ -456,6 +465,14 @@ void ProxyWindowAttached::setWindow(ProxyWindowBase* window) { emit this->windowChanged(); } +bool ProxiedWindow::event(QEvent* event) { + if (event->type() == QEvent::DevicePixelRatioChange) { + emit this->devicePixelRatioChanged(); + } + + return this->QQuickWindow::event(event); +} + 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 6c7946b1..6f02e05a 100644 --- a/src/window/proxywindow.hpp +++ b/src/window/proxywindow.hpp @@ -44,6 +44,7 @@ class ProxyWindowBase: public Reloadable { Q_PROPERTY(bool visible READ isVisible WRITE setVisible NOTIFY visibleChanged); Q_PROPERTY(qint32 width READ width WRITE setWidth NOTIFY widthChanged); Q_PROPERTY(qint32 height READ height WRITE setHeight NOTIFY heightChanged); + Q_PROPERTY(qreal devicePixelRatio READ devicePixelRatio NOTIFY devicePixelRatioChanged); Q_PROPERTY(QuickshellScreenInfo* screen READ screen WRITE setScreen NOTIFY screenChanged); Q_PROPERTY(QColor color READ color WRITE setColor NOTIFY colorChanged); Q_PROPERTY(PendingRegion* mask READ mask WRITE setMask NOTIFY maskChanged); @@ -97,6 +98,8 @@ public: [[nodiscard]] virtual qint32 height() const; virtual void setHeight(qint32 height); + [[nodiscard]] qreal devicePixelRatio() const; + [[nodiscard]] virtual QuickshellScreenInfo* screen() const; virtual void setScreen(QuickshellScreenInfo* screen); @@ -122,6 +125,7 @@ signals: void yChanged(); void widthChanged(); void heightChanged(); + void devicePixelRatioChanged(); void windowTransformChanged(); void screenChanged(); void colorChanged(); @@ -192,8 +196,10 @@ public: signals: void exposed(); + void devicePixelRatioChanged(); protected: + bool event(QEvent* event) override; void exposeEvent(QExposeEvent* event) override; private: diff --git a/src/window/windowinterface.hpp b/src/window/windowinterface.hpp index 8603de31..2aca7a76 100644 --- a/src/window/windowinterface.hpp +++ b/src/window/windowinterface.hpp @@ -55,6 +55,12 @@ class WindowInterface: public Reloadable { Q_PROPERTY(bool backingWindowVisible READ isBackingWindowVisible NOTIFY backingWindowVisibleChanged); Q_PROPERTY(qint32 width READ width WRITE setWidth NOTIFY widthChanged); Q_PROPERTY(qint32 height READ height WRITE setHeight NOTIFY heightChanged); + /// The ratio between logical pixels and monitor pixels. + /// + /// Qt's coordinate system works in logical pixels, which equal N monitor pixels + /// depending on scale factor. This property returns the amount of monitor pixels + /// in a logical pixel for the current window. + Q_PROPERTY(qreal devicePixelRatio READ devicePixelRatio NOTIFY devicePixelRatioChanged); /// The screen that the window currently occupies. /// /// This may be modified to move the window to the given screen. @@ -147,6 +153,8 @@ public: [[nodiscard]] virtual qint32 height() const = 0; virtual void setHeight(qint32 height) = 0; + [[nodiscard]] virtual qreal devicePixelRatio() const = 0; + [[nodiscard]] virtual QuickshellScreenInfo* screen() const = 0; virtual void setScreen(QuickshellScreenInfo* screen) = 0; @@ -171,6 +179,7 @@ signals: void backingWindowVisibleChanged(); void widthChanged(); void heightChanged(); + void devicePixelRatioChanged(); void screenChanged(); void windowTransformChanged(); void colorChanged(); diff --git a/src/x11/panel_window.cpp b/src/x11/panel_window.cpp index bda4aa54..ac182f76 100644 --- a/src/x11/panel_window.cpp +++ b/src/x11/panel_window.cpp @@ -478,6 +478,7 @@ XPanelInterface::XPanelInterface(QObject* parent) QObject::connect(this->panel, &ProxyWindowBase::backerVisibilityChanged, this, &XPanelInterface::backingWindowVisibleChanged); QObject::connect(this->panel, &ProxyWindowBase::heightChanged, this, &XPanelInterface::heightChanged); QObject::connect(this->panel, &ProxyWindowBase::widthChanged, this, &XPanelInterface::widthChanged); + QObject::connect(this->panel, &ProxyWindowBase::devicePixelRatioChanged, this, &XPanelInterface::devicePixelRatioChanged); QObject::connect(this->panel, &ProxyWindowBase::screenChanged, this, &XPanelInterface::screenChanged); QObject::connect(this->panel, &ProxyWindowBase::windowTransformChanged, this, &XPanelInterface::windowTransformChanged); QObject::connect(this->panel, &ProxyWindowBase::colorChanged, this, &XPanelInterface::colorChanged); @@ -505,6 +506,7 @@ QQmlListProperty XPanelInterface::data() { return this->panel->data(); ProxyWindowBase* XPanelInterface::proxyWindow() const { return this->panel; } QQuickItem* XPanelInterface::contentItem() const { return this->panel->contentItem(); } bool XPanelInterface::isBackingWindowVisible() const { return this->panel->isVisibleDirect(); } +qreal XPanelInterface::devicePixelRatio() const { return this->panel->devicePixelRatio(); } // NOLINTBEGIN #define proxyPair(type, get, set) \ diff --git a/src/x11/panel_window.hpp b/src/x11/panel_window.hpp index 12645589..11041f22 100644 --- a/src/x11/panel_window.hpp +++ b/src/x11/panel_window.hpp @@ -127,6 +127,8 @@ public: [[nodiscard]] qint32 height() const override; void setHeight(qint32 height) override; + [[nodiscard]] virtual qreal devicePixelRatio() const override; + [[nodiscard]] QuickshellScreenInfo* screen() const override; void setScreen(QuickshellScreenInfo* screen) override; From 3c7dfcb220bf41bd13993191ac7014d1a7525bf4 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Wed, 22 Jan 2025 04:14:55 -0800 Subject: [PATCH 22/31] hyprland/ipc: handle renameworkspace --- src/wayland/hyprland/ipc/connection.cpp | 16 ++++++++++++++++ src/wayland/hyprland/ipc/workspace.cpp | 8 ++++++++ src/wayland/hyprland/ipc/workspace.hpp | 5 ++++- 3 files changed, 28 insertions(+), 1 deletion(-) diff --git a/src/wayland/hyprland/ipc/connection.cpp b/src/wayland/hyprland/ipc/connection.cpp index 1e1af05a..794ecff6 100644 --- a/src/wayland/hyprland/ipc/connection.cpp +++ b/src/wayland/hyprland/ipc/connection.cpp @@ -352,6 +352,22 @@ void HyprlandIpc::onEvent(HyprlandIpcEvent* event) { auto* monitor = this->findMonitorByName(monitorName, true); workspace->setMonitor(monitor); + } else if (event->name == "renameworkspace") { + auto args = event->parseView(2); + auto id = args.at(0).toInt(); + auto name = QString::fromUtf8(args.at(1)); + + const auto& mList = this->mWorkspaces.valueList(); + + auto workspaceIter = + std::ranges::find_if(mList, [id](const HyprlandWorkspace* m) { return m->id() == id; }); + + if (workspaceIter == mList.end()) return; + + qCDebug(logHyprlandIpc) << "Workspace with id" << id << "renamed from" + << (*workspaceIter)->name() << "to" << name; + + (*workspaceIter)->setName(name); } } diff --git a/src/wayland/hyprland/ipc/workspace.cpp b/src/wayland/hyprland/ipc/workspace.cpp index fbf8477f..153dea6b 100644 --- a/src/wayland/hyprland/ipc/workspace.cpp +++ b/src/wayland/hyprland/ipc/workspace.cpp @@ -11,7 +11,15 @@ namespace qs::hyprland::ipc { qint32 HyprlandWorkspace::id() const { return this->mId; } + QString HyprlandWorkspace::name() const { return this->mName; } + +void HyprlandWorkspace::setName(QString name) { + if (name == this->mName) return; + this->mName = std::move(name); + emit this->nameChanged(); +} + QVariantMap HyprlandWorkspace::lastIpcObject() const { return this->mLastIpcObject; } void HyprlandWorkspace::updateInitial(qint32 id, QString name) { diff --git a/src/wayland/hyprland/ipc/workspace.hpp b/src/wayland/hyprland/ipc/workspace.hpp index dab01eb3..5eedfe22 100644 --- a/src/wayland/hyprland/ipc/workspace.hpp +++ b/src/wayland/hyprland/ipc/workspace.hpp @@ -32,11 +32,14 @@ public: void updateFromObject(QVariantMap object); [[nodiscard]] qint32 id() const; + [[nodiscard]] QString name() const; + void setName(QString name); + [[nodiscard]] QVariantMap lastIpcObject() const; - void setMonitor(HyprlandMonitor* monitor); [[nodiscard]] HyprlandMonitor* monitor() const; + void setMonitor(HyprlandMonitor* monitor); signals: void idChanged(); From 6a017d63d6c7f2abb0ff58c04a87dd027c011a6e Mon Sep 17 00:00:00 2001 From: Richard Bainesly Date: Sat, 18 Jan 2025 16:13:58 -0500 Subject: [PATCH 23/31] fix single quote parsing --- src/core/desktopentry.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/core/desktopentry.cpp b/src/core/desktopentry.cpp index 063aacd6..75a088d9 100644 --- a/src/core/desktopentry.cpp +++ b/src/core/desktopentry.cpp @@ -213,7 +213,7 @@ QVector DesktopEntry::parseExecString(const QString& execString) { currentArgument += c; escape = 0; - } else if (c == u'"') { + } else if (c == u'"' || c == u'\'') { parsingString = false; } else { currentArgument += c; @@ -229,7 +229,7 @@ QVector DesktopEntry::parseExecString(const QString& execString) { percent = false; } else if (c == '%') { percent = true; - } else if (c == u'"') { + } else if (c == u'"' || c == u'\'') { parsingString = true; } else if (c == u' ') { if (!currentArgument.isEmpty()) { From b73eff0e47c715c685f1604a4a5f7be6e1a46e59 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Wed, 22 Jan 2025 19:38:18 -0800 Subject: [PATCH 24/31] core/screen: add model and serial number properties --- src/core/qmlscreen.cpp | 18 ++++++++++++++++++ src/core/qmlscreen.hpp | 6 ++++++ 2 files changed, 24 insertions(+) diff --git a/src/core/qmlscreen.cpp b/src/core/qmlscreen.cpp index 34588b77..105b4f01 100644 --- a/src/core/qmlscreen.cpp +++ b/src/core/qmlscreen.cpp @@ -42,6 +42,24 @@ QString QuickshellScreenInfo::name() const { return this->screen->name(); } +QString QuickshellScreenInfo::model() const { + if (this->screen == nullptr) { + this->warnDangling(); + return "{ NULL SCREEN }"; + } + + return this->screen->model(); +} + +QString QuickshellScreenInfo::serialNumber() const { + if (this->screen == nullptr) { + this->warnDangling(); + return "{ NULL SCREEN }"; + } + + return this->screen->serialNumber(); +} + qint32 QuickshellScreenInfo::x() const { if (this->screen == nullptr) { this->warnDangling(); diff --git a/src/core/qmlscreen.hpp b/src/core/qmlscreen.hpp index c74d3b2c..5e978bc0 100644 --- a/src/core/qmlscreen.hpp +++ b/src/core/qmlscreen.hpp @@ -29,6 +29,10 @@ class QuickshellScreenInfo: public QObject { /// /// Usually something like `DP-1`, `HDMI-1`, `eDP-1`. Q_PROPERTY(QString name READ name CONSTANT); + /// The model of the screen as seen by the operating system. + Q_PROPERTY(QString model READ model CONSTANT); + /// The serial number of the screen as seen by the operating system. + Q_PROPERTY(QString serialNumber READ serialNumber CONSTANT); Q_PROPERTY(qint32 x READ x NOTIFY geometryChanged); Q_PROPERTY(qint32 y READ y NOTIFY geometryChanged); Q_PROPERTY(qint32 width READ width NOTIFY geometryChanged); @@ -49,6 +53,8 @@ public: bool operator==(QuickshellScreenInfo& other) const; [[nodiscard]] QString name() const; + [[nodiscard]] QString model() const; + [[nodiscard]] QString serialNumber() const; [[nodiscard]] qint32 x() const; [[nodiscard]] qint32 y() const; [[nodiscard]] qint32 width() const; From c6791cf1f2bf6cafa70faee138a244907dfc8ee6 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Wed, 22 Jan 2025 20:13:29 -0800 Subject: [PATCH 25/31] core/window: fix screen assignments being completely broken --- src/wayland/wlr_layershell.cpp | 2 +- src/window/proxywindow.cpp | 48 ++++++++++++++++------------------ src/window/proxywindow.hpp | 3 ++- 3 files changed, 26 insertions(+), 27 deletions(-) diff --git a/src/wayland/wlr_layershell.cpp b/src/wayland/wlr_layershell.cpp index 1a338522..a649603c 100644 --- a/src/wayland/wlr_layershell.cpp +++ b/src/wayland/wlr_layershell.cpp @@ -90,8 +90,8 @@ void WlrLayershell::setHeight(qint32 height) { } void WlrLayershell::setScreen(QuickshellScreenInfo* screen) { - this->ProxyWindowBase::setScreen(screen); this->ext->setUseWindowScreen(screen != nullptr); + this->ProxyWindowBase::setScreen(screen); } // NOLINTBEGIN diff --git a/src/window/proxywindow.cpp b/src/window/proxywindow.cpp index f75fc641..8ce80c20 100644 --- a/src/window/proxywindow.cpp +++ b/src/window/proxywindow.cpp @@ -3,6 +3,7 @@ #include #include #include +#include #include #include #include @@ -200,9 +201,6 @@ void ProxyWindowBase::completeWindow() { if (this->mScreen != nullptr && this->window->screen() != this->mScreen) { if (this->window->isVisible()) this->window->setVisible(false); this->window->setScreen(this->mScreen); - } else if (this->mScreen == nullptr) { - this->mScreen = this->window->screen(); - QObject::connect(this->mScreen, &QObject::destroyed, this, &ProxyWindowBase::onScreenDestroyed); } this->setWidth(this->mWidth); @@ -327,39 +325,39 @@ void ProxyWindowBase::setHeight(qint32 height) { void ProxyWindowBase::setScreen(QuickshellScreenInfo* screen) { auto* qscreen = screen == nullptr ? nullptr : screen->screen; - if (qscreen == this->mScreen) return; + auto newMScreen = this->mScreen != qscreen; - if (this->mScreen != nullptr) { + if (this->mScreen && newMScreen) { QObject::disconnect(this->mScreen, nullptr, this, nullptr); } - if (this->window == nullptr) { - emit this->screenChanged(); - } else { - auto reshow = this->isVisibleDirect(); - if (reshow) this->setVisibleDirect(false); - if (this->window != nullptr) this->window->setScreen(qscreen); - if (reshow) this->setVisibleDirect(true); + if (this->qscreen() != qscreen) { + this->mScreen = qscreen; + if (this->window == nullptr) { + emit this->screenChanged(); + } else if (qscreen) { + auto reshow = this->isVisibleDirect(); + if (reshow) this->setVisibleDirect(false); + if (this->window != nullptr) this->window->setScreen(qscreen); + if (reshow) this->setVisibleDirect(true); + } } - if (qscreen) this->mScreen = qscreen; - else this->mScreen = this->window->screen(); - - QObject::connect(this->mScreen, &QObject::destroyed, this, &ProxyWindowBase::onScreenDestroyed); + if (qscreen && newMScreen) { + QObject::connect(this->mScreen, &QObject::destroyed, this, &ProxyWindowBase::onScreenDestroyed); + } } void ProxyWindowBase::onScreenDestroyed() { this->mScreen = nullptr; } +QScreen* ProxyWindowBase::qscreen() const { + if (this->window) return this->window->screen(); + if (this->mScreen) return this->mScreen; + return QGuiApplication::primaryScreen(); +} + QuickshellScreenInfo* ProxyWindowBase::screen() const { - QScreen* qscreen = nullptr; - - if (this->window == nullptr) { - if (this->mScreen != nullptr) qscreen = this->mScreen; - } else { - qscreen = this->window->screen(); - } - - return QuickshellTracked::instance()->screenInfo(qscreen); + return QuickshellTracked::instance()->screenInfo(this->qscreen()); } QColor ProxyWindowBase::color() const { return this->mColor; } diff --git a/src/window/proxywindow.hpp b/src/window/proxywindow.hpp index 6f02e05a..92a85f4f 100644 --- a/src/window/proxywindow.hpp +++ b/src/window/proxywindow.hpp @@ -100,7 +100,8 @@ public: [[nodiscard]] qreal devicePixelRatio() const; - [[nodiscard]] virtual QuickshellScreenInfo* screen() const; + [[nodiscard]] QScreen* qscreen() const; + [[nodiscard]] QuickshellScreenInfo* screen() const; virtual void setScreen(QuickshellScreenInfo* screen); [[nodiscard]] QColor color() const; From cdaff2967f1db0e5ce377cc35b2f35cb9af9e8eb Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Wed, 22 Jan 2025 23:10:49 -0800 Subject: [PATCH 26/31] core/icon: stop reusing image ids (dbusmenu, notifications) Fixes issues caused by the QML engine caching old pixmaps using the same IDs as new ones, notably dbusmenu icons. --- src/core/imageprovider.cpp | 22 ++++++++++------- src/core/imageprovider.hpp | 21 ++++++++++++----- src/dbus/dbusmenu/dbusmenu.cpp | 26 ++++++++++----------- src/dbus/dbusmenu/dbusmenu.hpp | 25 ++++++++++---------- src/services/notifications/dbusimage.hpp | 18 +++++++++----- src/services/notifications/notification.cpp | 14 +++++------ src/services/notifications/notification.hpp | 5 ++-- src/services/status_notifier/item.cpp | 2 +- 8 files changed, 75 insertions(+), 58 deletions(-) diff --git a/src/core/imageprovider.cpp b/src/core/imageprovider.cpp index 256faaed..47f284c7 100644 --- a/src/core/imageprovider.cpp +++ b/src/core/imageprovider.cpp @@ -1,5 +1,6 @@ #include "imageprovider.hpp" +#include #include #include #include @@ -7,10 +8,14 @@ #include #include #include +#include namespace { +namespace { QMap liveImages; // NOLINT +quint32 handleIndex = 0; // NOLINT +} // namespace void parseReq(const QString& req, QString& target, QString& param) { auto splitIdx = req.indexOf('/'); @@ -24,14 +29,9 @@ void parseReq(const QString& req, QString& target, QString& param) { } // namespace -QsImageHandle::QsImageHandle(QQmlImageProviderBase::ImageType type, QObject* parent) - : QObject(parent) - , type(type) { - { - auto dbg = QDebug(&this->id); - dbg.nospace() << static_cast(this); - } - +QsImageHandle::QsImageHandle(QQmlImageProviderBase::ImageType type) + : type(type) + , id(QString::number(++handleIndex)) { liveImages.insert(this->id, this); } @@ -85,3 +85,9 @@ QsPixmapProvider::requestPixmap(const QString& id, QSize* size, const QSize& req return QPixmap(); } } + +QString QsIndexedImageHandle::url() const { + return this->QsImageHandle::url() % '/' % QString::number(this->changeIndex); +} + +void QsIndexedImageHandle::imageChanged() { ++this->changeIndex; } diff --git a/src/core/imageprovider.hpp b/src/core/imageprovider.hpp index 5ea7843d..8568d4f7 100644 --- a/src/core/imageprovider.hpp +++ b/src/core/imageprovider.hpp @@ -20,15 +20,13 @@ public: QPixmap requestPixmap(const QString& id, QSize* size, const QSize& requestedSize) override; }; -class QsImageHandle: public QObject { - Q_OBJECT; - +class QsImageHandle { public: - explicit QsImageHandle(QQmlImageProviderBase::ImageType type, QObject* parent = nullptr); - ~QsImageHandle() override; + explicit QsImageHandle(QQmlImageProviderBase::ImageType type); + virtual ~QsImageHandle(); Q_DISABLE_COPY_MOVE(QsImageHandle); - [[nodiscard]] QString url() const; + [[nodiscard]] virtual QString url() const; virtual QImage requestImage(const QString& id, QSize* size, const QSize& requestedSize); virtual QPixmap requestPixmap(const QString& id, QSize* size, const QSize& requestedSize); @@ -37,3 +35,14 @@ private: QQmlImageProviderBase::ImageType type; QString id; }; + +class QsIndexedImageHandle: public QsImageHandle { +public: + explicit QsIndexedImageHandle(QQmlImageProviderBase::ImageType type): QsImageHandle(type) {} + + [[nodiscard]] QString url() const override; + void imageChanged(); + +private: + quint32 changeIndex = 0; +}; diff --git a/src/dbus/dbusmenu/dbusmenu.cpp b/src/dbus/dbusmenu/dbusmenu.cpp index 0267af8e..2b633b76 100644 --- a/src/dbus/dbusmenu/dbusmenu.cpp +++ b/src/dbus/dbusmenu/dbusmenu.cpp @@ -59,8 +59,8 @@ QString DBusMenuItem::icon() const { this->iconName, this->menu->iconThemePath.value().join(':') ); - } else if (this->image != nullptr) { - return this->image->url(); + } else if (this->image.hasData()) { + return this->image.url(); } else return nullptr; } @@ -113,7 +113,7 @@ void DBusMenuItem::updateProperties(const QVariantMap& properties, const QString auto originalEnabled = this->mEnabled; auto originalVisible = this->visible; auto originalIconName = this->iconName; - auto* originalImage = this->image; + auto imageChanged = false; auto originalIsSeparator = this->mSeparator; auto originalButtonType = this->mButtonType; auto originalToggleState = this->mCheckState; @@ -173,12 +173,16 @@ void DBusMenuItem::updateProperties(const QVariantMap& properties, const QString if (iconData.canConvert()) { auto data = iconData.value(); if (data.isEmpty()) { - this->image = nullptr; - } else if (this->image == nullptr || this->image->data != data) { - this->image = new DBusMenuPngImage(data, this); + imageChanged = this->image.hasData(); + this->image.data.clear(); + } else if (!this->image.hasData() || this->image.data != data) { + imageChanged = true; + this->image.data = data; + this->image.imageChanged(); } } else if (removed.isEmpty() || removed.contains("icon-data")) { - this->image = nullptr; + imageChanged = this->image.hasData(); + image.data.clear(); } auto type = properties.value("type"); @@ -239,17 +243,13 @@ void DBusMenuItem::updateProperties(const QVariantMap& properties, const QString if (this->mSeparator != originalIsSeparator) emit this->isSeparatorChanged(); if (this->displayChildren != originalDisplayChildren) emit this->hasChildrenChanged(); - if (this->iconName != originalIconName || this->image != originalImage) { - if (this->image != originalImage) { - delete originalImage; - } - + if (this->iconName != originalIconName || imageChanged) { emit this->iconChanged(); } qCDebug(logDbusMenu).nospace() << "Updated properties of " << this << " { label=" << this->mText << ", enabled=" << this->mEnabled << ", visible=" << this->visible - << ", iconName=" << this->iconName << ", iconData=" << this->image + << ", iconName=" << this->iconName << ", iconData=" << &this->image << ", separator=" << this->mSeparator << ", toggleType=" << this->mButtonType << ", toggleState=" << this->mCheckState diff --git a/src/dbus/dbusmenu/dbusmenu.hpp b/src/dbus/dbusmenu/dbusmenu.hpp index 35afa98e..1a8b399e 100644 --- a/src/dbus/dbusmenu/dbusmenu.hpp +++ b/src/dbus/dbusmenu/dbusmenu.hpp @@ -30,7 +30,17 @@ namespace qs::dbus::dbusmenu { using menu::QsMenuEntry; class DBusMenu; -class DBusMenuPngImage; +class DBusMenuItem; + +class DBusMenuPngImage: public QsIndexedImageHandle { +public: + explicit DBusMenuPngImage(): QsIndexedImageHandle(QQuickImageProvider::Image) {} + + [[nodiscard]] bool hasData() const { return !data.isEmpty(); } + QImage requestImage(const QString& id, QSize* size, const QSize& requestedSize) override; + + QByteArray data; +}; ///! Menu item shared by an external program. /// Menu item shared by an external program via the @@ -93,7 +103,7 @@ private: bool visible = true; bool mSeparator = false; QString iconName; - DBusMenuPngImage* image = nullptr; + DBusMenuPngImage image; menu::QsMenuButtonType::Enum mButtonType = menu::QsMenuButtonType::None; Qt::CheckState mCheckState = Qt::Unchecked; bool displayChildren = false; @@ -156,17 +166,6 @@ private: QDebug operator<<(QDebug debug, DBusMenu* menu); -class DBusMenuPngImage: public QsImageHandle { -public: - explicit DBusMenuPngImage(QByteArray data, DBusMenuItem* parent) - : QsImageHandle(QQuickImageProvider::Image, parent) - , data(std::move(data)) {} - - QImage requestImage(const QString& id, QSize* size, const QSize& requestedSize) override; - - QByteArray data; -}; - class DBusMenuHandle; QDebug operator<<(QDebug debug, const DBusMenuHandle* handle); diff --git a/src/services/notifications/dbusimage.hpp b/src/services/notifications/dbusimage.hpp index d81d1e74..c310d95e 100644 --- a/src/services/notifications/dbusimage.hpp +++ b/src/services/notifications/dbusimage.hpp @@ -1,7 +1,5 @@ #pragma once -#include - #include #include #include @@ -23,14 +21,22 @@ struct DBusNotificationImage { const QDBusArgument& operator>>(const QDBusArgument& argument, DBusNotificationImage& pixmap); const QDBusArgument& operator<<(QDBusArgument& argument, const DBusNotificationImage& pixmap); -class NotificationImage: public QsImageHandle { +class NotificationImage: public QsIndexedImageHandle { public: - explicit NotificationImage(DBusNotificationImage image, QObject* parent) - : QsImageHandle(QQuickAsyncImageProvider::Image, parent) - , image(std::move(image)) {} + explicit NotificationImage(): QsIndexedImageHandle(QQuickAsyncImageProvider::Image) {} + + [[nodiscard]] bool hasData() const { return !this->image.data.isEmpty(); } + void clear() { this->image.data.clear(); } + + [[nodiscard]] DBusNotificationImage& writeImage() { + this->imageChanged(); + return this->image; + } QImage requestImage(const QString& id, QSize* size, const QSize& requestedSize) override; +private: DBusNotificationImage image; }; + } // namespace qs::service::notifications diff --git a/src/services/notifications/notification.cpp b/src/services/notifications/notification.cpp index 51a64154..2703cf6d 100644 --- a/src/services/notifications/notification.cpp +++ b/src/services/notifications/notification.cpp @@ -1,5 +1,4 @@ #include "notification.hpp" -#include #include #include @@ -117,13 +116,12 @@ void Notification::updateProperties( QString imagePath; - if (!imageDataName.isEmpty()) { + if (imageDataName.isEmpty()) { + this->mImagePixmap.clear(); + } else { auto value = hints.value(imageDataName).value(); - DBusNotificationImage image; - value >> image; - if (this->mImagePixmap) this->mImagePixmap->deleteLater(); - this->mImagePixmap = new NotificationImage(std::move(image), this); - imagePath = this->mImagePixmap->url(); + value >> this->mImagePixmap.writeImage(); + imagePath = this->mImagePixmap.url(); } // don't store giant byte arrays longer than necessary @@ -131,7 +129,7 @@ void Notification::updateProperties( hints.remove("image_data"); hints.remove("icon_data"); - if (!this->mImagePixmap) { + if (!this->mImagePixmap.hasData()) { QString imagePathName; if (hints.contains("image-path")) imagePathName = "image-path"; else if (hints.contains("image_path")) imagePathName = "image_path"; diff --git a/src/services/notifications/notification.hpp b/src/services/notifications/notification.hpp index 25b9e330..85fe023d 100644 --- a/src/services/notifications/notification.hpp +++ b/src/services/notifications/notification.hpp @@ -12,11 +12,10 @@ #include "../../core/retainable.hpp" #include "../../core/util.hpp" +#include "dbusimage.hpp" namespace qs::service::notifications { -class NotificationImage; - ///! The urgency level of a Notification. /// See @@Notification.urgency. class NotificationUrgency: public QObject { @@ -187,7 +186,7 @@ private: quint32 mId; NotificationCloseReason::Enum mCloseReason = NotificationCloseReason::Dismissed; bool mLastGeneration = false; - NotificationImage* mImagePixmap = nullptr; + NotificationImage mImagePixmap; QList mActions; // clang-format off diff --git a/src/services/status_notifier/item.cpp b/src/services/status_notifier/item.cpp index d145c451..42847399 100644 --- a/src/services/status_notifier/item.cpp +++ b/src/services/status_notifier/item.cpp @@ -282,7 +282,7 @@ void StatusNotifierItem::onGetAllFailed() const { } TrayImageHandle::TrayImageHandle(StatusNotifierItem* item) - : QsImageHandle(QQmlImageProviderBase::Pixmap, item) + : QsImageHandle(QQmlImageProviderBase::Pixmap) , item(item) {} QPixmap From b289bfa504dc786e3ad9535d465431e739cb02d8 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Thu, 23 Jan 2025 14:00:16 -0800 Subject: [PATCH 27/31] hyprland/surface: add visibleMask --- .../hyprland/surface/hyprland-surface-v1.xml | 30 +++- src/wayland/hyprland/surface/manager.cpp | 4 +- src/wayland/hyprland/surface/qml.cpp | 160 ++++++++++++++---- src/wayland/hyprland/surface/qml.hpp | 22 +++ src/wayland/hyprland/surface/surface.cpp | 38 ++++- src/wayland/hyprland/surface/surface.hpp | 12 +- 6 files changed, 230 insertions(+), 36 deletions(-) diff --git a/src/wayland/hyprland/surface/hyprland-surface-v1.xml b/src/wayland/hyprland/surface/hyprland-surface-v1.xml index 2f683365..c4b1424f 100644 --- a/src/wayland/hyprland/surface/hyprland-surface-v1.xml +++ b/src/wayland/hyprland/surface/hyprland-surface-v1.xml @@ -34,7 +34,7 @@ This protocol exposes hyprland-specific wl_surface properties. - + This interface allows a client to create hyprland surface objects. @@ -63,7 +63,7 @@ - + This interface allows access to hyprland-specific properties of a wl_surface. @@ -96,5 +96,31 @@ + + + + This request sets the region of the surface that contains visible content. + Visible content refers to content that has an alpha value greater than zero. + + The visible region is an optimization hint for the compositor that lets it + avoid drawing parts of the surface that are not visible. Setting a visible region + that does not contain all content in the surface may result in missing content + not being drawn. + + The visible region is specified in buffer-local coordinates. + + The compositor ignores the parts of the visible region that fall outside of the surface. + When all parts of the region fall outside of the buffer geometry, the compositor may + avoid rendering the surface entirely. + + The initial value for the visible region is empty. Setting the + visible region has copy semantics, and the wl_region object can be destroyed immediately. + A NULL wl_region causes the visible region to be set to empty. + + Does not take effect until wl_surface.commit is called. + + + + diff --git a/src/wayland/hyprland/surface/manager.cpp b/src/wayland/hyprland/surface/manager.cpp index 31829bb6..6354255e 100644 --- a/src/wayland/hyprland/surface/manager.cpp +++ b/src/wayland/hyprland/surface/manager.cpp @@ -7,13 +7,13 @@ namespace qs::hyprland::surface::impl { -HyprlandSurfaceManager::HyprlandSurfaceManager(): QWaylandClientExtensionTemplate(1) { +HyprlandSurfaceManager::HyprlandSurfaceManager(): QWaylandClientExtensionTemplate(2) { this->initialize(); } HyprlandSurface* HyprlandSurfaceManager::createHyprlandExtension(QtWaylandClient::QWaylandWindow* surface) { - return new HyprlandSurface(this->get_hyprland_surface(surface->surface())); + return new HyprlandSurface(this->get_hyprland_surface(surface->surface()), surface); } HyprlandSurfaceManager* HyprlandSurfaceManager::instance() { diff --git a/src/wayland/hyprland/surface/qml.cpp b/src/wayland/hyprland/surface/qml.cpp index 5150487f..b00ee33e 100644 --- a/src/wayland/hyprland/surface/qml.cpp +++ b/src/wayland/hyprland/surface/qml.cpp @@ -1,17 +1,20 @@ #include "qml.hpp" #include +#include #include #include #include #include +#include #include #include +#include #include +#include "../../../core/region.hpp" #include "../../../window/proxywindow.hpp" #include "../../../window/windowinterface.hpp" -#include "../../util.hpp" #include "manager.hpp" #include "surface.hpp" @@ -40,6 +43,15 @@ HyprlandWindow::HyprlandWindow(ProxyWindowBase* window): QObject(nullptr), proxy &HyprlandWindow::onWindowConnected ); + QObject::connect(window, &ProxyWindowBase::polished, this, &HyprlandWindow::onWindowPolished); + + QObject::connect( + window, + &ProxyWindowBase::devicePixelRatioChanged, + this, + &HyprlandWindow::updateVisibleMask + ); + QObject::connect(window, &QObject::destroyed, this, &HyprlandWindow::onProxyWindowDestroyed); if (window->backingWindow()) { @@ -60,14 +72,76 @@ void HyprlandWindow::setOpacity(qreal opacity) { this->mOpacity = opacity; - if (this->surface) { - this->surface->setOpacity(opacity); - qs::wayland::util::scheduleCommit(this->proxyWindow); + if (this->surface && this->proxyWindow) { + this->pendingPolish.opacity = true; + this->proxyWindow->schedulePolish(); } emit this->opacityChanged(); } +PendingRegion* HyprlandWindow::visibleMask() const { return this->mVisibleMask; } + +void HyprlandWindow::setVisibleMask(PendingRegion* mask) { + if (mask == this->mVisibleMask) return; + + if (this->mVisibleMask) { + QObject::disconnect(this->mVisibleMask, nullptr, this, nullptr); + } + + this->mVisibleMask = mask; + + if (mask) { + QObject::connect(mask, &QObject::destroyed, this, &HyprlandWindow::onVisibleMaskDestroyed); + QObject::connect(mask, &PendingRegion::changed, this, &HyprlandWindow::updateVisibleMask); + } + + this->updateVisibleMask(); + emit this->visibleMaskChanged(); +} + +void HyprlandWindow::onVisibleMaskDestroyed() { + this->mVisibleMask = nullptr; + this->updateVisibleMask(); + emit this->visibleMaskChanged(); +} + +void HyprlandWindow::updateVisibleMask() { + if (!this->surface || !this->proxyWindow) return; + + this->pendingPolish.visibleMask = true; + this->proxyWindow->schedulePolish(); +} + +void HyprlandWindow::onWindowPolished() { + if (!this->surface) return; + + if (this->pendingPolish.opacity) { + this->surface->setOpacity(this->mOpacity); + this->pendingPolish.opacity = false; + } + + if (this->pendingPolish.visibleMask) { + QRegion mask; + if (this->mVisibleMask != nullptr) { + mask = + this->mVisibleMask->applyTo(QRect(0, 0, this->mWindow->width(), this->mWindow->height())); + } + + auto dpr = this->proxyWindow->devicePixelRatio(); + if (dpr != 1.0) { + mask = QHighDpi::scale(mask, dpr); + } + + if (mask.isEmpty() && this->mVisibleMask) { + mask = QRect(-1, -1, 1, 1); + } + + this->surface->setVisibleRegion(mask); + this->pendingPolish.visibleMask = false; + } +} + void HyprlandWindow::onWindowConnected() { this->mWindow = this->proxyWindow->backingWindow(); // disconnected by destructor @@ -86,33 +160,46 @@ void HyprlandWindow::onWindowVisibleChanged() { if (!this->mWindow->handle()) { this->mWindow->create(); } + } - this->mWaylandWindow = dynamic_cast(this->mWindow->handle()); + auto* window = dynamic_cast(this->mWindow->handle()); + if (window == this->mWaylandWindow) return; - if (this->mWaylandWindow) { - // disconnected by destructor + if (this->mWaylandWindow) { + QObject::disconnect(this->mWaylandWindow, nullptr, this, nullptr); + } - QObject::connect( - this->mWaylandWindow, - &QWaylandWindow::surfaceCreated, - this, - &HyprlandWindow::onWaylandSurfaceCreated - ); + this->mWaylandWindow = window; + if (!window) return; - QObject::connect( - this->mWaylandWindow, - &QWaylandWindow::surfaceDestroyed, - this, - &HyprlandWindow::onWaylandSurfaceDestroyed - ); + QObject::connect( + this->mWaylandWindow, + &QObject::destroyed, + this, + &HyprlandWindow::onWaylandWindowDestroyed + ); - if (this->mWaylandWindow->surface()) { - this->onWaylandSurfaceCreated(); - } - } + QObject::connect( + this->mWaylandWindow, + &QWaylandWindow::surfaceCreated, + this, + &HyprlandWindow::onWaylandSurfaceCreated + ); + + QObject::connect( + this->mWaylandWindow, + &QWaylandWindow::surfaceDestroyed, + this, + &HyprlandWindow::onWaylandSurfaceDestroyed + ); + + if (this->mWaylandWindow->surface()) { + this->onWaylandSurfaceCreated(); } } +void HyprlandWindow::onWaylandWindowDestroyed() { this->mWaylandWindow = nullptr; } + void HyprlandWindow::onWaylandSurfaceCreated() { auto* manager = impl::HyprlandSurfaceManager::instance(); @@ -122,12 +209,26 @@ void HyprlandWindow::onWaylandSurfaceCreated() { return; } - auto* ext = manager->createHyprlandExtension(this->mWaylandWindow); - this->surface = std::unique_ptr(ext); + auto v = this->mWaylandWindow->property("hyprland_window_ext"); + if (v.canConvert()) { + auto* windowExt = v.value(); + if (windowExt != this && windowExt->surface) { + this->surface.swap(windowExt->surface); + } + } - if (this->mOpacity != 1.0) { - this->surface->setOpacity(this->mOpacity); - qs::wayland::util::scheduleCommit(this->proxyWindow); + if (!this->surface) { + auto* ext = manager->createHyprlandExtension(this->mWaylandWindow); + this->surface = std::unique_ptr(ext); + } + + this->mWaylandWindow->setProperty("hyprland_window_ext", QVariant::fromValue(this)); + + this->pendingPolish.opacity = this->mOpacity != 1.0; + this->pendingPolish.visibleMask = this->mVisibleMask; + + if (this->pendingPolish.opacity || this->pendingPolish.visibleMask) { + this->proxyWindow->schedulePolish(); } } @@ -144,8 +245,9 @@ void HyprlandWindow::onProxyWindowDestroyed() { // Deleting it when the proxy window is deleted will cause a full opacity frame between the destruction of the // hyprland_surface_v1 and wl_surface objects. + this->proxyWindow = nullptr; + if (this->surface == nullptr) { - this->proxyWindow = nullptr; this->deleteLater(); } } diff --git a/src/wayland/hyprland/surface/qml.hpp b/src/wayland/hyprland/surface/qml.hpp index ce32a967..157b8f32 100644 --- a/src/wayland/hyprland/surface/qml.hpp +++ b/src/wayland/hyprland/surface/qml.hpp @@ -9,6 +9,7 @@ #include #include +#include "../../../core/region.hpp" #include "../../../window/proxywindow.hpp" #include "surface.hpp" @@ -31,11 +32,18 @@ namespace qs::hyprland::surface { /// [hyprland-surface-v1]: https://github.com/hyprwm/hyprland-protocols/blob/main/protocols/hyprland-surface-v1.xml class HyprlandWindow: public QObject { Q_OBJECT; + // clang-format off /// A multiplier for the window's overall opacity, ranging from 1.0 to 0.0. Overall opacity includes the opacity of /// both the window content *and* visual effects such as blur that apply to it. /// /// Default: 1.0 Q_PROPERTY(qreal opacity READ opacity WRITE setOpacity NOTIFY opacityChanged); + /// A hint to the compositor that only certain regions of the surface should be rendered. + /// This can be used to avoid rendering large empty regions of a window which can increase + /// performance, especially if the window is blurred. The mask should include all pixels + /// of the window that do not have an alpha value of 0. + Q_PROPERTY(PendingRegion* visibleMask READ visibleMask WRITE setVisibleMask NOTIFY visibleMaskChanged); + // clang-format on QML_ELEMENT; QML_UNCREATABLE("HyprlandWindow can only be used as an attached object."); QML_ATTACHED(HyprlandWindow); @@ -48,17 +56,25 @@ public: [[nodiscard]] qreal opacity() const; void setOpacity(qreal opacity); + [[nodiscard]] PendingRegion* visibleMask() const; + virtual void setVisibleMask(PendingRegion* mask); + static HyprlandWindow* qmlAttachedProperties(QObject* object); signals: void opacityChanged(); + void visibleMaskChanged(); private slots: void onWindowConnected(); void onWindowVisibleChanged(); + void onWaylandWindowDestroyed(); void onWaylandSurfaceCreated(); void onWaylandSurfaceDestroyed(); void onProxyWindowDestroyed(); + void onVisibleMaskDestroyed(); + void onWindowPolished(); + void updateVisibleMask(); private: void disconnectWaylandWindow(); @@ -67,7 +83,13 @@ private: QWindow* mWindow = nullptr; QtWaylandClient::QWaylandWindow* mWaylandWindow = nullptr; + struct { + bool opacity : 1 = false; + bool visibleMask : 1 = false; + } pendingPolish; + qreal mOpacity = 1.0; + PendingRegion* mVisibleMask = nullptr; std::unique_ptr surface; }; diff --git a/src/wayland/hyprland/surface/surface.cpp b/src/wayland/hyprland/surface/surface.cpp index d1aa24fb..487da40b 100644 --- a/src/wayland/hyprland/surface/surface.cpp +++ b/src/wayland/hyprland/surface/surface.cpp @@ -1,19 +1,53 @@ #include "surface.hpp" +#include +#include +#include +#include +#include #include #include +#include #include #include namespace qs::hyprland::surface::impl { -HyprlandSurface::HyprlandSurface(::hyprland_surface_v1* surface) - : QtWayland::hyprland_surface_v1(surface) {} +HyprlandSurface::HyprlandSurface( + ::hyprland_surface_v1* surface, + QtWaylandClient::QWaylandWindow* backer +) + : QtWayland::hyprland_surface_v1(surface) + , backer(backer) + , backerSurface(backer->surface()) {} HyprlandSurface::~HyprlandSurface() { this->destroy(); } +bool HyprlandSurface::surfaceEq(wl_surface* surface) const { + return surface == this->backerSurface; +} + void HyprlandSurface::setOpacity(qreal opacity) { this->set_opacity(wl_fixed_from_double(opacity)); } +void HyprlandSurface::setVisibleRegion(const QRegion& region) { + if (this->version() < HYPRLAND_SURFACE_V1_SET_VISIBLE_REGION_SINCE_VERSION) { + qWarning() << "Cannot set hyprland surface visible region: compositor does not support " + "hyprland_surface_v1.set_visible_region"; + return; + } + + if (region.isEmpty()) { + this->set_visible_region(nullptr); + } else { + static const auto* waylandIntegration = QtWaylandClient::QWaylandIntegration::instance(); + auto* display = waylandIntegration->display(); + + auto* wlRegion = display->createRegion(region); + this->set_visible_region(wlRegion); + wl_region_destroy(wlRegion); // NOLINT(misc-include-cleaner) + } +} + } // namespace qs::hyprland::surface::impl diff --git a/src/wayland/hyprland/surface/surface.hpp b/src/wayland/hyprland/surface/surface.hpp index a27e50e3..1c8b5486 100644 --- a/src/wayland/hyprland/surface/surface.hpp +++ b/src/wayland/hyprland/surface/surface.hpp @@ -1,21 +1,31 @@ #pragma once +#include #include +#include #include #include #include #include +#include #include namespace qs::hyprland::surface::impl { class HyprlandSurface: public QtWayland::hyprland_surface_v1 { public: - explicit HyprlandSurface(::hyprland_surface_v1* surface); + explicit HyprlandSurface(::hyprland_surface_v1* surface, QtWaylandClient::QWaylandWindow* backer); ~HyprlandSurface() override; Q_DISABLE_COPY_MOVE(HyprlandSurface); + [[nodiscard]] bool surfaceEq(wl_surface* surface) const; + void setOpacity(qreal opacity); + void setVisibleRegion(const QRegion& region); + +private: + QtWaylandClient::QWaylandWindow* backer; + wl_surface* backerSurface = nullptr; }; } // namespace qs::hyprland::surface::impl From 325be8857c36279f102b9acc7a06d88b62e2e60c Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Fri, 24 Jan 2025 16:30:32 -0800 Subject: [PATCH 28/31] core/command: add option to select newest matching instance --- src/launch/command.cpp | 11 ++++++----- src/launch/launch_p.hpp | 1 + src/launch/parsecommand.cpp | 13 +++++++++---- 3 files changed, 16 insertions(+), 9 deletions(-) diff --git a/src/launch/command.cpp b/src/launch/command.cpp index eb27df74..449fab79 100644 --- a/src/launch/command.cpp +++ b/src/launch/command.cpp @@ -102,9 +102,10 @@ int locateConfigFile(CommandState& cmd, QString& path) { return 0; } -void sortInstances(QVector& list) { - std::ranges::sort(list, [](const InstanceLockInfo& a, const InstanceLockInfo& b) { - return a.instance.launchTime < b.instance.launchTime; +void sortInstances(QVector& list, bool newestFirst) { + std::ranges::sort(list, [=](const InstanceLockInfo& a, const InstanceLockInfo& b) { + auto r = a.instance.launchTime < b.instance.launchTime; + return newestFirst ? !r : r; }); }; @@ -153,7 +154,7 @@ int selectInstance(CommandState& cmd, InstanceLockInfo* instance) { path = QDir(basePath->filePath("by-path")).filePath(pathId); auto instances = QsPaths::collectInstances(path); - sortInstances(instances); + sortInstances(instances, cmd.config.newest); if (instances.isEmpty()) { qCInfo(logBare) << "No running instances for" << configFilePath; @@ -227,7 +228,7 @@ int listInstances(CommandState& cmd) { qCInfo(logBare) << "Use --all to list all instances."; } } else { - sortInstances(instances); + sortInstances(instances, cmd.config.newest); if (cmd.output.json) { auto array = QJsonArray(); diff --git a/src/launch/launch_p.hpp b/src/launch/launch_p.hpp index d752edbc..1b59de86 100644 --- a/src/launch/launch_p.hpp +++ b/src/launch/launch_p.hpp @@ -49,6 +49,7 @@ struct CommandState { QStringOption path; QStringOption manifest; QStringOption name; + bool newest = false; } config; struct { diff --git a/src/launch/parsecommand.cpp b/src/launch/parsecommand.cpp index bee9dd00..9a88c4f5 100644 --- a/src/launch/parsecommand.cpp +++ b/src/launch/parsecommand.cpp @@ -16,7 +16,7 @@ int parseCommand(int argc, char** argv, CommandState& state) { .argv = argv, }; - auto addConfigSelection = [&](CLI::App* cmd) { + auto addConfigSelection = [&](CLI::App* cmd, bool withNewestOption = false) { auto* group = cmd->add_option_group("Config Selection") ->description("If no options in this group are specified,\n" "$XDG_CONFIG_HOME/quickshell/shell.qml will be used."); @@ -37,6 +37,11 @@ int parseCommand(int argc, char** argv, CommandState& state) { "otherwise it is the name of a folder in $XDG_CONFIG_HOME/quickshell.") ->envname("QS_CONFIG_NAME"); + if (withNewestOption) { + group->add_flag("-n,--newest", state.config.newest) + ->description("Operate on the most recently launched instance instead of the oldest"); + } + return group; }; @@ -146,7 +151,7 @@ int parseCommand(int argc, char** argv, CommandState& state) { sub->add_flag("-j,--json", state.output.json, "Output the list as a json."); - addConfigSelection(sub)->excludes(all); + addConfigSelection(sub, true)->excludes(all); addLoggingOptions(sub, false, true); state.subcommand.list = sub; @@ -156,7 +161,7 @@ int parseCommand(int argc, char** argv, CommandState& state) { 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); + addConfigSelection(sub, true)->excludes(instance); addLoggingOptions(sub, false, true); state.subcommand.kill = sub; @@ -182,7 +187,7 @@ int parseCommand(int argc, char** argv, CommandState& state) { ->excludes(arguments); auto* instance = addInstanceSelection(sub); - addConfigSelection(sub)->excludes(instance); + addConfigSelection(sub, true)->excludes(instance); addLoggingOptions(sub, false, true); sub->require_option(); From 420529362f06f70c8d6d63c0ff59789794d7919e Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Fri, 24 Jan 2025 23:53:31 -0800 Subject: [PATCH 29/31] core/clock: expose date as a QDateTime --- src/core/clock.cpp | 35 ++++++++++++--------------------- src/core/clock.hpp | 49 ++++++++++++++++++++++++++++++++-------------- 2 files changed, 47 insertions(+), 37 deletions(-) diff --git a/src/core/clock.cpp b/src/core/clock.cpp index ebb7e92a..90938d21 100644 --- a/src/core/clock.cpp +++ b/src/core/clock.cpp @@ -6,8 +6,6 @@ #include #include -#include "util.hpp" - SystemClock::SystemClock(QObject* parent): QObject(parent) { QObject::connect(&this->timer, &QTimer::timeout, this, &SystemClock::onTimeout); this->update(); @@ -48,19 +46,16 @@ void SystemClock::update() { void SystemClock::setTime(const QDateTime& targetTime) { auto currentTime = QDateTime::currentDateTime(); auto offset = currentTime.msecsTo(targetTime); - auto dtime = offset > -500 && offset < 500 ? targetTime : currentTime; - auto time = dtime.time(); + this->currentTime = offset > -500 && offset < 500 ? targetTime : currentTime; - auto secondPrecision = this->mPrecision >= SystemClock::Seconds; - auto secondChanged = this->setSeconds(secondPrecision ? time.second() : 0); + auto time = this->currentTime.time(); + this->currentTime.setTime(QTime( + this->mPrecision >= SystemClock::Hours ? time.hour() : 0, + this->mPrecision >= SystemClock::Minutes ? time.minute() : 0, + this->mPrecision >= SystemClock::Seconds ? time.second() : 0 + )); - auto minutePrecision = this->mPrecision >= SystemClock::Minutes; - auto minuteChanged = this->setMinutes(minutePrecision ? time.minute() : 0); - - auto hourPrecision = this->mPrecision >= SystemClock::Hours; - auto hourChanged = this->setHours(hourPrecision ? time.hour() : 0); - - DropEmitter::call(secondChanged, minuteChanged, hourChanged); + emit this->dateChanged(); } void SystemClock::schedule(const QDateTime& targetTime) { @@ -76,11 +71,11 @@ void SystemClock::schedule(const QDateTime& targetTime) { auto nextTime = offset > 0 && offset < 500 ? targetTime : currentTime; auto baseTimeT = nextTime.time(); - nextTime.setTime( - {hourPrecision ? baseTimeT.hour() : 0, - minutePrecision ? baseTimeT.minute() : 0, - secondPrecision ? baseTimeT.second() : 0} - ); + nextTime.setTime(QTime( + hourPrecision ? baseTimeT.hour() : 0, + minutePrecision ? baseTimeT.minute() : 0, + secondPrecision ? baseTimeT.second() : 0 + )); if (secondPrecision) nextTime = nextTime.addSecs(1); else if (minutePrecision) nextTime = nextTime.addSecs(60); @@ -91,7 +86,3 @@ void SystemClock::schedule(const QDateTime& targetTime) { this->timer.start(static_cast(delay)); this->targetTime = nextTime; } - -DEFINE_MEMBER_GETSET(SystemClock, hours, setHours); -DEFINE_MEMBER_GETSET(SystemClock, minutes, setMinutes); -DEFINE_MEMBER_GETSET(SystemClock, seconds, setSeconds); diff --git a/src/core/clock.hpp b/src/core/clock.hpp index 3e669589..67461911 100644 --- a/src/core/clock.hpp +++ b/src/core/clock.hpp @@ -7,9 +7,26 @@ #include #include -#include "util.hpp" - ///! System clock accessor. +/// SystemClock is a view into the system's clock. +/// It updates at hour, minute, or second intervals depending on @@precision. +/// +/// # Examples +/// ```qml +/// SystemClock { +/// id: clock +/// precision: SystemClock.Seconds +/// } +/// +/// @@QtQuick.Text { +/// text: Qt.formatDateTime(clock.date, "hh:mm:ss - yyyy-MM-dd") +/// } +/// ``` +/// +/// > [!WARNING] Clock updates will trigger within 50ms of the system clock changing, +/// > however this can be either before or after the clock changes (+-50ms). If you +/// > need a date object, use @@date instead of constructing a new one, or the time +/// > of the constructed object could be off by up to a second. class SystemClock: public QObject { Q_OBJECT; /// If the clock should update. Defaults to true. @@ -18,12 +35,17 @@ class SystemClock: public QObject { Q_PROPERTY(bool enabled READ enabled WRITE setEnabled NOTIFY enabledChanged); /// The precision the clock should measure at. Defaults to `SystemClock.Seconds`. Q_PROPERTY(SystemClock::Enum precision READ precision WRITE setPrecision NOTIFY precisionChanged); + /// The current date and time. + /// + /// > [!TIP] You can use @@QtQml.Qt.formatDateTime() to get the time as a string in + /// > your format of choice. + Q_PROPERTY(QDateTime date READ date NOTIFY dateChanged); /// The current hour. - Q_PROPERTY(quint32 hours READ hours NOTIFY hoursChanged); + Q_PROPERTY(quint32 hours READ hours NOTIFY dateChanged); /// The current minute, or 0 if @@precision is `SystemClock.Hours`. - Q_PROPERTY(quint32 minutes READ minutes NOTIFY minutesChanged); + Q_PROPERTY(quint32 minutes READ minutes NOTIFY dateChanged); /// The current second, or 0 if @@precision is `SystemClock.Hours` or `SystemClock.Minutes`. - Q_PROPERTY(quint32 seconds READ seconds NOTIFY secondsChanged); + Q_PROPERTY(quint32 seconds READ seconds NOTIFY dateChanged); QML_ELEMENT; public: @@ -43,12 +65,15 @@ public: [[nodiscard]] SystemClock::Enum precision() const; void setPrecision(SystemClock::Enum precision); + [[nodiscard]] QDateTime date() const { return this->currentTime; } + [[nodiscard]] quint32 hours() const { return this->currentTime.time().hour(); } + [[nodiscard]] quint32 minutes() const { return this->currentTime.time().minute(); } + [[nodiscard]] quint32 seconds() const { return this->currentTime.time().second(); } + signals: void enabledChanged(); void precisionChanged(); - void hoursChanged(); - void minutesChanged(); - void secondsChanged(); + void dateChanged(); private slots: void onTimeout(); @@ -56,17 +81,11 @@ private slots: private: bool mEnabled = true; SystemClock::Enum mPrecision = SystemClock::Seconds; - quint32 mHours = 0; - quint32 mMinutes = 0; - quint32 mSeconds = 0; QTimer timer; + QDateTime currentTime; QDateTime targetTime; void update(); void setTime(const QDateTime& targetTime); void schedule(const QDateTime& targetTime); - - DECLARE_PRIVATE_MEMBER(SystemClock, hours, setHours, mHours, hoursChanged); - DECLARE_PRIVATE_MEMBER(SystemClock, minutes, setMinutes, mMinutes, minutesChanged); - DECLARE_PRIVATE_MEMBER(SystemClock, seconds, setSeconds, mSeconds, secondsChanged); }; From 9417d6fa57a1c0a7c711dcdeeb7c43d150382e30 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Sat, 25 Jan 2025 01:00:42 -0800 Subject: [PATCH 30/31] core/command: deprecate `qs msg` --- src/launch/command.cpp | 17 ++++------- src/launch/launch_p.hpp | 7 +++-- src/launch/parsecommand.cpp | 61 +++++++++++++++++++++++++------------ 3 files changed, 53 insertions(+), 32 deletions(-) diff --git a/src/launch/command.cpp b/src/launch/command.cpp index 449fab79..f814b5ff 100644 --- a/src/launch/command.cpp +++ b/src/launch/command.cpp @@ -285,26 +285,21 @@ int killInstances(CommandState& cmd) { }); } -int msgInstance(CommandState& cmd) { +int ipcCommand(CommandState& cmd) { InstanceLockInfo instance; auto r = selectInstance(cmd, &instance); if (r != 0) return r; return IpcClient::connect(instance.instance.instanceId, [&](IpcClient& client) { - if (cmd.ipc.info) { - return qs::io::ipc::comm::queryMetadata(&client, *cmd.ipc.target, *cmd.ipc.function); + if (*cmd.ipc.show || cmd.ipc.showOld) { + return qs::io::ipc::comm::queryMetadata(&client, *cmd.ipc.target, *cmd.ipc.name); } else { QVector arguments; for (auto& arg: cmd.ipc.arguments) { arguments += *arg; } - return qs::io::ipc::comm::callFunction( - &client, - *cmd.ipc.target, - *cmd.ipc.function, - arguments - ); + return qs::io::ipc::comm::callFunction(&client, *cmd.ipc.target, *cmd.ipc.name, arguments); } return -1; @@ -423,8 +418,8 @@ int runCommand(int argc, char** argv, QCoreApplication* coreApplication) { return listInstances(state); } else if (*state.subcommand.kill) { return killInstances(state); - } else if (*state.subcommand.msg) { - return msgInstance(state); + } else if (*state.subcommand.msg || *state.ipc.ipc) { + return ipcCommand(state); } else { if (strcmp(qVersion(), QT_VERSION_STR) != 0) { qWarning() << "\033[31mQuickshell was built against Qt" << QT_VERSION_STR diff --git a/src/launch/launch_p.hpp b/src/launch/launch_p.hpp index 1b59de86..a9a515c4 100644 --- a/src/launch/launch_p.hpp +++ b/src/launch/launch_p.hpp @@ -68,9 +68,12 @@ struct CommandState { } output; struct { - bool info = false; + CLI::App* ipc = nullptr; + CLI::App* show = nullptr; + CLI::App* call = nullptr; + bool showOld = false; QStringOption target; - QStringOption function; + QStringOption name; std::vector arguments; } ipc; diff --git a/src/launch/parsecommand.cpp b/src/launch/parsecommand.cpp index 9a88c4f5..2c082fec 100644 --- a/src/launch/parsecommand.cpp +++ b/src/launch/parsecommand.cpp @@ -135,7 +135,7 @@ int parseCommand(int argc, char** argv, CommandState& state) { ->description("Rules to apply to the log being read, in the format of QT_LOGGING_RULES."); auto* instance = addInstanceSelection(sub)->excludes(file); - addConfigSelection(sub)->excludes(instance)->excludes(file); + addConfigSelection(sub, true)->excludes(instance)->excludes(file); addLoggingOptions(sub, false); state.subcommand.log = sub; @@ -168,29 +168,52 @@ int parseCommand(int argc, char** argv, CommandState& state) { } { - 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."); - - auto* function = sub->add_option("function", state.ipc.function) - ->description("The function to call in the target.") - ->needs(target); - - auto* arguments = sub->add_option("arguments", state.ipc.arguments) - ->description("Arguments to the called function.") - ->needs(function) - ->allow_extra_args(); - - sub->add_flag("-s,--show", state.ipc.info) - ->description("Print information about a function or target if given, or all available " - "targets if not.") - ->excludes(arguments); + auto* sub = cli->add_subcommand("ipc", "Communicate with other Quickshell instances.") + ->require_subcommand(); + state.ipc.ipc = sub; auto* instance = addInstanceSelection(sub); addConfigSelection(sub, true)->excludes(instance); addLoggingOptions(sub, false, true); - sub->require_option(); + { + auto* show = sub->add_subcommand("show", "Print information about available IPC targets."); + state.ipc.show = show; + } + + { + auto* call = sub->add_subcommand("call", "Call an IpcHandler function."); + state.ipc.call = call; + + call->add_option("target", state.ipc.target, "The target to message."); + + call->add_option("function", state.ipc.name) + ->description("The function to call in the target."); + + call->add_option("arguments", state.ipc.arguments) + ->description("Arguments to the called function.") + ->allow_extra_args(); + } + } + + { + auto* sub = cli->add_subcommand("msg", "[DEPRECATED] Moved to `ipc call`.")->require_option(); + + sub->add_option("target", state.ipc.target, "The target to message."); + + sub->add_option("function", state.ipc.name)->description("The function to call in the target."); + + sub->add_option("arguments", state.ipc.arguments) + ->description("Arguments to the called function.") + ->allow_extra_args(); + + sub->add_flag("-s,--show", state.ipc.showOld) + ->description("Print information about a function or target if given, or all available " + "targets if not."); + + auto* instance = addInstanceSelection(sub); + addConfigSelection(sub, true)->excludes(instance); + addLoggingOptions(sub, false, true); state.subcommand.msg = sub; } From 4f2610dece8b1b5f1d24b4d046a2bfbe2f2e2cc6 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Sun, 26 Jan 2025 03:57:07 -0800 Subject: [PATCH 31/31] io/ipchandler: add `prop get` --- src/io/ipc.cpp | 27 +++++++++ src/io/ipc.hpp | 26 ++++++++- src/io/ipccomm.cpp | 107 +++++++++++++++++++++++++++++++----- src/io/ipccomm.hpp | 18 +++++- src/io/ipchandler.cpp | 52 ++++++++++++++++++ src/io/ipchandler.hpp | 45 +++++++++++---- src/ipc/ipccommand.hpp | 3 +- src/launch/command.cpp | 2 + src/launch/launch_p.hpp | 1 + src/launch/parsecommand.cpp | 12 ++++ 10 files changed, 262 insertions(+), 31 deletions(-) diff --git a/src/io/ipc.cpp b/src/io/ipc.cpp index 37a37eb3..768299ed 100644 --- a/src/io/ipc.cpp +++ b/src/io/ipc.cpp @@ -1,9 +1,12 @@ #include "ipc.hpp" +#include #include #include #include #include +#include +#include namespace qs::io::ipc { @@ -14,6 +17,12 @@ const BoolIpcType BoolIpcType::INSTANCE {}; const DoubleIpcType DoubleIpcType::INSTANCE {}; const ColorIpcType ColorIpcType::INSTANCE {}; +void* IpcType::copyStorage(const void* data) const { + auto* storage = this->createStorage(); + memcpy(storage, data, this->size()); + return storage; +} + const IpcType* IpcType::ipcType(const QMetaType& metaType) { if (metaType.id() == QMetaType::Void) return &VoidIpcType::INSTANCE; if (metaType.id() == QMetaType::QString) return &StringIpcType::INSTANCE; @@ -70,12 +79,18 @@ void IpcTypeSlot::replace(void* value) { this->storage = value; } +void IpcTypeSlot::replace(const QVariant& value) { + this->replace(this->mType->copyStorage(value.constData())); +} + const char* VoidIpcType::name() const { return "void"; } const char* VoidIpcType::genericArgumentName() const { return "void"; } +qsizetype VoidIpcType::size() const { return 0; } // string const char* StringIpcType::name() const { return "string"; } const char* StringIpcType::genericArgumentName() const { return "QString"; } +qsizetype StringIpcType::size() const { return sizeof(QString); } void* StringIpcType::fromString(const QString& string) const { return new QString(string); } QString StringIpcType::toString(void* slot) const { return *static_cast(slot); } void* StringIpcType::createStorage() const { return new QString(); } @@ -84,6 +99,7 @@ void StringIpcType::destroyStorage(void* slot) const { delete static_cast(slo // bool const char* BoolIpcType::name() const { return "bool"; } const char* BoolIpcType::genericArgumentName() const { return "bool"; } +qsizetype BoolIpcType::size() const { return sizeof(bool); } void* BoolIpcType::fromString(const QString& string) const { if (string == "true") return new bool(true); @@ -121,6 +138,7 @@ void BoolIpcType::destroyStorage(void* slot) const { delete static_cast(s // double const char* DoubleIpcType::name() const { return "real"; } const char* DoubleIpcType::genericArgumentName() const { return "double"; } +qsizetype DoubleIpcType::size() const { return sizeof(double); } void* DoubleIpcType::fromString(const QString& string) const { auto ok = false; @@ -139,6 +157,7 @@ void DoubleIpcType::destroyStorage(void* slot) const { delete static_castname % '(' % paramString % "): " % this->returnType; } +QString WirePropertyDefinition::toString() const { + return "property " % this->name % ": " % this->type; +} + QString WireTargetDefinition::toString() const { QString accum = "target " % this->name; @@ -174,6 +197,10 @@ QString WireTargetDefinition::toString() const { accum += "\n " % func.toString(); } + for (const auto& prop: this->properties) { + accum += "\n " % prop.toString(); + } + return accum; } diff --git a/src/io/ipc.hpp b/src/io/ipc.hpp index 924f045e..d2b865a2 100644 --- a/src/io/ipc.hpp +++ b/src/io/ipc.hpp @@ -3,6 +3,7 @@ #include #include #include +#include #include "../ipc/ipc.hpp" @@ -21,10 +22,12 @@ public: [[nodiscard]] virtual const char* name() const = 0; [[nodiscard]] virtual const char* genericArgumentName() const = 0; + [[nodiscard]] virtual qsizetype size() const = 0; [[nodiscard]] virtual void* fromString(const QString& /*string*/) const { return nullptr; } [[nodiscard]] virtual QString toString(void* /*slot*/) const { return ""; } [[nodiscard]] virtual void* createStorage() const { return nullptr; } virtual void destroyStorage(void* /*slot*/) const {} + void* copyStorage(const void* data) const; static const IpcType* ipcType(const QMetaType& metaType); }; @@ -43,6 +46,7 @@ public: [[nodiscard]] QGenericReturnArgument asGenericReturnArgument(); void replace(void* value); + void replace(const QVariant& value); private: const IpcType* mType = nullptr; @@ -53,6 +57,7 @@ class VoidIpcType: public IpcType { public: [[nodiscard]] const char* name() const override; [[nodiscard]] const char* genericArgumentName() const override; + [[nodiscard]] qsizetype size() const override; static const VoidIpcType INSTANCE; }; @@ -61,6 +66,7 @@ class StringIpcType: public IpcType { public: [[nodiscard]] const char* name() const override; [[nodiscard]] const char* genericArgumentName() const override; + [[nodiscard]] qsizetype size() const override; [[nodiscard]] void* fromString(const QString& string) const override; [[nodiscard]] QString toString(void* slot) const override; [[nodiscard]] void* createStorage() const override; @@ -73,6 +79,7 @@ class IntIpcType: public IpcType { public: [[nodiscard]] const char* name() const override; [[nodiscard]] const char* genericArgumentName() const override; + [[nodiscard]] qsizetype size() const override; [[nodiscard]] void* fromString(const QString& string) const override; [[nodiscard]] QString toString(void* slot) const override; [[nodiscard]] void* createStorage() const override; @@ -85,6 +92,7 @@ class BoolIpcType: public IpcType { public: [[nodiscard]] const char* name() const override; [[nodiscard]] const char* genericArgumentName() const override; + [[nodiscard]] qsizetype size() const override; [[nodiscard]] void* fromString(const QString& string) const override; [[nodiscard]] QString toString(void* slot) const override; [[nodiscard]] void* createStorage() const override; @@ -97,6 +105,7 @@ class DoubleIpcType: public IpcType { public: [[nodiscard]] const char* name() const override; [[nodiscard]] const char* genericArgumentName() const override; + [[nodiscard]] qsizetype size() const override; [[nodiscard]] void* fromString(const QString& string) const override; [[nodiscard]] QString toString(void* slot) const override; [[nodiscard]] void* createStorage() const override; @@ -109,6 +118,7 @@ class ColorIpcType: public IpcType { public: [[nodiscard]] const char* name() const override; [[nodiscard]] const char* genericArgumentName() const override; + [[nodiscard]] qsizetype size() const override; [[nodiscard]] void* fromString(const QString& string) const override; [[nodiscard]] QString toString(void* slot) const override; [[nodiscard]] void* createStorage() const override; @@ -127,13 +137,23 @@ struct WireFunctionDefinition { DEFINE_SIMPLE_DATASTREAM_OPS(WireFunctionDefinition, data.name, data.returnType, data.arguments); -struct WireTargetDefinition { +struct WirePropertyDefinition { QString name; - QVector functions; + QString type; [[nodiscard]] QString toString() const; }; -DEFINE_SIMPLE_DATASTREAM_OPS(WireTargetDefinition, data.name, data.functions); +DEFINE_SIMPLE_DATASTREAM_OPS(WirePropertyDefinition, data.name, data.type); + +struct WireTargetDefinition { + QString name; + QVector functions; + QVector properties; + + [[nodiscard]] QString toString() const; +}; + +DEFINE_SIMPLE_DATASTREAM_OPS(WireTargetDefinition, data.name, data.functions, data.properties); } // namespace qs::io::ipc diff --git a/src/io/ipccomm.cpp b/src/io/ipccomm.cpp index f2a9118a..7203a307 100644 --- a/src/io/ipccomm.cpp +++ b/src/io/ipccomm.cpp @@ -21,16 +21,17 @@ namespace qs::io::ipc::comm { struct NoCurrentGeneration: std::monostate {}; struct TargetNotFound: std::monostate {}; -struct FunctionNotFound: std::monostate {}; +struct EntryNotFound: std::monostate {}; using QueryResponse = std::variant< std::monostate, NoCurrentGeneration, TargetNotFound, - FunctionNotFound, + EntryNotFound, QVector, WireTargetDefinition, - WireFunctionDefinition>; + WireFunctionDefinition, + WirePropertyDefinition>; void QueryMetadataCommand::exec(qs::ipc::IpcServerConnection* conn) const { auto resp = conn->responseStream(); @@ -44,16 +45,24 @@ void QueryMetadataCommand::exec(qs::ipc::IpcServerConnection* conn) const { auto* handler = registry->findHandler(this->target); if (handler) { - if (this->function.isEmpty()) { + if (this->name.isEmpty()) { resp << handler->wireDef(); } else { - auto* func = handler->findFunction(this->function); + auto* func = handler->findFunction(this->name); if (func) { resp << func->wireDef(); - } else { - resp << FunctionNotFound(); + return; } + + auto* prop = handler->findProperty(this->name); + + if (prop) { + resp << prop->wireDef(); + return; + } + + resp << EntryNotFound(); } } else { resp << TargetNotFound(); @@ -64,8 +73,8 @@ void QueryMetadataCommand::exec(qs::ipc::IpcServerConnection* conn) const { } } -int queryMetadata(IpcClient* client, const QString& target, const QString& function) { - client->sendMessage(IpcCommand(QueryMetadataCommand {.target = target, .function = function})); +int queryMetadata(IpcClient* client, const QString& target, const QString& name) { + client->sendMessage(IpcCommand(QueryMetadataCommand {.target = target, .name = name})); QueryResponse slot; if (!client->waitForResponse(slot)) return -1; @@ -82,9 +91,11 @@ int queryMetadata(IpcClient* client, const QString& target, const QString& funct qCInfo(logBare).noquote() << std::get(slot).toString(); } else if (std::holds_alternative(slot)) { qCInfo(logBare).noquote() << std::get(slot).toString(); + } else if (std::holds_alternative(slot)) { + qCInfo(logBare).noquote() << std::get(slot).toString(); } else if (std::holds_alternative(slot)) { qCCritical(logBare) << "Target not found."; - } else if (std::holds_alternative(slot)) { + } else if (std::holds_alternative(slot)) { qCCritical(logBare) << "Function not found."; } else if (std::holds_alternative(slot)) { qCCritical(logBare) << "Not ready to accept queries yet."; @@ -119,7 +130,7 @@ using StringCallResponse = std::variant< std::monostate, NoCurrentGeneration, TargetNotFound, - FunctionNotFound, + EntryNotFound, ArgParseFailed, Completed>; @@ -137,7 +148,7 @@ void StringCallCommand::exec(qs::ipc::IpcServerConnection* conn) const { auto* func = handler->findFunction(this->function); if (!func) { - resp << FunctionNotFound(); + resp << EntryNotFound(); return; } @@ -223,7 +234,7 @@ int callFunction( qCCritical(logBare).noquote() << "Function definition:" << error.definition.toString(); } else if (std::holds_alternative(slot)) { qCCritical(logBare) << "Target not found."; - } else if (std::holds_alternative(slot)) { + } else if (std::holds_alternative(slot)) { qCCritical(logBare) << "Function not found."; } else if (std::holds_alternative(slot)) { qCCritical(logBare) << "Not ready to accept queries yet."; @@ -233,4 +244,74 @@ int callFunction( return -1; } + +struct PropertyValue { + QString value; +}; + +DEFINE_SIMPLE_DATASTREAM_OPS(PropertyValue, data.value); + +using StringPropReadResponse = + std::variant; + +void StringPropReadCommand::exec(qs::ipc::IpcServerConnection* conn) const { + auto resp = conn->responseStream(); + + if (auto* generation = EngineGeneration::currentGeneration()) { + auto* registry = IpcHandlerRegistry::forGeneration(generation); + + auto* handler = registry->findHandler(this->target); + if (!handler) { + resp << TargetNotFound(); + return; + } + + auto* prop = handler->findProperty(this->property); + if (!prop) { + resp << EntryNotFound(); + return; + } + + auto slot = IpcTypeSlot(prop->type); + prop->read(handler, slot); + + resp << PropertyValue { + .value = slot.type()->toString(slot.get()), + }; + } else { + conn->respond(StringCallResponse(NoCurrentGeneration())); + } +} + +int getProperty(IpcClient* client, const QString& target, const QString& property) { + if (target.isEmpty()) { + qCCritical(logBare) << "Target required to send message."; + return -1; + } else if (property.isEmpty()) { + qCCritical(logBare) << "Property required to send message."; + return -1; + } + + client->sendMessage(IpcCommand(StringPropReadCommand {.target = target, .property = property})); + + StringPropReadResponse slot; + if (!client->waitForResponse(slot)) return -1; + + if (std::holds_alternative(slot)) { + auto& result = std::get(slot); + QTextStream(stdout) << result.value << Qt::endl; + return 0; + } else if (std::holds_alternative(slot)) { + qCCritical(logBare) << "Target not found."; + } else if (std::holds_alternative(slot)) { + qCCritical(logBare) << "Property not found."; + } else if (std::holds_alternative(slot)) { + qCCritical(logBare) << "Not ready to accept queries yet."; + } else { + qCCritical(logIpc) << "Received invalid IPC response from" << client; + } + + return -1; +} + } // namespace qs::io::ipc::comm diff --git a/src/io/ipccomm.hpp b/src/io/ipccomm.hpp index 69463983..bc7dbf97 100644 --- a/src/io/ipccomm.hpp +++ b/src/io/ipccomm.hpp @@ -2,6 +2,7 @@ #include #include +#include #include "../ipc/ipc.hpp" @@ -9,12 +10,12 @@ namespace qs::io::ipc::comm { struct QueryMetadataCommand { QString target; - QString function; + QString name; void exec(qs::ipc::IpcServerConnection* conn) const; }; -DEFINE_SIMPLE_DATASTREAM_OPS(QueryMetadataCommand, data.target, data.function); +DEFINE_SIMPLE_DATASTREAM_OPS(QueryMetadataCommand, data.target, data.name); struct StringCallCommand { QString target; @@ -27,7 +28,7 @@ struct StringCallCommand { DEFINE_SIMPLE_DATASTREAM_OPS(StringCallCommand, data.target, data.function, data.arguments); void handleMsg(qs::ipc::IpcServerConnection* conn); -int queryMetadata(qs::ipc::IpcClient* client, const QString& target, const QString& function); +int queryMetadata(qs::ipc::IpcClient* client, const QString& target, const QString& name); int callFunction( qs::ipc::IpcClient* client, @@ -36,4 +37,15 @@ int callFunction( const QVector& arguments ); +struct StringPropReadCommand { + QString target; + QString property; + + void exec(qs::ipc::IpcServerConnection* conn) const; +}; + +DEFINE_SIMPLE_DATASTREAM_OPS(StringPropReadCommand, data.target, data.property); + +int getProperty(qs::ipc::IpcClient* client, const QString& target, const QString& property); + } // namespace qs::io::ipc::comm diff --git a/src/io/ipchandler.cpp b/src/io/ipchandler.cpp index 510b2055..517e4505 100644 --- a/src/io/ipchandler.cpp +++ b/src/io/ipchandler.cpp @@ -107,6 +107,32 @@ WireFunctionDefinition IpcFunction::wireDef() const { return wire; } +bool IpcProperty::resolve(QString& error) { + this->type = IpcType::ipcType(this->property.metaType()); + + if (!this->type) { + error = QString("Type %1 cannot be used across IPC.").arg(this->property.metaType().name()); + return false; + } + + return true; +} + +void IpcProperty::read(QObject* target, IpcTypeSlot& slot) const { + slot.replace(this->property.read(target)); +} + +QString IpcProperty::toString() const { + return QString("property ") % this->property.name() % ": " % this->type->name(); +} + +WirePropertyDefinition IpcProperty::wireDef() const { + WirePropertyDefinition wire; + wire.name = this->property.name(); + wire.type = this->type->name(); + return wire; +} + IpcCallStorage::IpcCallStorage(const IpcFunction& function): returnSlot(function.returnType) { for (const auto& arg: function.argumentTypes) { this->argumentSlots.emplace_back(arg); @@ -153,6 +179,21 @@ void IpcHandler::onPostReload() { } } + for (auto i = smeta.propertyCount(); i != meta->propertyCount(); i++) { + const auto& property = meta->property(i); + if (!property.isReadable() || !property.hasNotifySignal()) continue; + + auto ipcProp = IpcProperty(property); + QString error; + + if (!ipcProp.resolve(error)) { + qmlWarning(this).nospace().noquote() + << "Error parsing property \"" << property.name() << "\": " << error; + } else { + this->propertyMap.insert(property.name(), ipcProp); + } + } + this->complete = true; this->updateRegistration(); @@ -270,6 +311,10 @@ WireTargetDefinition IpcHandler::wireDef() const { wire.functions += func.wireDef(); } + for (const auto& prop: this->propertyMap.values()) { + wire.properties += prop.wireDef(); + } + return wire; } @@ -307,6 +352,13 @@ IpcFunction* IpcHandler::findFunction(const QString& name) { else return &*itr; } +IpcProperty* IpcHandler::findProperty(const QString& name) { + auto itr = this->propertyMap.find(name); + + if (itr == this->propertyMap.end()) return nullptr; + else return &*itr; +} + IpcHandler* IpcHandlerRegistry::findHandler(const QString& target) { return this->handlers.value(target); } diff --git a/src/io/ipchandler.hpp b/src/io/ipchandler.hpp index cc4ee5f4..e6b24ba1 100644 --- a/src/io/ipchandler.hpp +++ b/src/io/ipchandler.hpp @@ -53,14 +53,28 @@ private: friend class IpcFunction; }; +class IpcProperty { +public: + explicit IpcProperty(QMetaProperty property): property(property) {} + + bool resolve(QString& error); + void read(QObject* target, IpcTypeSlot& slot) const; + + [[nodiscard]] QString toString() const; + [[nodiscard]] WirePropertyDefinition wireDef() const; + + QMetaProperty property; + const IpcType* type = nullptr; +}; + class IpcHandlerRegistry; ///! Handler for IPC message calls. /// Each IpcHandler is registered into a per-instance map by its unique @@target. -/// Functions defined on the IpcHandler can be called by `qs msg`. +/// Functions and properties defined on the IpcHandler can be accessed via `qs ipc`. /// /// #### Handler Functions -/// IPC handler functions can be called by `qs msg` as long as they have at most 10 +/// IPC handler functions can be called by `qs ipc call` as long as they have at most 10 /// arguments, and all argument types along with the return type are listed below. /// /// **Argument and return types must be explicitly specified or they will not @@ -112,9 +126,9 @@ class IpcHandlerRegistry; /// } /// } /// ``` -/// The list of registered targets can be inspected using `qs msg -s`. +/// The list of registered targets can be inspected using `qs ipc show`. /// ```sh -/// $ qs msg -s +/// $ qs ipc show /// target rect /// function setColor(color: color): void /// function getColor(): color @@ -124,18 +138,22 @@ class IpcHandlerRegistry; /// function getRadius(): int /// ``` /// -/// and then invoked using `qs msg`. +/// and then invoked using `qs ipc call`. /// ```sh -/// $ qs msg rect setColor orange -/// $ qs msg rect setAngle 40.5 -/// $ qs msg rect setRadius 30 -/// $ qs msg rect getColor +/// $ qs ipc call rect setColor orange +/// $ qs ipc call rect setAngle 40.5 +/// $ qs ipc call rect setRadius 30 +/// $ qs ipc call rect getColor /// #ffffa500 -/// $ qs msg rect getAngle +/// $ qs ipc call rect getAngle /// 40.5 -/// $ qs msg rect getRadius +/// $ qs ipc call rect getRadius /// 30 /// ``` +/// +/// #### Properties +/// Properties of an IpcHanlder can be read using `qs ipc prop get` as long as they are +/// of an IPC compatible type. See the table above for compatible types. class IpcHandler : public QObject , public PostReloadHook { @@ -162,12 +180,16 @@ public: QString listMembers(qsizetype indent); [[nodiscard]] IpcFunction* findFunction(const QString& name); + [[nodiscard]] IpcProperty* findProperty(const QString& name); [[nodiscard]] WireTargetDefinition wireDef() const; signals: void enabledChanged(); void targetChanged(); +private slots: + //void handleIpcPropertyChange(); + private: void updateRegistration(bool destroying = false); @@ -183,6 +205,7 @@ private: bool complete = false; QHash functionMap; + QHash propertyMap; friend class IpcHandlerRegistry; }; diff --git a/src/ipc/ipccommand.hpp b/src/ipc/ipccommand.hpp index c2e5059f..b221b460 100644 --- a/src/ipc/ipccommand.hpp +++ b/src/ipc/ipccommand.hpp @@ -15,6 +15,7 @@ using IpcCommand = std::variant< std::monostate, IpcKillCommand, qs::io::ipc::comm::QueryMetadataCommand, - qs::io::ipc::comm::StringCallCommand>; + qs::io::ipc::comm::StringCallCommand, + qs::io::ipc::comm::StringPropReadCommand>; } // namespace qs::ipc diff --git a/src/launch/command.cpp b/src/launch/command.cpp index f814b5ff..00ad613e 100644 --- a/src/launch/command.cpp +++ b/src/launch/command.cpp @@ -293,6 +293,8 @@ int ipcCommand(CommandState& cmd) { return IpcClient::connect(instance.instance.instanceId, [&](IpcClient& client) { if (*cmd.ipc.show || cmd.ipc.showOld) { return qs::io::ipc::comm::queryMetadata(&client, *cmd.ipc.target, *cmd.ipc.name); + } else if (*cmd.ipc.getprop) { + return qs::io::ipc::comm::getProperty(&client, *cmd.ipc.target, *cmd.ipc.name); } else { QVector arguments; for (auto& arg: cmd.ipc.arguments) { diff --git a/src/launch/launch_p.hpp b/src/launch/launch_p.hpp index a9a515c4..77808450 100644 --- a/src/launch/launch_p.hpp +++ b/src/launch/launch_p.hpp @@ -71,6 +71,7 @@ struct CommandState { CLI::App* ipc = nullptr; CLI::App* show = nullptr; CLI::App* call = nullptr; + CLI::App* getprop = nullptr; bool showOld = false; QStringOption target; QStringOption name; diff --git a/src/launch/parsecommand.cpp b/src/launch/parsecommand.cpp index 2c082fec..1edbf01e 100644 --- a/src/launch/parsecommand.cpp +++ b/src/launch/parsecommand.cpp @@ -194,6 +194,18 @@ int parseCommand(int argc, char** argv, CommandState& state) { ->description("Arguments to the called function.") ->allow_extra_args(); } + + { + auto* prop = + sub->add_subcommand("prop", "Manipulate IpcHandler properties.")->require_subcommand(); + + { + auto* get = prop->add_subcommand("get", "Read the value of a property."); + state.ipc.getprop = get; + get->add_option("target", state.ipc.target, "The target to read the property of."); + get->add_option("property", state.ipc.name)->description("The property to read."); + } + } } {