diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 4b13d45..0c05419 100644 --- a/src/CMakeLists.txt +++ b/src/CMakeLists.txt @@ -11,6 +11,7 @@ add_subdirectory(window) add_subdirectory(io) add_subdirectory(widgets) add_subdirectory(ui) +add_subdirectory(windowmanager) if (CRASH_HANDLER) add_subdirectory(crash) diff --git a/src/wayland/CMakeLists.txt b/src/wayland/CMakeLists.txt index ca49c8f..db53f37 100644 --- a/src/wayland/CMakeLists.txt +++ b/src/wayland/CMakeLists.txt @@ -123,6 +123,8 @@ list(APPEND WAYLAND_MODULES Quickshell.Wayland._IdleNotify) add_subdirectory(shortcuts_inhibit) list(APPEND WAYLAND_MODULES Quickshell.Wayland._ShortcutsInhibitor) +add_subdirectory(windowmanager) + # widgets for qmenu target_link_libraries(quickshell-wayland PRIVATE Qt::Quick Qt::Widgets Qt::WaylandClient Qt::WaylandClientPrivate diff --git a/src/wayland/windowmanager/CMakeLists.txt b/src/wayland/windowmanager/CMakeLists.txt new file mode 100644 index 0000000..c8d9d98 --- /dev/null +++ b/src/wayland/windowmanager/CMakeLists.txt @@ -0,0 +1,19 @@ +qt_add_library(quickshell-wayland-windowsystem STATIC + windowmanager.cpp + windowset.cpp + ext_workspace.cpp +) + +add_library(quickshell-wayland-windowsystem-init OBJECT init.cpp) +target_link_libraries(quickshell-wayland-windowsystem-init PRIVATE Qt::Quick) + +wl_proto(wlp-ext-workspace ext-workspace-v1 "${WAYLAND_PROTOCOLS}/staging/ext-workspace") + +target_link_libraries(quickshell-wayland-windowsystem PRIVATE + Qt::Quick Qt::WaylandClient Qt::WaylandClientPrivate wayland-client + wlp-ext-foreign-toplevel wlp-ext-workspace +) + +qs_pch(quickshell-wayland-windowsystem SET large) + +target_link_libraries(quickshell PRIVATE quickshell-wayland-windowsystem quickshell-wayland-windowsystem-init) diff --git a/src/wayland/windowmanager/ext_workspace.cpp b/src/wayland/windowmanager/ext_workspace.cpp new file mode 100644 index 0000000..fcb9ffa --- /dev/null +++ b/src/wayland/windowmanager/ext_workspace.cpp @@ -0,0 +1,176 @@ +#include "ext_workspace.hpp" + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "../../core/logcat.hpp" + +namespace qs::wayland::workspace { + +QS_LOGGING_CATEGORY(logWorkspace, "quickshell.wm.wayland.workspace", QtWarningMsg); + +WorkspaceManager::WorkspaceManager(): QWaylandClientExtensionTemplate(1) { this->initialize(); } + +WorkspaceManager* WorkspaceManager::instance() { + static auto* instance = new WorkspaceManager(); + return instance; +} + +void WorkspaceManager::ext_workspace_manager_v1_workspace_group( + ::ext_workspace_group_handle_v1* handle +) { + auto* group = new WorkspaceGroup(handle); + qCDebug(logWorkspace) << "Created group" << group; + this->mGroups.insert(handle, group); + emit this->groupCreated(group); +} + +void WorkspaceManager::ext_workspace_manager_v1_workspace(::ext_workspace_handle_v1* handle) { + auto* workspace = new Workspace(handle); + qCDebug(logWorkspace) << "Created workspace" << workspace; + this->mWorkspaces.insert(handle, workspace); + emit this->workspaceCreated(workspace); +}; + +void WorkspaceManager::destroyWorkspace(Workspace* workspace) { + this->mWorkspaces.remove(workspace->object()); + this->destroyedWorkspaces.append(workspace); + emit this->workspaceDestroyed(workspace); +} + +void WorkspaceManager::destroyGroup(WorkspaceGroup* group) { + this->mGroups.remove(group->object()); + this->destroyedGroups.append(group); + emit this->groupDestroyed(group); +} + +void WorkspaceManager::ext_workspace_manager_v1_done() { + qCDebug(logWorkspace) << "Workspace changes done"; + emit this->serverCommit(); + + for (auto* workspace: this->destroyedWorkspaces) delete workspace; + for (auto* group: this->destroyedGroups) delete group; + this->destroyedWorkspaces.clear(); + this->destroyedGroups.clear(); +} + +void WorkspaceManager::ext_workspace_manager_v1_finished() { + qCWarning(logWorkspace) << "ext_workspace_manager_v1.finished() was received"; +} + +Workspace::~Workspace() { + if (this->isInitialized()) this->destroy(); +} + +void Workspace::ext_workspace_handle_v1_id(const QString& id) { + qCDebug(logWorkspace) << "Updated id for workspace" << this << "to" << id; + this->id = id; +} + +void Workspace::ext_workspace_handle_v1_name(const QString& name) { + qCDebug(logWorkspace) << "Updated name for workspace" << this << "to" << name; + this->name = name; +} + +void Workspace::ext_workspace_handle_v1_coordinates(wl_array* coordinates) { + this->coordinates.clear(); + + auto* data = static_cast(coordinates->data); + auto size = static_cast(coordinates->size / sizeof(qint32)); + + for (auto i = 0; i != size; ++i) { + this->coordinates.append(data[i]); // NOLINT + } + + qCDebug(logWorkspace) << "Updated coordinates for workspace" << this << "to" << this->coordinates; +} + +void Workspace::ext_workspace_handle_v1_state(quint32 state) { + this->active = state & ext_workspace_handle_v1::state_active; + this->urgent = state & ext_workspace_handle_v1::state_urgent; + this->hidden = state & ext_workspace_handle_v1::state_hidden; + + qCDebug(logWorkspace).nospace() << "Updated state for workspace " << this + << " to [active: " << this->active << ", urgent: " << this->urgent + << ", hidden: " << this->hidden << ']'; +} + +void Workspace::ext_workspace_handle_v1_capabilities(quint32 capabilities) { + this->canActivate = capabilities & ext_workspace_handle_v1::workspace_capabilities_activate; + this->canDeactivate = capabilities & ext_workspace_handle_v1::workspace_capabilities_deactivate; + this->canRemove = capabilities & ext_workspace_handle_v1::workspace_capabilities_remove; + this->canAssign = capabilities & ext_workspace_handle_v1::workspace_capabilities_assign; + + qCDebug(logWorkspace).nospace() << "Updated capabilities for workspace " << this + << " to [activate: " << this->canActivate + << ", deactivate: " << this->canDeactivate + << ", remove: " << this->canRemove + << ", assign: " << this->canAssign << ']'; +} + +void Workspace::ext_workspace_handle_v1_removed() { + qCDebug(logWorkspace) << "Destroyed workspace" << this; + WorkspaceManager::instance()->destroyWorkspace(this); + this->destroy(); +} + +void Workspace::enterGroup(WorkspaceGroup* group) { this->group = group; } + +void Workspace::leaveGroup(WorkspaceGroup* group) { + if (this->group == group) this->group = nullptr; +} + +WorkspaceGroup::~WorkspaceGroup() { + if (this->isInitialized()) this->destroy(); +} + +void WorkspaceGroup::ext_workspace_group_handle_v1_capabilities(quint32 capabilities) { + this->canCreateWorkspace = + capabilities & ext_workspace_group_handle_v1::group_capabilities_create_workspace; + + qCDebug(logWorkspace).nospace() << "Updated capabilities for group " << this + << " to [create_workspace: " << this->canCreateWorkspace << ']'; +} + +void WorkspaceGroup::ext_workspace_group_handle_v1_output_enter(::wl_output* output) { + qCDebug(logWorkspace) << "Output" << output << "added to group" << this; + this->screens.addOutput(output); +} + +void WorkspaceGroup::ext_workspace_group_handle_v1_output_leave(::wl_output* output) { + qCDebug(logWorkspace) << "Output" << output << "removed from group" << this; + this->screens.removeOutput(output); +} + +void WorkspaceGroup::ext_workspace_group_handle_v1_workspace_enter( + ::ext_workspace_handle_v1* handle +) { + auto* workspace = WorkspaceManager::instance()->mWorkspaces.value(handle); + qCDebug(logWorkspace) << "Workspace" << workspace << "added to group" << this; + + if (workspace) workspace->enterGroup(this); +} + +void WorkspaceGroup::ext_workspace_group_handle_v1_workspace_leave( + ::ext_workspace_handle_v1* handle +) { + auto* workspace = WorkspaceManager::instance()->mWorkspaces.value(handle); + qCDebug(logWorkspace) << "Workspace" << workspace << "removed from group" << this; + + if (workspace) workspace->leaveGroup(this); +} + +void WorkspaceGroup::ext_workspace_group_handle_v1_removed() { + qCDebug(logWorkspace) << "Destroyed group" << this; + WorkspaceManager::instance()->destroyGroup(this); + this->destroy(); +} + +} // namespace qs::wayland::workspace diff --git a/src/wayland/windowmanager/ext_workspace.hpp b/src/wayland/windowmanager/ext_workspace.hpp new file mode 100644 index 0000000..6aff209 --- /dev/null +++ b/src/wayland/windowmanager/ext_workspace.hpp @@ -0,0 +1,117 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include "../../core/logcat.hpp" +#include "../output_tracking.hpp" + +namespace qs::wayland::workspace { + +QS_DECLARE_LOGGING_CATEGORY(logWorkspace); + +class WorkspaceGroup; +class Workspace; + +class WorkspaceManager + : public QWaylandClientExtensionTemplate + , public QtWayland::ext_workspace_manager_v1 { + Q_OBJECT; + +public: + static WorkspaceManager* instance(); + + [[nodiscard]] QList workspaces() { return this->mWorkspaces.values(); } + +signals: + void serverCommit(); + void workspaceCreated(Workspace* workspace); + void workspaceDestroyed(Workspace* workspace); + void groupCreated(WorkspaceGroup* group); + void groupDestroyed(WorkspaceGroup* group); + +protected: + void ext_workspace_manager_v1_workspace_group(::ext_workspace_group_handle_v1* handle) override; + void ext_workspace_manager_v1_workspace(::ext_workspace_handle_v1* handle) override; + void ext_workspace_manager_v1_done() override; + void ext_workspace_manager_v1_finished() override; + +private: + WorkspaceManager(); + + void destroyGroup(WorkspaceGroup* group); + void destroyWorkspace(Workspace* workspace); + + QHash<::ext_workspace_handle_v1*, Workspace*> mWorkspaces; + QHash<::ext_workspace_group_handle_v1*, WorkspaceGroup*> mGroups; + QList destroyedGroups; + QList destroyedWorkspaces; + + friend class Workspace; + friend class WorkspaceGroup; +}; + +class Workspace: public QtWayland::ext_workspace_handle_v1 { +public: + Workspace(::ext_workspace_handle_v1* handle): QtWayland::ext_workspace_handle_v1(handle) {} + ~Workspace() override; + Q_DISABLE_COPY_MOVE(Workspace); + + QString id; + QString name; + QList coordinates; + WorkspaceGroup* group = nullptr; + + bool active : 1 = false; + bool urgent : 1 = false; + bool hidden : 1 = false; + + bool canActivate : 1 = false; + bool canDeactivate : 1 = false; + bool canRemove : 1 = false; + bool canAssign : 1 = false; + +protected: + void ext_workspace_handle_v1_id(const QString& id) override; + void ext_workspace_handle_v1_name(const QString& name) override; + void ext_workspace_handle_v1_coordinates(wl_array* coordinates) override; + void ext_workspace_handle_v1_state(quint32 state) override; + void ext_workspace_handle_v1_capabilities(quint32 capabilities) override; + void ext_workspace_handle_v1_removed() override; + +private: + void enterGroup(WorkspaceGroup* group); + void leaveGroup(WorkspaceGroup* group); + + friend class WorkspaceGroup; +}; + +class WorkspaceGroup: public QtWayland::ext_workspace_group_handle_v1 { +public: + WorkspaceGroup(::ext_workspace_group_handle_v1* handle) + : QtWayland::ext_workspace_group_handle_v1(handle) {} + + ~WorkspaceGroup() override; + Q_DISABLE_COPY_MOVE(WorkspaceGroup); + + WlOutputTracker screens; + bool canCreateWorkspace : 1 = false; + +protected: + void ext_workspace_group_handle_v1_capabilities(quint32 capabilities) override; + void ext_workspace_group_handle_v1_output_enter(::wl_output* output) override; + void ext_workspace_group_handle_v1_output_leave(::wl_output* output) override; + void ext_workspace_group_handle_v1_workspace_enter(::ext_workspace_handle_v1* handle) override; + void ext_workspace_group_handle_v1_workspace_leave(::ext_workspace_handle_v1* handle) override; + void ext_workspace_group_handle_v1_removed() override; +}; + +} // namespace qs::wayland::workspace diff --git a/src/wayland/windowmanager/init.cpp b/src/wayland/windowmanager/init.cpp new file mode 100644 index 0000000..88be01a --- /dev/null +++ b/src/wayland/windowmanager/init.cpp @@ -0,0 +1,23 @@ +#include +#include +#include + +#include "../../core/plugin.hpp" + +namespace qs::wm::wayland { +void installWmProvider(); +} + +namespace { + +class WaylandWmPlugin: public QsEnginePlugin { + QList dependencies() override { return {"window"}; } + + bool applies() override { return QGuiApplication::platformName() == "wayland"; } + + void init() override { qs::wm::wayland::installWmProvider(); } +}; + +QS_REGISTER_PLUGIN(WaylandWmPlugin); + +} // namespace diff --git a/src/wayland/windowmanager/windowmanager.cpp b/src/wayland/windowmanager/windowmanager.cpp new file mode 100644 index 0000000..16245d0 --- /dev/null +++ b/src/wayland/windowmanager/windowmanager.cpp @@ -0,0 +1,21 @@ +#include "windowmanager.hpp" + +#include "../../windowmanager/windowmanager.hpp" +#include "windowset.hpp" + +namespace qs::wm::wayland { + +WaylandWindowManager* WaylandWindowManager::instance() { + static auto* instance = []() { + auto* wm = new WaylandWindowManager(); + WindowsetManager::instance(); + return wm; + }(); + return instance; +} + +void installWmProvider() { // NOLINT (misc-use-internal-linkage) + qs::wm::WindowManager::setProvider([]() { return WaylandWindowManager::instance(); }); +} + +} // namespace qs::wm::wayland diff --git a/src/wayland/windowmanager/windowmanager.hpp b/src/wayland/windowmanager/windowmanager.hpp new file mode 100644 index 0000000..9d48efd --- /dev/null +++ b/src/wayland/windowmanager/windowmanager.hpp @@ -0,0 +1,17 @@ +#pragma once + +#include + +#include "../../windowmanager/windowmanager.hpp" +#include "windowset.hpp" + +namespace qs::wm::wayland { + +class WaylandWindowManager: public WindowManager { + Q_OBJECT; + +public: + static WaylandWindowManager* instance(); +}; + +} // namespace qs::wm::wayland diff --git a/src/wayland/windowmanager/windowset.cpp b/src/wayland/windowmanager/windowset.cpp new file mode 100644 index 0000000..a0cf65b --- /dev/null +++ b/src/wayland/windowmanager/windowset.cpp @@ -0,0 +1,252 @@ +#include "windowset.hpp" + +#include +#include +#include +#include +#include +#include +#include + +#include "../../windowmanager/windowmanager.hpp" +#include "../../windowmanager/windowset.hpp" +#include "ext_workspace.hpp" + +namespace qs::wm::wayland { + +WindowsetManager::WindowsetManager() { + auto* impl = impl::WorkspaceManager::instance(); + + QObject::connect( + impl, + &impl::WorkspaceManager::serverCommit, + this, + &WindowsetManager::onServerCommit + ); + + QObject::connect( + impl, + &impl::WorkspaceManager::workspaceCreated, + this, + &WindowsetManager::onWindowsetCreated + ); + + QObject::connect( + impl, + &impl::WorkspaceManager::workspaceDestroyed, + this, + &WindowsetManager::onWindowsetDestroyed + ); + + QObject::connect( + impl, + &impl::WorkspaceManager::groupCreated, + this, + &WindowsetManager::onProjectionCreated + ); + + QObject::connect( + impl, + &impl::WorkspaceManager::groupDestroyed, + this, + &WindowsetManager::onProjectionDestroyed + ); +} + +void WindowsetManager::scheduleCommit() { + if (this->commitScheduled) { + qCDebug(impl::logWorkspace) << "Workspace commit already scheduled."; + return; + } + + qCDebug(impl::logWorkspace) << "Scheduling workspace commit..."; + this->commitScheduled = true; + + QMetaObject::invokeMethod(this, &WindowsetManager::doCommit, Qt::QueuedConnection); +} + +void WindowsetManager::doCommit() { // NOLINT + qCDebug(impl::logWorkspace) << "Committing workspaces..."; + impl::WorkspaceManager::instance()->commit(); + this->commitScheduled = false; +} + +void WindowsetManager::onServerCommit() { + // Projections are created/destroyed around windowsets to avoid any nulls making it + // to the qml engine. + + Qt::beginPropertyUpdateGroup(); + + auto* wm = WindowManager::instance(); + auto windowsets = wm->bWindowsets.value(); + auto projections = wm->bWindowsetProjections.value(); + + for (auto* projImpl: this->pendingProjectionCreations) { + auto* projection = new WlWindowsetProjection(this, projImpl); + this->projectionsByImpl.insert(projImpl, projection); + projections.append(projection); + } + + for (auto* wsImpl: this->pendingWindowsetCreations) { + auto* ws = new WlWindowset(this, wsImpl); + this->windowsetByImpl.insert(wsImpl, ws); + windowsets.append(ws); + } + + for (auto* wsImpl: this->pendingWindowsetDestructions) { + windowsets.removeOne(this->windowsetByImpl.value(wsImpl)); + this->windowsetByImpl.remove(wsImpl); + } + + for (auto* projImpl: this->pendingProjectionDestructions) { + projections.removeOne(this->projectionsByImpl.value(projImpl)); + this->projectionsByImpl.remove(projImpl); + } + + for (auto* ws: windowsets) { + static_cast(ws)->commitImpl(); // NOLINT + } + + for (auto* projection: projections) { + static_cast(projection)->commitImpl(); // NOLINT + } + + this->pendingWindowsetCreations.clear(); + this->pendingWindowsetDestructions.clear(); + this->pendingProjectionCreations.clear(); + this->pendingProjectionDestructions.clear(); + + wm->bWindowsets = windowsets; + wm->bWindowsetProjections = projections; + + Qt::endPropertyUpdateGroup(); +} + +void WindowsetManager::onWindowsetCreated(impl::Workspace* workspace) { + this->pendingWindowsetCreations.append(workspace); +} + +void WindowsetManager::onWindowsetDestroyed(impl::Workspace* workspace) { + if (!this->pendingWindowsetCreations.removeOne(workspace)) { + this->pendingWindowsetDestructions.append(workspace); + } +} + +void WindowsetManager::onProjectionCreated(impl::WorkspaceGroup* group) { + this->pendingProjectionCreations.append(group); +} + +void WindowsetManager::onProjectionDestroyed(impl::WorkspaceGroup* group) { + if (!this->pendingProjectionCreations.removeOne(group)) { + this->pendingProjectionDestructions.append(group); + } +} + +WindowsetManager* WindowsetManager::instance() { + static auto* instance = new WindowsetManager(); + return instance; +} + +WlWindowset::WlWindowset(WindowsetManager* manager, impl::Workspace* impl) + : Windowset(manager) + , impl(impl) { + this->commitImpl(); +} + +void WlWindowset::commitImpl() { + Qt::beginPropertyUpdateGroup(); + this->bId = this->impl->id; + this->bName = this->impl->name; + this->bCoordinates = this->impl->coordinates; + this->bActive = this->impl->active; + this->bShouldDisplay = !this->impl->hidden; + this->bUrgent = this->impl->urgent; + this->bCanActivate = this->impl->canActivate; + this->bCanDeactivate = this->impl->canDeactivate; + this->bCanSetProjection = this->impl->canAssign; + this->bProjection = this->manager()->projectionsByImpl.value(this->impl->group); + Qt::endPropertyUpdateGroup(); +} + +void WlWindowset::activate() { + if (!this->bCanActivate) { + qCritical(logWorkspace) << this << "cannot be activated"; + return; + } + + qCDebug(impl::logWorkspace) << "Calling activate() for" << this; + this->impl->activate(); + WindowsetManager::instance()->scheduleCommit(); +} + +void WlWindowset::deactivate() { + if (!this->bCanDeactivate) { + qCritical(logWorkspace) << this << "cannot be deactivated"; + return; + } + + qCDebug(impl::logWorkspace) << "Calling deactivate() for" << this; + this->impl->deactivate(); + WindowsetManager::instance()->scheduleCommit(); +} + +void WlWindowset::remove() { + if (!this->bCanRemove) { + qCritical(logWorkspace) << this << "cannot be removed"; + return; + } + + qCDebug(impl::logWorkspace) << "Calling remove() for" << this; + this->impl->remove(); + WindowsetManager::instance()->scheduleCommit(); +} + +void WlWindowset::setProjection(WindowsetProjection* projection) { + if (!this->bCanSetProjection) { + qCritical(logWorkspace) << this << "cannot be assigned to a projection"; + return; + } + + if (!projection) { + qCritical(logWorkspace) << "Cannot set a windowset's projection to null"; + return; + } + + WlWindowsetProjection* wlProjection = nullptr; + if (auto* p = dynamic_cast(projection)) { + wlProjection = p; + } else if (auto* p = dynamic_cast(projection)) { + // In the 99% case, there will only be a single windowset on a screen. + // In the 1% case, the oldest projection (first in list) is most likely the desired one. + auto* screen = p->screen(); + for (const auto& proj: WindowsetManager::instance()->projectionsByImpl.values()) { + if (proj->bQScreens.value().contains(screen)) { + wlProjection = proj; + break; + } + } + } + + if (!wlProjection) { + qCritical(logWorkspace) << "Cannot set a windowset's projection to" << projection + << "as no wayland projection could be derived."; + return; + } + + qCDebug(impl::logWorkspace) << "Assigning" << this << "to" << projection; + this->impl->assign(wlProjection->impl->object()); + WindowsetManager::instance()->scheduleCommit(); +} + +WlWindowsetProjection::WlWindowsetProjection(WindowsetManager* manager, impl::WorkspaceGroup* impl) + : WindowsetProjection(manager) + , impl(impl) { + this->commitImpl(); +} + +void WlWindowsetProjection::commitImpl() { + // TODO: will not commit the correct screens if missing qt repr at commit time + this->bQScreens = this->impl->screens.screens(); +} + +} // namespace qs::wm::wayland diff --git a/src/wayland/windowmanager/windowset.hpp b/src/wayland/windowmanager/windowset.hpp new file mode 100644 index 0000000..52d1c63 --- /dev/null +++ b/src/wayland/windowmanager/windowset.hpp @@ -0,0 +1,85 @@ +#pragma once + +#include +#include +#include +#include +#include + +#include "../../windowmanager/windowset.hpp" +#include "ext_workspace.hpp" + +namespace qs::wm::wayland { +namespace impl = qs::wayland::workspace; + +class WlWindowset; +class WlWindowsetProjection; + +class WindowsetManager: public QObject { + Q_OBJECT; + +public: + static WindowsetManager* instance(); + + void scheduleCommit(); + +private slots: + void doCommit(); + void onServerCommit(); + void onWindowsetCreated(impl::Workspace* workspace); + void onWindowsetDestroyed(impl::Workspace* workspace); + void onProjectionCreated(impl::WorkspaceGroup* group); + void onProjectionDestroyed(impl::WorkspaceGroup* group); + +private: + WindowsetManager(); + + bool commitScheduled = false; + + QList pendingWindowsetCreations; + QList pendingWindowsetDestructions; + QHash windowsetByImpl; + + QList pendingProjectionCreations; + QList pendingProjectionDestructions; + QHash projectionsByImpl; + + friend class WlWindowset; +}; + +class WlWindowset: public Windowset { +public: + WlWindowset(WindowsetManager* manager, impl::Workspace* impl); + + void commitImpl(); + + void activate() override; + void deactivate() override; + void remove() override; + void setProjection(WindowsetProjection* projection) override; + + [[nodiscard]] WindowsetManager* manager() { + return static_cast(this->parent()); // NOLINT + } + +private: + impl::Workspace* impl = nullptr; +}; + +class WlWindowsetProjection: public WindowsetProjection { +public: + WlWindowsetProjection(WindowsetManager* manager, impl::WorkspaceGroup* impl); + + void commitImpl(); + + [[nodiscard]] WindowsetManager* manager() { + return static_cast(this->parent()); // NOLINT + } + +private: + impl::WorkspaceGroup* impl = nullptr; + + friend class WlWindowset; +}; + +} // namespace qs::wm::wayland diff --git a/src/windowmanager/CMakeLists.txt b/src/windowmanager/CMakeLists.txt new file mode 100644 index 0000000..3c032f4 --- /dev/null +++ b/src/windowmanager/CMakeLists.txt @@ -0,0 +1,20 @@ +qt_add_library(quickshell-windowmanager STATIC + screenprojection.cpp + windowmanager.cpp + windowset.cpp +) + +qt_add_qml_module(quickshell-windowmanager + URI Quickshell.WindowManager + VERSION 0.1 + DEPENDENCIES QtQuick +) + +qs_add_module_deps_light(quickshell-windowmanager Quickshell) + +install_qml_module(quickshell-windowmanager) + +qs_module_pch(quickshell-windowmanager SET large) + +target_link_libraries(quickshell-windowmanager PRIVATE Qt::Quick) +target_link_libraries(quickshell PRIVATE quickshell-windowmanagerplugin) diff --git a/src/windowmanager/module.md b/src/windowmanager/module.md new file mode 100644 index 0000000..3480d60 --- /dev/null +++ b/src/windowmanager/module.md @@ -0,0 +1,10 @@ +name = "Quickshell.WindowManager" +description = "Window manager interface" +headers = [ + "windowmanager.hpp", + "windowset.hpp", + "screenprojection.hpp", +] +----- +Currently only supports the [ext-workspace-v1](https://wayland.app/protocols/ext-workspace-v1) wayland protocol. +Support will be expanded in future releases. diff --git a/src/windowmanager/screenprojection.cpp b/src/windowmanager/screenprojection.cpp new file mode 100644 index 0000000..74c4b20 --- /dev/null +++ b/src/windowmanager/screenprojection.cpp @@ -0,0 +1,32 @@ +#include "screenprojection.hpp" + +#include +#include +#include + +#include "windowmanager.hpp" +#include "windowset.hpp" + +namespace qs::wm { + +ScreenProjection::ScreenProjection(QScreen* screen, QObject* parent) + : WindowsetProjection(parent) + , mScreen(screen) { + this->bQScreens = {screen}; + this->bWindowsets.setBinding([this]() { + QList result; + for (auto* ws: WindowManager::instance()->bindableWindowsets().value()) { + auto* proj = ws->bindableProjection().value(); + if (proj && proj->bindableQScreens().value().contains(this->mScreen)) { + result.append(ws); + } + } + return result; + }); +} + +QScreen* ScreenProjection::screen() const { + return this->mScreen; +} + +} // namespace qs::wm diff --git a/src/windowmanager/screenprojection.hpp b/src/windowmanager/screenprojection.hpp new file mode 100644 index 0000000..6b0f31e --- /dev/null +++ b/src/windowmanager/screenprojection.hpp @@ -0,0 +1,34 @@ +#pragma once + +#include +#include +#include +#include + +#include "windowset.hpp" + +namespace qs::wm { + +///! WindowsetProjection covering one specific screen. +/// A ScreenProjection is a special type of @@WindowsetProjection which aggregates +/// all windowsets across all projections covering a specific screen. +/// +/// When used with @@Windowset.setProjection(), an arbitrary projection on the screen +/// will be picked. Usually there is only one. +/// +/// Use @@WindowManager.screenProjection() to get a ScreenProjection for a given screen. +class ScreenProjection: public WindowsetProjection { + Q_OBJECT; + QML_ELEMENT; + QML_UNCREATABLE(""); + +public: + ScreenProjection(QScreen* screen, QObject* parent); + + [[nodiscard]] QScreen* screen() const; + +private: + QScreen* mScreen; +}; + +} // namespace qs::wm diff --git a/src/windowmanager/test/manual/WorkspaceDelegate.qml b/src/windowmanager/test/manual/WorkspaceDelegate.qml new file mode 100644 index 0000000..4ebd7f2 --- /dev/null +++ b/src/windowmanager/test/manual/WorkspaceDelegate.qml @@ -0,0 +1,86 @@ +import QtQuick +import QtQuick.Controls.Fusion +import QtQuick.Layouts +import Quickshell +import Quickshell.Widgets +import Quickshell.WindowManager + +WrapperRectangle { + id: delegate + required property Windowset modelData; + color: modelData.active ? "green" : "gray" + + ColumnLayout { + Label { text: delegate.modelData.toString() } + Label { text: `Id: ${delegate.modelData.id} Name: ${delegate.modelData.name}` } + Label { text: `Coordinates: ${delegate.modelData.coordinates.toString()}`} + + RowLayout { + Label { text: "Group:" } + ComboBox { + Layout.fillWidth: true + implicitContentWidthPolicy: ComboBox.WidestText + enabled: delegate.modelData.canSetProjection + model: [...WindowManager.windowsetProjections].map(w => w.toString()) + currentIndex: WindowManager.windowsetProjections.indexOf(delegate.modelData.projection) + onActivated: i => delegate.modelData.setProjection(WindowManager.windowsetProjections[i]) + } + } + + RowLayout { + Label { text: "Screen:" } + ComboBox { + Layout.fillWidth: true + implicitContentWidthPolicy: ComboBox.WidestText + enabled: delegate.modelData.canSetProjection + model: [...Quickshell.screens].map(w => w.name) + currentIndex: Quickshell.screens.indexOf(delegate.modelData.projection.screens[0]) + onActivated: i => delegate.modelData.setProjection(WindowManager.screenProjection(Quickshell.screens[i])) + } + } + + + RowLayout { + DisplayCheckBox { + text: "Active" + checked: delegate.modelData.active + } + + DisplayCheckBox { + text: "Urgent" + checked: delegate.modelData.urgent + } + + DisplayCheckBox { + text: "Should Display" + checked: delegate.modelData.shouldDisplay + } + } + + RowLayout { + Button { + text: "Activate" + enabled: delegate.modelData.canActivate + onClicked: delegate.modelData.activate() + } + + Button { + text: "Deactivate" + enabled: delegate.modelData.canDeactivate + onClicked: delegate.modelData.deactivate() + } + + Button { + text: "Remove" + enabled: delegate.modelData.canRemove + onClicked: delegate.modelData.remove() + } + } + } + + component DisplayCheckBox: CheckBox { + enabled: false + palette.disabled.text: parent.palette.active.text + palette.disabled.windowText: parent.palette.active.windowText + } +} diff --git a/src/windowmanager/test/manual/screenproj.qml b/src/windowmanager/test/manual/screenproj.qml new file mode 100644 index 0000000..d06036c --- /dev/null +++ b/src/windowmanager/test/manual/screenproj.qml @@ -0,0 +1,45 @@ +import QtQuick +import QtQuick.Controls.Fusion +import QtQuick.Layouts +import Quickshell +import Quickshell.Widgets +import Quickshell.WindowManager + +FloatingWindow { + ScrollView { + anchors.fill: parent + + ColumnLayout { + Repeater { + model: Quickshell.screens + + WrapperRectangle { + id: delegate + required property ShellScreen modelData + color: "slategray" + margin: 5 + + ColumnLayout { + Label { text: `Screen: ${delegate.modelData.name}` } + + Repeater { + model: ScriptModel { + values: WindowManager.screenProjection(delegate.modelData).windowsets + } + + WorkspaceDelegate {} + } + } + } + } + + Repeater { + model: ScriptModel { + values: WindowManager.windowsets.filter(w => w.projection == null) + } + + WorkspaceDelegate {} + } + } + } +} diff --git a/src/windowmanager/test/manual/workspaces.qml b/src/windowmanager/test/manual/workspaces.qml new file mode 100644 index 0000000..d6fdf05 --- /dev/null +++ b/src/windowmanager/test/manual/workspaces.qml @@ -0,0 +1,46 @@ +import QtQuick +import QtQuick.Controls.Fusion +import QtQuick.Layouts +import Quickshell +import Quickshell.Widgets +import Quickshell.WindowManager + +FloatingWindow { + ScrollView { + anchors.fill: parent + + ColumnLayout { + Repeater { + model: WindowManager.windowsetProjections + + WrapperRectangle { + id: delegate + required property WindowsetProjection modelData + color: "slategray" + margin: 5 + + ColumnLayout { + Label { text: delegate.modelData.toString() } + Label { text: `Screens: ${delegate.modelData.screens.map(s => s.name)}` } + + Repeater { + model: ScriptModel { + values: delegate.modelData.windowsets + } + + WorkspaceDelegate {} + } + } + } + } + + Repeater { + model: ScriptModel { + values: WindowManager.windowsets.filter(w => w.projection == null) + } + + WorkspaceDelegate {} + } + } + } +} diff --git a/src/windowmanager/windowmanager.cpp b/src/windowmanager/windowmanager.cpp new file mode 100644 index 0000000..6b51db1 --- /dev/null +++ b/src/windowmanager/windowmanager.cpp @@ -0,0 +1,41 @@ +#include "windowmanager.hpp" +#include +#include + +#include + +#include "../core/qmlscreen.hpp" +#include "screenprojection.hpp" + +namespace qs::wm { + +std::function WindowManager::provider; + +void WindowManager::setProvider(std::function provider) { + WindowManager::provider = std::move(provider); +} + +WindowManager* WindowManager::instance() { + static auto* instance = WindowManager::provider(); + return instance; +} + +ScreenProjection* WindowManager::screenProjection(QuickshellScreenInfo* screen) { + auto* qscreen = screen->screen; + auto it = this->mScreenProjections.find(qscreen); + if (it != this->mScreenProjections.end()) { + return *it; + } + + auto* projection = new ScreenProjection(qscreen, this); + this->mScreenProjections.insert(qscreen, projection); + + QObject::connect(qscreen, &QObject::destroyed, this, [this, projection, qscreen]() { + this->mScreenProjections.remove(qscreen); + delete projection; + }); + + return projection; +} + +} // namespace qs::wm diff --git a/src/windowmanager/windowmanager.hpp b/src/windowmanager/windowmanager.hpp new file mode 100644 index 0000000..054e485 --- /dev/null +++ b/src/windowmanager/windowmanager.hpp @@ -0,0 +1,91 @@ +#pragma once + +#include + +#include +#include +#include +#include +#include +#include +#include + +#include "../core/qmlscreen.hpp" +#include "screenprojection.hpp" +#include "windowset.hpp" + +namespace qs::wm { + +class WindowManager: public QObject { + Q_OBJECT; + +public: + static void setProvider(std::function provider); + static WindowManager* instance(); + + Q_INVOKABLE ScreenProjection* screenProjection(QuickshellScreenInfo* screen); + + [[nodiscard]] QBindable> bindableWindowsets() const { + return &this->bWindowsets; + } + + [[nodiscard]] QBindable> bindableWindowsetProjections() const { + return &this->bWindowsetProjections; + } + +signals: + void windowsetsChanged(); + void windowsetProjectionsChanged(); + +public: + Q_OBJECT_BINDABLE_PROPERTY( + WindowManager, + QList, + bWindowsets, + &WindowManager::windowsetsChanged + ); + + Q_OBJECT_BINDABLE_PROPERTY( + WindowManager, + QList, + bWindowsetProjections, + &WindowManager::windowsetProjectionsChanged + ); + +private: + static std::function provider; + QHash mScreenProjections; +}; + +///! Window management interfaces exposed by the window manager. +class WindowManagerQml: public QObject { + Q_OBJECT; + QML_NAMED_ELEMENT(WindowManager); + QML_SINGLETON; + // clang-format off + /// All windowsets tracked by the WM across all projections. + Q_PROPERTY(QList windowsets READ default BINDABLE bindableWindowsets); + /// All windowset projections tracked by the WM. Does not include + /// internal projections from @@screenProjection(). + Q_PROPERTY(QList windowsetProjections READ default BINDABLE bindableWindowsetProjections); + // clang-format on + +public: + /// Returns an internal WindowsetProjection that covers a single screen and contains all + /// windowsets on that screen, regardless of the WM-specified projection. Depending on + /// how the WM lays out its actual projections, multiple ScreenProjections may contain + /// the same Windowsets. + Q_INVOKABLE static ScreenProjection* screenProjection(QuickshellScreenInfo* screen) { + return WindowManager::instance()->screenProjection(screen); + } + + [[nodiscard]] static QBindable> bindableWindowsets() { + return WindowManager::instance()->bindableWindowsets(); + } + + [[nodiscard]] static QBindable> bindableWindowsetProjections() { + return WindowManager::instance()->bindableWindowsetProjections(); + } +}; + +} // namespace qs::wm diff --git a/src/windowmanager/windowset.cpp b/src/windowmanager/windowset.cpp new file mode 100644 index 0000000..6231c40 --- /dev/null +++ b/src/windowmanager/windowset.cpp @@ -0,0 +1,45 @@ +#include "windowset.hpp" + +#include +#include +#include +#include + +#include "../core/qmlglobal.hpp" +#include "windowmanager.hpp" + +namespace qs::wm { + +Q_LOGGING_CATEGORY(logWorkspace, "quickshell.wm.workspace", QtWarningMsg); + +void Windowset::activate() { qCCritical(logWorkspace) << this << "cannot be activated"; } +void Windowset::deactivate() { qCCritical(logWorkspace) << this << "cannot be deactivated"; } +void Windowset::remove() { qCCritical(logWorkspace) << this << "cannot be removed"; } + +void Windowset::setProjection(WindowsetProjection* /*projection*/) { + qCCritical(logWorkspace) << this << "cannot be assigned to a projection"; +} + +WindowsetProjection::WindowsetProjection(QObject* parent): QObject(parent) { + this->bWindowsets.setBinding([this] { + QList result; + for (auto* ws: WindowManager::instance()->bindableWindowsets().value()) { + if (ws->bindableProjection().value() == this) { + result.append(ws); + } + } + return result; + }); + + this->bScreens.setBinding([this] { + QList screens; + + for (auto* screen: this->bQScreens.value()) { + screens.append(QuickshellTracked::instance()->screenInfo(screen)); + } + + return screens; + }); +} + +} // namespace qs::wm diff --git a/src/windowmanager/windowset.hpp b/src/windowmanager/windowset.hpp new file mode 100644 index 0000000..51cbd9b --- /dev/null +++ b/src/windowmanager/windowset.hpp @@ -0,0 +1,175 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +class QuickshellScreenInfo; + +namespace qs::wm { + +Q_DECLARE_LOGGING_CATEGORY(logWorkspace); + +class WindowsetProjection; + +///! A group of windows worked with by a user, usually known as a Workspace or Tag. +/// A Windowset is a generic type that encompasses both "Workspaces" and "Tags" in window managers. +/// Because the definition encompasses both you may not necessarily need all features. +class Windowset: public QObject { + Q_OBJECT; + QML_ELEMENT; + QML_UNCREATABLE(""); + // clang-format off + /// A persistent internal identifier for the windowset. This property should be identical + /// across restarts and destruction/recreation of a windowset. + Q_PROPERTY(QString id READ default NOTIFY idChanged BINDABLE bindableId); + /// Human readable name of the windowset. + Q_PROPERTY(QString name READ default NOTIFY nameChanged BINDABLE bindableName); + /// Coordinates of the workspace, represented as an N-dimensional array. Most WMs + /// will only expose one coordinate. If more than one is exposed, the first is + /// conventionally X, the second Y, and the third Z. + Q_PROPERTY(QList coordinates READ default NOTIFY coordinatesChanged BINDABLE bindableCoordinates); + /// True if the windowset is currently active. In a workspace based WM, this means the + /// represented workspace is current. In a tag based WM, this means the represented tag + /// is active. + Q_PROPERTY(bool active READ default NOTIFY activeChanged BINDABLE bindableActive); + /// The projection this windowset is a member of. A projection is the set of screens covered by + /// a windowset. + Q_PROPERTY(WindowsetProjection* projection READ default NOTIFY projectionChanged BINDABLE bindableProjection); + /// If false, this windowset should generally be hidden from workspace pickers. + Q_PROPERTY(bool shouldDisplay READ default NOTIFY shouldDisplayChanged BINDABLE bindableShouldDisplay); + /// If true, a window in this windowset has been marked as urgent. + Q_PROPERTY(bool urgent READ default NOTIFY urgentChanged BINDABLE bindableUrgent); + /// If true, the windowset can be activated. In a workspace based WM, this will make the workspace + /// current, in a tag based wm, the tag will be activated. + Q_PROPERTY(bool canActivate READ default NOTIFY canActivateChanged BINDABLE bindableCanActivate); + /// If true, the windowset can be deactivated. In a workspace based WM, deactivation is usually implicit + /// and based on activation of another workspace. + Q_PROPERTY(bool canDeactivate READ default NOTIFY canDeactivateChanged BINDABLE bindableCanDeactivate); + /// If true, the windowset can be removed. This may be done implicitly by the WM as well. + Q_PROPERTY(bool canRemove READ default NOTIFY canRemoveChanged BINDABLE bindableCanRemove); + /// If true, the windowset can be moved to a different projection. + Q_PROPERTY(bool canSetProjection READ default NOTIFY canSetProjectionChanged BINDABLE bindableCanSetProjection); + // clang-format on + +public: + explicit Windowset(QObject* parent): QObject(parent) {} + + /// Activate the windowset, making it the current workspace on a workspace based WM, or activating + /// the tag on a tag based WM. Requires @@canActivate. + Q_INVOKABLE virtual void activate(); + /// Deactivate the windowset, hiding it. Requires @@canDeactivate. + Q_INVOKABLE virtual void deactivate(); + /// Remove or destroy the windowset. Requires @@canRemove. + Q_INVOKABLE virtual void remove(); + /// Move the windowset to a different projection. A projection represents the set of screens + /// a workspace spans. Requires @@canSetProjection. + Q_INVOKABLE virtual void setProjection(WindowsetProjection* projection); + + [[nodiscard]] QBindable bindableId() const { return &this->bId; } + [[nodiscard]] QBindable bindableName() const { return &this->bName; } + [[nodiscard]] QBindable> bindableCoordinates() const { return &this->bCoordinates; } + [[nodiscard]] QBindable bindableActive() const { return &this->bActive; } + + [[nodiscard]] QBindable bindableProjection() const { + return &this->bProjection; + } + + [[nodiscard]] QBindable bindableShouldDisplay() const { return &this->bShouldDisplay; } + [[nodiscard]] QBindable bindableUrgent() const { return &this->bUrgent; } + [[nodiscard]] QBindable bindableCanActivate() const { return &this->bCanActivate; } + [[nodiscard]] QBindable bindableCanDeactivate() const { return &this->bCanDeactivate; } + [[nodiscard]] QBindable bindableCanRemove() const { return &this->bCanRemove; } + + [[nodiscard]] QBindable bindableCanSetProjection() const { + return &this->bCanSetProjection; + } + +signals: + void idChanged(); + void nameChanged(); + void coordinatesChanged(); + void activeChanged(); + void projectionChanged(); + void shouldDisplayChanged(); + void urgentChanged(); + void canActivateChanged(); + void canDeactivateChanged(); + void canRemoveChanged(); + void canSetProjectionChanged(); + +protected: + // clang-format off + Q_OBJECT_BINDABLE_PROPERTY(Windowset, QString, bId, &Windowset::idChanged); + Q_OBJECT_BINDABLE_PROPERTY(Windowset, QString, bName, &Windowset::nameChanged); + Q_OBJECT_BINDABLE_PROPERTY(Windowset, QList, bCoordinates); + Q_OBJECT_BINDABLE_PROPERTY(Windowset, bool, bActive, &Windowset::activeChanged); + Q_OBJECT_BINDABLE_PROPERTY(Windowset, WindowsetProjection*, bProjection, &Windowset::projectionChanged); + Q_OBJECT_BINDABLE_PROPERTY(Windowset, bool, bShouldDisplay, &Windowset::shouldDisplayChanged); + Q_OBJECT_BINDABLE_PROPERTY(Windowset, bool, bUrgent, &Windowset::urgentChanged); + Q_OBJECT_BINDABLE_PROPERTY(Windowset, bool, bCanActivate, &Windowset::canActivateChanged); + Q_OBJECT_BINDABLE_PROPERTY(Windowset, bool, bCanDeactivate, &Windowset::canDeactivateChanged); + Q_OBJECT_BINDABLE_PROPERTY(Windowset, bool, bCanRemove, &Windowset::canRemoveChanged); + Q_OBJECT_BINDABLE_PROPERTY(Windowset, bool, bCanSetProjection, &Windowset::canSetProjectionChanged); + // clang-format on +}; + +///! A space occupiable by a Windowset. +/// A WindowsetProjection represents a space that can be occupied by one or more @@Windowset$s. +/// The space is one or more screens. Multiple projections may occupy the same screens. +/// +/// @@WindowManager.screenProjection() can be used to get a projection representing all +/// @@Windowset$s on a given screen regardless of the WM's actual projection layout. +class WindowsetProjection: public QObject { + Q_OBJECT; + QML_ELEMENT; + QML_UNCREATABLE(""); + // clang-format off + /// Screens the windowset projection spans, often a single screen or all screens. + Q_PROPERTY(QList screens READ default NOTIFY screensChanged BINDABLE bindableScreens); + /// Windowsets that are currently present on the projection. + Q_PROPERTY(QList windowsets READ default NOTIFY windowsetsChanged BINDABLE bindableWindowsets); + // clang-format on + +public: + explicit WindowsetProjection(QObject* parent); + + [[nodiscard]] QBindable> bindableScreens() const { + return &this->bScreens; + } + + [[nodiscard]] QBindable> bindableQScreens() const { return &this->bQScreens; } + + [[nodiscard]] QBindable> bindableWindowsets() const { + return &this->bWindowsets; + } + +signals: + void screensChanged(); + void windowsetsChanged(); + +protected: + Q_OBJECT_BINDABLE_PROPERTY(WindowsetProjection, QList, bQScreens); + + Q_OBJECT_BINDABLE_PROPERTY( + WindowsetProjection, + QList, + bScreens, + &WindowsetProjection::screensChanged + ); + + Q_OBJECT_BINDABLE_PROPERTY( + WindowsetProjection, + QList, + bWindowsets, + &WindowsetProjection::windowsetsChanged + ); +}; + +} // namespace qs::wm