diff --git a/docs/user/keybinds.md b/docs/user/keybinds.md index cf141582..09952180 100644 --- a/docs/user/keybinds.md +++ b/docs/user/keybinds.md @@ -127,6 +127,9 @@ focused output, by index. They do not wrap around: `workspace-previous` on the first workspace is a silent no-op. On a dynamic output, `workspace-next` reaches the trailing empty workspace, which becomes active as usual. +`workspace-move-down` and `workspace-move-up` move the focused workspace up or down +on the focused output. They do not wrap around either. + The matching window actions can be bound independently: ```toml diff --git a/examples/config.toml b/examples/config.toml index 7c0cdda2..46858359 100644 --- a/examples/config.toml +++ b/examples/config.toml @@ -210,6 +210,8 @@ follows_mouse = false # "Mod+Period" = "workspace-next" # "Mod+Shift+Comma" = "window-move-to-workspace-previous" # "Mod+Shift+Period" = "window-move-to-workspace-next" +# "Mod+Ctrl+Comma" = "workspace-move-up" +# "Mod+Ctrl+Period" = "workspace-move-down" # "Mod+Ctrl+H" = "output-focus-left" # "Mod+Shift+O" = "dpms-off" # input activity powers displays back on # "Mod+Ctrl+O" = "dpms-off:DP-1" # target one connector diff --git a/src/config/keybind_parse.cpp b/src/config/keybind_parse.cpp index a89f8e0d..a491bf0e 100644 --- a/src/config/keybind_parse.cpp +++ b/src/config/keybind_parse.cpp @@ -207,10 +207,12 @@ namespace umbriel { {"window-toggle-maximize", "", KeybindAction::ToggleMaximize}, {"window-toggle-maximize-to-edges", "", KeybindAction::ToggleMaximizeToEdges}, {"window-toggle-pinned", "", KeybindAction::TogglePinned}, + {"workspace-move-down", "", KeybindAction::WorkspaceMoveDown}, {"workspace-move-to-output-down", "", KeybindAction::WorkspaceMoveToOutputDown}, {"workspace-move-to-output-left", "", KeybindAction::WorkspaceMoveToOutputLeft}, {"workspace-move-to-output-right", "", KeybindAction::WorkspaceMoveToOutputRight}, {"workspace-move-to-output-up", "", KeybindAction::WorkspaceMoveToOutputUp}, + {"workspace-move-up", "", KeybindAction::WorkspaceMoveUp}, {"workspace-next", "", KeybindAction::WorkspaceNext}, {"workspace-previous", "", KeybindAction::WorkspacePrevious}, {"workspace-set-layout", "", KeybindAction::WorkspaceSetLayout, diff --git a/src/config/keybind_parse.h b/src/config/keybind_parse.h index 16113026..76f24964 100644 --- a/src/config/keybind_parse.h +++ b/src/config/keybind_parse.h @@ -88,6 +88,8 @@ namespace umbriel { WorkspaceSetLayout, DpmsOff, DpmsOn, + WorkspaceMoveDown, + WorkspaceMoveUp, Count, }; diff --git a/src/scene/cheatsheet_rows.cpp b/src/scene/cheatsheet_rows.cpp index fbbcacb3..ec38f421 100644 --- a/src/scene/cheatsheet_rows.cpp +++ b/src/scene/cheatsheet_rows.cpp @@ -306,6 +306,8 @@ namespace { case A::WindowMoveToWorkspacePrevious: case A::WorkspaceNext: case A::WorkspacePrevious: + case A::WorkspaceMoveDown: + case A::WorkspaceMoveUp: case A::WorkspaceSetLayout: case A::WorkspaceMoveToOutputLeft: case A::WorkspaceMoveToOutputRight: diff --git a/src/server/actions.cpp b/src/server/actions.cpp index b40cd7a2..26fd7cb4 100644 --- a/src/server/actions.cpp +++ b/src/server/actions.cpp @@ -628,6 +628,15 @@ namespace umbriel { return true; } + template bool actionWorkspaceMove(Server& server, const Keybind& /*bind*/, std::string* /*error*/) { + Workspace* workspace = activeWorkspace(server); + if (workspace == nullptr || workspace->group() == nullptr) { + return true; + } + workspace->group()->moveActiveWorkspace(Direction); + return true; + } + template bool actionLayoutScroll(Server& server, const Keybind& bind, std::string* /*error*/) { const int multiplier = bind.wheel != WheelDirection::None && tiledDragActive(server) ? 2 : 1; scrollActiveLayout(server, multiplier); @@ -927,6 +936,8 @@ namespace umbriel { &actionWorkspaceSetLayout, &actionDpms, &actionDpms, + &actionWorkspaceMove<1>, + &actionWorkspaceMove<-1>, }; consteval bool everyActionHasHandler() { diff --git a/src/workspace/workspace.cpp b/src/workspace/workspace.cpp index 3660395c..a01735bb 100644 --- a/src/workspace/workspace.cpp +++ b/src/workspace/workspace.cpp @@ -15,7 +15,9 @@ #include #include #include +#include #include +#include #include "wlr.h" // clang-format on @@ -949,6 +951,41 @@ namespace umbriel { return result; } + bool WorkspaceGroup::moveActiveWorkspace(int direction) { + if (m_active == nullptr || m_workspaces.size() < 2 || direction == 0 || m_output == nullptr) { + return false; + } + const size_t index = m_active->index(); + const auto target = static_cast(index) + direction; + if (target < 0 || target >= static_cast(m_workspaces.size())) { + return false; + } + if (m_dynamic && direction > 0) { + const bool targetIsTrailingEmpty = static_cast(target) == m_workspaces.size() - 1 + && !m_workspaces[static_cast(target)]->hasViews(); + if (targetIsTrailingEmpty) { + return false; + } + } + slideFinish(); + std::swap(m_workspaces[index], m_workspaces[static_cast(target)]); + if (m_dynamic) { + refreshDynamicWorkspaceMetadata(); + if (m_workspaces.back()->hasViews()) { + appendDynamicWorkspace(); + } + } else { + for (const size_t slot : {index, static_cast(target)}) { + Workspace* moved = m_workspaces[slot].get(); + moved->rename(moved->name(), slot); + } + } + if (Overview* overview = m_server->overview(); overview != nullptr && overview->active()) { + overview->onWorkspaceInventoryChanged(this); + } + return true; + } + void WorkspaceGroup::reconcileInventory() { slideFinish(); const char* outputName = m_output->wlr()->name != nullptr ? m_output->wlr()->name : "output"; diff --git a/src/workspace/workspace.h b/src/workspace/workspace.h index 9ff76416..4ab6710a 100644 --- a/src/workspace/workspace.h +++ b/src/workspace/workspace.h @@ -179,6 +179,7 @@ namespace umbriel { // Insert an empty numbered workspace into a dynamic group and renumber the following workspaces. Static configured // groups cannot be extended this way and return null. Workspace* insertDynamicWorkspace(size_t index); + bool moveActiveWorkspace(int direction); void reconcileInventory(); void refreshLayouts(); void reconcileDynamic();