hyprland/focus_grab: add HyprlandFocusGrab

This commit is contained in:
outfoxxed 2024-05-03 03:08:32 -07:00
parent e7cfb5cf37
commit 87a884ca36
Signed by: outfoxxed
GPG key ID: 4C88A185FB89301E
15 changed files with 607 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,78 @@
#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();
}
}
bool FocusGrab::isActive() const { return this->active; }
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()) {
if (window->handle() == nullptr) {
window->create();
}
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->pendingAdditions.removeAll(waylandWindow);
this->remove_surface(waylandWindow->surface());
this->commitRequired = true;
}
}
void FocusGrab::addWaylandWindow(QWaylandWindow* window) {
this->add_surface(window->surface());
this->pendingAdditions.append(window);
this->commitRequired = true;
}
void FocusGrab::sync() {
if (this->commitRequired) {
this->commit();
this->commitRequired = false;
// the protocol will always send cleared() when the grab is deactivated,
// even if it was due to window destruction, so we don't need to track it.
if (!this->pendingAdditions.isEmpty()) {
this->pendingAdditions.clear();
if (!this->active) {
this->active = true;
emit this->activated();
}
}
}
}
void FocusGrab::hyprland_focus_grab_v1_cleared() {
this->active = false;
emit this->cleared();
}
} // namespace qs::hyprland::focus_grab

View file

@ -0,0 +1,46 @@
#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);
[[nodiscard]] bool isActive() const;
void addWindow(QWindow* window);
void removeWindow(QWindow* window);
void sync();
signals:
void activated();
void cleared();
private:
void hyprland_focus_grab_v1_cleared() override;
void addWaylandWindow(QWaylandWindow* window);
QList<QWaylandWindow*> pendingAdditions;
bool commitRequired = false;
bool active = false;
};
} // namespace qs::hyprland::focus_grab

View file

@ -0,0 +1,128 @@
<?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>
<request name="destroy" type="destructor">
<description summary="destroy the focus grab manager">
Destroy the focus grab manager.
This doesn't destroy existing focus grab objects.
</description>
</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 will be passed to the client
and a compositor-picked surface in the whitelist will receive a
wl_keyboard::enter event if a whitelisted surface is not already entered.
Upon meeting implementation-defined criteria usually meaning a mouse or
touch input outside of any whitelisted surfaces, the compositor will
clear the whitelist, rendering the grab inert and sending the cleared
event. The same will happen if another focus grab or similar action
is started at the compositor's discretion.
</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.
If the grab was active and the removed surface was entered by the
keyboard, another surface will be entered on commit.
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
become inert.
</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 by the compositor,
regardless of cause.
</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,131 @@
#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;
void HyprlandFocusGrab::componentComplete() { this->tryActivate(); }
bool HyprlandFocusGrab::isActive() const { return this->grab != nullptr && this->grab->isActive(); }
void HyprlandFocusGrab::setActive(bool active) {
if (active == this->targetActive) return;
this->targetActive = active;
if (!active) {
delete this->grab;
this->grab = nullptr;
emit this->activeChanged();
} else {
this->tryActivate();
}
}
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::onGrabActivated() { emit this->activeChanged(); }
void HyprlandFocusGrab::onGrabCleared() {
emit this->cleared();
this->setActive(false);
}
void HyprlandFocusGrab::onProxyConnected() {
if (this->grab != nullptr) {
this->grab->addWindow(qobject_cast<ProxyWindowBase*>(this->sender())->backingWindow());
this->grab->sync();
}
}
void HyprlandFocusGrab::tryActivate() {
if (!this->targetActive || this->isActive()) return;
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::activated, this, &HyprlandFocusGrab::onGrabActivated);
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->sync();
}
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->grab != nullptr && 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->grab != nullptr && newProxy->backingWindow() != nullptr) {
this->grab->addWindow(newProxy->backingWindow());
}
}
}
this->trackedProxies = newProxy;
if (this->grab != nullptr) this->grab->sync();
}
} // namespace qs::hyprland

View file

@ -0,0 +1,111 @@
#pragma once
#include <qlist.h>
#include <qobject.h>
#include <qqmlintegration.h>
#include <qqmllist.h>
#include <qqmlparserstatus.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
/// cleared 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
, public QQmlParserStatus {
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.
///
/// This property will change to false once the grab is dismissed.
/// It will not change to true until the grab begins, which requires
/// at least one visible window.
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) {}
void classBegin() override {}
void componentComplete() override;
[[nodiscard]] bool isActive() const;
void setActive(bool active);
[[nodiscard]] QObjectList windows() const;
void setWindows(QObjectList windows);
signals:
/// Sent whenever the compositor clears the focus grab.
///
/// This may be in response to all windows being removed
/// from the list or simultaneously hidden, in addition to
/// a normal clear.
void cleared();
void activeChanged();
void windowsChanged();
private slots:
void onGrabActivated();
void onGrabCleared();
void onProxyConnected();
private:
void tryActivate();
void syncWindows();
bool targetActive = false;
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"
]
-----