From b5b9c1f6c352f5e495f580618f5d176497f7814b Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Fri, 7 Jun 2024 04:31:20 -0700 Subject: [PATCH] wayland/toplevel_management: add foreign toplevel management --- .clang-tidy | 1 + BUILD.md | 15 +- CMakeLists.txt | 2 + src/wayland/CMakeLists.txt | 5 + src/wayland/module.md | 1 + .../toplevel_management/CMakeLists.txt | 22 ++ src/wayland/toplevel_management/handle.cpp | 228 +++++++++++++++ src/wayland/toplevel_management/handle.hpp | 77 +++++ src/wayland/toplevel_management/manager.cpp | 67 +++++ src/wayland/toplevel_management/manager.hpp | 47 +++ src/wayland/toplevel_management/qml.cpp | 153 ++++++++++ src/wayland/toplevel_management/qml.hpp | 140 +++++++++ ...oreign-toplevel-management-unstable-v1.xml | 270 ++++++++++++++++++ 13 files changed, 1026 insertions(+), 2 deletions(-) create mode 100644 src/wayland/toplevel_management/CMakeLists.txt create mode 100644 src/wayland/toplevel_management/handle.cpp create mode 100644 src/wayland/toplevel_management/handle.hpp create mode 100644 src/wayland/toplevel_management/manager.cpp create mode 100644 src/wayland/toplevel_management/manager.hpp create mode 100644 src/wayland/toplevel_management/qml.cpp create mode 100644 src/wayland/toplevel_management/qml.hpp create mode 100644 src/wayland/toplevel_management/wlr-foreign-toplevel-management-unstable-v1.xml diff --git a/.clang-tidy b/.clang-tidy index 6362e662..1da445cd 100644 --- a/.clang-tidy +++ b/.clang-tidy @@ -5,6 +5,7 @@ Checks: > -*, bugprone-*, -bugprone-easily-swappable-parameters, + -bugprone-forward-declararion-namespace, concurrency-*, cppcoreguidelines-*, -cppcoreguidelines-owning-memory, diff --git a/BUILD.md b/BUILD.md index c9909598..3c3e7125 100644 --- a/BUILD.md +++ b/BUILD.md @@ -59,20 +59,31 @@ Dependencies: - `wayland-protocols` #### Wlroots Layershell -Enables wlroots layershell integration through the [wlr-layer-shell-unstable-v1] protocol, +Enables wlroots layershell integration through the [zwlr-layer-shell-v1] protocol, enabling use cases such as bars overlays and backgrounds. This feature has no extra dependencies. To disable: `-DWAYLAND_WLR_LAYERSHELL=OFF` -[wlr-layer-shell-unstable-v1]: https://wayland.app/protocols/wlr-layer-shell-unstable-v1 +[zwlr-layer-shell-v1]: https://wayland.app/protocols/wlr-layer-shell-unstable-v1 #### Session Lock Enables session lock support through the [ext-session-lock-v1] protocol, which allows quickshell to be used as a session lock under compatible wayland compositors. +To disable: `-DWAYLAND_SESSION_LOCK=OFF` + [ext-session-lock-v1]: https://wayland.app/protocols/ext-session-lock-v1 + +#### Foreign Toplevel Management +Enables management of windows of other clients through the [zwlr-foreign-toplevel-management-v1] protocol, +which allows quickshell to be used as a session lock under compatible wayland compositors. + +[zwlr-foreign-toplevel-management-v1]: https://wayland.app/protocols/wlr-foreign-toplevel-management-unstable-v1 + +To disable: `-DWAYLAND_TOPLEVEL_MANAGEMENT=OFF` + ### 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 246428ec..7af6b6cf 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -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(WAYLAND_TOPLEVEL_MANAGEMENT "Support the zwlr_foreign_toplevel_management_v1 wayland protocol" ON) option(X11 "Enable X11 support" ON) option(HYPRLAND "Support hyprland specific features" ON) option(HYPRLAND_IPC "Hyprland IPC" ON) @@ -31,6 +32,7 @@ message(STATUS " Wayland: ${WAYLAND}") if (WAYLAND) message(STATUS " Wlroots Layershell: ${WAYLAND_WLR_LAYERSHELL}") message(STATUS " Session Lock: ${WAYLAND_SESSION_LOCK}") + message(STATUS " Toplevel Management: ${WAYLAND_TOPLEVEL_MANAGEMENT}") endif () message(STATUS " X11: ${X11}") message(STATUS " Services") diff --git a/src/wayland/CMakeLists.txt b/src/wayland/CMakeLists.txt index f20bc11d..ac8f42bb 100644 --- a/src/wayland/CMakeLists.txt +++ b/src/wayland/CMakeLists.txt @@ -71,6 +71,11 @@ if (WAYLAND_SESSION_LOCK) add_subdirectory(session_lock) endif() +if (WAYLAND_TOPLEVEL_MANAGEMENT) + add_subdirectory(toplevel_management) + list(APPEND WAYLAND_MODULES Quickshell.Wayland._ToplevelManagement) +endif() + if (HYPRLAND) add_subdirectory(hyprland) endif() diff --git a/src/wayland/module.md b/src/wayland/module.md index 7a427df9..d6376e39 100644 --- a/src/wayland/module.md +++ b/src/wayland/module.md @@ -4,5 +4,6 @@ headers = [ "wlr_layershell/window.hpp", "wlr_layershell.hpp", "session_lock.hpp", + "toplevel_management/qml.hpp", ] ----- diff --git a/src/wayland/toplevel_management/CMakeLists.txt b/src/wayland/toplevel_management/CMakeLists.txt new file mode 100644 index 00000000..4537c201 --- /dev/null +++ b/src/wayland/toplevel_management/CMakeLists.txt @@ -0,0 +1,22 @@ +qt_add_library(quickshell-wayland-toplevel-management STATIC + manager.cpp + handle.cpp + qml.cpp +) + +qt_add_qml_module(quickshell-wayland-toplevel-management + URI Quickshell.Wayland._ToplevelManagement + VERSION 0.1 +) + +wl_proto(quickshell-wayland-toplevel-management + wlr-foreign-toplevel-management-unstable-v1 + "${CMAKE_CURRENT_SOURCE_DIR}/wlr-foreign-toplevel-management-unstable-v1.xml" +) + +target_link_libraries(quickshell-wayland-toplevel-management PRIVATE ${QT_DEPS} wayland-client) + +qs_pch(quickshell-wayland-toplevel-management) +qs_pch(quickshell-wayland-toplevel-managementplugin) + +target_link_libraries(quickshell PRIVATE quickshell-wayland-toplevel-managementplugin) diff --git a/src/wayland/toplevel_management/handle.cpp b/src/wayland/toplevel_management/handle.cpp new file mode 100644 index 00000000..8c2886b4 --- /dev/null +++ b/src/wayland/toplevel_management/handle.cpp @@ -0,0 +1,228 @@ +#include "handle.hpp" +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "manager.hpp" +#include "qwayland-wlr-foreign-toplevel-management-unstable-v1.h" +#include "wayland-wlr-foreign-toplevel-management-unstable-v1-client-protocol.h" + +namespace qs::wayland::toplevel_management::impl { + +QString ToplevelHandle::appId() const { return this->mAppId; } +QString ToplevelHandle::title() const { return this->mTitle; } +QVector ToplevelHandle::visibleScreens() const { return this->mVisibleScreens; } +ToplevelHandle* ToplevelHandle::parent() const { return this->mParent; } +bool ToplevelHandle::activated() const { return this->mActivated; } +bool ToplevelHandle::maximized() const { return this->mMaximized; } +bool ToplevelHandle::minimized() const { return this->mMinimized; } +bool ToplevelHandle::fullscreen() const { return this->mFullscreen; } + +void ToplevelHandle::activate() { + auto* display = QtWaylandClient::QWaylandIntegration::instance()->display(); + auto* inputDevice = display->lastInputDevice(); + if (inputDevice == nullptr) return; + this->QtWayland::zwlr_foreign_toplevel_handle_v1::activate(inputDevice->object()); +} + +void ToplevelHandle::setMaximized(bool maximized) { + if (maximized) this->set_maximized(); + else this->unset_maximized(); +} + +void ToplevelHandle::setMinimized(bool minimized) { + if (minimized) this->set_minimized(); + else this->unset_minimized(); +} + +void ToplevelHandle::setFullscreen(bool fullscreen) { + if (fullscreen) this->set_fullscreen(nullptr); + else this->unset_fullscreen(); +} + +void ToplevelHandle::fullscreenOn(QScreen* screen) { + auto* waylandScreen = dynamic_cast(screen->handle()); + this->set_fullscreen(waylandScreen != nullptr ? waylandScreen->output() : nullptr); +} + +void ToplevelHandle::setRectangle(QWindow* window, QRect rect) { + if (window == nullptr) { + // will be cleared by the compositor if the surface is destroyed + if (this->rectWindow != nullptr) { + auto* waylandWindow = + dynamic_cast(this->rectWindow->handle()); + + if (waylandWindow != nullptr) { + this->set_rectangle(waylandWindow->surface(), 0, 0, 0, 0); + } + } + + QObject::disconnect(this->rectWindow, nullptr, this, nullptr); + this->rectWindow = nullptr; + return; + } + + if (this->rectWindow != window) { + if (this->rectWindow != nullptr) { + QObject::disconnect(this->rectWindow, nullptr, this, nullptr); + } + + this->rectWindow = window; + QObject::connect(window, &QObject::destroyed, this, &ToplevelHandle::onRectWindowDestroyed); + } + + if (auto* waylandWindow = dynamic_cast(window->handle())) { + this->set_rectangle(waylandWindow->surface(), rect.x(), rect.y(), rect.width(), rect.height()); + } else { + QObject::connect(window, &QWindow::visibleChanged, this, [this, window, rect]() { + if (window->isVisible()) { + if (window->handle() == nullptr) { + window->create(); + } + + auto* waylandWindow = dynamic_cast(window->handle()); + this->set_rectangle( + waylandWindow->surface(), + rect.x(), + rect.y(), + rect.width(), + rect.height() + ); + } + }); + } +} + +void ToplevelHandle::onRectWindowDestroyed() { this->rectWindow = nullptr; } + +void ToplevelHandle::zwlr_foreign_toplevel_handle_v1_done() { + qCDebug(logToplevelManagement) << this << "got done"; + auto wasReady = this->isReady; + this->isReady = true; + + if (!wasReady) { + emit this->ready(); + } +} + +void ToplevelHandle::zwlr_foreign_toplevel_handle_v1_closed() { + qCDebug(logToplevelManagement) << this << "closed"; + this->destroy(); + emit this->closed(); + delete this; +} + +void ToplevelHandle::zwlr_foreign_toplevel_handle_v1_app_id(const QString& appId) { + qCDebug(logToplevelManagement) << this << "got appid" << appId; + this->mAppId = appId; + emit this->appIdChanged(); +} + +void ToplevelHandle::zwlr_foreign_toplevel_handle_v1_title(const QString& title) { + qCDebug(logToplevelManagement) << this << "got toplevel" << title; + this->mTitle = title; + emit this->titleChanged(); +} + +void ToplevelHandle::zwlr_foreign_toplevel_handle_v1_state(wl_array* stateArray) { + auto activated = false; + auto maximized = false; + auto minimized = false; + auto fullscreen = false; + + // wl_array_for_each is illegal in C++ so it is manually expanded. + auto* state = static_cast<::zwlr_foreign_toplevel_handle_v1_state*>(stateArray->data); + auto size = stateArray->size / sizeof(::zwlr_foreign_toplevel_handle_v1_state); + for (size_t i = 0; i < size; i++) { + auto flag = state[i]; // NOLINT + switch (flag) { + case ZWLR_FOREIGN_TOPLEVEL_HANDLE_V1_STATE_ACTIVATED: activated = true; break; + case ZWLR_FOREIGN_TOPLEVEL_HANDLE_V1_STATE_MAXIMIZED: maximized = true; break; + case ZWLR_FOREIGN_TOPLEVEL_HANDLE_V1_STATE_MINIMIZED: minimized = true; break; + case ZWLR_FOREIGN_TOPLEVEL_HANDLE_V1_STATE_FULLSCREEN: fullscreen = true; break; + } + } + + qCDebug(logToplevelManagement) << this << "got state update - activated:" << activated + << "maximized:" << maximized << "minimized:" << minimized + << "fullscreen:" << fullscreen; + + if (activated != this->mActivated) { + this->mActivated = activated; + emit this->activatedChanged(); + } + + if (maximized != this->mMaximized) { + this->mMaximized = maximized; + emit this->maximizedChanged(); + } + + if (minimized != this->mMinimized) { + this->mMinimized = minimized; + emit this->minimizedChanged(); + } + + if (fullscreen != this->mFullscreen) { + this->mFullscreen = fullscreen; + emit this->fullscreenChanged(); + } +} + +void ToplevelHandle::zwlr_foreign_toplevel_handle_v1_output_enter(wl_output* output) { + auto* display = QtWaylandClient::QWaylandIntegration::instance()->display(); + auto* screen = display->screenForOutput(output)->screen(); + + qCDebug(logToplevelManagement) << this << "got output enter" << screen; + + this->mVisibleScreens.push_back(screen); + emit this->visibleScreenAdded(screen); +} + +void ToplevelHandle::zwlr_foreign_toplevel_handle_v1_output_leave(wl_output* output) { + auto* display = QtWaylandClient::QWaylandIntegration::instance()->display(); + auto* screen = display->screenForOutput(output)->screen(); + + qCDebug(logToplevelManagement) << this << "got output leave" << screen; + + emit this->visibleScreenRemoved(screen); + this->mVisibleScreens.removeOne(screen); +} + +void ToplevelHandle::zwlr_foreign_toplevel_handle_v1_parent( + ::zwlr_foreign_toplevel_handle_v1* parent +) { + auto* handle = ToplevelManager::instance()->handleFor(parent); + qCDebug(logToplevelManagement) << this << "got parent" << handle; + + if (handle != this->mParent) { + if (this->mParent != nullptr) { + QObject::disconnect(this->mParent, nullptr, this, nullptr); + } + + this->mParent = handle; + + if (handle != nullptr) { + QObject::connect(handle, &ToplevelHandle::closed, this, &ToplevelHandle::onParentClosed); + } + + emit this->parentChanged(); + } +} + +void ToplevelHandle::onParentClosed() { + this->mParent = nullptr; + emit this->parentChanged(); +} + +} // namespace qs::wayland::toplevel_management::impl diff --git a/src/wayland/toplevel_management/handle.hpp b/src/wayland/toplevel_management/handle.hpp new file mode 100644 index 00000000..a49afe82 --- /dev/null +++ b/src/wayland/toplevel_management/handle.hpp @@ -0,0 +1,77 @@ +#pragma once + +#include +#include +#include +#include +#include + +#include "wayland-wlr-foreign-toplevel-management-unstable-v1-client-protocol.h" + +namespace qs::wayland::toplevel_management::impl { + +class ToplevelHandle + : public QObject + , public QtWayland::zwlr_foreign_toplevel_handle_v1 { + Q_OBJECT; + +public: + [[nodiscard]] QString appId() const; + [[nodiscard]] QString title() const; + [[nodiscard]] QVector visibleScreens() const; + [[nodiscard]] ToplevelHandle* parent() const; + [[nodiscard]] bool activated() const; + [[nodiscard]] bool maximized() const; + [[nodiscard]] bool minimized() const; + [[nodiscard]] bool fullscreen() const; + + void activate(); + void setMaximized(bool maximized); + void setMinimized(bool minimized); + void setFullscreen(bool fullscreen); + void fullscreenOn(QScreen* screen); + void setRectangle(QWindow* window, QRect rect); + +signals: + // sent after the first done event. + void ready(); + // sent right before delete this. + void closed(); + + void appIdChanged(); + void titleChanged(); + void visibleScreenAdded(QScreen* screen); + void visibleScreenRemoved(QScreen* screen); + void parentChanged(); + void activatedChanged(); + void maximizedChanged(); + void minimizedChanged(); + void fullscreenChanged(); + +private slots: + void onParentClosed(); + void onRectWindowDestroyed(); + +private: + void zwlr_foreign_toplevel_handle_v1_done() override; + void zwlr_foreign_toplevel_handle_v1_closed() override; + void zwlr_foreign_toplevel_handle_v1_app_id(const QString& appId) override; + void zwlr_foreign_toplevel_handle_v1_title(const QString& title) override; + void zwlr_foreign_toplevel_handle_v1_state(wl_array* stateArray) override; + void zwlr_foreign_toplevel_handle_v1_output_enter(wl_output* output) override; + void zwlr_foreign_toplevel_handle_v1_output_leave(wl_output* output) override; + void zwlr_foreign_toplevel_handle_v1_parent(::zwlr_foreign_toplevel_handle_v1* parent) override; + + bool isReady = false; + QString mAppId; + QString mTitle; + QVector mVisibleScreens; + ToplevelHandle* mParent = nullptr; + bool mActivated = false; + bool mMaximized = false; + bool mMinimized = false; + bool mFullscreen = false; + QWindow* rectWindow = nullptr; +}; + +} // namespace qs::wayland::toplevel_management::impl diff --git a/src/wayland/toplevel_management/manager.cpp b/src/wayland/toplevel_management/manager.cpp new file mode 100644 index 00000000..bd477b49 --- /dev/null +++ b/src/wayland/toplevel_management/manager.cpp @@ -0,0 +1,67 @@ +#include "manager.hpp" + +#include +#include +#include +#include +#include +#include + +#include "handle.hpp" +#include "wayland-wlr-foreign-toplevel-management-unstable-v1-client-protocol.h" + +namespace qs::wayland::toplevel_management::impl { + +Q_LOGGING_CATEGORY(logToplevelManagement, "quickshell.wayland.toplevelManagement", QtWarningMsg); + +ToplevelManager::ToplevelManager(): QWaylandClientExtensionTemplate(3) { this->initialize(); } + +bool ToplevelManager::available() const { return this->isActive(); } + +const QVector& ToplevelManager::readyToplevels() const { + return this->mReadyToplevels; +} + +ToplevelHandle* ToplevelManager::handleFor(::zwlr_foreign_toplevel_handle_v1* toplevel) { + if (toplevel == nullptr) return nullptr; + + for (auto* other: this->mToplevels) { + if (other->object() == toplevel) return other; + } + + return nullptr; +} + +ToplevelManager* ToplevelManager::instance() { + static auto* instance = new ToplevelManager(); // NOLINT + return instance; +} + +void ToplevelManager::zwlr_foreign_toplevel_manager_v1_toplevel( + ::zwlr_foreign_toplevel_handle_v1* toplevel +) { + auto* handle = new ToplevelHandle(); + QObject::connect(handle, &ToplevelHandle::closed, this, &ToplevelManager::onToplevelClosed); + QObject::connect(handle, &ToplevelHandle::ready, this, &ToplevelManager::onToplevelReady); + + qCDebug(logToplevelManagement) << "Toplevel handle created" << handle; + this->mToplevels.push_back(handle); + + // Not done in constructor as a close could technically be picked up immediately on init, + // making touching the handle a UAF. + handle->init(toplevel); +} + +void ToplevelManager::onToplevelReady() { + auto* handle = qobject_cast(this->sender()); + this->mReadyToplevels.push_back(handle); + emit this->toplevelReady(handle); +} + +void ToplevelManager::onToplevelClosed() { + auto* handle = qobject_cast(this->sender()); + this->mReadyToplevels.removeOne(handle); + this->mToplevels.removeOne(handle); +} + +} // namespace qs::wayland::toplevel_management::impl diff --git a/src/wayland/toplevel_management/manager.hpp b/src/wayland/toplevel_management/manager.hpp new file mode 100644 index 00000000..41848de1 --- /dev/null +++ b/src/wayland/toplevel_management/manager.hpp @@ -0,0 +1,47 @@ +#pragma once + +#include +#include +#include +#include +#include + +#include "wayland-wlr-foreign-toplevel-management-unstable-v1-client-protocol.h" + +namespace qs::wayland::toplevel_management::impl { + +class ToplevelHandle; + +Q_DECLARE_LOGGING_CATEGORY(logToplevelManagement); + +class ToplevelManager + : public QWaylandClientExtensionTemplate + , public QtWayland::zwlr_foreign_toplevel_manager_v1 { + Q_OBJECT; + +public: + [[nodiscard]] bool available() const; + [[nodiscard]] const QVector& readyToplevels() const; + [[nodiscard]] ToplevelHandle* handleFor(::zwlr_foreign_toplevel_handle_v1* toplevel); + + static ToplevelManager* instance(); + +signals: + void toplevelReady(ToplevelHandle* toplevel); + +protected: + explicit ToplevelManager(); + + void zwlr_foreign_toplevel_manager_v1_toplevel(::zwlr_foreign_toplevel_handle_v1* toplevel + ) override; + +private slots: + void onToplevelReady(); + void onToplevelClosed(); + +private: + QVector mToplevels; + QVector mReadyToplevels; +}; + +} // namespace qs::wayland::toplevel_management::impl diff --git a/src/wayland/toplevel_management/qml.cpp b/src/wayland/toplevel_management/qml.cpp new file mode 100644 index 00000000..2042262b --- /dev/null +++ b/src/wayland/toplevel_management/qml.cpp @@ -0,0 +1,153 @@ +#include "qml.hpp" + +#include +#include + +#include "../../core/model.hpp" +#include "../../core/proxywindow.hpp" +#include "../../core/qmlscreen.hpp" +#include "../../core/windowinterface.hpp" +#include "handle.hpp" +#include "manager.hpp" + +namespace qs::wayland::toplevel_management { + +Toplevel::Toplevel(impl::ToplevelHandle* handle, QObject* parent): QObject(parent), handle(handle) { + // clang-format off + QObject::connect(handle, &impl::ToplevelHandle::closed, this, &Toplevel::onClosed); + QObject::connect(handle, &impl::ToplevelHandle::appIdChanged, this, &Toplevel::appIdChanged); + QObject::connect(handle, &impl::ToplevelHandle::titleChanged, this, &Toplevel::titleChanged); + QObject::connect(handle, &impl::ToplevelHandle::parentChanged, this, &Toplevel::parentChanged); + QObject::connect(handle, &impl::ToplevelHandle::activatedChanged, this, &Toplevel::activatedChanged); + QObject::connect(handle, &impl::ToplevelHandle::maximizedChanged, this, &Toplevel::maximizedChanged); + QObject::connect(handle, &impl::ToplevelHandle::minimizedChanged, this, &Toplevel::minimizedChanged); + QObject::connect(handle, &impl::ToplevelHandle::fullscreenChanged, this, &Toplevel::fullscreenChanged); + // clang-format on +} + +void Toplevel::onClosed() { + emit this->closed(); + delete this; +} + +void Toplevel::activate() { this->handle->activate(); } + +QString Toplevel::appId() const { return this->handle->appId(); } +QString Toplevel::title() const { return this->handle->title(); } + +Toplevel* Toplevel::parent() const { + return ToplevelManager::instance()->forImpl(this->handle->parent()); +} + +bool Toplevel::activated() const { return this->handle->activated(); } + +bool Toplevel::maximized() const { return this->handle->maximized(); } +void Toplevel::setMaximized(bool maximized) { this->handle->setMaximized(maximized); } + +bool Toplevel::minimized() const { return this->handle->minimized(); } +void Toplevel::setMinimized(bool minimized) { this->handle->setMinimized(minimized); } + +bool Toplevel::fullscreen() const { return this->handle->fullscreen(); } +void Toplevel::setFullscreen(bool fullscreen) { this->handle->setFullscreen(fullscreen); } + +void Toplevel::fullscreenOn(QuickshellScreenInfo* screen) { + auto* qscreen = screen != nullptr ? screen->screen : nullptr; + this->handle->fullscreenOn(qscreen); +} + +void Toplevel::setRectangle(QObject* window, QRect rect) { + auto* proxyWindow = qobject_cast(window); + + if (proxyWindow == nullptr) { + if (auto* iface = qobject_cast(window)) { + proxyWindow = iface->proxyWindow(); + } + } + + if (proxyWindow != this->rectWindow) { + if (this->rectWindow != nullptr) { + QObject::disconnect(this->rectWindow, nullptr, this, nullptr); + } + + this->rectWindow = proxyWindow; + + if (proxyWindow != nullptr) { + QObject::connect( + proxyWindow, + &QObject::destroyed, + this, + &Toplevel::onRectangleProxyDestroyed + ); + + QObject::connect( + proxyWindow, + &ProxyWindowBase::windowConnected, + this, + &Toplevel::onRectangleProxyConnected + ); + } + } + + this->rectangle = rect; + this->handle->setRectangle(proxyWindow->backingWindow(), rect); +} + +void Toplevel::unsetRectangle() { this->setRectangle(nullptr, QRect()); } + +void Toplevel::onRectangleProxyConnected() { + this->handle->setRectangle(this->rectWindow->backingWindow(), this->rectangle); +} + +void Toplevel::onRectangleProxyDestroyed() { + this->rectWindow = nullptr; + this->rectangle = QRect(); +} + +ToplevelManager::ToplevelManager() { + auto* manager = impl::ToplevelManager::instance(); + + QObject::connect( + manager, + &impl::ToplevelManager::toplevelReady, + this, + &ToplevelManager::onToplevelReady + ); + + for (auto* handle: manager->readyToplevels()) { + this->onToplevelReady(handle); + } +} + +Toplevel* ToplevelManager::forImpl(impl::ToplevelHandle* impl) const { + if (impl == nullptr) return nullptr; + + for (auto* toplevel: this->mToplevels.valueList()) { + if (toplevel->handle == impl) return toplevel; + } + + return nullptr; +} + +ObjectModel* ToplevelManager::toplevels() { return &this->mToplevels; } + +void ToplevelManager::onToplevelReady(impl::ToplevelHandle* handle) { + auto* toplevel = new Toplevel(handle, this); + QObject::connect(toplevel, &Toplevel::closed, this, &ToplevelManager::onToplevelClosed); + this->mToplevels.insertObject(toplevel); +} + +void ToplevelManager::onToplevelClosed() { + auto* toplevel = qobject_cast(this->sender()); + this->mToplevels.removeObject(toplevel); +} + +ToplevelManager* ToplevelManager::instance() { + static auto* instance = new ToplevelManager(); // NOLINT + return instance; +} + +ObjectModel* ToplevelManagerQml::toplevels() { + return ToplevelManager::instance()->toplevels(); +} + +} // namespace qs::wayland::toplevel_management diff --git a/src/wayland/toplevel_management/qml.hpp b/src/wayland/toplevel_management/qml.hpp new file mode 100644 index 00000000..8bb1d551 --- /dev/null +++ b/src/wayland/toplevel_management/qml.hpp @@ -0,0 +1,140 @@ +#pragma once + +#include +#include +#include + +#include "../../core/model.hpp" +#include "../../core/proxywindow.hpp" +#include "../../core/qmlscreen.hpp" + +namespace qs::wayland::toplevel_management { + +namespace impl { +class ToplevelManager; +class ToplevelHandle; +} // namespace impl + +///! Window from another application. +/// A window/toplevel from another application, retrievable from +/// the [ToplevelManager](../toplevelmanager). +class Toplevel: public QObject { + Q_OBJECT; + Q_PROPERTY(QString appId READ appId NOTIFY appIdChanged); + Q_PROPERTY(QString title READ title NOTIFY titleChanged); + /// Parent toplevel if this toplevel is a modal/dialog, otherwise null. + Q_PROPERTY(Toplevel* parent READ parent NOTIFY parentChanged); + /// If the window is currently activated or focused. + /// + /// Activation can be requested with the `activate()` function. + Q_PROPERTY(bool activated READ activated NOTIFY activatedChanged); + /// If the window is currently maximized. + /// + /// Maximization can be requested by setting this property, though it may + /// be ignored by the compositor. + Q_PROPERTY(bool maximized READ maximized WRITE setMaximized NOTIFY maximizedChanged); + /// If the window is currently minimized. + /// + /// Minimization can be requested by setting this property, though it may + /// be ignored by the compositor. + Q_PROPERTY(bool minimized READ minimized WRITE setMinimized NOTIFY minimizedChanged); + /// If the window is currently fullscreen. + /// + /// Fullscreen can be requested by setting this property, though it may + /// be ignored by the compositor. + /// Fullscreen can be requested on a specific screen with the `fullscreenOn()` function. + Q_PROPERTY(bool fullscreen READ fullscreen WRITE setFullscreen NOTIFY fullscreenChanged); + QML_ELEMENT; + QML_UNCREATABLE("Toplevels must be acquired from the ToplevelManager."); + +public: + explicit Toplevel(impl::ToplevelHandle* handle, QObject* parent); + + /// Request that this toplevel is activated. + /// The request may be ignored by the compositor. + Q_INVOKABLE void activate(); + + /// Request that this toplevel is fullscreened on a specific screen. + /// The request may be ignored by the compositor. + Q_INVOKABLE void fullscreenOn(QuickshellScreenInfo* screen); + + /// Provide a hint to the compositor where the visual representation + /// of this toplevel is relative to a quickshell window. + /// This hint can be used visually in operations like minimization. + Q_INVOKABLE void setRectangle(QObject* window, QRect rect); + Q_INVOKABLE void unsetRectangle(); + + [[nodiscard]] QString appId() const; + [[nodiscard]] QString title() const; + [[nodiscard]] Toplevel* parent() const; + [[nodiscard]] bool activated() const; + + [[nodiscard]] bool maximized() const; + void setMaximized(bool maximized); + + [[nodiscard]] bool minimized() const; + void setMinimized(bool minimized); + + [[nodiscard]] bool fullscreen() const; + void setFullscreen(bool fullscreen); + +signals: + void closed(); + void appIdChanged(); + void titleChanged(); + void parentChanged(); + void activatedChanged(); + void maximizedChanged(); + void minimizedChanged(); + void fullscreenChanged(); + +private slots: + void onClosed(); + void onRectangleProxyConnected(); + void onRectangleProxyDestroyed(); + +private: + impl::ToplevelHandle* handle; + ProxyWindowBase* rectWindow = nullptr; + QRect rectangle; + + friend class ToplevelManager; +}; + +class ToplevelManager: public QObject { + Q_OBJECT; + +public: + Toplevel* forImpl(impl::ToplevelHandle* impl) const; + + [[nodiscard]] ObjectModel* toplevels(); + + static ToplevelManager* instance(); + +private slots: + void onToplevelReady(impl::ToplevelHandle* handle); + void onToplevelClosed(); + +private: + explicit ToplevelManager(); + + ObjectModel mToplevels {this}; +}; + +///! Exposes a list of Toplevels. +/// Exposes a list of windows from other applications as [Toplevel](../toplevel)s via the +/// [zwlr-foreign-toplevel-management-v1](https://wayland.app/protocols/wlr-foreign-toplevel-management-unstable-v1) +/// wayland protocol. +class ToplevelManagerQml: public QObject { + Q_OBJECT; + Q_PROPERTY(ObjectModel* toplevels READ toplevels CONSTANT); + QML_NAMED_ELEMENT(ToplevelManager); + QML_SINGLETON; + +public: + explicit ToplevelManagerQml(QObject* parent = nullptr): QObject(parent) {} + + [[nodiscard]] static ObjectModel* toplevels(); +}; + +} // namespace qs::wayland::toplevel_management diff --git a/src/wayland/toplevel_management/wlr-foreign-toplevel-management-unstable-v1.xml b/src/wayland/toplevel_management/wlr-foreign-toplevel-management-unstable-v1.xml new file mode 100644 index 00000000..44505bbb --- /dev/null +++ b/src/wayland/toplevel_management/wlr-foreign-toplevel-management-unstable-v1.xml @@ -0,0 +1,270 @@ + + + + Copyright © 2018 Ilia Bozhinov + + Permission to use, copy, modify, distribute, and sell this + software and its documentation for any purpose is hereby granted + without fee, provided that the above copyright notice appear in + all copies and that both that copyright notice and this permission + notice appear in supporting documentation, and that the name of + the copyright holders not be used in advertising or publicity + pertaining to distribution of the software without specific, + written prior permission. The copyright holders make no + representations about the suitability of this software for any + purpose. It is provided "as is" without express or implied + warranty. + + THE COPYRIGHT HOLDERS DISCLAIM ALL WARRANTIES WITH REGARD TO THIS + SOFTWARE, INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY AND + FITNESS, IN NO EVENT SHALL THE COPYRIGHT HOLDERS BE LIABLE FOR ANY + SPECIAL, INDIRECT OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN + AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, + ARISING OUT OF OR IN CONNECTION WITH THE USE OR PERFORMANCE OF + THIS SOFTWARE. + + + + + The purpose of this protocol is to enable the creation of taskbars + and docks by providing them with a list of opened applications and + letting them request certain actions on them, like maximizing, etc. + + After a client binds the zwlr_foreign_toplevel_manager_v1, each opened + toplevel window will be sent via the toplevel event + + + + + This event is emitted whenever a new toplevel window is created. It + is emitted for all toplevels, regardless of the app that has created + them. + + All initial details of the toplevel(title, app_id, states, etc.) will + be sent immediately after this event via the corresponding events in + zwlr_foreign_toplevel_handle_v1. + + + + + + + Indicates the client no longer wishes to receive events for new toplevels. + However the compositor may emit further toplevel_created events, until + the finished event is emitted. + + The client must not send any more requests after this one. + + + + + + This event indicates that the compositor is done sending events to the + zwlr_foreign_toplevel_manager_v1. The server will destroy the object + immediately after sending this request, so it will become invalid and + the client should free any resources associated with it. + + + + + + + A zwlr_foreign_toplevel_handle_v1 object represents an opened toplevel + window. Each app may have multiple opened toplevels. + + Each toplevel has a list of outputs it is visible on, conveyed to the + client with the output_enter and output_leave events. + + + + + This event is emitted whenever the title of the toplevel changes. + + + + + + + This event is emitted whenever the app-id of the toplevel changes. + + + + + + + This event is emitted whenever the toplevel becomes visible on + the given output. A toplevel may be visible on multiple outputs. + + + + + + + This event is emitted whenever the toplevel stops being visible on + the given output. It is guaranteed that an entered-output event + with the same output has been emitted before this event. + + + + + + + Requests that the toplevel be maximized. If the maximized state actually + changes, this will be indicated by the state event. + + + + + + Requests that the toplevel be unmaximized. If the maximized state actually + changes, this will be indicated by the state event. + + + + + + Requests that the toplevel be minimized. If the minimized state actually + changes, this will be indicated by the state event. + + + + + + Requests that the toplevel be unminimized. If the minimized state actually + changes, this will be indicated by the state event. + + + + + + Request that this toplevel be activated on the given seat. + There is no guarantee the toplevel will be actually activated. + + + + + + + The different states that a toplevel can have. These have the same meaning + as the states with the same names defined in xdg-toplevel + + + + + + + + + + + This event is emitted immediately after the zlw_foreign_toplevel_handle_v1 + is created and each time the toplevel state changes, either because of a + compositor action or because of a request in this protocol. + + + + + + + + This event is sent after all changes in the toplevel state have been + sent. + + This allows changes to the zwlr_foreign_toplevel_handle_v1 properties + to be seen as atomic, even if they happen via multiple events. + + + + + + Send a request to the toplevel to close itself. The compositor would + typically use a shell-specific method to carry out this request, for + example by sending the xdg_toplevel.close event. However, this gives + no guarantees the toplevel will actually be destroyed. If and when + this happens, the zwlr_foreign_toplevel_handle_v1.closed event will + be emitted. + + + + + + The rectangle of the surface specified in this request corresponds to + the place where the app using this protocol represents the given toplevel. + It can be used by the compositor as a hint for some operations, e.g + minimizing. The client is however not required to set this, in which + case the compositor is free to decide some default value. + + If the client specifies more than one rectangle, only the last one is + considered. + + The dimensions are given in surface-local coordinates. + Setting width=height=0 removes the already-set rectangle. + + + + + + + + + + + + + + + + This event means the toplevel has been destroyed. It is guaranteed there + won't be any more events for this zwlr_foreign_toplevel_handle_v1. The + toplevel itself becomes inert so any requests will be ignored except the + destroy request. + + + + + + Destroys the zwlr_foreign_toplevel_handle_v1 object. + + This request should be called either when the client does not want to + use the toplevel anymore or after the closed event to finalize the + destruction of the object. + + + + + + + + Requests that the toplevel be fullscreened on the given output. If the + fullscreen state and/or the outputs the toplevel is visible on actually + change, this will be indicated by the state and output_enter/leave + events. + + The output parameter is only a hint to the compositor. Also, if output + is NULL, the compositor should decide which output the toplevel will be + fullscreened on, if at all. + + + + + + + Requests that the toplevel be unfullscreened. If the fullscreen state + actually changes, this will be indicated by the state event. + + + + + + + + This event is emitted whenever the parent of the toplevel changes. + + No event is emitted when the parent handle is destroyed by the client. + + + + +