feat: add clickthrough mask to windows

This commit is contained in:
outfoxxed 2024-02-13 06:11:00 -08:00
parent 5f75c40b67
commit 82aa7d45d3
Signed by: outfoxxed
GPG key ID: 4C88A185FB89301E
7 changed files with 329 additions and 2 deletions

View file

@ -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)

2
docs

@ -1 +1 @@
Subproject commit 27b3274027251ebf382e31546ef2b350ae2f7b0e
Subproject commit 94f07543939dfe682bb382f6802cbe9ff3eea061

View file

@ -8,6 +8,7 @@ headers = [
"variants.hpp",
"proxywindow.hpp",
"layershell.hpp",
"region.hpp",
]
-----
The core types provided by QuickShell

View file

@ -4,9 +4,13 @@
#include <qqmllist.h>
#include <qquickitem.h>
#include <qquickwindow.h>
#include <qregion.h>
#include <qtmetamacros.h>
#include <qtypes.h>
#include <qwindow.h>
#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

View file

@ -10,6 +10,7 @@
#include <qtmetamacros.h>
#include <qtypes.h>
#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<QObject> 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<QObject> 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<QObject> dataBacker(QQmlListProperty<QObject>* prop);
@ -102,6 +153,8 @@ private:
static void dataClear(QQmlListProperty<QObject>* prop);
static void dataReplace(QQmlListProperty<QObject>* prop, qsizetype i, QObject* obj);
static void dataRemoveLast(QQmlListProperty<QObject>* prop);
PendingRegion* mMask = nullptr;
};
// qt attempts to resize the window but fails because wayland

107
src/cpp/region.cpp Normal file
View file

@ -0,0 +1,107 @@
#include "region.hpp"
#include <cmath>
#include <qobject.h>
#include <qpoint.h>
#include <qqmllist.h>
#include <qquickitem.h>
#include <qregion.h>
#include <qtmetamacros.h>
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> PendingRegion::regions() {
return QQmlListProperty<PendingRegion>(
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<int>(origin.x()),
static_cast<int>(origin.y()),
static_cast<int>(std::ceil(size.x())),
static_cast<int>(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<PendingRegion>* prop, PendingRegion* region) {
auto* self = static_cast<PendingRegion*>(prop->object); // NOLINT
region->setParent(self);
self->mRegions.append(region);
QObject::connect(region, &PendingRegion::changed, self, &PendingRegion::childrenChanged);
emit self->childrenChanged();
}

125
src/cpp/region.hpp Normal file
View file

@ -0,0 +1,125 @@
#pragma once
#include <qobject.h>
#include <qqmlengine.h>
#include <qqmlintegration.h>
#include <qqmllist.h>
#include <qquickitem.h>
#include <qregion.h>
#include <qtmetamacros.h>
#include <qtypes.h>
/// 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<PendingRegion> regions READ regions);
Q_CLASSINFO("DefaultProperty", "regions");
QML_NAMED_ELEMENT(Region);
public:
explicit PendingRegion(QObject* parent = nullptr);
void setItem(QQuickItem* item);
QQmlListProperty<PendingRegion> 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<PendingRegion>* prop, PendingRegion* region);
QQuickItem* mItem = nullptr;
qint32 mX = 0;
qint32 mY = 0;
qint32 mWidth = 0;
qint32 mHeight = 0;
QList<PendingRegion*> mRegions;
};