hyprland/focus_grab: add HyprlandFocusGrab

This commit is contained in:
outfoxxed 2024-05-03 03:08:32 -07:00
parent e7cfb5cf37
commit 618835fb0d
Signed by: outfoxxed
GPG key ID: 4C88A185FB89301E
15 changed files with 540 additions and 0 deletions

View file

@ -14,6 +14,7 @@ option(SOCKETS "Enable unix socket support" ON)
option(WAYLAND "Enable wayland support" ON)
option(WAYLAND_WLR_LAYERSHELL "Support the zwlr_layer_shell_v1 wayland protocol" ON)
option(WAYLAND_SESSION_LOCK "Support the ext_session_lock_v1 wayland protocol" ON)
option(HYPRLAND "Support hyprland specific features" ON)
option(SERVICE_STATUS_NOTIFIER "StatusNotifierItem service" ON)
message(STATUS "Quickshell configuration")
@ -27,6 +28,7 @@ if (WAYLAND)
endif ()
message(STATUS " Services")
message(STATUS " StatusNotifier: ${SERVICE_STATUS_NOTIFIER}")
message(STATUS " Hyprland: ${HYPRLAND}")
if (NOT DEFINED GIT_REVISION)
execute_process(

View file

@ -88,6 +88,7 @@ void ProxyWindowBase::createWindow() {
}
void ProxyWindowBase::deleteWindow() {
if (this->window != nullptr) emit this->windowDestroyed();
if (auto* window = this->disownWindow()) {
if (auto* generation = EngineGeneration::findObjectGeneration(this)) {
generation->deregisterIncubationController(window->incubationController());

View file

@ -102,6 +102,7 @@ public:
signals:
void windowConnected();
void windowDestroyed();
void visibleChanged();
void backerVisibilityChanged();
void xChanged();

View file

@ -68,6 +68,10 @@ if (WAYLAND_SESSION_LOCK)
add_subdirectory(session_lock)
endif()
if (HYPRLAND)
add_subdirectory(hyprland)
endif()
target_link_libraries(quickshell-wayland PRIVATE ${QT_DEPS})
target_link_libraries(quickshell-wayland-init PRIVATE ${QT_DEPS})

View file

@ -0,0 +1,11 @@
qt_add_library(quickshell-hyprland STATIC)
qt_add_qml_module(quickshell-hyprland URI Quickshell.Hyprland VERSION 0.1)
add_subdirectory(focus_grab)
target_link_libraries(quickshell-hyprland PRIVATE ${QT_DEPS})
qs_pch(quickshell-hyprland)
qs_pch(quickshell-hyprlandplugin)
target_link_libraries(quickshell PRIVATE quickshell-hyprlandplugin)

View file

@ -0,0 +1,19 @@
qt_add_library(quickshell-hyprland-focus-grab STATIC
manager.cpp
grab.cpp
qml.cpp
)
qt_add_qml_module(quickshell-hyprland-focus-grab URI Quickshell.Hyprland._FocusGrab VERSION 0.1)
add_library(quickshell-hyprland-focus-grab-init OBJECT init.cpp)
wl_proto(quickshell-hyprland-focus-grab hyprland-focus-grab-v1 "${CMAKE_CURRENT_SOURCE_DIR}/hyprland-focus-grab-v1.xml")
target_link_libraries(quickshell-hyprland-focus-grab PRIVATE ${QT_DEPS} wayland-client)
target_link_libraries(quickshell-hyprland-focus-grab-init PRIVATE ${QT_DEPS})
qs_pch(quickshell-hyprland-focus-grab)
qs_pch(quickshell-hyprland-focus-grabplugin)
qs_pch(quickshell-hyprland-focus-grab-init)
target_link_libraries(quickshell PRIVATE quickshell-hyprland-focus-grabplugin quickshell-hyprland-focus-grab-init)

View file

@ -0,0 +1,56 @@
#include "grab.hpp"
#include <private/qwaylandwindow_p.h>
#include <qobject.h>
#include <qtmetamacros.h>
#include <qwindow.h>
#include <wayland-hyprland-focus-grab-v1-client-protocol.h>
namespace qs::hyprland::focus_grab {
FocusGrab::FocusGrab(::hyprland_focus_grab_v1* grab) { this->init(grab); }
FocusGrab::~FocusGrab() {
if (this->isInitialized()) {
this->destroy();
}
}
void FocusGrab::addWindow(QWindow* window) {
if (auto* waylandWindow = dynamic_cast<QWaylandWindow*>(window->handle())) {
this->addWaylandWindow(waylandWindow);
} else {
QObject::connect(window, &QWindow::visibleChanged, this, [this, window]() {
if (window->isVisible()) {
auto* waylandWindow = dynamic_cast<QWaylandWindow*>(window->handle());
this->addWaylandWindow(waylandWindow);
this->sync();
}
});
}
}
void FocusGrab::removeWindow(QWindow* window) {
QObject::disconnect(window, nullptr, this, nullptr);
if (auto* waylandWindow = dynamic_cast<QWaylandWindow*>(window->handle())) {
this->remove_surface(waylandWindow->surface());
this->commitRequired = true;
}
}
void FocusGrab::addWaylandWindow(QWaylandWindow* window) {
this->add_surface(window->surface());
this->commitRequired = true;
}
void FocusGrab::sync() {
if (this->commitRequired) {
this->commit();
this->commitRequired = false;
}
}
void FocusGrab::hyprland_focus_grab_v1_cleared() { emit this->cleared(); }
} // namespace qs::hyprland::focus_grab

View file

@ -0,0 +1,41 @@
#pragma once
#include <private/qwaylandwindow_p.h>
#include <qlist.h>
#include <qobject.h>
#include <qtclasshelpermacros.h>
#include <qtmetamacros.h>
#include <qwayland-hyprland-focus-grab-v1.h>
#include <qwindow.h>
#include <wayland-hyprland-focus-grab-v1-client-protocol.h>
namespace qs::hyprland::focus_grab {
using HyprlandFocusGrab = QtWayland::hyprland_focus_grab_v1;
class FocusGrab
: public QObject
, public HyprlandFocusGrab {
using QWaylandWindow = QtWaylandClient::QWaylandWindow;
Q_OBJECT;
public:
explicit FocusGrab(::hyprland_focus_grab_v1* grab);
~FocusGrab() override;
Q_DISABLE_COPY_MOVE(FocusGrab);
void addWindow(QWindow* window);
void removeWindow(QWindow* window);
void sync();
signals:
void cleared();
private:
void hyprland_focus_grab_v1_cleared() override;
void addWaylandWindow(QWaylandWindow* window);
bool commitRequired = false;
};
} // namespace qs::hyprland::focus_grab

View file

@ -0,0 +1,118 @@
<?xml version="1.0" encoding="UTF-8"?>
<protocol name="hyprland_focus_grab_v1">
<copyright>
Copyright © 2024 outfoxxed
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.
</copyright>
<description summary="limit input focus to a set of surfaces">
This protocol allows clients to limit input focus to a specific set
of surfaces and receive a notification when the limiter is removed as
detailed below.
</description>
<interface name="hyprland_focus_grab_manager_v1" version="1">
<description summary="manager for focus grab objects">
This interface allows a client to create surface grab objects.
</description>
<request name="create_grab">
<description summary="create a focus grab object">
Create a surface grab object.
</description>
<arg name="grab" type="new_id" interface="hyprland_focus_grab_v1"/>
</request>
</interface>
<interface name="hyprland_focus_grab_v1" version="1">
<description summary="input focus limiter">
This interface restricts input focus to a specified whitelist of
surfaces as long as the focus grab object exists and has at least
one comitted surface.
Mouse and touch events inside a whitelisted surface will be passed
to the surface normally, while events outside of a whitelisted surface
will clear the grab object. Keyboard events are passed to the last
focused surface in the whitelist.
The compositor will clear the list of surfaces, rendering the grab object
inert, when receiving a mouse or touch input that is not inside one of
the specified surfaces. The list will also be cleared if another surface
grab is started through this protocol or xdg_popup::drag.
</description>
<request name="add_surface">
<description summary="add a surface to the focus whitelist">
Add a surface to the whitelist. Destroying the surface is treated the
same as an explicit call to remove_surface and duplicate additions are
ignored.
Does not take effect until commit is called.
</description>
<arg name="surface" type="object" interface="wl_surface"/>
</request>
<request name="remove_surface">
<description summary="remove a surface from the focus whitelist">
Remove a surface from the whitelist. Destroying the surface is treated
the same as an explicit call to this function.
Does not take effect until commit is called.
</description>
<arg name="surface" type="object" interface="wl_surface"/>
</request>
<request name="commit">
<description summary="commit the focus whitelist">
Commit pending changes to the surface whitelist.
If the list previously had no entries and now has at least one, the grab
will start. If it previously had entries and now has none, the grab will
be removed.
</description>
</request>
<request name="destroy" type="destructor">
<description summary="destroy the focus grab">
Destroy the grab object and remove the grab if active.
</description>
</request>
<event name="cleared">
<description summary="the focus grab was cleared">
Sent when an active grab is cancelled via an input outside of a
whitelisted surface, explicit removal of the last whitelisted
and comitted surface via remove_surface, or destruction of the last
whitelisted and comitted surface.
</description>
</event>
</interface>
</protocol>

View file

@ -0,0 +1,20 @@
#include <qqml.h>
#include "../../../core/plugin.hpp"
namespace {
class HyprlandFocusGrabPlugin: public QuickshellPlugin {
void registerTypes() override {
qmlRegisterModuleImport(
"Quickshell.Hyprland",
QQmlModuleImportModuleAny,
"Quickshell.Hyprland._FocusGrab",
QQmlModuleImportLatest
);
}
};
QS_REGISTER_PLUGIN(HyprlandFocusGrabPlugin);
} // namespace

View file

@ -0,0 +1,27 @@
#include "manager.hpp"
#include <qwaylandclientextension.h>
#include "grab.hpp"
namespace qs::hyprland::focus_grab {
FocusGrabManager::FocusGrabManager(): QWaylandClientExtensionTemplate<FocusGrabManager>(1) {
this->initialize();
}
bool FocusGrabManager::available() const { return this->isActive(); }
FocusGrab* FocusGrabManager::createGrab() { return new FocusGrab(this->create_grab()); }
FocusGrabManager* FocusGrabManager::instance() {
static FocusGrabManager* instance = nullptr; // NOLINT
if (instance == nullptr) {
instance = new FocusGrabManager();
}
return instance;
}
} // namespace qs::hyprland::focus_grab

View file

@ -0,0 +1,22 @@
#pragma once
#include <qwayland-hyprland-focus-grab-v1.h>
#include <qwaylandclientextension.h>
namespace qs::hyprland::focus_grab {
using HyprlandFocusGrabManager = QtWayland::hyprland_focus_grab_manager_v1;
class FocusGrab;
class FocusGrabManager
: public QWaylandClientExtensionTemplate<FocusGrabManager>
, public HyprlandFocusGrabManager {
public:
explicit FocusGrabManager();
[[nodiscard]] bool available() const;
[[nodiscard]] FocusGrab* createGrab();
static FocusGrabManager* instance();
};
} // namespace qs::hyprland::focus_grab

View file

@ -0,0 +1,117 @@
#include "qml.hpp"
#include <utility>
#include <qlist.h>
#include <qlogging.h>
#include <qobject.h>
#include <qqmllist.h>
#include <qtmetamacros.h>
#include <qwindow.h>
#include "../../../core/proxywindow.hpp"
#include "../../../core/windowinterface.hpp"
#include "grab.hpp"
#include "manager.hpp"
namespace qs::hyprland {
using focus_grab::FocusGrab;
using focus_grab::FocusGrabManager;
bool HyprlandFocusGrab::isActive() const { return this->grab != nullptr; }
void HyprlandFocusGrab::setActive(bool active) {
if (active == this->isActive()) return;
if (!active) {
delete this->grab;
this->grab = nullptr;
emit this->activeChanged();
} else {
auto* manager = FocusGrabManager::instance();
if (!manager->isActive()) {
qWarning() << "The active compositor does not support the hyprland_focus_grab_v1 protocol. "
"HyprlandFocusGrab will not work.";
qWarning() << "** Learn why $XDG_CURRENT_DESKTOP sucks and download a better compositor "
"today at https://hyprland.org";
return;
}
this->grab = manager->createGrab();
this->grab->setParent(this);
QObject::connect(this->grab, &FocusGrab::cleared, this, &HyprlandFocusGrab::onGrabCleared);
for (auto* proxy: this->trackedProxies) {
if (proxy->backingWindow() != nullptr) {
this->grab->addWindow(proxy->backingWindow());
}
}
this->grab->commit();
emit this->activeChanged();
}
}
QObjectList HyprlandFocusGrab::windows() const { return this->windowObjects; }
void HyprlandFocusGrab::setWindows(QObjectList windows) {
if (windows == this->windowObjects) return;
this->windowObjects = std::move(windows);
this->syncWindows();
emit this->windowsChanged();
}
void HyprlandFocusGrab::onGrabCleared() { this->setActive(false); }
void HyprlandFocusGrab::onProxyConnected() {
if (this->isActive()) {
this->grab->addWindow(qobject_cast<ProxyWindowBase*>(this->sender())->backingWindow());
this->grab->commit();
}
}
void HyprlandFocusGrab::syncWindows() {
auto newProxy = QList<ProxyWindowBase*>();
for (auto* windowObject: this->windowObjects) {
auto* proxyWindow = qobject_cast<ProxyWindowBase*>(windowObject);
if (proxyWindow == nullptr) {
if (auto* iface = qobject_cast<WindowInterface*>(windowObject)) {
proxyWindow = iface->proxyWindow();
}
}
if (proxyWindow != nullptr) {
newProxy.push_back(proxyWindow);
}
}
for (auto* oldWindow: this->trackedProxies) {
if (!newProxy.contains(oldWindow)) {
QObject::disconnect(oldWindow, nullptr, this, nullptr);
if (this->isActive() && oldWindow->backingWindow() != nullptr) {
this->grab->removeWindow(oldWindow->backingWindow());
}
}
}
for (auto* newProxy: newProxy) {
if (!this->trackedProxies.contains(newProxy)) {
QObject::connect(
newProxy,
&ProxyWindowBase::windowConnected,
this,
&HyprlandFocusGrab::onProxyConnected
);
if (this->isActive() && newProxy->backingWindow() != nullptr) {
this->grab->addWindow(newProxy->backingWindow());
}
}
}
this->trackedProxies = newProxy;
if (this->isActive()) this->grab->sync();
}
} // namespace qs::hyprland

View file

@ -0,0 +1,95 @@
#pragma once
#include <qlist.h>
#include <qobject.h>
#include <qqmlintegration.h>
#include <qqmllist.h>
#include <qtmetamacros.h>
#include <qtypes.h>
#include <qwindow.h>
class ProxyWindowBase;
namespace qs::hyprland {
namespace focus_grab {
class FocusGrab;
}
///! Input focus grabber
/// Object for managing input focus grabs via the [hyprland_focus_grab_v1]
/// wayland protocol.
///
/// When enabled, all of the windows listed in the `windows` property will
/// receive input normally, and will retain keyboard focus even if the mouse
/// is moved off of them. When areas of the screen that are not part of a listed
/// window are clicked or touched, the grab will become inactive and emit the
/// activeChanged signal.
///
/// This is useful for implementing dismissal of popup type windows.
/// ```qml
/// import Quickshell
/// import Quickshell.Hyprland
/// import QtQuick.Controls
///
/// ShellRoot {
/// FloatingWindow {
/// id: window
///
/// Button {
/// anchors.centerIn: parent
/// text: grab.active ? "Remove exclusive focus" : "Take exclusive focus"
/// onClicked: grab.active = !grab.active
/// }
///
/// HyprlandFocusGrab {
/// id: grab
/// windows: [ window ]
/// }
/// }
/// }
/// ```
///
/// [hyprland_focus_grab_v1]: https://github.com/hyprwm/hyprland-protocols/blob/main/protocols/hyprland-global-shortcuts-v1.xml
class HyprlandFocusGrab: public QObject {
Q_OBJECT;
/// If the focus grab is active. Defaults to false.
///
/// When set to true, an input grab will be created for the listed windows.
/// If no windows are listed, the grab will not be initiated until there is
/// at least one.
///
/// This property will change to false once the grab is dismissed.
Q_PROPERTY(bool active READ isActive WRITE setActive NOTIFY activeChanged);
/// The list of windows to whitelist for input.
Q_PROPERTY(QList<QObject*> windows READ windows WRITE setWindows NOTIFY windowsChanged);
QML_ELEMENT;
public:
explicit HyprlandFocusGrab(QObject* parent = nullptr): QObject(parent) {}
[[nodiscard]] bool isActive() const;
void setActive(bool active);
[[nodiscard]] QObjectList windows() const;
void setWindows(QObjectList windows);
signals:
void activeChanged();
void windowsChanged();
private slots:
void onGrabCleared();
void onProxyConnected();
private:
void syncWindows();
QObjectList windowObjects;
QList<ProxyWindowBase*> trackedProxies;
QList<QWindow*> trackedWindows;
focus_grab::FocusGrab* grab = nullptr;
};
}; // namespace qs::hyprland

View file

@ -0,0 +1,6 @@
name = "Quickshell.Hyprland"
description = "Hyprland specific Quickshell types"
headers = [
"focus_grab/qml.hpp"
]
-----