From 82aa7d45d36309f8a3f42823dc0a72c51fa1ee2e Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Tue, 13 Feb 2024 06:11:00 -0800 Subject: [PATCH] feat: add clickthrough mask to windows --- CMakeLists.txt | 1 + docs | 2 +- src/cpp/module.md | 1 + src/cpp/proxywindow.cpp | 40 +++++++++++++ src/cpp/proxywindow.hpp | 55 +++++++++++++++++- src/cpp/region.cpp | 107 ++++++++++++++++++++++++++++++++++ src/cpp/region.hpp | 125 ++++++++++++++++++++++++++++++++++++++++ 7 files changed, 329 insertions(+), 2 deletions(-) create mode 100644 src/cpp/region.cpp create mode 100644 src/cpp/region.hpp diff --git a/CMakeLists.txt b/CMakeLists.txt index c372f145..db9fe596 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -38,6 +38,7 @@ qt_add_executable(quickshell src/cpp/qmlglobal.cpp src/cpp/qmlscreen.cpp src/cpp/watcher.cpp + src/cpp/region.cpp ) qt_add_qml_module(quickshell URI QuickShell) diff --git a/docs b/docs index 27b32740..94f07543 160000 --- a/docs +++ b/docs @@ -1 +1 @@ -Subproject commit 27b3274027251ebf382e31546ef2b350ae2f7b0e +Subproject commit 94f07543939dfe682bb382f6802cbe9ff3eea061 diff --git a/src/cpp/module.md b/src/cpp/module.md index f7e1d8ec..c24bc8b4 100644 --- a/src/cpp/module.md +++ b/src/cpp/module.md @@ -8,6 +8,7 @@ headers = [ "variants.hpp", "proxywindow.hpp", "layershell.hpp", + "region.hpp", ] ----- The core types provided by QuickShell diff --git a/src/cpp/proxywindow.cpp b/src/cpp/proxywindow.cpp index dc4394be..96f1928f 100644 --- a/src/cpp/proxywindow.cpp +++ b/src/cpp/proxywindow.cpp @@ -4,9 +4,13 @@ #include #include #include +#include +#include #include #include +#include "region.hpp" + ProxyWindowBase::~ProxyWindowBase() { if (this->window != nullptr) { this->window->deleteLater(); @@ -22,11 +26,17 @@ void ProxyWindowBase::earlyInit(QObject* old) { this->window = oldpw->disownWindow(); } + this->window->setMask(QRegion()); + // clang-format off QObject::connect(this->window, &QWindow::visibilityChanged, this, &ProxyWindowBase::visibleChanged); QObject::connect(this->window, &QWindow::widthChanged, this, &ProxyWindowBase::widthChanged); QObject::connect(this->window, &QWindow::heightChanged, this, &ProxyWindowBase::heightChanged); QObject::connect(this->window, &QQuickWindow::colorChanged, this, &ProxyWindowBase::colorChanged); + + QObject::connect(this, &ProxyWindowBase::maskChanged, this, &ProxyWindowBase::onMaskChanged); + QObject::connect(this, &ProxyWindowBase::widthChanged, this, &ProxyWindowBase::onMaskChanged); + QObject::connect(this, &ProxyWindowBase::heightChanged, this, &ProxyWindowBase::onMaskChanged); // clang-format on } @@ -55,6 +65,36 @@ PROXYPROP(qint32, width, setWidth); PROXYPROP(qint32, height, setHeight); PROXYPROP(QColor, color, setColor); +PendingRegion* ProxyWindowBase::mask() { return this->mMask; } + +void ProxyWindowBase::setMask(PendingRegion* mask) { + if (this->mMask != nullptr) { + this->mMask->deleteLater(); + } + + if (mask != nullptr) { + mask->setParent(this); + this->mMask = mask; + QObject::connect(mask, &PendingRegion::changed, this, &ProxyWindowBase::maskChanged); + emit this->maskChanged(); + } +} + +void ProxyWindowBase::onMaskChanged() { + 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->setMask(mask); +} + // see: // https://code.qt.io/cgit/qt/qtdeclarative.git/tree/src/quick/items/qquickwindow.cpp // https://code.qt.io/cgit/qt/qtdeclarative.git/tree/src/quick/items/qquickitem.cpp diff --git a/src/cpp/proxywindow.hpp b/src/cpp/proxywindow.hpp index 099bf2d5..27ab57b1 100644 --- a/src/cpp/proxywindow.hpp +++ b/src/cpp/proxywindow.hpp @@ -10,6 +10,7 @@ #include #include +#include "region.hpp" #include "scavenge.hpp" // Proxy to an actual window exposing a limited property set with the ability to @@ -33,7 +34,7 @@ class ProxyWindowBase: public Scavenger { /// The visibility of the window. /// /// > [!INFO] Windows are not visible by default so you will need to set this to make the window - /// appear. + /// > appear. 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); @@ -52,6 +53,49 @@ class ProxyWindowBase: public Scavenger { /// > } /// > ``` Q_PROPERTY(QColor color READ color WRITE setColor NOTIFY colorChanged); + /// The clickthrough mask. Defaults to null. + /// + /// If non null then the clickable areas of the window will be determined by the provided region. + /// + /// ```qml + /// ProxyShellWindow { + /// // The mask region is set to `rect`, meaning only `rect` is clickable. + /// // All other clicks pass through the window to ones behind it. + /// mask: Region { item: rect } + /// + /// Rectangle { + /// id: rect + /// + /// anchors.centerIn: parent + /// width: 100 + /// height: 100 + /// } + /// } + /// ``` + /// + /// If the provided region's intersection mode is `Combine` (the default), + /// then the region will be used as is. Otherwise it will be applied on top of the window region. + /// + /// For example, setting the intersection mode to `Xor` will invert the mask and make everything in + /// the mask region not clickable and pass through clicks inside it through the window. + /// + /// ```qml + /// ProxyShellWindow { + /// // The mask region is set to `rect`, but the intersection mode is set to `Xor`. + /// // This inverts the mask causing all clicks inside `rect` to be passed to the window + /// // behind this one. + /// mask: Region { item: rect; intersection: Intersection.Xor } + /// + /// Rectangle { + /// id: rect + /// + /// anchors.centerIn: parent + /// width: 100 + /// height: 100 + /// } + /// } + /// ``` + Q_PROPERTY(PendingRegion* mask READ mask WRITE setMask NOTIFY maskChanged); Q_PROPERTY(QQmlListProperty data READ data); Q_CLASSINFO("DefaultProperty", "data"); @@ -86,6 +130,9 @@ public: QColor color(); void setColor(QColor value); + PendingRegion* mask(); + void setMask(PendingRegion* mask); + QQmlListProperty data(); signals: @@ -93,6 +140,10 @@ signals: void widthChanged(qint32 width); void heightChanged(qint32 width); void colorChanged(QColor color); + void maskChanged(); + +private slots: + void onMaskChanged(); private: static QQmlListProperty dataBacker(QQmlListProperty* prop); @@ -102,6 +153,8 @@ private: static void dataClear(QQmlListProperty* prop); static void dataReplace(QQmlListProperty* prop, qsizetype i, QObject* obj); static void dataRemoveLast(QQmlListProperty* prop); + + PendingRegion* mMask = nullptr; }; // qt attempts to resize the window but fails because wayland diff --git a/src/cpp/region.cpp b/src/cpp/region.cpp new file mode 100644 index 00000000..9826dbd5 --- /dev/null +++ b/src/cpp/region.cpp @@ -0,0 +1,107 @@ +#include "region.hpp" +#include + +#include +#include +#include +#include +#include +#include + +PendingRegion::PendingRegion(QObject* parent): QObject(parent) { + QObject::connect(this, &PendingRegion::shapeChanged, this, &PendingRegion::changed); + QObject::connect(this, &PendingRegion::intersectionChanged, this, &PendingRegion::changed); + QObject::connect(this, &PendingRegion::itemChanged, this, &PendingRegion::changed); + QObject::connect(this, &PendingRegion::xChanged, this, &PendingRegion::changed); + QObject::connect(this, &PendingRegion::yChanged, this, &PendingRegion::changed); + QObject::connect(this, &PendingRegion::widthChanged, this, &PendingRegion::changed); + QObject::connect(this, &PendingRegion::heightChanged, this, &PendingRegion::changed); + QObject::connect(this, &PendingRegion::childrenChanged, this, &PendingRegion::changed); +} + +void PendingRegion::setItem(QQuickItem* item) { + if (this->mItem != nullptr) { + QObject::disconnect(this->mItem, nullptr, this, nullptr); + } + + this->mItem = item; + + QObject::connect(this->mItem, &QQuickItem::xChanged, this, &PendingRegion::itemChanged); + QObject::connect(this->mItem, &QQuickItem::yChanged, this, &PendingRegion::itemChanged); + QObject::connect(this->mItem, &QQuickItem::widthChanged, this, &PendingRegion::itemChanged); + QObject::connect(this->mItem, &QQuickItem::heightChanged, this, &PendingRegion::itemChanged); +} + +void PendingRegion::onItemDestroyed() { this->mItem = nullptr; } + +QQmlListProperty PendingRegion::regions() { + return QQmlListProperty( + this, + nullptr, + PendingRegion::regionsAppend, + nullptr, + nullptr, + nullptr, + nullptr, + nullptr + ); +} + +bool PendingRegion::empty() const { + return this->mItem == nullptr && this->mX == 0 && this->mY == 0 && this->mWidth == 0 + && this->mHeight == 0; +} + +QRegion PendingRegion::build() const { + auto type = QRegion::Rectangle; + switch (this->mShape) { + case RegionShape::Rect: type = QRegion::Rectangle; break; + case RegionShape::Ellipse: type = QRegion::Ellipse; break; + } + + QRegion region; + + if (this->empty()) { + region = QRegion(); + } else if (this->mItem != nullptr) { + auto origin = this->mItem->mapToScene(QPointF(0, 0)); + auto extent = this->mItem->mapToScene(QPointF(this->mItem->width(), this->mItem->height())); + auto size = extent - origin; + + region = QRegion( + static_cast(origin.x()), + static_cast(origin.y()), + static_cast(std::ceil(size.x())), + static_cast(std::ceil(size.y())), + type + ); + } else { + region = QRegion(this->mX, this->mY, this->mWidth, this->mHeight, type); + } + + for (const auto& childRegion: this->mRegions) { + region = childRegion->applyTo(region); + } + + return region; +} + +QRegion PendingRegion::applyTo(QRegion& region) const { + switch (this->mIntersection) { + case Intersection::Combine: region = region.united(this->build()); break; + case Intersection::Subtract: region = region.subtracted(this->build()); break; + case Intersection::Intersect: region = region.intersected(this->build()); break; + case Intersection::Xor: region = region.xored(this->build()); break; + } + + return region; +} + +void PendingRegion::regionsAppend(QQmlListProperty* prop, PendingRegion* region) { + auto* self = static_cast(prop->object); // NOLINT + region->setParent(self); + self->mRegions.append(region); + + QObject::connect(region, &PendingRegion::changed, self, &PendingRegion::childrenChanged); + emit self->childrenChanged(); +} diff --git a/src/cpp/region.hpp b/src/cpp/region.hpp new file mode 100644 index 00000000..06654ca9 --- /dev/null +++ b/src/cpp/region.hpp @@ -0,0 +1,125 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include + +/// Shape of a Region. +namespace RegionShape { // NOLINT +Q_NAMESPACE; +QML_ELEMENT; + +enum Enum { + Rect = 0, + Ellipse = 1, +}; +Q_ENUM_NS(Enum); + +} // namespace RegionShape + +///! Intersection strategy for Regions. +namespace Intersection { // NOLINT +Q_NAMESPACE; +QML_ELEMENT; + +enum Enum { + /// Combine this region, leaving a union of this and the other region. (opposite of `Subtract`) + Combine = 0, + /// Subtract this region, cutting this region out of the other. (opposite of `Combine`) + Subtract = 1, + /// Create an intersection of this region and the other, leaving only + /// the area covered by both. (opposite of `Xor`) + Intersect = 2, + /// Create an intersection of this region and the other, leaving only + /// the area not covered by both. (opposite of `Intersect`) + Xor = 3, +}; +Q_ENUM_NS(Enum); + +} // namespace Intersection + +///! A composable region used as a mask. +class PendingRegion: public QObject { + Q_OBJECT; + /// Defaults to `Rect`. + Q_PROPERTY(RegionShape::Enum shape MEMBER mShape NOTIFY shapeChanged); + /// The way this region interacts with its parent region. Defaults to `Combine`. + Q_PROPERTY(Intersection::Enum intersection MEMBER mIntersection NOTIFY intersectionChanged); + + /// The item that determines the geometry of the region. + /// `item` overrides `x`, `y`, `width` and `height`. + Q_PROPERTY(QQuickItem* item MEMBER mItem WRITE setItem NOTIFY itemChanged); + + /// Defaults to 0. Does nothing if `item` is set. + Q_PROPERTY(qint32 x MEMBER mX NOTIFY xChanged); + /// Defaults to 0. Does nothing if `item` is set. + Q_PROPERTY(qint32 y MEMBER mY NOTIFY yChanged); + /// Defaults to 0. Does nothing if `item` is set. + Q_PROPERTY(qint32 width MEMBER mWidth NOTIFY widthChanged); + /// Defaults to 0. Does nothing if `item` is set. + Q_PROPERTY(qint32 height MEMBER mHeight NOTIFY heightChanged); + + /// Regions to apply on top of this region. + /// + /// Regions can be nested to create a more complex region. + /// For example this will create a square region with a cutout in the middle. + /// ```qml + /// Region { + /// width: 100; height: 100; + /// + /// Region { + /// x: 50; y: 50; + /// width: 50; height: 50; + /// intersection: Intersection.Subtract + /// } + /// } + /// ``` + Q_PROPERTY(QQmlListProperty regions READ regions); + Q_CLASSINFO("DefaultProperty", "regions"); + QML_NAMED_ELEMENT(Region); + +public: + explicit PendingRegion(QObject* parent = nullptr); + + void setItem(QQuickItem* item); + + QQmlListProperty regions(); + + [[nodiscard]] bool empty() const; + [[nodiscard]] QRegion build() const; + [[nodiscard]] QRegion applyTo(QRegion& region) const; + + RegionShape::Enum mShape = RegionShape::Rect; + Intersection::Enum mIntersection = Intersection::Combine; + +signals: + void shapeChanged(); + void intersectionChanged(); + void itemChanged(); + void xChanged(); + void yChanged(); + void widthChanged(); + void heightChanged(); + void childrenChanged(); + void changed(); + +private slots: + void onItemDestroyed(); + +private: + static void regionsAppend(QQmlListProperty* prop, PendingRegion* region); + + QQuickItem* mItem = nullptr; + + qint32 mX = 0; + qint32 mY = 0; + qint32 mWidth = 0; + qint32 mHeight = 0; + + QList mRegions; +};