diff --git a/assets/translations/en.json b/assets/translations/en.json index 1f5d1a122e..f25d28c50b 100644 --- a/assets/translations/en.json +++ b/assets/translations/en.json @@ -2071,6 +2071,10 @@ "validate": { "description": "Confirm selections, submit inputs, or activate the focused item", "label": "Confirm" + }, + "window-switcher-per-workspace": { + "description": "Restrict Alt-Tab window switching to windows on the current workspace only", + "label": "Switch Windows: Current Workspace Only" } }, "lockscreen": { diff --git a/src/config/config_overrides.cpp b/src/config/config_overrides.cpp index a03e38eb41..1ecb856471 100644 --- a/src/config/config_overrides.cpp +++ b/src/config/config_overrides.cpp @@ -348,6 +348,7 @@ namespace { && widgetMapEqual(a.widgets, b.widgets) && desktopWidgetsConfigEqual(a.desktopWidgets, b.desktopWidgets) && a.hotCorners == b.hotCorners + && a.windowSwitcher == b.windowSwitcher && lockscreenWidgetsConfigEqual(a.lockscreenWidgets, b.lockscreenWidgets) && a.wallpaper == b.wallpaper && a.backdrop == b.backdrop @@ -739,6 +740,7 @@ ConfigChangeSet computeConfigChangeSet(const Config& prev, const Config& next) { .hotCorners = !(prev.hotCorners == next.hotCorners), .storage = !(prev.storage == next.storage), .accessibility = !(prev.accessibility == next.accessibility), + .windowSwitcher = !(prev.windowSwitcher == next.windowSwitcher), }; } diff --git a/src/config/config_types.h b/src/config/config_types.h index b6b2388174..463cbb2997 100644 --- a/src/config/config_types.h +++ b/src/config/config_types.h @@ -1524,6 +1524,12 @@ struct HotCornersConfig { bool operator==(const HotCornersConfig&) const = default; }; +struct WindowSwitcherConfig { + bool perWorkspace = false; + + bool operator==(const WindowSwitcherConfig&) const = default; +}; + struct Config { std::vector bars; std::unordered_map widgets; @@ -1535,6 +1541,7 @@ struct Config { DesktopWidgetsConfig desktopWidgets; HotCornersConfig hotCorners; StorageConfig storage; + WindowSwitcherConfig windowSwitcher; ShellConfig shell; OsdConfig osd; NotificationConfig notification; @@ -1586,6 +1593,7 @@ struct ConfigChangeSet { bool hotCorners = true; bool storage = true; bool accessibility = true; + bool windowSwitcher = true; [[nodiscard]] bool any() const noexcept { return bars @@ -1615,7 +1623,8 @@ struct ConfigChangeSet { || plugins || hotCorners || storage - || accessibility; + || accessibility + || windowSwitcher; } }; diff --git a/src/config/schema/config_schema.cpp b/src/config/schema/config_schema.cpp index 40193dff11..be895c26e9 100644 --- a/src/config/schema/config_schema.cpp +++ b/src/config/schema/config_schema.cpp @@ -409,6 +409,13 @@ namespace noctalia::config::schema { return s; } + const Schema& windowSwitcherSchema() { + static const Schema s = { + field(&WindowSwitcherConfig::perWorkspace, "per_workspace"), + }; + return s; + } + namespace { const Schema& brightnessMonitorSchema() { static const Schema s = { diff --git a/src/config/schema/config_schema.h b/src/config/schema/config_schema.h index 90d02b3a56..debf251a76 100644 --- a/src/config/schema/config_schema.h +++ b/src/config/schema/config_schema.h @@ -25,6 +25,7 @@ namespace noctalia::config::schema { const Schema& controlCenterSchema(); const Schema& pluginsSchema(); const Schema& hotCornersSchema(); + const Schema& windowSwitcherSchema(); const Schema& calendarSchema(); const Schema& keybindsSchema(); const Schema& hooksSchema(); diff --git a/src/config/schema/config_sections.cpp b/src/config/schema/config_sections.cpp index 2b8aaeece2..4a28ed8736 100644 --- a/src/config/schema/config_sections.cpp +++ b/src/config/schema/config_sections.cpp @@ -66,6 +66,7 @@ namespace noctalia::config::schema { t.push_back(makeSection("keybinds", &Config::keybinds, keybindsSchema())); t.push_back(makeSection("dock", &Config::dock, dockSchema())); t.push_back(makeSection("hot_corners", &Config::hotCorners, hotCornersSchema())); + t.push_back(makeSection("window_switcher", &Config::windowSwitcher, windowSwitcherSchema())); t.push_back(makeSection("control_center", &Config::controlCenter, controlCenterSchema())); t.push_back(makeSection("plugins", &Config::plugins, pluginsSchema())); t.push_back(makeSection("hooks", &Config::hooks, hooksSchema())); diff --git a/src/shell/settings/settings_registry.cpp b/src/shell/settings/settings_registry.cpp index 85002c215d..1f430671b8 100644 --- a/src/shell/settings/settings_registry.cpp +++ b/src/shell/settings/settings_registry.cpp @@ -2081,6 +2081,12 @@ namespace settings { KeybindListSetting{.items = effectiveKeybindItems(cfg.keybinds.tabNext, KeybindAction::TabNext), .maxItems = 4}, "keybind shortcut hotkey tab focus pane" )); + entries.push_back(makeEntry( + SettingsSection::Keybinds, "window-switcher", + tr("settings.schema.keybinds.window-switcher-per-workspace.label"), + tr("settings.schema.keybinds.window-switcher-per-workspace.description"), {"window_switcher", "per_workspace"}, + ToggleSetting{cfg.windowSwitcher.perWorkspace}, "window switcher alt tab workspace cycle restrict current" + )); // Niri-specific integrations if (env.niriOverviewTypeToLaunchSupported || env.niriBackdropSupported) { diff --git a/src/shell/switcher/window_switcher.cpp b/src/shell/switcher/window_switcher.cpp index 9c96e989a2..f228d22b84 100644 --- a/src/shell/switcher/window_switcher.cpp +++ b/src/shell/switcher/window_switcher.cpp @@ -322,9 +322,22 @@ namespace { } } + // Finds the currently active workspace's key on the given output, using the same + // "workspaces() + .active" idiom as WorkspacesWidget::activeWorkspaceIndex(). + [[nodiscard]] std::optional + activeWorkspaceKeyFor(const CompositorPlatform& platform, wl_output* output) { + for (const auto& workspace : platform.workspaces(output)) { + if (workspace.active) { + return workspace.id; + } + } + return std::nullopt; + } + void buildWindowEntries( const CompositorPlatform& platform, IconResolver& iconResolver, int iconSize, - std::vector& out, const std::optional& focusedId + std::vector& out, const std::optional& focusedId, + const std::optional& activeWorkspaceKey ) { std::unordered_map assignmentById; assignmentById.reserve(32); @@ -386,6 +399,16 @@ namespace { addCandidate(std::move(candidate), key); } + // Windows with no workspace assignment (empty workspaceKey) are always kept: on + // compositors whose backend doesn't populate workspace-window assignments, filtering + // them out would empty the switcher entirely, so per-workspace restriction degrades + // to a no-op there instead of hiding every window. + if (activeWorkspaceKey.has_value()) { + std::erase_if(candidates, [&](const WindowSwitcherCandidate& candidate) { + return !candidate.workspaceKey.empty() && candidate.workspaceKey != *activeWorkspaceKey; + }); + } + std::ranges::stable_sort(candidates, [](const WindowSwitcherCandidate& a, const WindowSwitcherCandidate& b) { if (a.workspaceKey != b.workspaceKey) { return a.workspaceKey < b.workspaceKey; @@ -594,9 +617,9 @@ void WindowSwitcher::show(wl_output* output) { } const bool wasActive = m_active; + m_output = output; refreshWindows(); - m_output = output; if (wasActive) { cycleSelection(1); } else { @@ -641,7 +664,13 @@ void WindowSwitcher::refreshWindows() { IconResolver iconResolver; const int iconSize = 96; - buildWindowEntries(*m_platform, iconResolver, iconSize, m_windows, m_platform->focusedCompositorWindowId()); + const std::optional activeWorkspaceKey = + (m_config != nullptr && m_config->config().windowSwitcher.perWorkspace) + ? activeWorkspaceKeyFor(*m_platform, m_output) + : std::nullopt; + buildWindowEntries( + *m_platform, iconResolver, iconSize, m_windows, m_platform->focusedCompositorWindowId(), activeWorkspaceKey + ); if (selectedKey.has_value()) { for (std::size_t i = 0; i < m_windows.size(); ++i) {