From 49fe1ca43fbaf8770874dbada04ff6075bae4250 Mon Sep 17 00:00:00 2001 From: outfoxxed Date: Sat, 21 Jun 2025 12:57:15 -0700 Subject: [PATCH] wip ext-ws --- src/CMakeLists.txt | 1 + src/wayland/CMakeLists.txt | 2 + src/wayland/windowmanager/CMakeLists.txt | 20 ++ src/wayland/windowmanager/ext_workspace.cpp | 169 ++++++++++++++++ src/wayland/windowmanager/ext_workspace.hpp | 117 +++++++++++ src/wayland/windowmanager/init.cpp | 21 ++ src/wayland/windowmanager/windowmanager.cpp | 14 ++ src/wayland/windowmanager/windowmanager.hpp | 25 +++ src/wayland/windowmanager/workspace.cpp | 198 +++++++++++++++++++ src/wayland/windowmanager/workspace.hpp | 85 ++++++++ src/windowmanager/CMakeLists.txt | 18 ++ src/windowmanager/test/manual/workspaces.qml | 112 +++++++++++ src/windowmanager/windowmanager.cpp | 18 ++ src/windowmanager/windowmanager.hpp | 50 +++++ src/windowmanager/workspace.cpp | 31 +++ src/windowmanager/workspace.hpp | 119 +++++++++++ src/windowmanager/workspacemodel.cpp | 1 + src/windowmanager/workspacemodel.hpp | 39 ++++ 18 files changed, 1040 insertions(+) create mode 100644 src/wayland/windowmanager/CMakeLists.txt create mode 100644 src/wayland/windowmanager/ext_workspace.cpp create mode 100644 src/wayland/windowmanager/ext_workspace.hpp create mode 100644 src/wayland/windowmanager/init.cpp create mode 100644 src/wayland/windowmanager/windowmanager.cpp create mode 100644 src/wayland/windowmanager/windowmanager.hpp create mode 100644 src/wayland/windowmanager/workspace.cpp create mode 100644 src/wayland/windowmanager/workspace.hpp create mode 100644 src/windowmanager/CMakeLists.txt create mode 100644 src/windowmanager/test/manual/workspaces.qml create mode 100644 src/windowmanager/windowmanager.cpp create mode 100644 src/windowmanager/windowmanager.hpp create mode 100644 src/windowmanager/workspace.cpp create mode 100644 src/windowmanager/workspace.hpp create mode 100644 src/windowmanager/workspacemodel.cpp create mode 100644 src/windowmanager/workspacemodel.hpp diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt index 52db00a..95e5a40 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_REPORTER) add_subdirectory(crash) diff --git a/src/wayland/CMakeLists.txt b/src/wayland/CMakeLists.txt index 1d6543e..ede66c5 100644 --- a/src/wayland/CMakeLists.txt +++ b/src/wayland/CMakeLists.txt @@ -114,6 +114,8 @@ if (HYPRLAND) add_subdirectory(hyprland) endif() +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..9e03b14 --- /dev/null +++ b/src/wayland/windowmanager/CMakeLists.txt @@ -0,0 +1,20 @@ +qt_add_library(quickshell-wayland-windowsystem STATIC + windowmanager.cpp + workspace.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-foreign-toplevel ext-foreign-toplevel-list-v1 "${WAYLAND_PROTOCOLS}/staging/ext-foreign-toplevel-list") +wl_proto(wlp-ext-workspace ext-workspace-v1 "${WAYLAND_PROTOCOLS}/staging/ext-workspace") + +target_link_libraries(quickshell-wayland-windowsystem PRIVATE + Qt::WaylandClient Qt::WaylandClientPrivate wayland-client + Qt::Quick # for pch? potentially, check w/ gcc + + wlp-ext-foreign-toplevel wlp-ext-workspace +) + +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..3e4c099 --- /dev/null +++ b/src/wayland/windowmanager/ext_workspace.cpp @@ -0,0 +1,169 @@ +#include "ext_workspace.hpp" + +#include +#include +#include +#include +#include +#include + +namespace qs::wayland::workspace { + +Q_LOGGING_CATEGORY(logWorkspace, "quickshell.wm.wayland.workspace"); + +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(uint32_t 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..9dfac1b --- /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 + +#include "../output_tracking.hpp" + +namespace qs::wayland::workspace { + +Q_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(uint32_t state) override; + void ext_workspace_handle_v1_capabilities(uint32_t 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(uint32_t 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..fa336d7 --- /dev/null +++ b/src/wayland/windowmanager/init.cpp @@ -0,0 +1,21 @@ +#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..5f4a450 --- /dev/null +++ b/src/wayland/windowmanager/windowmanager.cpp @@ -0,0 +1,14 @@ +#include "windowmanager.hpp" + +namespace qs::wm::wayland { + +WaylandWindowManager* WaylandWindowManager::instance() { + static auto* instance = new WaylandWindowManager(); + return instance; +} + +void installWmProvider() { + 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..c732d6a --- /dev/null +++ b/src/wayland/windowmanager/windowmanager.hpp @@ -0,0 +1,25 @@ +#pragma once + +#include + +#include "../../windowmanager/windowmanager.hpp" +#include "workspace.hpp" + +namespace qs::wm::wayland { + +class WaylandWindowManager: public WindowManager { + Q_OBJECT; + +public: + static WaylandWindowManager* instance(); + + [[nodiscard]] UntypedObjectModel* workspaces() const override { + return &WorkspaceManager::instance()->mWorkspaces; + } + + [[nodiscard]] UntypedObjectModel* workspaceGroups() const override { + return &WorkspaceManager::instance()->mWorkspaceGroups; + } +}; + +} // namespace qs::wm::wayland diff --git a/src/wayland/windowmanager/workspace.cpp b/src/wayland/windowmanager/workspace.cpp new file mode 100644 index 0000000..07bf3da --- /dev/null +++ b/src/wayland/windowmanager/workspace.cpp @@ -0,0 +1,198 @@ +#include "workspace.hpp" + +#include +#include +#include +#include +#include + +#include "ext_workspace.hpp" + +namespace qs::wm::wayland { + +WorkspaceManager::WorkspaceManager() { + auto* impl = impl::WorkspaceManager::instance(); + + QObject::connect( + impl, + &impl::WorkspaceManager::serverCommit, + this, + &WorkspaceManager::onServerCommit + ); + + QObject::connect( + impl, + &impl::WorkspaceManager::workspaceCreated, + this, + &WorkspaceManager::onWorkspaceCreated + ); + + QObject::connect( + impl, + &impl::WorkspaceManager::workspaceDestroyed, + this, + &WorkspaceManager::onWorkspaceDestroyed + ); + + QObject::connect( + impl, + &impl::WorkspaceManager::groupCreated, + this, + &WorkspaceManager::onGroupCreated + ); + + QObject::connect( + impl, + &impl::WorkspaceManager::groupDestroyed, + this, + &WorkspaceManager::onGroupDestroyed + ); +} + +void WorkspaceManager::commit() { + qCDebug(impl::logWorkspace) << "Committing workspaces"; + impl::WorkspaceManager::instance()->commit(); +} + +void WorkspaceManager::onServerCommit() { + // Groups are created/destroyed around workspaces to avoid any nulls making it + // to the qml engine. + + for (auto* groupImpl: this->pendingGroupCreations) { + auto* group = new WlWorkspaceGroup(this, groupImpl); + this->groupsByImpl.insert(groupImpl, group); + this->mWorkspaceGroups.insertObject(group); + } + + for (auto* wsImpl: this->pendingWorkspaceCreations) { + auto* ws = new WlWorkspace(this, wsImpl); + this->workspaceByImpl.insert(wsImpl, ws); + this->mWorkspaces.insertObject(ws); + } + + for (auto* wsImpl: this->pendingWorkspaceDestructions) { + this->mWorkspaces.removeObject(this->workspaceByImpl.value(wsImpl)); + this->workspaceByImpl.remove(wsImpl); + } + + for (auto* groupImpl: this->pendingGroupDestructions) { + this->mWorkspaceGroups.removeObject(this->groupsByImpl.value(groupImpl)); + this->groupsByImpl.remove(groupImpl); + } + + for (auto* ws: this->mWorkspaces.valueList()) ws->commitImpl(); + for (auto* group: this->mWorkspaceGroups.valueList()) group->commitImpl(); + + this->pendingWorkspaceCreations.clear(); + this->pendingWorkspaceDestructions.clear(); + this->pendingGroupCreations.clear(); + this->pendingGroupDestructions.clear(); +} + +void WorkspaceManager::onWorkspaceCreated(impl::Workspace* workspace) { + this->pendingWorkspaceCreations.append(workspace); +} + +void WorkspaceManager::onWorkspaceDestroyed(impl::Workspace* workspace) { + if (!this->pendingWorkspaceCreations.removeOne(workspace)) { + this->pendingWorkspaceDestructions.append(workspace); + } +} + +void WorkspaceManager::onGroupCreated(impl::WorkspaceGroup* group) { + this->pendingGroupCreations.append(group); +} + +void WorkspaceManager::onGroupDestroyed(impl::WorkspaceGroup* group) { + if (!this->pendingGroupCreations.removeOne(group)) { + this->pendingGroupDestructions.append(group); + } +} + +WorkspaceManager* WorkspaceManager::instance() { + static auto* instance = new WorkspaceManager(); + return instance; +} + +WlWorkspace::WlWorkspace(WorkspaceManager* manager, impl::Workspace* impl) + : Workspace(manager) + , impl(impl) { + this->commitImpl(); +} + +void WlWorkspace::commitImpl() { + Qt::beginPropertyUpdateGroup(); + this->bId = this->impl->id; + this->bName = this->impl->name; + 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->bCanSetGroup = this->impl->canAssign; + this->bGroup = this->manager()->groupsByImpl.value(this->impl->group); + Qt::endPropertyUpdateGroup(); +} + +void WlWorkspace::activate() { + if (!this->bCanActivate) { + qCritical(logWorkspace) << this << "cannot be activated"; + return; + } + + qCDebug(impl::logWorkspace) << "Calling activate() for" << this; + this->impl->activate(); + WorkspaceManager::commit(); +} + +void WlWorkspace::deactivate() { + if (!this->bCanDeactivate) { + qCritical(logWorkspace) << this << "cannot be deactivated"; + return; + } + + qCDebug(impl::logWorkspace) << "Calling deactivate() for" << this; + this->impl->deactivate(); + WorkspaceManager::commit(); +} + +void WlWorkspace::remove() { + if (!this->bCanRemove) { + qCritical(logWorkspace) << this << "cannot be removed"; + return; + } + + qCDebug(impl::logWorkspace) << "Calling remove() for" << this; + this->impl->remove(); + WorkspaceManager::commit(); +} + +void WlWorkspace::setGroup(WorkspaceGroup* group) { + if (!this->bCanSetGroup) { + qCritical(logWorkspace) << this << "cannot be assigned to a group"; + return; + } + + if (!group) { + qCritical(logWorkspace) << "Cannot set a workspace's group to null"; + return; + } + + qCDebug(impl::logWorkspace) << "Assigning" << this << "to" << group; + // NOLINTNEXTLINE: A WorkspaceGroup will always be a WlWorkspaceGroup under wayland. + this->impl->assign(static_cast(group)->impl->object()); + WorkspaceManager::commit(); +} + +WlWorkspaceGroup::WlWorkspaceGroup(WorkspaceManager* manager, impl::WorkspaceGroup* impl) + : WorkspaceGroup(manager) + , impl(impl) { + this->commitImpl(); +} + +void WlWorkspaceGroup::commitImpl() { + // TODO: will not commit the correct screens if missing qt repr at commit time + this->bScreens = this->impl->screens.screens(); +} + +} // namespace qs::wm::wayland diff --git a/src/wayland/windowmanager/workspace.hpp b/src/wayland/windowmanager/workspace.hpp new file mode 100644 index 0000000..82742a9 --- /dev/null +++ b/src/wayland/windowmanager/workspace.hpp @@ -0,0 +1,85 @@ +#pragma once + +#include +#include +#include +#include + +#include "../../core/model.hpp" +#include "../../windowmanager/workspace.hpp" +#include "ext_workspace.hpp" + +namespace qs::wm::wayland { +namespace impl = qs::wayland::workspace; + +class WlWorkspace; +class WlWorkspaceGroup; + +class WorkspaceManager: public QObject { + Q_OBJECT; + +public: + static WorkspaceManager* instance(); + + ObjectModel mWorkspaces {this}; + ObjectModel mWorkspaceGroups {this}; + + static void commit(); + +private slots: + void onServerCommit(); + void onWorkspaceCreated(impl::Workspace* workspace); + void onWorkspaceDestroyed(impl::Workspace* workspace); + void onGroupCreated(impl::WorkspaceGroup* group); + void onGroupDestroyed(impl::WorkspaceGroup* group); + +private: + WorkspaceManager(); + + QList pendingWorkspaceCreations; + QList pendingWorkspaceDestructions; + QHash workspaceByImpl; + + QList pendingGroupCreations; + QList pendingGroupDestructions; + QHash groupsByImpl; + + friend class WlWorkspace; +}; + +class WlWorkspace: public Workspace { +public: + WlWorkspace(WorkspaceManager* manager, impl::Workspace* impl); + + void commitImpl(); + + void activate() override; + void deactivate() override; + void remove() override; + void setGroup(WorkspaceGroup* group) override; + + [[nodiscard]] WorkspaceManager* manager() { + return static_cast(this->parent()); // NOLINT + } + +private: + impl::Workspace* impl = nullptr; +}; + +class WlWorkspaceGroup: public WorkspaceGroup { +public: + WlWorkspaceGroup(WorkspaceManager* manager, impl::WorkspaceGroup* impl); + + void commitImpl(); + + [[nodiscard]] WorkspaceManager* manager() { + return static_cast(this->parent()); // NOLINT + } + +private: + impl::WorkspaceGroup* impl = nullptr; + + friend class WlWorkspace; +}; + +} // namespace qs::wm::wayland diff --git a/src/windowmanager/CMakeLists.txt b/src/windowmanager/CMakeLists.txt new file mode 100644 index 0000000..365c43a --- /dev/null +++ b/src/windowmanager/CMakeLists.txt @@ -0,0 +1,18 @@ +qt_add_library(quickshell-windowmanager STATIC + windowmanager.cpp + workspace.cpp + workspacemodel.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) + +target_link_libraries(quickshell-windowmanager PRIVATE Qt::Quick) +target_link_libraries(quickshell PRIVATE quickshell-windowmanager) diff --git a/src/windowmanager/test/manual/workspaces.qml b/src/windowmanager/test/manual/workspaces.qml new file mode 100644 index 0000000..b379b47 --- /dev/null +++ b/src/windowmanager/test/manual/workspaces.qml @@ -0,0 +1,112 @@ +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.workspaceGroups + + WrapperRectangle { + id: delegate + required property WorkspaceGroup 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: [...WindowManager.workspaces.values].filter(w => w.group == delegate.modelData) + } + + WorkspaceDelegate {} + } + } + } + } + + Repeater { + model: ScriptModel { + values: WindowManager.workspaces.values.filter(w => w.group == null) + } + + WorkspaceDelegate {} + } + } + } + + component WorkspaceDelegate: WrapperRectangle { + id: delegate + required property Workspace modelData; + color: modelData.active ? "green" : "gray" + + ColumnLayout { + Label { text: delegate.modelData.toString() } + Label { text: `Id: ${delegate.modelData.id} Name: ${delegate.modelData.name}` } + + RowLayout { + Label { text: "Group:" } + ComboBox { + Layout.fillWidth: true + implicitContentWidthPolicy: ComboBox.WidestText + enabled: delegate.modelData.canSetGroup + model: [...WindowManager.workspaceGroups.values].map(w => w.toString()) + currentIndex: WindowManager.workspaceGroups.values.indexOf(delegate.modelData.group) + onActivated: i => delegate.modelData.setGroup(WindowManager.workspaceGroups.values[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/windowmanager.cpp b/src/windowmanager/windowmanager.cpp new file mode 100644 index 0000000..7c6a6cc --- /dev/null +++ b/src/windowmanager/windowmanager.cpp @@ -0,0 +1,18 @@ +#include "windowmanager.hpp" +#include +#include + +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; +} + +} // namespace qs::wm diff --git a/src/windowmanager/windowmanager.hpp b/src/windowmanager/windowmanager.hpp new file mode 100644 index 0000000..03ebfc1 --- /dev/null +++ b/src/windowmanager/windowmanager.hpp @@ -0,0 +1,50 @@ +#pragma once + +#include + +#include +#include +#include + +#include "../core/model.hpp" +#include "workspace.hpp" + +namespace qs::wm { + +class WindowManager: public QObject { + Q_OBJECT; + +public: + static void setProvider(std::function provider); + static WindowManager* instance(); + + [[nodiscard]] virtual UntypedObjectModel* workspaces() const { + return UntypedObjectModel::emptyInstance(); + } + + [[nodiscard]] virtual UntypedObjectModel* workspaceGroups() const { + return UntypedObjectModel::emptyInstance(); + } + +private: + static std::function provider; +}; + +class WindowManagerQml: public QObject { + Q_OBJECT; + QML_NAMED_ELEMENT(WindowManager); + QML_SINGLETON; + Q_PROPERTY(UntypedObjectModel* workspaces READ workspaces CONSTANT); + Q_PROPERTY(UntypedObjectModel* workspaceGroups READ workspaceGroups CONSTANT); + +public: + [[nodiscard]] static UntypedObjectModel* workspaces() { + return WindowManager::instance()->workspaces(); + } + + [[nodiscard]] static UntypedObjectModel* workspaceGroups() { + return WindowManager::instance()->workspaceGroups(); + } +}; + +} // namespace qs::wm diff --git a/src/windowmanager/workspace.cpp b/src/windowmanager/workspace.cpp new file mode 100644 index 0000000..472234e --- /dev/null +++ b/src/windowmanager/workspace.cpp @@ -0,0 +1,31 @@ +#include "workspace.hpp" + +#include +#include +#include + +#include "../core/qmlglobal.hpp" + +namespace qs::wm { + +Q_LOGGING_CATEGORY(logWorkspace, "quickshell.wm.workspace", QtWarningMsg); + +void Workspace::activate() { qCCritical(logWorkspace) << this << "cannot be activated"; } +void Workspace::deactivate() { qCCritical(logWorkspace) << this << "cannot be deactivated"; } +void Workspace::remove() { qCCritical(logWorkspace) << this << "cannot be removed"; } + +void Workspace::setGroup(WorkspaceGroup* /*group*/) { + qCCritical(logWorkspace) << this << "cannot be assigned to a group"; +} + +void WorkspaceGroup::onScreensChanged() { + mCachedScreens.clear(); + + for (auto* screen: this->bScreens.value()) { + mCachedScreens.append(QuickshellTracked::instance()->screenInfo(screen)); + } + + emit this->screensChanged(); +} + +} // namespace qs::wm diff --git a/src/windowmanager/workspace.hpp b/src/windowmanager/workspace.hpp new file mode 100644 index 0000000..aa148a0 --- /dev/null +++ b/src/windowmanager/workspace.hpp @@ -0,0 +1,119 @@ +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +class QuickshellScreenInfo; + +namespace qs::wm { + +Q_DECLARE_LOGGING_CATEGORY(logWorkspace); + +class WorkspaceGroup; + +class Workspace: public QObject { + Q_OBJECT; + QML_ELEMENT; + QML_UNCREATABLE(""); + // clang-format off + // persistent id + Q_PROPERTY(QString id READ default BINDABLE bindableId NOTIFY idChanged); + Q_PROPERTY(QString name READ default BINDABLE bindableName NOTIFY nameChanged); + // currently visible + Q_PROPERTY(bool active READ default BINDABLE bindableActive NOTIFY activeChanged); + Q_PROPERTY(WorkspaceGroup* group READ default BINDABLE bindableGroup NOTIFY groupChanged); + // in workspace pickers + Q_PROPERTY(bool shouldDisplay READ default BINDABLE bindableShouldDisplay NOTIFY shouldDisplayChanged); + Q_PROPERTY(bool urgent READ default BINDABLE bindableUrgent NOTIFY urgentChanged); + Q_PROPERTY(bool canActivate READ default BINDABLE bindableCanActivate NOTIFY canActivateChanged); + Q_PROPERTY(bool canDeactivate READ default BINDABLE bindableCanDeactivate NOTIFY canDeactivateChanged); + Q_PROPERTY(bool canRemove READ default BINDABLE bindableCanRemove NOTIFY canRemoveChanged); + Q_PROPERTY(bool canSetGroup READ default BINDABLE bindableCanSetGroup NOTIFY canSetGroupChanged); + // clang-format on + +public: + explicit Workspace(QObject* parent): QObject(parent) {} + + Q_INVOKABLE virtual void activate(); + Q_INVOKABLE virtual void deactivate(); + Q_INVOKABLE virtual void remove(); + Q_INVOKABLE virtual void setGroup(WorkspaceGroup* group); + + [[nodiscard]] QBindable bindableId() const { return &this->bId; } + [[nodiscard]] QBindable bindableName() const { return &this->bName; } + [[nodiscard]] QBindable bindableActive() const { return &this->bActive; } + [[nodiscard]] QBindable bindableGroup() const { return &this->bGroup; } + [[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 bindableCanSetGroup() const { return &this->bCanSetGroup; } + +signals: + void idChanged(); + void nameChanged(); + void activeChanged(); + void groupChanged(); + void shouldDisplayChanged(); + void urgentChanged(); + void canActivateChanged(); + void canDeactivateChanged(); + void canRemoveChanged(); + void canSetGroupChanged(); + +protected: + Q_OBJECT_BINDABLE_PROPERTY(Workspace, QString, bId, &Workspace::idChanged); + Q_OBJECT_BINDABLE_PROPERTY(Workspace, QString, bName, &Workspace::nameChanged); + Q_OBJECT_BINDABLE_PROPERTY(Workspace, bool, bActive, &Workspace::activeChanged); + Q_OBJECT_BINDABLE_PROPERTY(Workspace, WorkspaceGroup*, bGroup, &Workspace::groupChanged); + Q_OBJECT_BINDABLE_PROPERTY(Workspace, bool, bShouldDisplay, &Workspace::shouldDisplayChanged); + Q_OBJECT_BINDABLE_PROPERTY(Workspace, bool, bUrgent, &Workspace::urgentChanged); + Q_OBJECT_BINDABLE_PROPERTY(Workspace, bool, bCanActivate, &Workspace::canActivateChanged); + Q_OBJECT_BINDABLE_PROPERTY(Workspace, bool, bCanDeactivate, &Workspace::canDeactivateChanged); + Q_OBJECT_BINDABLE_PROPERTY(Workspace, bool, bCanRemove, &Workspace::canRemoveChanged); + Q_OBJECT_BINDABLE_PROPERTY(Workspace, bool, bCanSetGroup, &Workspace::canSetGroupChanged); + //Q_OBJECT_BINDABLE_PROPERTY(Workspace, qint32, bIndex); +}; + +class WorkspaceGroup: public QObject { + Q_OBJECT; + QML_ELEMENT; + QML_UNCREATABLE(""); + /// Screens the workspace group is present on. + /// + /// > [!WARNING] This is not a model. Use @@Quickshell.ScriptModel if you need it to + /// > behave like one. + Q_PROPERTY(QList screens READ screens NOTIFY screensChanged); + +public: + explicit WorkspaceGroup(QObject* parent): QObject(parent) {} + + [[nodiscard]] const QList& screens() const { return this->mCachedScreens; } + +signals: + void screensChanged(); + +private slots: + void onScreensChanged(); + +protected: + Q_OBJECT_BINDABLE_PROPERTY( + WorkspaceGroup, + QList, + bScreens, + &WorkspaceGroup::onScreensChanged + ); + +private: + QList mCachedScreens; +}; + +} // namespace qs::wm diff --git a/src/windowmanager/workspacemodel.cpp b/src/windowmanager/workspacemodel.cpp new file mode 100644 index 0000000..f6941fb --- /dev/null +++ b/src/windowmanager/workspacemodel.cpp @@ -0,0 +1 @@ +#include "workspacemodel.hpp" diff --git a/src/windowmanager/workspacemodel.hpp b/src/windowmanager/workspacemodel.hpp new file mode 100644 index 0000000..aeb855f --- /dev/null +++ b/src/windowmanager/workspacemodel.hpp @@ -0,0 +1,39 @@ +#pragma once + +#include +#include +#include +#include +#include + +namespace qs::windowsystem { + +class WorkspaceModel: public QObject { + Q_OBJECT; + QML_ELEMENT; + +public: + enum ConflictStrategy : quint8 { + KeepFirst = 0, + ShowDuplicates, + }; + Q_ENUM(ConflictStrategy); + +signals: + void fromChanged(); + void toChanged(); + void screensChanged(); + void conflictStrategyChanged(); + +private: + Q_OBJECT_BINDABLE_PROPERTY(WorkspaceModel, qint32, bFrom, &WorkspaceModel::fromChanged); + Q_OBJECT_BINDABLE_PROPERTY(WorkspaceModel, qint32, bTo, &WorkspaceModel::toChanged); + Q_OBJECT_BINDABLE_PROPERTY( + WorkspaceModel, + ConflictStrategy, + bConflictStrategy, + &WorkspaceModel::conflictStrategyChanged + ); +}; + +} // namespace qs::windowsystem