File feature-virt-desk-per-scr.patch of Package kwin6

From 4079dd29c87be375547fab8546c0ffb505737d88 Mon Sep 17 00:00:00 2001
From: Hynek Schlindenbuch <schlindenbuch.h@seznam.cz>
Date: Tue, 18 Nov 2025 10:40:09 +0100
Subject: [PATCH 01/63] virtualdesktops: Add per-output desktops

VirtualDesktopManager now tracks the current virtual desktop separately
for each output. By default it switches the desktop on all outputs
together, but there is now an option to enable switching desktops
separately for each output.

CCBUG: 107302
---
 src/activation.cpp                            |   6 +-
 src/activities.h                              |   1 +
 src/dbusinterface.cpp                         |   4 +-
 src/effect/effecthandler.cpp                  |   1 +
 src/keyboard_layout_switching.cpp             |   1 +
 src/layers.cpp                                |   2 +-
 src/placement.cpp                             |  12 +-
 .../windowsrunnerinterface.cpp                |   2 +-
 src/scripting/desktopbackgrounditem.cpp       |   2 +-
 src/scripting/workspace_wrapper.cpp           |   1 +
 src/sm.cpp                                    |   1 +
 src/tabbox/tabbox.cpp                         |   2 +-
 src/tiles/tile.cpp                            |   2 +-
 src/tiles/tilemanager.cpp                     |   6 +-
 src/useractions.cpp                           |   4 +-
 src/virtualdesktops.cpp                       | 109 ++++++++++++------
 src/virtualdesktops.h                         |  12 +-
 src/window.cpp                                |  10 +-
 src/workspace.cpp                             |  10 +-
 src/x11window.cpp                             |   4 +-
 20 files changed, 117 insertions(+), 75 deletions(-)

diff --git a/src/activation.cpp b/src/activation.cpp
index 1598ddd37eb..8da3d8a37b6 100644
--- a/src/activation.cpp
+++ b/src/activation.cpp
@@ -305,7 +305,7 @@ void Workspace::activateWindow(Window *window, bool force)
             VirtualDesktopManager::self()->setCurrent(window->desktops().constLast());
             break;
         case Options::ActivationDesktopPolicy::BringToCurrentDesktop:
-            window->enterDesktop(VirtualDesktopManager::self()->currentDesktop());
+            window->enterDesktop(VirtualDesktopManager::self()->currentDesktop(window->output()));
             break;
         case Options::ActivationDesktopPolicy::DoNothing:
             break;
@@ -433,8 +433,8 @@ void Workspace::activateNextWindow(Window *window)
     Window *focusCandidate = nullptr;
 
     if (options->focusPolicyIsReasonable()) {
-        VirtualDesktop *desktop = VirtualDesktopManager::self()->currentDesktop();
         LogicalOutput *output = window ? window->output() : workspace()->activeOutput();
+        VirtualDesktop *desktop = VirtualDesktopManager::self()->currentDesktop(output);
 
         if (!focusCandidate && showingDesktop()) {
             focusCandidate = findDesktop(desktop, output); // to not break the state
@@ -483,7 +483,7 @@ void Workspace::switchToOutput(LogicalOutput *output)
         return;
     }
     closeActivePopup();
-    VirtualDesktop *desktop = VirtualDesktopManager::self()->currentDesktop();
+    VirtualDesktop *desktop = VirtualDesktopManager::self()->currentDesktop(output);
     Window *get_focus = m_focusChain->getForActivation(desktop, output);
     if (get_focus == nullptr) {
         get_focus = findDesktop(desktop, output);
diff --git a/src/activities.h b/src/activities.h
index cdaeda7e678..d866e09bc54 100644
--- a/src/activities.h
+++ b/src/activities.h
@@ -88,6 +88,7 @@ private:
     QString m_previous;
     QString m_current;
     KActivities::Controller *m_controller;
+    // TODO: VD: store the last virtual desktop separately for each output
     std::unordered_map<QString, QString> m_lastVirtualDesktop;
     KSharedConfig::Ptr m_config;
 };
diff --git a/src/dbusinterface.cpp b/src/dbusinterface.cpp
index 177316459a6..7d1761d5de8 100644
--- a/src/dbusinterface.cpp
+++ b/src/dbusinterface.cpp
@@ -298,8 +298,8 @@ VirtualDesktopManagerDBusInterface::VirtualDesktopManagerDBusInterface(VirtualDe
                                                  QStringLiteral("org.kde.KWin.VirtualDesktopManager"),
                                                  this);
 
-    connect(m_manager, &VirtualDesktopManager::currentChanged, this, [this]() {
-        Q_EMIT currentChanged(m_manager->currentDesktop()->id());
+    connect(m_manager, &VirtualDesktopManager::currentChanged, this, [this](VirtualDesktop *oldDesktop, VirtualDesktop *newDesktop) {
+        Q_EMIT currentChanged(newDesktop->id());
     });
 
     connect(m_manager, &VirtualDesktopManager::countChanged, this, [this](uint previousCount, uint newCount) {
diff --git a/src/effect/effecthandler.cpp b/src/effect/effecthandler.cpp
index 865500cd349..125a5023728 100644
--- a/src/effect/effecthandler.cpp
+++ b/src/effect/effecthandler.cpp
@@ -836,6 +836,7 @@ QString EffectsHandler::currentActivity() const
 #endif
 }
 
+// TODO: VD: Output?
 VirtualDesktop *EffectsHandler::currentDesktop() const
 {
     return VirtualDesktopManager::self()->currentDesktop();
diff --git a/src/keyboard_layout_switching.cpp b/src/keyboard_layout_switching.cpp
index 90128cf84b3..16472c3d497 100644
--- a/src/keyboard_layout_switching.cpp
+++ b/src/keyboard_layout_switching.cpp
@@ -149,6 +149,7 @@ quint32 getLayout(const T &layouts, const U &reference)
 
 void VirtualDesktopPolicy::desktopChanged()
 {
+    // TODO: VD: Fix this.
     auto d = VirtualDesktopManager::self()->currentDesktop();
     if (!d) {
         return;
diff --git a/src/layers.cpp b/src/layers.cpp
index 29f40865b87..e2e5b586e3b 100644
--- a/src/layers.cpp
+++ b/src/layers.cpp
@@ -311,7 +311,7 @@ void Workspace::raiseOrLowerWindow(Window *window)
         return;
     }
 
-    VirtualDesktop *desktop = VirtualDesktopManager::self()->currentDesktop();
+    VirtualDesktop *desktop = VirtualDesktopManager::self()->currentDesktop(window->output());
     LogicalOutput *output = options->isSeparateScreenFocus() ? window->output() : nullptr;
     Layer layer = computeLayer(window);
 
diff --git a/src/placement.cpp b/src/placement.cpp
index 5074285437b..21434ad76df 100644
--- a/src/placement.cpp
+++ b/src/placement.cpp
@@ -170,7 +170,7 @@ std::optional<PlacementCommand> Placement::placeSmart(const Window *window, cons
     long int overlap, min_overlap = 0;
     int x_optimal, y_optimal;
     int possible;
-    VirtualDesktop *const desktop = window->isOnCurrentDesktop() ? VirtualDesktopManager::self()->currentDesktop() : window->desktops().front();
+    VirtualDesktop *const desktop = window->isOnCurrentDesktop() ? VirtualDesktopManager::self()->currentDesktop(window->output()) : window->desktops().front();
 
     int cxl, cxr, cyt, cyb; // temp coords
     int xl, xr, yt, yb; // temp coords
@@ -533,7 +533,7 @@ RectF Placement::cascadeIfCovering(const Window *window, const RectF &geometry,
     const QPointF offset = workspace()->cascadeOffset(area);
     Q_ASSERT(!offset.isNull());
 
-    VirtualDesktop *const desktop = window->isOnCurrentDesktop() ? VirtualDesktopManager::self()->currentDesktop() : window->desktops().front();
+    VirtualDesktop *const desktop = window->isOnCurrentDesktop() ? VirtualDesktopManager::self()->currentDesktop(window->output()) : window->desktops().front();
 
     RectF possibleGeo = geometry;
     bool noOverlap = false;
@@ -801,7 +801,7 @@ qreal Workspace::packPositionLeft(const Window *window, qreal oldX, bool leftEdg
     if (oldX <= newX) {
         return oldX;
     }
-    VirtualDesktop *const desktop = window->isOnCurrentDesktop() ? VirtualDesktopManager::self()->currentDesktop() : window->desktops().front();
+    VirtualDesktop *const desktop = window->isOnCurrentDesktop() ? VirtualDesktopManager::self()->currentDesktop(window->output()) : window->desktops().front();
     for (auto it = m_windows.constBegin(), end = m_windows.constEnd(); it != end; ++it) {
         if (isIrrelevant(*it, window, desktop)) {
             continue;
@@ -828,7 +828,7 @@ qreal Workspace::packPositionRight(const Window *window, qreal oldX, bool rightE
     if (oldX >= newX) {
         return oldX;
     }
-    VirtualDesktop *const desktop = window->isOnCurrentDesktop() ? VirtualDesktopManager::self()->currentDesktop() : window->desktops().front();
+    VirtualDesktop *const desktop = window->isOnCurrentDesktop() ? VirtualDesktopManager::self()->currentDesktop(window->output()) : window->desktops().front();
     for (auto it = m_windows.constBegin(), end = m_windows.constEnd(); it != end; ++it) {
         if (isIrrelevant(*it, window, desktop)) {
             continue;
@@ -856,7 +856,7 @@ qreal Workspace::packPositionUp(const Window *window, qreal oldY, bool topEdge)
     if (oldY <= newY) {
         return oldY;
     }
-    VirtualDesktop *const desktop = window->isOnCurrentDesktop() ? VirtualDesktopManager::self()->currentDesktop() : window->desktops().front();
+    VirtualDesktop *const desktop = window->isOnCurrentDesktop() ? VirtualDesktopManager::self()->currentDesktop(window->output()) : window->desktops().front();
     for (auto it = m_windows.constBegin(), end = m_windows.constEnd(); it != end; ++it) {
         if (isIrrelevant(*it, window, desktop)) {
             continue;
@@ -883,7 +883,7 @@ qreal Workspace::packPositionDown(const Window *window, qreal oldY, bool bottomE
     if (oldY >= newY) {
         return oldY;
     }
-    VirtualDesktop *const desktop = window->isOnCurrentDesktop() ? VirtualDesktopManager::self()->currentDesktop() : window->desktops().front();
+    VirtualDesktop *const desktop = window->isOnCurrentDesktop() ? VirtualDesktopManager::self()->currentDesktop(window->output()) : window->desktops().front();
     for (auto it = m_windows.constBegin(), end = m_windows.constEnd(); it != end; ++it) {
         if (isIrrelevant(*it, window, desktop)) {
             continue;
diff --git a/src/plugins/krunner-integration/windowsrunnerinterface.cpp b/src/plugins/krunner-integration/windowsrunnerinterface.cpp
index cb030605079..c616a29b126 100644
--- a/src/plugins/krunner-integration/windowsrunnerinterface.cpp
+++ b/src/plugins/krunner-integration/windowsrunnerinterface.cpp
@@ -272,7 +272,7 @@ RemoteMatch WindowsRunner::windowsMatch(const Window *window, const WindowsRunne
     const QList<VirtualDesktop *> desktops = window->desktops();
     bool allDesktops = window->isOnAllDesktops();
 
-    const VirtualDesktop *targetDesktop = VirtualDesktopManager::self()->currentDesktop();
+    const VirtualDesktop *targetDesktop = VirtualDesktopManager::self()->currentDesktop(window->output());
     // Show on current desktop unless window is only attached to other desktop, in this case show on the first attached desktop
     if (!allDesktops && !window->isOnCurrentDesktop() && !desktops.isEmpty()) {
         targetDesktop = desktops.first();
diff --git a/src/scripting/desktopbackgrounditem.cpp b/src/scripting/desktopbackgrounditem.cpp
index f0a876bba1e..c774b420cf9 100644
--- a/src/scripting/desktopbackgrounditem.cpp
+++ b/src/scripting/desktopbackgrounditem.cpp
@@ -95,7 +95,7 @@ void DesktopBackgroundItem::updateWindow()
 
     VirtualDesktop *desktop = m_desktop;
     if (!desktop) {
-        desktop = VirtualDesktopManager::self()->currentDesktop();
+        desktop = VirtualDesktopManager::self()->currentDesktop(m_output);
     }
 
     QString activity = m_activity;
diff --git a/src/scripting/workspace_wrapper.cpp b/src/scripting/workspace_wrapper.cpp
index 9277012e010..dcab5d5647b 100644
--- a/src/scripting/workspace_wrapper.cpp
+++ b/src/scripting/workspace_wrapper.cpp
@@ -60,6 +60,7 @@ WorkspaceWrapper::WorkspaceWrapper(QObject *parent)
     connect(Cursors::self()->mouse(), &Cursor::posChanged, this, &WorkspaceWrapper::cursorPosChanged);
 }
 
+// TODO: VD: Output?
 VirtualDesktop *WorkspaceWrapper::currentDesktop() const
 {
     return VirtualDesktopManager::self()->currentDesktop();
diff --git a/src/sm.cpp b/src/sm.cpp
index 04778cc7301..cd31b173fe5 100644
--- a/src/sm.cpp
+++ b/src/sm.cpp
@@ -134,6 +134,7 @@ void SessionManager::storeSession(const QString &sessionName, SMSavePhase phase)
         // but both Qt and KDE treat phase1 and phase2 separately,
         // which results in different sessionkey and different config file :(
         m_sessionActiveClient = active_client;
+        // TODO: VD: Store VD for all outputs
         m_sessionDesktop = VirtualDesktopManager::self()->current();
     } else if (phase == SMSavePhase2) {
         cg.writeEntry("count", count);
diff --git a/src/tabbox/tabbox.cpp b/src/tabbox/tabbox.cpp
index 81c2ddf7da0..ec5e78857fa 100644
--- a/src/tabbox/tabbox.cpp
+++ b/src/tabbox/tabbox.cpp
@@ -60,7 +60,7 @@ QString TabBoxHandlerImpl::desktopName(Window *client) const
     if (!client->isOnAllDesktops()) {
         return client->desktops().last()->name();
     }
-    return VirtualDesktopManager::self()->currentDesktop()->name();
+    return VirtualDesktopManager::self()->currentDesktop(client->output())->name();
 }
 
 Window *TabBoxHandlerImpl::nextClientFocusChain(Window *client) const
diff --git a/src/tiles/tile.cpp b/src/tiles/tile.cpp
index a94e2005cef..18276f7fbf9 100644
--- a/src/tiles/tile.cpp
+++ b/src/tiles/tile.cpp
@@ -73,7 +73,7 @@ VirtualDesktop *Tile::desktop() const
 
 bool Tile::isActive() const
 {
-    return m_desktop == VirtualDesktopManager::self()->currentDesktop();
+    return m_desktop == VirtualDesktopManager::self()->currentDesktop(m_tiling->output());
 }
 
 bool Tile::supportsResizeGravity(Gravity gravity)
diff --git a/src/tiles/tilemanager.cpp b/src/tiles/tilemanager.cpp
index a3975256250..f1506736adf 100644
--- a/src/tiles/tilemanager.cpp
+++ b/src/tiles/tilemanager.cpp
@@ -29,7 +29,7 @@ namespace KWin
 QDebug operator<<(QDebug debug, const TileManager *tileManager)
 {
     if (tileManager) {
-        QList<Tile *> tiles({tileManager->rootTile(VirtualDesktopManager::self()->currentDesktop())});
+        QList<Tile *> tiles({tileManager->rootTile(VirtualDesktopManager::self()->currentDesktop(tileManager->output()))});
         QList<Tile *> tilePath;
         QString indent(QStringLiteral("|-"));
         debug << tileManager->metaObject()->className() << '(' << static_cast<const void *>(tileManager) << ')' << '\n';
@@ -119,12 +119,12 @@ RootTile *TileManager::rootTile(VirtualDesktop *desktop) const
 
 RootTile *TileManager::rootTile() const
 {
-    return m_rootTiles.value(VirtualDesktopManager::self()->currentDesktop());
+    return m_rootTiles.value(VirtualDesktopManager::self()->currentDesktop(m_output));
 }
 
 QuickRootTile *TileManager::quickRootTile() const
 {
-    return m_quickRootTiles.value(VirtualDesktopManager::self()->currentDesktop());
+    return m_quickRootTiles.value(VirtualDesktopManager::self()->currentDesktop(m_output));
 }
 
 QuickRootTile *TileManager::quickRootTile(VirtualDesktop *desktop) const
diff --git a/src/useractions.cpp b/src/useractions.cpp
index db9a1c4bca0..ba61f7accf9 100644
--- a/src/useractions.cpp
+++ b/src/useractions.cpp
@@ -1328,7 +1328,7 @@ void Workspace::slotWindowLower()
                     requestFocus(next, false);
                 }
             } else {
-                activateWindow(topWindowOnDesktop(VirtualDesktopManager::self()->currentDesktop()));
+                activateWindow(topWindowOnDesktop(VirtualDesktopManager::self()->currentDesktop(m_activeWindow->output())));
             }
         }
     }
@@ -1519,7 +1519,7 @@ void Workspace::switchWindow(Direction direction)
         return;
     }
     Window *window = m_activeWindow;
-    VirtualDesktop *desktop = VirtualDesktopManager::self()->currentDesktop();
+    VirtualDesktop *desktop = VirtualDesktopManager::self()->currentDesktop(m_activeWindow->output());
 
     // Center of the active window
     QPoint curPos(window->x() + window->width() / 2, window->y() + window->height() / 2);
diff --git a/src/virtualdesktops.cpp b/src/virtualdesktops.cpp
index ed8eead19c1..f5ccd323eb2 100644
--- a/src/virtualdesktops.cpp
+++ b/src/virtualdesktops.cpp
@@ -10,6 +10,7 @@
 #include "virtualdesktops.h"
 #include "input.h"
 #include "wayland/plasmavirtualdesktop.h"
+#include "workspace.h"
 // KDE
 #include <KConfigGroup>
 #include <KGlobalAccel>
@@ -103,6 +104,7 @@ void VirtualDesktopManager::setVirtualDesktopManagement(PlasmaVirtualDesktopMana
         const QList<PlasmaVirtualDesktopInterface *> deskIfaces = m_virtualDesktopManagement->desktops();
         for (auto *deskInt : deskIfaces) {
             if (deskInt->id() == currentDesktop()->id()) {
+                // TODO: VD: Figure out how to replace the bool
                 deskInt->setActive(true);
             } else {
                 deskInt->setActive(false);
@@ -209,6 +211,8 @@ KWIN_SINGLETON_FACTORY_VARIABLE(VirtualDesktopManager, s_manager)
 VirtualDesktopManager::VirtualDesktopManager(QObject *parent)
     : QObject(parent)
     , m_navigationWrapsAround(false)
+    // TODO: VD: Change this back to false and add config
+    , m_perOutputVirtualDesktops(true)
 #if KWIN_BUILD_X11
     , m_rootInfo(nullptr)
 #endif
@@ -231,6 +235,7 @@ void VirtualDesktopManager::setRootInfo(NETRootInfo *info)
     if (m_rootInfo) {
         if (RootInfo::desktopEnabled()) {
             updateRootInfo();
+            // TODO: VD: Figure out what to do with rootinfo
             m_rootInfo->setCurrentDesktop(currentDesktop()->x11DesktopNumber());
             for (auto *vd : std::as_const(m_desktops)) {
                 m_rootInfo->setDesktopName(vd->x11DesktopNumber(), vd->name().toUtf8().data());
@@ -277,9 +282,8 @@ void VirtualDesktopManager::moveTo(Direction direction, bool wrap)
 
 VirtualDesktop *VirtualDesktopManager::above(VirtualDesktop *desktop, bool wrap) const
 {
-    Q_ASSERT(m_current);
     if (!desktop) {
-        desktop = m_current;
+        desktop = currentDesktop();
     }
     QPoint coords = m_grid.gridCoords(desktop);
     Q_ASSERT(coords.x() >= 0);
@@ -301,9 +305,8 @@ VirtualDesktop *VirtualDesktopManager::above(VirtualDesktop *desktop, bool wrap)
 
 VirtualDesktop *VirtualDesktopManager::toRight(VirtualDesktop *desktop, bool wrap) const
 {
-    Q_ASSERT(m_current);
     if (!desktop) {
-        desktop = m_current;
+        desktop = currentDesktop();
     }
     QPoint coords = m_grid.gridCoords(desktop);
     Q_ASSERT(coords.x() >= 0);
@@ -325,9 +328,8 @@ VirtualDesktop *VirtualDesktopManager::toRight(VirtualDesktop *desktop, bool wra
 
 VirtualDesktop *VirtualDesktopManager::below(VirtualDesktop *desktop, bool wrap) const
 {
-    Q_ASSERT(m_current);
     if (!desktop) {
-        desktop = m_current;
+        desktop = currentDesktop();
     }
     QPoint coords = m_grid.gridCoords(desktop);
     Q_ASSERT(coords.x() >= 0);
@@ -350,9 +352,8 @@ VirtualDesktop *VirtualDesktopManager::below(VirtualDesktop *desktop, bool wrap)
 
 VirtualDesktop *VirtualDesktopManager::toLeft(VirtualDesktop *desktop, bool wrap) const
 {
-    Q_ASSERT(m_current);
     if (!desktop) {
-        desktop = m_current;
+        desktop = currentDesktop();
     }
     QPoint coords = m_grid.gridCoords(desktop);
     Q_ASSERT(coords.x() >= 0);
@@ -374,9 +375,8 @@ VirtualDesktop *VirtualDesktopManager::toLeft(VirtualDesktop *desktop, bool wrap
 
 VirtualDesktop *VirtualDesktopManager::next(VirtualDesktop *desktop, bool wrap) const
 {
-    Q_ASSERT(m_current);
     if (!desktop) {
-        desktop = m_current;
+        desktop = currentDesktop();
     }
     auto it = std::find(m_desktops.begin(), m_desktops.end(), desktop);
     Q_ASSERT(it != m_desktops.end());
@@ -393,9 +393,8 @@ VirtualDesktop *VirtualDesktopManager::next(VirtualDesktop *desktop, bool wrap)
 
 VirtualDesktop *VirtualDesktopManager::previous(VirtualDesktop *desktop, bool wrap) const
 {
-    Q_ASSERT(m_current);
     if (!desktop) {
-        desktop = m_current;
+        desktop = currentDesktop();
     }
     auto it = std::find(m_desktops.begin(), m_desktops.end(), desktop);
     Q_ASSERT(it != m_desktops.end());
@@ -519,9 +518,15 @@ void VirtualDesktopManager::removeVirtualDesktop(VirtualDesktop *desktop)
 #endif
     }
 
-    if (m_current == desktop) {
-        m_current = (i < m_desktops.count()) ? m_desktops.at(i) : m_desktops.constLast();
-        Q_EMIT currentChanged(desktop, m_current);
+    VirtualDesktop *newDesktop = (i < m_desktops.count()) ? m_desktops.at(i) : m_desktops.constLast();
+
+    for (auto [key, value] : m_currentDesktops.asKeyValueRange()) {
+        if (value != desktop) {
+            continue;
+        }
+        value = newDesktop;
+        // TODO: VD: Emit per-output event
+        Q_EMIT currentChanged(desktop, newDesktop);
     }
 
     updateLayout();
@@ -570,12 +575,19 @@ void VirtualDesktopManager::moveVirtualDesktop(VirtualDesktop *desktop, int posi
 
 uint VirtualDesktopManager::current() const
 {
-    return m_current ? m_current->x11DesktopNumber() : 0;
+    VirtualDesktop *d = currentDesktop();
+    return d ? d->x11DesktopNumber() : 0;
 }
 
-VirtualDesktop *VirtualDesktopManager::currentDesktop() const
+// TODO: VD: Go through all usages and make sure that they're OK.
+VirtualDesktop *VirtualDesktopManager::currentDesktop(Output *output) const
 {
-    return m_current;
+    if (!output) {
+        output = workspace()->activeOutput();
+    }
+    VirtualDesktop *result = m_currentDesktops[output];
+    // TODO: VD: Should it fallback to 0?
+    return result ? result : m_desktops.at(0);
 }
 
 bool VirtualDesktopManager::setCurrent(uint newDesktop)
@@ -583,20 +595,35 @@ bool VirtualDesktopManager::setCurrent(uint newDesktop)
     if (newDesktop < 1 || newDesktop > count()) {
         return false;
     }
-    auto d = desktopForX11Id(newDesktop);
+    VirtualDesktop *d = desktopForX11Id(newDesktop);
     Q_ASSERT(d);
     return setCurrent(d);
 }
 
+// TODO: VD: Go through all usages and make sure that they're OK.
 bool VirtualDesktopManager::setCurrent(VirtualDesktop *newDesktop)
 {
     Q_ASSERT(newDesktop);
-    if (m_current == newDesktop) {
+    Output *output = workspace()->activeOutput();
+    VirtualDesktop *oldDesktop = m_currentDesktops[output];
+    if (oldDesktop == newDesktop) {
         return false;
     }
-    VirtualDesktop *oldDesktop = currentDesktop();
-    m_current = newDesktop;
-    Q_EMIT currentChanged(oldDesktop, newDesktop);
+    if (m_perOutputVirtualDesktops) {
+        m_currentDesktops[output] = newDesktop;
+        // TODO: VD: Emit per-output event
+        Q_EMIT currentChanged(oldDesktop, newDesktop);
+        return true;
+    }
+    for (auto [key, value] : m_currentDesktops.asKeyValueRange()) {
+        if (value == newDesktop) {
+            continue;
+        }
+        auto oldDesktop = value;
+        value = newDesktop;
+        // TODO: VD: Emit per-output event
+        Q_EMIT currentChanged(oldDesktop, newDesktop);
+    }
     return true;
 }
 
@@ -613,10 +640,14 @@ void VirtualDesktopManager::setCount(uint count)
     if ((uint)m_desktops.count() > count) {
         const auto desktopsToRemove = m_desktops.mid(count);
         m_desktops.resize(count);
-        if (m_current && desktopsToRemove.contains(m_current)) {
-            VirtualDesktop *oldCurrent = m_current;
-            m_current = m_desktops.last();
-            Q_EMIT currentChanged(oldCurrent, m_current);
+        for (auto [key, value] : m_currentDesktops.asKeyValueRange()) {
+            if (!desktopsToRemove.contains(value)) {
+                continue;
+            }
+            VirtualDesktop *oldDesktop = value;
+            value = m_desktops.last();
+            // TODO: VD: Emit per-output event
+            Q_EMIT currentChanged(oldDesktop, value);
         }
         for (auto desktop : desktopsToRemove) {
             Q_EMIT desktopRemoved(desktop);
@@ -648,8 +679,10 @@ void VirtualDesktopManager::setCount(uint count)
         }
     }
 
-    if (!m_current) {
-        m_current = m_desktops.at(0);
+    for (auto output : workspace()->outputs()) {
+        if (!m_currentDesktops.contains(output)) {
+            m_currentDesktops.insert(output, m_desktops.at(0));
+        }
     }
 
     updateLayout();
@@ -866,17 +899,18 @@ void VirtualDesktopManager::initShortcuts()
 
 void VirtualDesktopManager::gestureReleasedY()
 {
+    auto current = currentDesktop();
     // Note that if desktop wrapping is disabled and there's no desktop above or below,
     // above() and below() will return the current desktop.
-    VirtualDesktop *target = m_current;
+    VirtualDesktop *target = current;
     if (m_currentDesktopOffset.y() <= -GESTURE_SWITCH_THRESHOLD) {
-        target = above(m_current, isNavigationWrappingAround());
+        target = above(current, isNavigationWrappingAround());
     } else if (m_currentDesktopOffset.y() >= GESTURE_SWITCH_THRESHOLD) {
-        target = below(m_current, isNavigationWrappingAround());
+        target = below(current, isNavigationWrappingAround());
     }
 
     // If the current desktop has not changed, consider that the gesture has been canceled.
-    if (m_current != target) {
+    if (current != target) {
         setCurrent(target);
     } else {
         Q_EMIT currentChangingCancelled();
@@ -888,15 +922,16 @@ void VirtualDesktopManager::gestureReleasedX()
 {
     // Note that if desktop wrapping is disabled and there's no desktop to left or right,
     // toLeft() and toRight() will return the current desktop.
-    VirtualDesktop *target = m_current;
+    VirtualDesktop *current = currentDesktop();
+    VirtualDesktop *target = current;
     if (m_currentDesktopOffset.x() <= -GESTURE_SWITCH_THRESHOLD) {
-        target = toLeft(m_current, isNavigationWrappingAround());
+        target = toLeft(current, isNavigationWrappingAround());
     } else if (m_currentDesktopOffset.x() >= GESTURE_SWITCH_THRESHOLD) {
-        target = toRight(m_current, isNavigationWrappingAround());
+        target = toRight(current, isNavigationWrappingAround());
     }
 
     // If the current desktop has not changed, consider that the gesture has been canceled.
-    if (m_current != target) {
+    if (current != target) {
         setCurrent(target);
     } else {
         Q_EMIT currentChangingCancelled();
diff --git a/src/virtualdesktops.h b/src/virtualdesktops.h
index 73689827f5a..6945db11820 100644
--- a/src/virtualdesktops.h
+++ b/src/virtualdesktops.h
@@ -8,6 +8,7 @@
 */
 #pragma once
 // KWin
+#include "core/output.h"
 #include "effect/globals.h"
 #include <kwin_export.h>
 // Qt includes
@@ -143,7 +144,7 @@ class KWIN_EXPORT VirtualDesktopManager : public QObject
     Q_PROPERTY(uint count READ count WRITE setCount NOTIFY countChanged)
 
     /**
-     * The id of the virtual desktop which is currently in use.
+     * The id of the virtual desktop which is currently in use by the active output.
      */
     Q_PROPERTY(uint current READ current WRITE setCurrent NOTIFY currentChanged)
 
@@ -184,18 +185,18 @@ public:
     uint rows() const;
 
     /**
-     * @returns The ID of the current desktop.
+     * @returns The ID of the current desktop on the active output.
      * @see setCurrent
      * @see currentChanged
      */
     uint current() const;
 
     /**
-     * @returns The current desktop
+     * @returns The current desktop on the active output.
      * @see setCurrent
      * @see currentChanged
      */
-    VirtualDesktop *currentDesktop() const;
+    VirtualDesktop *currentDesktop(Output *output = nullptr) const;
 
     /**
      * Moves to the desktop through the algorithm described by Direction.
@@ -529,9 +530,10 @@ private:
     QAction *addAction(const QString &name, const QString &label, const QKeySequence &key, void (VirtualDesktopManager::*slot)());
 
     QList<VirtualDesktop *> m_desktops;
-    QPointer<VirtualDesktop> m_current;
+    QHash<Output *, VirtualDesktop *> m_currentDesktops;
     quint32 m_rows = 2;
     bool m_navigationWrapsAround;
+    bool m_perOutputVirtualDesktops;
     VirtualDesktopGrid m_grid;
     // TODO: QPointer
 #if KWIN_BUILD_X11
diff --git a/src/window.cpp b/src/window.cpp
index 7ee0ebe4030..8cb0081a08d 100644
--- a/src/window.cpp
+++ b/src/window.cpp
@@ -672,7 +672,7 @@ void Window::autoRaise()
 bool Window::isMostRecentlyRaised() const
 {
     // The last window in the unconstrained stacking order is the most recently raised one.
-    return workspace()->topWindowOnDesktop(VirtualDesktopManager::self()->currentDesktop(), nullptr, true, false) == this;
+    return workspace()->topWindowOnDesktop(VirtualDesktopManager::self()->currentDesktop(output()), nullptr, true, false) == this;
 }
 
 bool Window::wantsTabFocus() const
@@ -798,7 +798,7 @@ void Window::setOnAllDesktops(bool b)
     if (b) {
         setDesktops({});
     } else {
-        setDesktops({VirtualDesktopManager::self()->currentDesktop()});
+        setDesktops({VirtualDesktopManager::self()->currentDesktop(output())});
     }
 }
 
@@ -827,7 +827,7 @@ bool Window::isOnDesktop(VirtualDesktop *desktop) const
 
 bool Window::isOnCurrentDesktop() const
 {
-    return isOnDesktop(VirtualDesktopManager::self()->currentDesktop());
+    return isOnDesktop(VirtualDesktopManager::self()->currentDesktop(output()));
 }
 
 Qt::Edge Window::titlebarPosition() const
@@ -2871,7 +2871,7 @@ void Window::pointerEnterEvent(const QPointF &globalPos)
         return;
     }
 
-    if (options->isAutoRaise() && !isDesktop() && !isDock() && workspace()->focusChangeEnabled() && globalPos != workspace()->focusMousePosition() && workspace()->topWindowOnDesktop(VirtualDesktopManager::self()->currentDesktop(), options->isSeparateScreenFocus() ? output() : nullptr) != this) {
+    if (options->isAutoRaise() && !isDesktop() && !isDock() && workspace()->focusChangeEnabled() && globalPos != workspace()->focusMousePosition() && workspace()->topWindowOnDesktop(VirtualDesktopManager::self()->currentDesktop(output()), options->isSeparateScreenFocus() ? output() : nullptr) != this) {
         startAutoRaise();
     }
 
@@ -3973,7 +3973,7 @@ void Window::checkWorkspacePosition(RectF oldGeometry, const VirtualDesktop *old
         oldGeometry = newGeom;
     }
 
-    VirtualDesktop *desktop = !isOnCurrentDesktop() ? desktops().constLast() : VirtualDesktopManager::self()->currentDesktop();
+    VirtualDesktop *desktop = !isOnCurrentDesktop() ? desktops().constLast() : VirtualDesktopManager::self()->currentDesktop(output());
     if (!oldDesktop) {
         oldDesktop = desktop;
     }
diff --git a/src/workspace.cpp b/src/workspace.cpp
index dcddad54969..d5301926065 100644
--- a/src/workspace.cpp
+++ b/src/workspace.cpp
@@ -199,8 +199,8 @@ void Workspace::init()
 
     connect(this, &Workspace::windowRemoved, m_focusChain.get(), &FocusChain::remove);
     connect(this, &Workspace::windowActivated, m_focusChain.get(), &FocusChain::setActiveWindow);
-    connect(VirtualDesktopManager::self(), &VirtualDesktopManager::currentChanged, m_focusChain.get(), [this]() {
-        m_focusChain->setCurrentDesktop(VirtualDesktopManager::self()->currentDesktop());
+    connect(VirtualDesktopManager::self(), &VirtualDesktopManager::currentChanged, m_focusChain.get(), [this](VirtualDesktop *oldDesktop, VirtualDesktop *newDesktop) {
+        m_focusChain->setCurrentDesktop(newDesktop);
     });
     connect(options, &Options::separateScreenFocusChanged, m_focusChain.get(), &FocusChain::setSeparateScreenFocus);
     m_focusChain->setSeparateScreenFocus(options->isSeparateScreenFocus());
@@ -1602,7 +1602,7 @@ void Workspace::sendWindowToDesktops(Window *window, const QList<VirtualDesktop
         raiseWindow(window);
         // but set a new active window on the current desktop
         if (wasActive) {
-            activateWindowOnDesktop(VirtualDesktopManager::self()->currentDesktop());
+            activateWindowOnDesktop(VirtualDesktopManager::self()->currentDesktop(window->output()));
         }
     }
 
@@ -2483,7 +2483,7 @@ RectF Workspace::clientArea(clientAreaOption opt, const Window *window, const Lo
 {
     const VirtualDesktop *desktop;
     if (window->isOnCurrentDesktop()) {
-        desktop = VirtualDesktopManager::self()->currentDesktop();
+        desktop = VirtualDesktopManager::self()->currentDesktop(window->output());
     } else {
         desktop = window->desktops().constLast();
     }
@@ -3099,7 +3099,7 @@ TileManager *Workspace::tileManager(LogicalOutput *output) const
 
 RootTile *Workspace::rootTile(LogicalOutput *output) const
 {
-    return rootTile(output, VirtualDesktopManager::self()->currentDesktop());
+    return rootTile(output, VirtualDesktopManager::self()->currentDesktop(output));
 }
 
 RootTile *Workspace::rootTile(LogicalOutput *output, VirtualDesktop *desktop) const
diff --git a/src/x11window.cpp b/src/x11window.cpp
index 34596786fd7..a6c90196de7 100644
--- a/src/x11window.cpp
+++ b/src/x11window.cpp
@@ -516,7 +516,7 @@ bool X11Window::manage(xcb_window_t w, bool isMapped)
             if (on_all) {
                 initialDesktops = QList<VirtualDesktop *>{};
             } else if (on_current) {
-                initialDesktops = QList<VirtualDesktop *>{VirtualDesktopManager::self()->currentDesktop()};
+                initialDesktops = QList<VirtualDesktop *>{VirtualDesktopManager::self()->currentDesktop(output())};
             } else if (maincl) {
                 initialDesktops = maincl->desktops();
             }
@@ -563,7 +563,7 @@ bool X11Window::manage(xcb_window_t w, bool isMapped)
         if (isDesktop()) {
             initialDesktops = QList<VirtualDesktop *>{};
         } else {
-            initialDesktops = QList<VirtualDesktop *>{VirtualDesktopManager::self()->currentDesktop()};
+            initialDesktops = QList<VirtualDesktop *>{VirtualDesktopManager::self()->currentDesktop(output())};
         }
     }
     setDesktops(rules()->checkDesktops(*initialDesktops, !isMapped));
-- 
GitLab


From 335b1f31c99fa855c036fc41953ef2d35f755156 Mon Sep 17 00:00:00 2001
From: Hynek Schlindenbuch <schlindenbuch.h@seznam.cz>
Date: Wed, 19 Nov 2025 09:41:13 +0100
Subject: [PATCH 02/63] WIP: add output to VDM::currentChanged

---
 src/dbusinterface.cpp               |  6 ++++-
 src/scripting/workspace_wrapper.cpp |  1 +
 src/virtualdesktops.cpp             | 18 +++++++-------
 src/virtualdesktops.h               |  2 +-
 src/workspace.cpp                   | 38 +++++++++++++++++------------
 src/workspace.h                     |  6 ++---
 6 files changed, 41 insertions(+), 30 deletions(-)

diff --git a/src/dbusinterface.cpp b/src/dbusinterface.cpp
index 7d1761d5de8..6cf68277bf4 100644
--- a/src/dbusinterface.cpp
+++ b/src/dbusinterface.cpp
@@ -298,7 +298,11 @@ VirtualDesktopManagerDBusInterface::VirtualDesktopManagerDBusInterface(VirtualDe
                                                  QStringLiteral("org.kde.KWin.VirtualDesktopManager"),
                                                  this);
 
-    connect(m_manager, &VirtualDesktopManager::currentChanged, this, [this](VirtualDesktop *oldDesktop, VirtualDesktop *newDesktop) {
+    // TODO: VD: What about this?
+    connect(m_manager, &VirtualDesktopManager::currentChanged, this, [this](VirtualDesktop *oldDesktop, VirtualDesktop *newDesktop, Output *output) {
+        if (output != workspace()->activeOutput()) {
+            return;
+        }
         Q_EMIT currentChanged(newDesktop->id());
     });
 
diff --git a/src/scripting/workspace_wrapper.cpp b/src/scripting/workspace_wrapper.cpp
index dcab5d5647b..2d793082306 100644
--- a/src/scripting/workspace_wrapper.cpp
+++ b/src/scripting/workspace_wrapper.cpp
@@ -43,6 +43,7 @@ WorkspaceWrapper::WorkspaceWrapper(QObject *parent)
     connect(vds, &VirtualDesktopManager::desktopRemoved, this, &WorkspaceWrapper::desktopsChanged);
     connect(vds, &VirtualDesktopManager::desktopMoved, this, &WorkspaceWrapper::desktopsChanged);
     connect(vds, &VirtualDesktopManager::layoutChanged, this, &WorkspaceWrapper::desktopLayoutChanged);
+    // TODO: VD: What about this?
     connect(vds, &VirtualDesktopManager::currentChanged, this, &WorkspaceWrapper::currentDesktopChanged);
 #if KWIN_BUILD_ACTIVITIES
     if (KWin::Activities *activities = ws->activities()) {
diff --git a/src/virtualdesktops.cpp b/src/virtualdesktops.cpp
index f5ccd323eb2..b81b61088ff 100644
--- a/src/virtualdesktops.cpp
+++ b/src/virtualdesktops.cpp
@@ -100,7 +100,11 @@ void VirtualDesktopManager::setVirtualDesktopManagement(PlasmaVirtualDesktopMana
         removeVirtualDesktop(id);
     });
 
-    connect(this, &VirtualDesktopManager::currentChanged, m_virtualDesktopManagement, [this]() {
+    connect(this, &VirtualDesktopManager::currentChanged, m_virtualDesktopManagement, [this](VirtualDesktop *oldDesktop, VirtualDesktop *newDesktop, Output *output) {
+        // TODO: VD: is this correct?
+        if (output != workspace()->activeOutput()) {
+            return;
+        }
         const QList<PlasmaVirtualDesktopInterface *> deskIfaces = m_virtualDesktopManagement->desktops();
         for (auto *deskInt : deskIfaces) {
             if (deskInt->id() == currentDesktop()->id()) {
@@ -525,8 +529,7 @@ void VirtualDesktopManager::removeVirtualDesktop(VirtualDesktop *desktop)
             continue;
         }
         value = newDesktop;
-        // TODO: VD: Emit per-output event
-        Q_EMIT currentChanged(desktop, newDesktop);
+        Q_EMIT currentChanged(desktop, newDesktop, key);
     }
 
     updateLayout();
@@ -611,8 +614,7 @@ bool VirtualDesktopManager::setCurrent(VirtualDesktop *newDesktop)
     }
     if (m_perOutputVirtualDesktops) {
         m_currentDesktops[output] = newDesktop;
-        // TODO: VD: Emit per-output event
-        Q_EMIT currentChanged(oldDesktop, newDesktop);
+        Q_EMIT currentChanged(oldDesktop, newDesktop, output);
         return true;
     }
     for (auto [key, value] : m_currentDesktops.asKeyValueRange()) {
@@ -621,8 +623,7 @@ bool VirtualDesktopManager::setCurrent(VirtualDesktop *newDesktop)
         }
         auto oldDesktop = value;
         value = newDesktop;
-        // TODO: VD: Emit per-output event
-        Q_EMIT currentChanged(oldDesktop, newDesktop);
+        Q_EMIT currentChanged(oldDesktop, newDesktop, output);
     }
     return true;
 }
@@ -646,8 +647,7 @@ void VirtualDesktopManager::setCount(uint count)
             }
             VirtualDesktop *oldDesktop = value;
             value = m_desktops.last();
-            // TODO: VD: Emit per-output event
-            Q_EMIT currentChanged(oldDesktop, value);
+            Q_EMIT currentChanged(oldDesktop, value, key);
         }
         for (auto desktop : desktopsToRemove) {
             Q_EMIT desktopRemoved(desktop);
diff --git a/src/virtualdesktops.h b/src/virtualdesktops.h
index 6945db11820..7ff1648198f 100644
--- a/src/virtualdesktops.h
+++ b/src/virtualdesktops.h
@@ -423,7 +423,7 @@ Q_SIGNALS:
      * @param previousDesktop The virtual desktop changed from
      * @param newDesktop The virtual desktop changed to
      */
-    void currentChanged(KWin::VirtualDesktop *previousDesktop, KWin::VirtualDesktop *newDesktop);
+    void currentChanged(KWin::VirtualDesktop *previousDesktop, KWin::VirtualDesktop *newDesktop, KWin::Output *output);
 
     /**
      * Signal emitted for realtime desktop switching animations.
diff --git a/src/workspace.cpp b/src/workspace.cpp
index d5301926065..0f8c4461163 100644
--- a/src/workspace.cpp
+++ b/src/workspace.cpp
@@ -200,6 +200,7 @@ void Workspace::init()
     connect(this, &Workspace::windowRemoved, m_focusChain.get(), &FocusChain::remove);
     connect(this, &Workspace::windowActivated, m_focusChain.get(), &FocusChain::setActiveWindow);
     connect(VirtualDesktopManager::self(), &VirtualDesktopManager::currentChanged, m_focusChain.get(), [this](VirtualDesktop *oldDesktop, VirtualDesktop *newDesktop) {
+        // TODO: VD: What about this?
         m_focusChain->setCurrentDesktop(newDesktop);
     });
     connect(options, &Options::separateScreenFocusChanged, m_focusChain.get(), &FocusChain::setSeparateScreenFocus);
@@ -1021,9 +1022,10 @@ void Workspace::slotReconfigure()
     }
 }
 
-void Workspace::slotCurrentDesktopChanged(VirtualDesktop *oldDesktop, VirtualDesktop *newDesktop)
+void Workspace::slotCurrentDesktopChanged(VirtualDesktop *oldDesktop, VirtualDesktop *newDesktop, Output *output)
 {
-    updateWindowVisibilityAndActivateOnDesktopChange(newDesktop);
+    updateWindowVisibilityAndActivateOnDesktopChange(newDesktop, output);
+    // TODO: VD: What about this?
     Q_EMIT currentDesktopChanged(oldDesktop, m_moveResizeWindow);
 }
 
@@ -1038,7 +1040,7 @@ void Workspace::slotCurrentDesktopChangingCancelled()
     Q_EMIT currentDesktopChangingCancelled();
 }
 
-void Workspace::updateWindowVisibilityOnDesktopChange(VirtualDesktop *newDesktop)
+void Workspace::updateWindowVisibilityOnDesktopChange(VirtualDesktop *newDesktop, Output *output)
 {
 #if KWIN_BUILD_X11
     for (auto it = stacking_order.constBegin(); it != stacking_order.constEnd(); ++it) {
@@ -1046,13 +1048,14 @@ void Workspace::updateWindowVisibilityOnDesktopChange(VirtualDesktop *newDesktop
         if (!c) {
             continue;
         }
-        if (!(c->isOnDesktop(newDesktop) && c->isOnCurrentActivity()) && c != m_moveResizeWindow) {
+        if (!(c->isOnDesktop(newDesktop) && c->isOnCurrentActivity() && c->isOnOutput(output)) && c != m_moveResizeWindow) {
             (c)->updateVisibility();
         }
     }
+    // TODO: VD: Is this correct?
     // Now propagate the change, after hiding, before showing
-    if (rootInfo()) {
-        rootInfo()->setCurrentDesktop(VirtualDesktopManager::self()->current());
+    if (rootInfo() && output == m_activeOutput) {
+        rootInfo()->setCurrentDesktop(newDesktop->x11DesktopNumber());
     }
 #endif
 
@@ -1068,7 +1071,7 @@ void Workspace::updateWindowVisibilityOnDesktopChange(VirtualDesktop *newDesktop
         if (!c) {
             continue;
         }
-        if (c->isOnDesktop(newDesktop) && c->isOnCurrentActivity()) {
+        if (c->isOnDesktop(newDesktop) && c->isOnCurrentActivity() && c->isOnOutput(output)) {
             c->updateVisibility();
         }
     }
@@ -1078,31 +1081,32 @@ void Workspace::updateWindowVisibilityOnDesktopChange(VirtualDesktop *newDesktop
     }
 }
 
-void Workspace::updateWindowVisibilityAndActivateOnDesktopChange(VirtualDesktop *newDesktop)
+void Workspace::updateWindowVisibilityAndActivateOnDesktopChange(VirtualDesktop *newDesktop, Output *output)
 {
     closeActivePopup();
     ++block_focus;
     StackingUpdatesBlocker blocker(this);
-    updateWindowVisibilityOnDesktopChange(newDesktop);
+    updateWindowVisibilityOnDesktopChange(newDesktop, output);
     // Restore the focus on this desktop
     --block_focus;
 
     for (Window *window : std::as_const(m_windows)) {
-        if (!window->isOnDesktop(newDesktop)) {
+        if (!window->isOnDesktop(newDesktop) || !window->isOnOutput(output)) {
             continue;
         }
 
         Tile *tile = nullptr;
-        for (const auto &[output, manager] : m_tileManagers) {
-            if (Tile *candidate = manager->tileForWindow(window, newDesktop)) {
-                tile = candidate;
-            }
+        if (TileManager *manager = tileManager(output)) {
+            tile = manager->tileForWindow(window, newDesktop);
         }
 
         window->requestTile(tile);
     }
 
-    activateWindowOnDesktop(newDesktop);
+    // TODO: VD: is this correct?
+    if (output == m_activeOutput) {
+        activateWindowOnDesktop(newDesktop);
+    }
 }
 
 void Workspace::activateWindowOnDesktop(VirtualDesktop *desktop)
@@ -1175,7 +1179,9 @@ void Workspace::updateCurrentActivity(const QString &new_activity)
         return;
     }
 
-    updateWindowVisibilityAndActivateOnDesktopChange(VirtualDesktopManager::self()->currentDesktop());
+    for (Output *output : std::as_const(m_outputs)) {
+        updateWindowVisibilityAndActivateOnDesktopChange(VirtualDesktopManager::self()->currentDesktop(output), output);
+    }
 
     Q_EMIT currentActivityChanged();
 #endif
diff --git a/src/workspace.h b/src/workspace.h
index 6ba2dd11b0b..0ecbd47635c 100644
--- a/src/workspace.h
+++ b/src/workspace.h
@@ -531,7 +531,7 @@ private Q_SLOTS:
     void slotReloadConfig();
     void updateCurrentActivity(const QString &new_activity);
     // virtual desktop handling
-    void slotCurrentDesktopChanged(VirtualDesktop *previousDesktop, VirtualDesktop *newDesktop);
+    void slotCurrentDesktopChanged(VirtualDesktop *previousDesktop, VirtualDesktop *newDesktop, Output *output);
     void slotCurrentDesktopChanging(VirtualDesktop *currentDesktop, QPointF delta);
     void slotCurrentDesktopChangingCancelled();
     void slotDesktopAdded(VirtualDesktop *desktop);
@@ -614,8 +614,8 @@ private:
     //---------------------------------------------------------------------
 
     void closeActivePopup();
-    void updateWindowVisibilityOnDesktopChange(VirtualDesktop *newDesktop);
-    void updateWindowVisibilityAndActivateOnDesktopChange(VirtualDesktop *newDesktop);
+    void updateWindowVisibilityOnDesktopChange(VirtualDesktop *newDesktop, Output *output);
+    void updateWindowVisibilityAndActivateOnDesktopChange(VirtualDesktop *newDesktop, Output *output);
     void activateWindowOnDesktop(VirtualDesktop *desktop);
     Window *findWindowToActivateOnDesktop(VirtualDesktop *desktop);
     void removeWindow(Window *window);
-- 
GitLab


From 28cf316184ad12179d5940d69bd67f56e2b66051 Mon Sep 17 00:00:00 2001
From: Hynek Schlindenbuch <schlindenbuch.h@seznam.cz>
Date: Wed, 19 Nov 2025 10:03:49 +0100
Subject: [PATCH 03/63] make per-desktop keyboard layout work with per-output
 desktops

---
 src/keyboard_layout_switching.cpp | 1 +
 src/workspace.cpp                 | 4 ++++
 src/workspace.h                   | 1 +
 3 files changed, 6 insertions(+)

diff --git a/src/keyboard_layout_switching.cpp b/src/keyboard_layout_switching.cpp
index 16472c3d497..b88b9c26f44 100644
--- a/src/keyboard_layout_switching.cpp
+++ b/src/keyboard_layout_switching.cpp
@@ -98,6 +98,7 @@ VirtualDesktopPolicy::VirtualDesktopPolicy(Xkb *xkb, KeyboardLayout *layout, con
 {
     connect(VirtualDesktopManager::self(), &VirtualDesktopManager::currentChanged,
             this, &VirtualDesktopPolicy::desktopChanged);
+    connect(workspace(), &Workspace::activeOutputChanged, this, &VirtualDesktopPolicy::desktopChanged);
 
     connect(workspace()->sessionManager(), &SessionManager::prepareSessionSaveRequested, this, [this](const QString &name) {
         clearLayouts();
diff --git a/src/workspace.cpp b/src/workspace.cpp
index 0f8c4461163..0464111195b 100644
--- a/src/workspace.cpp
+++ b/src/workspace.cpp
@@ -2605,7 +2605,11 @@ LogicalOutput *Workspace::activeOutput() const
 
 void Workspace::setActiveOutput(LogicalOutput *output)
 {
+    if (m_activeOutput == output) {
+        return;
+    }
     m_activeOutput = output;
+    Q_EMIT activeOutputChanged(output);
 }
 
 void Workspace::setActiveOutput(const QPointF &pos)
diff --git a/src/workspace.h b/src/workspace.h
index 0ecbd47635c..fd33f69c919 100644
--- a/src/workspace.h
+++ b/src/workspace.h
@@ -565,6 +565,7 @@ Q_SIGNALS:
     void outputAdded(KWin::LogicalOutput *);
     void outputRemoved(KWin::LogicalOutput *);
     void outputsChanged();
+    void activeOutputChanged(KWin::Output *);
     /**
      * This signal is emitted when the stacking order changed, i.e. a window is risen
      * or lowered
-- 
GitLab


From 7f1cbd44b31a1e70f5b73d5eca8d441c27358a1d Mon Sep 17 00:00:00 2001
From: Hynek Schlindenbuch <schlindenbuch.h@seznam.cz>
Date: Wed, 19 Nov 2025 10:05:01 +0100
Subject: [PATCH 04/63] WIP: update pager when swiching active output

This is a temporary fix for easier testing. The problem before was that
the pager was only updated when a desktop changed on an output, not when
the active output changed (and outputs had different desktops).

Now the pager updates when the output changes, so it's a bit clearer
what's going on. Though it's still not right, because each output should
show the correct pager status (i.e. the active desktop in each output's
pager should be independent).
---
 src/virtualdesktops.cpp | 28 ++++++++++++++++++----------
 src/virtualdesktops.h   |  5 +++++
 2 files changed, 23 insertions(+), 10 deletions(-)

diff --git a/src/virtualdesktops.cpp b/src/virtualdesktops.cpp
index b81b61088ff..fa3bbd600c1 100644
--- a/src/virtualdesktops.cpp
+++ b/src/virtualdesktops.cpp
@@ -105,16 +105,10 @@ void VirtualDesktopManager::setVirtualDesktopManagement(PlasmaVirtualDesktopMana
         if (output != workspace()->activeOutput()) {
             return;
         }
-        const QList<PlasmaVirtualDesktopInterface *> deskIfaces = m_virtualDesktopManagement->desktops();
-        for (auto *deskInt : deskIfaces) {
-            if (deskInt->id() == currentDesktop()->id()) {
-                // TODO: VD: Figure out how to replace the bool
-                deskInt->setActive(true);
-            } else {
-                deskInt->setActive(false);
-            }
-        }
-        m_virtualDesktopManagement->scheduleDone();
+        updatePlasmaVirtualDesktops(newDesktop);
+    });
+    connect(workspace(), &Workspace::activeOutputChanged, this, [this](Output *output) {
+        updatePlasmaVirtualDesktops(currentDesktop(output));
     });
 
     std::for_each(m_desktops.constBegin(), m_desktops.constEnd(), createPlasmaVirtualDesktop);
@@ -953,6 +947,20 @@ void VirtualDesktopManager::initSwitchToShortcuts()
     }
 }
 
+void VirtualDesktopManager::updatePlasmaVirtualDesktops(VirtualDesktop *activeDesktop)
+{
+    const QList<PlasmaVirtualDesktopInterface *> deskIfaces = m_virtualDesktopManagement->desktops();
+    for (auto *deskInt : deskIfaces) {
+        if (deskInt->id() == activeDesktop->id()) {
+            // TODO: VD: Figure out how to replace the bool
+            deskInt->setActive(true);
+        } else {
+            deskInt->setActive(false);
+        }
+    }
+    m_virtualDesktopManagement->scheduleDone();
+}
+
 QAction *VirtualDesktopManager::addAction(const QString &name, const KLocalizedString &label, uint value, const QList<QKeySequence> &key, void (VirtualDesktopManager::*slot)())
 {
     QAction *a = new QAction(this);
diff --git a/src/virtualdesktops.h b/src/virtualdesktops.h
index 7ff1648198f..468ded2b34e 100644
--- a/src/virtualdesktops.h
+++ b/src/virtualdesktops.h
@@ -502,6 +502,11 @@ private:
      */
     void initSwitchToShortcuts();
 
+    /**
+     * Updates active status of plasma virtual desktops.
+     */
+    void updatePlasmaVirtualDesktops(VirtualDesktop *activeDesktop);
+
     /**
      * Creates an action and connects it to the @p slot in this Manager. This method is
      * meant to be used for the case that an additional information needs to be stored in
-- 
GitLab


From 2fa064ee8ec2ade6a0dc2d74d8e2df23452a742f Mon Sep 17 00:00:00 2001
From: Hynek Schlindenbuch <schlindenbuch.h@seznam.cz>
Date: Wed, 19 Nov 2025 10:39:22 +0100
Subject: [PATCH 05/63] WIP: add output to VDM::setCurrent

---
 src/activation.cpp                  |  2 +-
 src/effect/effecthandler.cpp        |  1 +
 src/screenedge.cpp                  |  2 +-
 src/scripting/workspace_wrapper.cpp |  1 +
 src/tabbox/tabbox.cpp               |  2 +-
 src/useractions.cpp                 | 10 +++++-----
 src/virtualdesktops.cpp             | 11 ++++++-----
 src/virtualdesktops.h               |  4 ++--
 src/x11window.cpp                   |  2 +-
 9 files changed, 19 insertions(+), 16 deletions(-)

diff --git a/src/activation.cpp b/src/activation.cpp
index 8da3d8a37b6..bcf87c68a56 100644
--- a/src/activation.cpp
+++ b/src/activation.cpp
@@ -302,7 +302,7 @@ void Workspace::activateWindow(Window *window, bool force)
         ++block_focus;
         switch (options->activationDesktopPolicy()) {
         case Options::ActivationDesktopPolicy::SwitchToOtherDesktop:
-            VirtualDesktopManager::self()->setCurrent(window->desktops().constLast());
+            VirtualDesktopManager::self()->setCurrent(window->desktops().constLast(), window->output());
             break;
         case Options::ActivationDesktopPolicy::BringToCurrentDesktop:
             window->enterDesktop(VirtualDesktopManager::self()->currentDesktop(window->output()));
diff --git a/src/effect/effecthandler.cpp b/src/effect/effecthandler.cpp
index 125a5023728..bbcd010b7ae 100644
--- a/src/effect/effecthandler.cpp
+++ b/src/effect/effecthandler.cpp
@@ -847,6 +847,7 @@ QList<VirtualDesktop *> EffectsHandler::desktops() const
     return VirtualDesktopManager::self()->desktops();
 }
 
+// TODO: VD: Output?
 void EffectsHandler::setCurrentDesktop(VirtualDesktop *desktop)
 {
     VirtualDesktopManager::self()->setCurrent(desktop);
diff --git a/src/screenedge.cpp b/src/screenedge.cpp
index 36650a8d3d4..468b8d60c39 100644
--- a/src/screenedge.cpp
+++ b/src/screenedge.cpp
@@ -507,7 +507,7 @@ void Edge::switchDesktop(const QPoint &cursorPos)
             return;
         }
     }
-    vds->setCurrent(desktop);
+    vds->setCurrent(desktop, m_output);
     if (vds->currentDesktop() != oldDesktop) {
         m_pushBackBlocked = true;
         input()->pointer()->warp(pos);
diff --git a/src/scripting/workspace_wrapper.cpp b/src/scripting/workspace_wrapper.cpp
index 2d793082306..285ddc83c54 100644
--- a/src/scripting/workspace_wrapper.cpp
+++ b/src/scripting/workspace_wrapper.cpp
@@ -72,6 +72,7 @@ QList<VirtualDesktop *> WorkspaceWrapper::desktops() const
     return VirtualDesktopManager::self()->desktops();
 }
 
+// TODO: VD: Output?
 void WorkspaceWrapper::setCurrentDesktop(VirtualDesktop *desktop)
 {
     if (!desktop) {
diff --git a/src/tabbox/tabbox.cpp b/src/tabbox/tabbox.cpp
index ec5e78857fa..9eaffe907a7 100644
--- a/src/tabbox/tabbox.cpp
+++ b/src/tabbox/tabbox.cpp
@@ -828,7 +828,7 @@ void TabBox::CDEWalkThroughWindows(bool forward)
             Workspace::self()->activateWindow(nc);
         } else {
             if (!nc->isOnCurrentDesktop()) {
-                VirtualDesktopManager::self()->setCurrent(nc->desktops().constLast());
+                VirtualDesktopManager::self()->setCurrent(nc->desktops().constLast(), nc->output());
             }
             Workspace::self()->raiseWindow(nc);
         }
diff --git a/src/useractions.cpp b/src/useractions.cpp
index ba61f7accf9..217d9908619 100644
--- a/src/useractions.cpp
+++ b/src/useractions.cpp
@@ -1412,11 +1412,11 @@ void windowToDesktop(Window *window, VirtualDesktopManager::Direction direction)
     const auto desktop = vds->inDirection(nullptr, direction, true);
     if (ws->moveResizeWindow()) {
         if (ws->moveResizeWindow() == window) {
-            vds->setCurrent(desktop);
+            vds->setCurrent(desktop, window->output());
         }
     } else {
         ws->setMoveResizeWindow(window);
-        vds->setCurrent(desktop);
+        vds->setCurrent(desktop, window->output());
         ws->setMoveResizeWindow(nullptr);
     }
 }
@@ -1455,18 +1455,18 @@ void activeWindowToDesktop(VirtualDesktopManager::Direction direction)
 {
     VirtualDesktopManager *vds = VirtualDesktopManager::self();
     Workspace *ws = Workspace::self();
-    VirtualDesktop *current = vds->currentDesktop();
+    VirtualDesktop *current = vds->currentDesktop(ws->activeWindow()->output());
     VirtualDesktop *newCurrent = VirtualDesktopManager::self()->inDirection(current, direction, options->isRollOverDesktops());
     if (newCurrent == current) {
         return;
     }
     if (ws->moveResizeWindow()) {
         if (ws->moveResizeWindow() == ws->activeWindow()) {
-            vds->setCurrent(newCurrent);
+            vds->setCurrent(newCurrent, ws->activeWindow()->output());
         }
     } else {
         ws->setMoveResizeWindow(ws->activeWindow());
-        vds->setCurrent(newCurrent);
+        vds->setCurrent(newCurrent, ws->activeWindow()->output());
         ws->setMoveResizeWindow(nullptr);
     }
 }
diff --git a/src/virtualdesktops.cpp b/src/virtualdesktops.cpp
index fa3bbd600c1..f95a3546a7d 100644
--- a/src/virtualdesktops.cpp
+++ b/src/virtualdesktops.cpp
@@ -576,7 +576,6 @@ uint VirtualDesktopManager::current() const
     return d ? d->x11DesktopNumber() : 0;
 }
 
-// TODO: VD: Go through all usages and make sure that they're OK.
 VirtualDesktop *VirtualDesktopManager::currentDesktop(Output *output) const
 {
     if (!output) {
@@ -587,21 +586,23 @@ VirtualDesktop *VirtualDesktopManager::currentDesktop(Output *output) const
     return result ? result : m_desktops.at(0);
 }
 
-bool VirtualDesktopManager::setCurrent(uint newDesktop)
+bool VirtualDesktopManager::setCurrent(uint newDesktop, Output *output)
 {
     if (newDesktop < 1 || newDesktop > count()) {
         return false;
     }
     VirtualDesktop *d = desktopForX11Id(newDesktop);
     Q_ASSERT(d);
-    return setCurrent(d);
+    return setCurrent(d, output);
 }
 
 // TODO: VD: Go through all usages and make sure that they're OK.
-bool VirtualDesktopManager::setCurrent(VirtualDesktop *newDesktop)
+bool VirtualDesktopManager::setCurrent(VirtualDesktop *newDesktop, Output *output)
 {
     Q_ASSERT(newDesktop);
-    Output *output = workspace()->activeOutput();
+    if (!output) {
+        output = workspace()->activeOutput();
+    }
     VirtualDesktop *oldDesktop = m_currentDesktops[output];
     if (oldDesktop == newDesktop) {
         return false;
diff --git a/src/virtualdesktops.h b/src/virtualdesktops.h
index 468ded2b34e..0486c54521b 100644
--- a/src/virtualdesktops.h
+++ b/src/virtualdesktops.h
@@ -347,7 +347,7 @@ public Q_SLOTS:
      * @see currentChanged
      * @see moveTo
      */
-    bool setCurrent(uint current);
+    bool setCurrent(uint current, Output *output = nullptr);
 
     /**
      * Set the current desktop to @a current.
@@ -356,7 +356,7 @@ public Q_SLOTS:
      * @see currentChanged
      * @see moveTo
      */
-    bool setCurrent(VirtualDesktop *current);
+    bool setCurrent(VirtualDesktop *current, Output *output = nullptr);
 
     /**
      * Updates the layout to a new number of rows. The number of columns will be calculated accordingly
diff --git a/src/x11window.cpp b/src/x11window.cpp
index a6c90196de7..9566437ccdf 100644
--- a/src/x11window.cpp
+++ b/src/x11window.cpp
@@ -869,7 +869,7 @@ bool X11Window::manage(xcb_window_t w, bool isMapped)
         // If session saving, force showing new windows (i.e. "save file?" dialogs etc.)
         // also force if activation is allowed
         if (!isOnCurrentDesktop() && !isMapped && !session && (allow || isSessionSaving)) {
-            VirtualDesktopManager::self()->setCurrent(desktopId());
+            VirtualDesktopManager::self()->setCurrent(desktopId(), output());
         }
 
         // If the window is on an inactive activity during session saving, temporarily force it to show.
-- 
GitLab


From 99d6846a1a3108dae104d89da4270e05d6a1bb30 Mon Sep 17 00:00:00 2001
From: Hynek Schlindenbuch <schlindenbuch.h@seznam.cz>
Date: Wed, 19 Nov 2025 13:02:40 +0100
Subject: [PATCH 06/63] make per-output virtual desktops configurable

---
 src/effect/effecthandler.h                    |  1 +
 src/kcms/desktop/ui/main.qml                  | 14 +++++++++
 src/kcms/desktop/virtualdesktopssettings.kcfg |  4 +++
 src/kwin.kcfg                                 |  3 ++
 src/options.cpp                               | 11 +++++++
 src/options.h                                 | 15 ++++++++++
 src/virtualdesktops.cpp                       | 18 ++++++++++--
 src/virtualdesktops.h                         | 29 +++++++++++++++++++
 src/workspace.cpp                             |  2 ++
 9 files changed, 95 insertions(+), 2 deletions(-)

diff --git a/src/effect/effecthandler.h b/src/effect/effecthandler.h
index c7d9fec61b4..48e12c872dd 100644
--- a/src/effect/effecthandler.h
+++ b/src/effect/effecthandler.h
@@ -129,6 +129,7 @@ class KWIN_EXPORT EffectsHandler : public QObject
     Q_PROPERTY(int workspaceWidth READ workspaceWidth)
     Q_PROPERTY(int workspaceHeight READ workspaceHeight)
     Q_PROPERTY(QList<KWin::VirtualDesktop *> desktops READ desktops)
+    // TODO: VD: Do we need this for perOutputVirtualDesktops as well?
     Q_PROPERTY(bool optionRollOverDesktops READ optionRollOverDesktops)
     Q_PROPERTY(KWin::LogicalOutput *activeScreen READ activeScreen)
     /**
diff --git a/src/kcms/desktop/ui/main.qml b/src/kcms/desktop/ui/main.qml
index 765c4aa2e7d..469542e0090 100644
--- a/src/kcms/desktop/ui/main.qml
+++ b/src/kcms/desktop/ui/main.qml
@@ -219,6 +219,20 @@ KCM.ScrollViewKCM {
                 }
             }
 
+            QQC2.CheckBox {
+                id: perOutputVirtualDesktops
+
+                text: i18n("Switch desktops independently for each screen")
+                enabled: !kcm.virtualDesktopsSettings.isImmutable("perOutputVirtualDesktops")
+                checked: kcm.virtualDesktopsSettings.perOutputVirtualDesktops
+                onToggled: kcm.virtualDesktopsSettings.perOutputVirtualDesktops = checked
+
+                KCM.SettingStateBinding {
+                    configObject: kcm.virtualDesktopsSettings
+                    settingName: "perOutputVirtualDesktops"
+                }
+            }
+
             RowLayout {
                 Layout.fillWidth: true
 
diff --git a/src/kcms/desktop/virtualdesktopssettings.kcfg b/src/kcms/desktop/virtualdesktopssettings.kcfg
index e4ac07f90aa..f74da807ce2 100644
--- a/src/kcms/desktop/virtualdesktopssettings.kcfg
+++ b/src/kcms/desktop/virtualdesktopssettings.kcfg
@@ -9,6 +9,10 @@
       <label>Whether or not, we circle through the virtual desktop when moving from one to the next</label>
       <default>false</default>
     </entry>
+    <entry name="perOutputVirtualDesktops" key="PerOutputVirtualDesktops" type="bool">
+      <label>Whether virtual desktops are switched independently for each screen</label>
+      <default>false</default>
+    </entry>
   </group>
   <group name="Plugins">
     <entry name="desktopChangeOsdEnabled" key="desktopchangeosdEnabled" type="bool">
diff --git a/src/kwin.kcfg b/src/kwin.kcfg
index b3deb52cc4b..fbb4e614c45 100644
--- a/src/kwin.kcfg
+++ b/src/kwin.kcfg
@@ -106,6 +106,9 @@
         <entry name="RollOverDesktops" type="Bool">
             <default>false</default>
         </entry>
+        <entry name="PerOutputVirtualDesktops" type="Bool">
+            <default>false</default>
+        </entry>
         <entry name="FocusStealingPreventionLevel" type="Int">
             <default>1</default>
             <min>0</min>
diff --git a/src/options.cpp b/src/options.cpp
index 8052d647baf..c6da7d4d833 100644
--- a/src/options.cpp
+++ b/src/options.cpp
@@ -48,6 +48,7 @@ Options::Options(QObject *parent)
     , m_edgeBarrier(0)
     , m_cornerBarrier(0)
     , m_rollOverDesktops(false)
+    , m_perOutputVirtualDesktops(false)
     , m_focusStealingPreventionLevel(FocusStealingPreventionLevel::None)
     , m_killPingTimeout(0)
     , m_xwaylandCrashPolicy(Options::defaultXwaylandCrashPolicy())
@@ -309,6 +310,15 @@ void Options::setRollOverDesktops(bool rollOverDesktops)
     Q_EMIT rollOverDesktopsChanged(m_rollOverDesktops);
 }
 
+void Options::setPerOutputVirtualDesktops(bool perOutputVirtualDesktops)
+{
+    if (m_perOutputVirtualDesktops == perOutputVirtualDesktops) {
+        return;
+    }
+    m_perOutputVirtualDesktops = perOutputVirtualDesktops;
+    Q_EMIT perOutputVirtualDesktopsChanged(m_perOutputVirtualDesktops);
+}
+
 void Options::setFocusStealingPreventionLevel(FocusStealingPreventionLevel focusStealingPreventionLevel)
 {
     if (!focusPolicyIsReasonable()) {
@@ -709,6 +719,7 @@ void Options::syncFromKcfgc()
     setNextFocusPrefersMouse(m_settings->nextFocusPrefersMouse());
     setSeparateScreenFocus(m_settings->separateScreenFocus());
     setRollOverDesktops(m_settings->rollOverDesktops());
+    setPerOutputVirtualDesktops(m_settings->perOutputVirtualDesktops());
     setFocusStealingPreventionLevel(FocusStealingPreventionLevel(m_settings->focusStealingPreventionLevel()));
     setActivationDesktopPolicy(m_settings->activationDesktopPolicy());
     setXwaylandCrashPolicy(m_settings->xwaylandCrashPolicy());
diff --git a/src/options.h b/src/options.h
index b25a4fa7179..e2694c04ad2 100644
--- a/src/options.h
+++ b/src/options.h
@@ -126,6 +126,10 @@ class KWIN_EXPORT Options : public QObject
      * Whether or not we roll over to the other edge when switching desktops past the edge.
      */
     Q_PROPERTY(bool rollOverDesktops READ isRollOverDesktops WRITE setRollOverDesktops NOTIFY rollOverDesktopsChanged)
+    /**
+     * Whether virtual desktops are switched independently for each screen.
+     */
+    Q_PROPERTY(bool perOutputVirtualDesktops READ isPerOutputVirtualDesktops WRITE setPerOutputVirtualDesktops NOTIFY perOutputVirtualDesktopsChanged)
     /**
      * 0 - 4 , see Workspace::allowWindowActivation()
      */
@@ -377,6 +381,14 @@ public:
         return m_rollOverDesktops;
     }
 
+    /**
+     * Whether or not we roll over to the other edge when switching desktops past the edge.
+     */
+    bool isPerOutputVirtualDesktops() const
+    {
+        return m_perOutputVirtualDesktops;
+    }
+
     /**
      * Returns the focus stealing prevention level.
      *
@@ -658,6 +670,7 @@ public:
     void setEdgeBarrier(int edgeBarrier);
     void setCornerBarrier(bool cornerBarrier);
     void setRollOverDesktops(bool rollOverDesktops);
+    void setPerOutputVirtualDesktops(bool perOutputVirtualDesktops);
     void setFocusStealingPreventionLevel(FocusStealingPreventionLevel focusStealingPreventionLevel);
     void setOperationTitlebarDblClick(WindowOperation operationTitlebarDblClick);
     void setOperationMaxButtonLeftClick(WindowOperation op);
@@ -830,6 +843,7 @@ Q_SIGNALS:
     void edgeBarrierChanged();
     void cornerBarrierChanged();
     void rollOverDesktopsChanged(bool enabled);
+    void perOutputVirtualDesktopsChanged(bool enabled);
     void focusStealingPreventionLevelChanged();
     void operationTitlebarDblClickChanged();
     void operationMaxButtonLeftClickChanged();
@@ -888,6 +902,7 @@ private:
     int m_edgeBarrier;
     bool m_cornerBarrier;
     bool m_rollOverDesktops;
+    bool m_perOutputVirtualDesktops;
     FocusStealingPreventionLevel m_focusStealingPreventionLevel;
     int m_killPingTimeout;
     XwaylandCrashPolicy m_xwaylandCrashPolicy;
diff --git a/src/virtualdesktops.cpp b/src/virtualdesktops.cpp
index f95a3546a7d..0df2f36062d 100644
--- a/src/virtualdesktops.cpp
+++ b/src/virtualdesktops.cpp
@@ -209,8 +209,7 @@ KWIN_SINGLETON_FACTORY_VARIABLE(VirtualDesktopManager, s_manager)
 VirtualDesktopManager::VirtualDesktopManager(QObject *parent)
     : QObject(parent)
     , m_navigationWrapsAround(false)
-    // TODO: VD: Change this back to false and add config
-    , m_perOutputVirtualDesktops(true)
+    , m_perOutputVirtualDesktops(false)
 #if KWIN_BUILD_X11
     , m_rootInfo(nullptr)
 #endif
@@ -1020,6 +1019,21 @@ void VirtualDesktopManager::setNavigationWrappingAround(bool enabled)
     Q_EMIT navigationWrappingAroundChanged();
 }
 
+void VirtualDesktopManager::setPerOutputVirtualDesktops(bool enabled)
+{
+    if (enabled == m_perOutputVirtualDesktops) {
+        return;
+    }
+    m_perOutputVirtualDesktops = enabled;
+    if (!enabled) {
+        VirtualDesktop *newDesktop = currentDesktop(workspace()->activeOutput());
+        for (const auto output : workspace()->outputs()) {
+            setCurrent(newDesktop, output);
+        }
+    }
+    Q_EMIT perOutputVirtualDesktopsChanged();
+}
+
 void VirtualDesktopManager::slotDown()
 {
     moveTo(Direction::Down, isNavigationWrappingAround());
diff --git a/src/virtualdesktops.h b/src/virtualdesktops.h
index 0486c54521b..3cf3c0947eb 100644
--- a/src/virtualdesktops.h
+++ b/src/virtualdesktops.h
@@ -152,6 +152,11 @@ class KWIN_EXPORT VirtualDesktopManager : public QObject
      * Whether navigation in the desktop layout wraps around at the borders.
      */
     Q_PROPERTY(bool navigationWrappingAround READ isNavigationWrappingAround WRITE setNavigationWrappingAround NOTIFY navigationWrappingAroundChanged)
+
+    /**
+     * Whether virtual desktops are switched independently for each screen.
+     */
+    Q_PROPERTY(bool perOutputVirtualDesktops READ isPerOutputVirtualDesktops WRITE setPerOutputVirtualDesktops NOTIFY perOutputVirtualDesktopsChanged)
 public:
     ~VirtualDesktopManager() override;
 
@@ -213,6 +218,13 @@ public:
      */
     bool isNavigationWrappingAround() const;
 
+    /**
+     * @returns @c true if virtual desktops are switched independently for each screen, @c false otherwise
+     * @see setPerOutputVirtualDesktops
+     * @see perOutputVirtualDesktopsChanged
+     */
+    bool isPerOutputVirtualDesktops() const;
+
     /**
      * @returns The layout aware virtual desktop grid used by this manager.
      */
@@ -375,6 +387,13 @@ public Q_SLOTS:
      */
     void setNavigationWrappingAround(bool enabled);
 
+    /**
+     * @param enabled switching virtual desktops independently for each screen
+     * @see isPerOutputVirtualDesktops
+     * @see perOutputVirtualDesktopsChanged
+     */
+    void setPerOutputVirtualDesktops(bool enabled);
+
     /**
      * Loads number of desktops and names from configuration file
      */
@@ -447,6 +466,11 @@ Q_SIGNALS:
      */
     void navigationWrappingAroundChanged();
 
+    /**
+     * Signal emitted whenever the perOutputVirtualDesktops property changes.
+     */
+    void perOutputVirtualDesktopsChanged();
+
 private Q_SLOTS:
     /**
      * Common slot for all "Switch to Desktop n" shortcuts.
@@ -584,6 +608,11 @@ inline bool VirtualDesktopManager::isNavigationWrappingAround() const
     return m_navigationWrapsAround;
 }
 
+inline bool VirtualDesktopManager::isPerOutputVirtualDesktops() const
+{
+    return m_perOutputVirtualDesktops;
+}
+
 inline void VirtualDesktopManager::setConfig(KSharedConfig::Ptr config)
 {
     m_config = std::move(config);
diff --git a/src/workspace.cpp b/src/workspace.cpp
index 0464111195b..cf7dd201825 100644
--- a/src/workspace.cpp
+++ b/src/workspace.cpp
@@ -215,6 +215,8 @@ void Workspace::init()
     connect(vds, &VirtualDesktopManager::currentChangingCancelled, this, &Workspace::slotCurrentDesktopChangingCancelled);
     vds->setNavigationWrappingAround(options->isRollOverDesktops());
     connect(options, &Options::rollOverDesktopsChanged, vds, &VirtualDesktopManager::setNavigationWrappingAround);
+    vds->setPerOutputVirtualDesktops(options->isPerOutputVirtualDesktops());
+    connect(options, &Options::perOutputVirtualDesktopsChanged, vds, &VirtualDesktopManager::setPerOutputVirtualDesktops);
     vds->setConfig(config);
 
     // Now we know how many desktops we'll have, thus we initialize the positioning object
-- 
GitLab


From 7960510b0c257f58063b469281b8225a769ae975 Mon Sep 17 00:00:00 2001
From: Hynek Schlindenbuch <schlindenbuch.h@seznam.cz>
Date: Wed, 19 Nov 2025 13:05:50 +0100
Subject: [PATCH 07/63] fix output in currentChanged with global desktops

---
 src/virtualdesktops.cpp | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/virtualdesktops.cpp b/src/virtualdesktops.cpp
index 0df2f36062d..0b929748123 100644
--- a/src/virtualdesktops.cpp
+++ b/src/virtualdesktops.cpp
@@ -617,7 +617,7 @@ bool VirtualDesktopManager::setCurrent(VirtualDesktop *newDesktop, Output *outpu
         }
         auto oldDesktop = value;
         value = newDesktop;
-        Q_EMIT currentChanged(oldDesktop, newDesktop, output);
+        Q_EMIT currentChanged(oldDesktop, newDesktop, key);
     }
     return true;
 }
-- 
GitLab


From aa7fd710c3732344844ec176b0a8eec5b6b785f0 Mon Sep 17 00:00:00 2001
From: Hynek Schlindenbuch <schlindenbuch.h@seznam.cz>
Date: Fri, 21 Nov 2025 09:44:08 +0100
Subject: [PATCH 08/63] WIP: fix currentDesktop usages via WorkspaceWrapper

---
 .../minimizeall/package/contents/code/main.js |  2 +-
 src/plugins/overview/qml/Main.qml             | 28 +++++++++++++------
 src/plugins/private/expoarea.cpp              |  2 +-
 .../private/qml/WindowHeapDelegate.qml        |  2 ++
 src/plugins/tileseditor/qml/Main.qml          | 15 ++++++++--
 src/plugins/windowview/qml/Main.qml           | 18 ++++++++++--
 src/scripting/workspace_wrapper.cpp           | 16 +++++++++--
 src/scripting/workspace_wrapper.h             | 16 ++++++++++-
 8 files changed, 81 insertions(+), 18 deletions(-)

diff --git a/src/plugins/minimizeall/package/contents/code/main.js b/src/plugins/minimizeall/package/contents/code/main.js
index b232fc01a43..b2602b469a1 100644
--- a/src/plugins/minimizeall/package/contents/code/main.js
+++ b/src/plugins/minimizeall/package/contents/code/main.js
@@ -11,7 +11,7 @@ var registeredBorders = [];
 
 function isRelevant(window) {
     return window.minimizable &&
-        (!window.desktops.length || window.desktops.includes(workspace.currentDesktop)) &&
+        (!window.desktops.length || window.desktops.includes(workspace.currentDesktopForScreen(window.output)) &&
         (!window.activities.length || window.activities.includes(workspace.currentActivity));
 }
 
diff --git a/src/plugins/overview/qml/Main.qml b/src/plugins/overview/qml/Main.qml
index ce021afe95b..c27e198a495 100644
--- a/src/plugins/overview/qml/Main.qml
+++ b/src/plugins/overview/qml/Main.qml
@@ -32,6 +32,7 @@ FocusScope {
     property bool organized: false
 
     property bool verticalDesktopBar: KWinComponents.Workspace.desktopGridHeight >= bar.desktopCount && KWinComponents.Workspace.desktopGridHeight != 1
+    property QtObject currentDesktop: KWinComponents.Workspace.currentDesktopForScreen(targetScreen)
 
     // The values of overviewVal and gridVal might not be 0 on startup,
     // but we always want to animate from 0 to those values. So, we initially
@@ -124,7 +125,7 @@ FocusScope {
     }
 
     function switchTo(desktop) {
-        KWinComponents.Workspace.currentDesktop = desktop;
+        KWinComponents.Workspace.setCurrentDesktopForScreen(desktop, targetScreen);
         effect.deactivate();
     }
 
@@ -171,7 +172,7 @@ FocusScope {
         }
         let newIndex = y * container.columns + x;
 
-        KWinComponents.Workspace.currentDesktop = allDesktopHeaps.itemAt(newIndex).desktop
+        KWinComponents.Workspace.setCurrentDesktopForScreen(allDesktopHeaps.itemAt(newIndex).desktop, targetScreen);
         allDesktopHeaps.itemAt(newIndex).nestedHeap.focus = true
         allDesktopHeaps.itemAt(newIndex).selectLastItem(invertedDirection);
         return true;
@@ -248,7 +249,7 @@ FocusScope {
         KWinComponents.DesktopBackground {
             id: backgroundItem
             activity: KWinComponents.Workspace.currentActivity
-            desktop: KWinComponents.Workspace.currentDesktop
+            desktop: currentDesktop
             outputName: targetScreen.name
             visible: false
         }
@@ -297,7 +298,7 @@ FocusScope {
             windowModel: stackModel
             desktopModel: desktopModel
             verticalDesktopBar: container.verticalDesktopBar
-            selectedDesktop: KWinComponents.Workspace.currentDesktop
+            selectedDesktop: currentDesktop
             heap: allDesktopHeaps.currentHeap
         }
     }
@@ -425,7 +426,7 @@ FocusScope {
 
                 required property QtObject desktop
                 required property int index
-                readonly property bool current: KWinComponents.Workspace.currentDesktop === desktop
+                readonly property bool current: currentDesktop === desktop
                 readonly property bool nearCurrent: Math.abs(deltaColumn) <= 1 && Math.abs(deltaRow) <= 1
                 readonly property var nestedHeap: heap
 
@@ -517,7 +518,7 @@ FocusScope {
                         anchors.fill: parent
                         anchors.margins: gridVal !== 0 ? Math.round(mainBackground.current * gridVal * (1.5 / gridScale.xScale)) : 0
                         activity: KWinComponents.Workspace.currentActivity
-                        desktop: KWinComponents.Workspace.currentDesktop
+                        desktop: currentDesktop
                         outputName: targetScreen.name
                         visible: false
                     }
@@ -583,7 +584,7 @@ FocusScope {
                     TapHandler {
                         acceptedButtons: Qt.LeftButton
                         onTapped: {
-                            KWinComponents.Workspace.currentDesktop = mainBackground.desktop;
+                            KWinComponents.Workspace.setCurrentDesktopForScreen(mainBackground.desktop, container.targetScreen);
                             container.effect.deactivate();
                         }
                     }
@@ -763,7 +764,7 @@ FocusScope {
 
     Repeater {
         model: KWinComponents.WindowFilterModel {
-            desktop: KWinComponents.Workspace.currentDesktop
+            desktop: currentDesktop
             screenName: targetScreen.name
             windowModel: stackModel
             windowType: KWinComponents.WindowFilterModel.Dock
@@ -798,4 +799,15 @@ FocusScope {
         container.verticalDesktopBar = container.verticalDesktopBar
         organized = true
     }
+
+    Connections {
+        target: KWinComponents.Workspace
+        onCurrentDesktopChanged: (previous, current, screen) => {
+            if (screen !== container.targetScreen) {
+                return;
+            }
+
+            container.currentDesktop = current;
+        }
+    }
 }
diff --git a/src/plugins/private/expoarea.cpp b/src/plugins/private/expoarea.cpp
index be067889270..313da186066 100644
--- a/src/plugins/private/expoarea.cpp
+++ b/src/plugins/private/expoarea.cpp
@@ -63,7 +63,7 @@ void ExpoArea::update()
     }
     const QRectF oldRect = m_rect;
 
-    m_rect = workspace()->clientArea(MaximizeArea, m_screen, VirtualDesktopManager::self()->currentDesktop());
+    m_rect = workspace()->clientArea(MaximizeArea, m_screen, VirtualDesktopManager::self()->currentDesktop(m_screen));
 
     // Map the area to the output local coordinates.
     m_rect.translate(-m_screen->geometry().topLeft());
diff --git a/src/plugins/private/qml/WindowHeapDelegate.qml b/src/plugins/private/qml/WindowHeapDelegate.qml
index c8779877b04..7433aa4be1d 100644
--- a/src/plugins/private/qml/WindowHeapDelegate.qml
+++ b/src/plugins/private/qml/WindowHeapDelegate.qml
@@ -29,6 +29,8 @@ ExpoCell {
     property Item contentItemParent: this
 
     // no desktops is a special value which means "All Desktops"
+    // TODO: VD: Does this need currentDesktopOnScreen? And if so, does it need to be live-updated as desktops change?
+    // It's used by overview and window view, but I can't see any difference in any of them.
     readonly property bool presentOnCurrentDesktop: !window.desktops.length || window.desktops.indexOf(KWinComponents.Workspace.currentDesktop) !== -1
     readonly property bool initialHidden: window.minimized
     readonly property bool activeHidden: {
diff --git a/src/plugins/tileseditor/qml/Main.qml b/src/plugins/tileseditor/qml/Main.qml
index 5fcbc0e0889..3f3162b4ec1 100644
--- a/src/plugins/tileseditor/qml/Main.qml
+++ b/src/plugins/tileseditor/qml/Main.qml
@@ -39,7 +39,8 @@ FocusScope {
 
     property bool active: false
 
-    readonly property QtObject rootTile: KWinComponents.Workspace.rootTile(root.targetScreen, KWinComponents.Workspace.currentDesktop)
+    property QtObject currentDesktop: KWinComponents.Workspace.currentDesktopForScreen(targetScreen)
+    readonly property QtObject rootTile: KWinComponents.Workspace.rootTile(root.targetScreen, currentDesktop)
 
     Component.onCompleted: {
         root.active = true;
@@ -61,7 +62,7 @@ FocusScope {
         Repeater {
             model: KWinComponents.WindowFilterModel {
                 activity: KWinComponents.Workspace.currentActivity
-                desktop: KWinComponents.Workspace.currentDesktop
+                desktop: root.currentDesktop
                 screenName: targetScreen.name
                 windowModel: KWinComponents.WindowModel {}
             }
@@ -272,4 +273,14 @@ FocusScope {
             }
         }
     }
+    Connections {
+        target: KWinComponents.Workspace
+        onCurrentDesktopChanged: (previous, current, screen) => {
+            if (screen !== root.targetScreen) {
+                return;
+            }
+
+            root.currentDesktop = current;
+        }
+    }
 }
diff --git a/src/plugins/windowview/qml/Main.qml b/src/plugins/windowview/qml/Main.qml
index 175b3801383..0038424b0bb 100644
--- a/src/plugins/windowview/qml/Main.qml
+++ b/src/plugins/windowview/qml/Main.qml
@@ -19,6 +19,7 @@ Item {
 
     readonly property QtObject effect: KWinComponents.SceneView.effect
     readonly property QtObject targetScreen: KWinComponents.SceneView.screen
+    property QtObject currentDesktop: KWinComponents.Workspace.currentDesktopForScreen(targetScreen)
 
     property bool animationEnabled: false
     property bool organized: false
@@ -63,7 +64,7 @@ Item {
 
     KWinComponents.DesktopBackground {
         activity: KWinComponents.Workspace.currentActivity
-        desktop: KWinComponents.Workspace.currentDesktop
+        desktop: currentDesktop
         outputName: targetScreen.name
 
         layer.enabled: true
@@ -185,7 +186,7 @@ Item {
                     switch (container.effect.mode) {
                         case WindowView.ModeCurrentDesktop:
                         case WindowView.ModeWindowClassCurrentDesktop:
-                            return KWinComponents.Workspace.currentDesktop;
+                            return container.currentDesktop;
                         default:
                             return undefined;
                     }
@@ -239,7 +240,7 @@ Item {
         asynchronous: true
 
         model: KWinComponents.WindowFilterModel {
-            desktop: KWinComponents.Workspace.currentDesktop
+            desktop: currentDesktop
             screenName: targetScreen.name
             windowModel: stackModel
             windowType: KWinComponents.WindowFilterModel.Dock
@@ -269,4 +270,15 @@ Item {
     }
 
     Component.onCompleted: start();
+
+    Connections {
+        target: KWinComponents.Workspace
+        onCurrentDesktopChanged: (previous, current, screen) => {
+            if (screen !== container.targetScreen) {
+                return;
+            }
+
+            container.currentDesktop = current;
+        }
+    }
 }
diff --git a/src/scripting/workspace_wrapper.cpp b/src/scripting/workspace_wrapper.cpp
index 285ddc83c54..07e4cb9e966 100644
--- a/src/scripting/workspace_wrapper.cpp
+++ b/src/scripting/workspace_wrapper.cpp
@@ -61,7 +61,6 @@ WorkspaceWrapper::WorkspaceWrapper(QObject *parent)
     connect(Cursors::self()->mouse(), &Cursor::posChanged, this, &WorkspaceWrapper::cursorPosChanged);
 }
 
-// TODO: VD: Output?
 VirtualDesktop *WorkspaceWrapper::currentDesktop() const
 {
     return VirtualDesktopManager::self()->currentDesktop();
@@ -72,7 +71,6 @@ QList<VirtualDesktop *> WorkspaceWrapper::desktops() const
     return VirtualDesktopManager::self()->desktops();
 }
 
-// TODO: VD: Output?
 void WorkspaceWrapper::setCurrentDesktop(VirtualDesktop *desktop)
 {
     if (!desktop) {
@@ -82,6 +80,20 @@ void WorkspaceWrapper::setCurrentDesktop(VirtualDesktop *desktop)
     VirtualDesktopManager::self()->setCurrent(desktop);
 }
 
+KWin::VirtualDesktop *WorkspaceWrapper::currentDesktopForScreen(KWin::Output *output) const
+{
+    return VirtualDesktopManager::self()->currentDesktop(output);
+}
+
+void WorkspaceWrapper::setCurrentDesktopForScreen(KWin::VirtualDesktop *desktop, KWin::Output *output)
+{
+    if (!desktop) {
+        qCWarning(KWIN_SCRIPTING) << "Invalid desktop passed to setCurrentDesktopForScreen";
+        return;
+    }
+    VirtualDesktopManager::self()->setCurrent(desktop, output);
+}
+
 Window *WorkspaceWrapper::activeWindow() const
 {
     return workspace()->activeWindow();
diff --git a/src/scripting/workspace_wrapper.h b/src/scripting/workspace_wrapper.h
index 685f8404310..0fcbeab3759 100644
--- a/src/scripting/workspace_wrapper.h
+++ b/src/scripting/workspace_wrapper.h
@@ -142,7 +142,7 @@ Q_SIGNALS:
     /**
      * This signal is emitted when the current virtual desktop changes.
      */
-    void currentDesktopChanged(KWin::VirtualDesktop *previous);
+    void currentDesktopChanged(KWin::VirtualDesktop *previous, KWin::VirtualDesktop *current, KWin::Output *output);
 
     /**
      * This signal is emitted when the cursor position changes.
@@ -218,6 +218,20 @@ public:
     VirtualDesktop *currentDesktop() const;
     void setCurrentDesktop(VirtualDesktop *desktop);
 
+    /**
+     * Returns current desktop on screen given by @p output.
+     *
+     * @since 6.6
+     */
+    Q_INVOKABLE KWin::VirtualDesktop *currentDesktopForScreen(KWin::LogicalOutput *output) const;
+
+    /**
+     * Sets current desktop on @p output.
+     *
+     * @since 6.6
+     */
+    Q_INVOKABLE void setCurrentDesktopForScreen(KWin::VirtualDesktop *desktop, KWin::LogicalOutput *output);
+
     Q_INVOKABLE KWin::LogicalOutput *screenAt(const QPointF &pos) const;
 
     /**
-- 
GitLab


From 77fe3f8f9fd896fd998ead31ea59d56821264619 Mon Sep 17 00:00:00 2001
From: Hynek Schlindenbuch <schlindenbuch.h@seznam.cz>
Date: Fri, 21 Nov 2025 12:09:42 +0100
Subject: [PATCH 09/63] WIP: add per-output desktops to EffectsHandler

---
 src/effect/effecthandler.cpp                  | 15 ++++++---------
 src/effect/effecthandler.h                    | 11 ++++++-----
 src/effect/effectwindow.cpp                   |  2 +-
 src/plugins/magiclamp/magiclamp.cpp           |  2 +-
 src/plugins/overview/overvieweffect.cpp       |  2 +-
 src/plugins/slide/slide.cpp                   |  4 +++-
 src/plugins/slide/slide.h                     |  2 +-
 src/plugins/slidingpopups/slidingpopups.cpp   |  4 ++--
 src/plugins/thumbnailaside/thumbnailaside.cpp |  2 +-
 src/workspace.cpp                             |  3 +--
 src/workspace.h                               |  2 +-
 11 files changed, 24 insertions(+), 25 deletions(-)

diff --git a/src/effect/effecthandler.cpp b/src/effect/effecthandler.cpp
index bbcd010b7ae..6a1403f2e52 100644
--- a/src/effect/effecthandler.cpp
+++ b/src/effect/effecthandler.cpp
@@ -165,9 +165,8 @@ EffectsHandler::EffectsHandler(Compositor *compositor, WorkspaceScene *scene)
             Q_EMIT showingDesktopChanged(showing);
         }
     });
-    connect(ws, &Workspace::currentDesktopChanged, this, [this](VirtualDesktop *old, Window *window) {
-        VirtualDesktop *newDesktop = VirtualDesktopManager::self()->currentDesktop();
-        Q_EMIT desktopChanged(old, newDesktop, window ? window->effectWindow() : nullptr);
+    connect(ws, &Workspace::currentDesktopChanged, this, [this](VirtualDesktop *old, VirtualDesktop *newDesktop, Output *output, Window *window) {
+        Q_EMIT desktopChanged(old, newDesktop, output, window ? window->effectWindow() : nullptr);
     });
     connect(ws, &Workspace::currentDesktopChanging, this, [this](VirtualDesktop *currentDesktop, QPointF offset, KWin::Window *window) {
         Q_EMIT desktopChanging(currentDesktop, offset, window ? window->effectWindow() : nullptr);
@@ -836,10 +835,9 @@ QString EffectsHandler::currentActivity() const
 #endif
 }
 
-// TODO: VD: Output?
-VirtualDesktop *EffectsHandler::currentDesktop() const
+VirtualDesktop *EffectsHandler::currentDesktop(Output *output) const
 {
-    return VirtualDesktopManager::self()->currentDesktop();
+    return VirtualDesktopManager::self()->currentDesktop(output);
 }
 
 QList<VirtualDesktop *> EffectsHandler::desktops() const
@@ -847,10 +845,9 @@ QList<VirtualDesktop *> EffectsHandler::desktops() const
     return VirtualDesktopManager::self()->desktops();
 }
 
-// TODO: VD: Output?
-void EffectsHandler::setCurrentDesktop(VirtualDesktop *desktop)
+void EffectsHandler::setCurrentDesktop(VirtualDesktop *desktop, Output *output)
 {
-    VirtualDesktopManager::self()->setCurrent(desktop);
+    VirtualDesktopManager::self()->setCurrent(desktop, output);
 }
 
 QSize EffectsHandler::desktopGridSize() const
diff --git a/src/effect/effecthandler.h b/src/effect/effecthandler.h
index 48e12c872dd..f05fb09e95b 100644
--- a/src/effect/effecthandler.h
+++ b/src/effect/effecthandler.h
@@ -321,17 +321,17 @@ public:
     QString currentActivity() const;
     // Desktops
     /**
-     * @returns The current desktop.
+     * @returns The current desktop on screen @a output.
      */
-    VirtualDesktop *currentDesktop() const;
+    VirtualDesktop *currentDesktop(KWin::Output *output = nullptr) const;
     /**
      * @returns Total number of desktops currently in existence.
      */
     QList<VirtualDesktop *> desktops() const;
     /**
-     * Set the current desktop to @a desktop.
+     * Set the current desktop to @a desktop on screen @a output.
      */
-    void setCurrentDesktop(KWin::VirtualDesktop *desktop);
+    void setCurrentDesktop(KWin::VirtualDesktop *desktop, KWin::Output *output = nullptr);
     /**
      * @returns The size of desktop layout in grid units.
      */
@@ -782,10 +782,11 @@ Q_SIGNALS:
      * Signal emitted when the current desktop changed.
      * @param oldDesktop The previously current desktop
      * @param newDesktop The new current desktop
+     * @param output The screen where the desktop was changed
      * @param with The window which is taken over to the new desktop, can be NULL
      * @since 4.9
      */
-    void desktopChanged(KWin::VirtualDesktop *oldDesktop, KWin::VirtualDesktop *newDesktop, KWin::EffectWindow *with);
+    void desktopChanged(KWin::VirtualDesktop *oldDesktop, KWin::VirtualDesktop *newDesktop, KWin::Output *output, KWin::EffectWindow *with);
 
     /**
      * Signal emitted while desktop is changing for animation.
diff --git a/src/effect/effectwindow.cpp b/src/effect/effectwindow.cpp
index 824fb44d9ea..dd92be28a83 100644
--- a/src/effect/effectwindow.cpp
+++ b/src/effect/effectwindow.cpp
@@ -168,7 +168,7 @@ bool EffectWindow::isOnCurrentActivity() const
 
 bool EffectWindow::isOnCurrentDesktop() const
 {
-    return isOnDesktop(effects->currentDesktop());
+    return isOnDesktop(effects->currentDesktop(screen()));
 }
 
 bool EffectWindow::isOnDesktop(VirtualDesktop *desktop) const
diff --git a/src/plugins/magiclamp/magiclamp.cpp b/src/plugins/magiclamp/magiclamp.cpp
index 6f1ccaf91a7..f0b651873f8 100644
--- a/src/plugins/magiclamp/magiclamp.cpp
+++ b/src/plugins/magiclamp/magiclamp.cpp
@@ -185,7 +185,7 @@ void MagicLampEffect::apply(EffectWindow *w, int mask, WindowPaintData &data, Wi
                 }
             } else {
                 // we did not find a panel, so it might be autohidden
-                QRectF iconScreen = effects->clientArea(ScreenArea, icon.topLeft(), effects->currentDesktop());
+                QRectF iconScreen = effects->clientArea(ScreenArea, icon.topLeft(), effects->currentDesktop(w->screen()));
                 // as the icon geometry could be overlap a screen edge we use an intersection
                 QRectF rect = iconScreen.intersected(icon);
                 // here we need a different assumption: icon geometry borders one screen edge
diff --git a/src/plugins/overview/overvieweffect.cpp b/src/plugins/overview/overvieweffect.cpp
index 4310fdcb4c6..bd3bfc78272 100644
--- a/src/plugins/overview/overvieweffect.cpp
+++ b/src/plugins/overview/overvieweffect.cpp
@@ -109,7 +109,7 @@ OverviewEffect::OverviewEffect()
         m_desktopOffset = desktopOffset;
         Q_EMIT desktopOffsetChanged();
     });
-    connect(effects, &EffectsHandler::desktopChanged, this, [this](VirtualDesktop *old, VirtualDesktop *current, EffectWindow *with) {
+    connect(effects, &EffectsHandler::desktopChanged, this, [this]() {
         m_desktopOffset = QPointF(0, 0);
         Q_EMIT desktopOffsetChanged();
     });
diff --git a/src/plugins/slide/slide.cpp b/src/plugins/slide/slide.cpp
index 9ee90665363..01bd1934fa4 100644
--- a/src/plugins/slide/slide.cpp
+++ b/src/plugins/slide/slide.cpp
@@ -369,7 +369,7 @@ void SlideEffect::finishedSwitching()
     effects->setActiveFullScreenEffect(nullptr);
 }
 
-void SlideEffect::desktopChanged(VirtualDesktop *old, VirtualDesktop *current, EffectWindow *with)
+void SlideEffect::desktopChanged(VirtualDesktop *old, VirtualDesktop *current, Output *output, EffectWindow *with)
 {
     if (m_switchingActivity || (effects->hasActiveFullScreenEffect() && effects->activeFullScreenEffect() != this)) {
         return;
@@ -392,6 +392,7 @@ void SlideEffect::desktopChanged(VirtualDesktop *old, VirtualDesktop *current, E
         break;
     }
 
+    // TODO: VD: use output
     startAnimation(previousPos, current, with);
 }
 
@@ -428,6 +429,7 @@ void SlideEffect::desktopChangingCancelled()
     // If the fingers have been lifted and the current desktop didn't change, start animation
     // to move back to the original virtual desktop.
     if (effects->activeFullScreenEffect() == this) {
+        // TODO: VD: fix this
         startAnimation(m_gesturePos, effects->currentDesktop(), nullptr);
     }
 }
diff --git a/src/plugins/slide/slide.h b/src/plugins/slide/slide.h
index ca184a0a531..14033e84e75 100644
--- a/src/plugins/slide/slide.h
+++ b/src/plugins/slide/slide.h
@@ -73,7 +73,7 @@ public:
     bool slideBackground() const;
 
 private Q_SLOTS:
-    void desktopChanged(VirtualDesktop *old, VirtualDesktop *current, EffectWindow *with);
+    void desktopChanged(VirtualDesktop *old, VirtualDesktop *current, Output *output, EffectWindow *with);
     void desktopChanging(VirtualDesktop *old, QPointF desktopOffset, EffectWindow *with);
     void desktopChangingCancelled();
     void windowAdded(EffectWindow *w);
diff --git a/src/plugins/slidingpopups/slidingpopups.cpp b/src/plugins/slidingpopups/slidingpopups.cpp
index 6dacda9bac2..ed69215ad52 100644
--- a/src/plugins/slidingpopups/slidingpopups.cpp
+++ b/src/plugins/slidingpopups/slidingpopups.cpp
@@ -243,7 +243,7 @@ void SlidingPopupsEffect::slotWindowHiddenChanged(EffectWindow *w)
 
 void SlidingPopupsEffect::setupAnimData(EffectWindow *w)
 {
-    const QRectF screenRect = effects->clientArea(FullScreenArea, w->screen(), effects->currentDesktop());
+    const QRectF screenRect = effects->clientArea(FullScreenArea, w->screen(), effects->currentDesktop(w->screen()));
     const QRectF windowGeo = w->frameGeometry();
     AnimationData &animData = m_animationsData[w];
 
@@ -396,7 +396,7 @@ void SlidingPopupsEffect::setupInputPanelSlide()
 
 QRectF SlidingPopupsEffect::damagedLogicalArea(EffectWindow *w, const AnimationData animData)
 {
-    const QRectF screenRect = effects->clientArea(FullScreenArea, w->screen(), effects->currentDesktop());
+    const QRectF screenRect = effects->clientArea(FullScreenArea, w->screen(), effects->currentDesktop(w->screen()));
     qreal splitPoint = 0;
     const QRectF geo = w->expandedGeometry();
 
diff --git a/src/plugins/thumbnailaside/thumbnailaside.cpp b/src/plugins/thumbnailaside/thumbnailaside.cpp
index fb219714146..c6e9c96ba17 100644
--- a/src/plugins/thumbnailaside/thumbnailaside.cpp
+++ b/src/plugins/thumbnailaside/thumbnailaside.cpp
@@ -171,7 +171,7 @@ void ThumbnailAsideEffect::arrange()
     if (!effectiveScreen) {
         effectiveScreen = effects->activeScreen();
     }
-    QRectF area = effects->clientArea(MaximizeArea, effectiveScreen, effects->currentDesktop());
+    QRectF area = effects->clientArea(MaximizeArea, effectiveScreen, effects->currentDesktop(effectiveScreen));
     double scale = area.height() / double(height);
     scale = std::min(scale, maxwidth / double(mwidth)); // don't be wider than maxwidth pixels
     int add = 0;
diff --git a/src/workspace.cpp b/src/workspace.cpp
index cf7dd201825..6a6819daf5f 100644
--- a/src/workspace.cpp
+++ b/src/workspace.cpp
@@ -1027,8 +1027,7 @@ void Workspace::slotReconfigure()
 void Workspace::slotCurrentDesktopChanged(VirtualDesktop *oldDesktop, VirtualDesktop *newDesktop, Output *output)
 {
     updateWindowVisibilityAndActivateOnDesktopChange(newDesktop, output);
-    // TODO: VD: What about this?
-    Q_EMIT currentDesktopChanged(oldDesktop, m_moveResizeWindow);
+    Q_EMIT currentDesktopChanged(oldDesktop, newDesktop, output, m_moveResizeWindow);
 }
 
 void Workspace::slotCurrentDesktopChanging(VirtualDesktop *currentDesktop, QPointF offset)
diff --git a/src/workspace.h b/src/workspace.h
index fd33f69c919..741ba8c2e1a 100644
--- a/src/workspace.h
+++ b/src/workspace.h
@@ -548,7 +548,7 @@ Q_SIGNALS:
 
     // Signals required for the scripting interface
     void currentActivityChanged();
-    void currentDesktopChanged(KWin::VirtualDesktop *previousDesktop, KWin::Window *);
+    void currentDesktopChanged(KWin::VirtualDesktop *previousDesktop, KWin::VirtualDesktop *newDesktop, KWin::Output *output, KWin::Window *);
     void currentDesktopChanging(KWin::VirtualDesktop *currentDesktop, QPointF delta, KWin::Window *); // for realtime animations
     void currentDesktopChangingCancelled();
     void windowAdded(KWin::Window *);
-- 
GitLab


From 04dc9ebf172ba19e14c146d97ab6d31d3dfcd2d0 Mon Sep 17 00:00:00 2001
From: Hynek Schlindenbuch <schlindenbuch.h@seznam.cz>
Date: Fri, 28 Nov 2025 12:19:25 +0100
Subject: [PATCH 10/63] fix after rebase: Output -> LogicalOutput

---
 src/dbusinterface.cpp               |  2 +-
 src/effect/effecthandler.cpp        |  6 +++---
 src/effect/effecthandler.h          |  6 +++---
 src/plugins/slide/slide.cpp         |  2 +-
 src/plugins/slide/slide.h           |  2 +-
 src/scripting/workspace_wrapper.cpp |  4 ++--
 src/scripting/workspace_wrapper.h   |  2 +-
 src/virtualdesktops.cpp             | 10 +++++-----
 src/virtualdesktops.h               | 10 +++++-----
 src/workspace.cpp                   |  8 ++++----
 src/workspace.h                     | 10 +++++-----
 11 files changed, 31 insertions(+), 31 deletions(-)

diff --git a/src/dbusinterface.cpp b/src/dbusinterface.cpp
index 6cf68277bf4..ad0ae455607 100644
--- a/src/dbusinterface.cpp
+++ b/src/dbusinterface.cpp
@@ -299,7 +299,7 @@ VirtualDesktopManagerDBusInterface::VirtualDesktopManagerDBusInterface(VirtualDe
                                                  this);
 
     // TODO: VD: What about this?
-    connect(m_manager, &VirtualDesktopManager::currentChanged, this, [this](VirtualDesktop *oldDesktop, VirtualDesktop *newDesktop, Output *output) {
+    connect(m_manager, &VirtualDesktopManager::currentChanged, this, [this](VirtualDesktop *oldDesktop, VirtualDesktop *newDesktop, LogicalOutput *output) {
         if (output != workspace()->activeOutput()) {
             return;
         }
diff --git a/src/effect/effecthandler.cpp b/src/effect/effecthandler.cpp
index 6a1403f2e52..3b4dc303192 100644
--- a/src/effect/effecthandler.cpp
+++ b/src/effect/effecthandler.cpp
@@ -165,7 +165,7 @@ EffectsHandler::EffectsHandler(Compositor *compositor, WorkspaceScene *scene)
             Q_EMIT showingDesktopChanged(showing);
         }
     });
-    connect(ws, &Workspace::currentDesktopChanged, this, [this](VirtualDesktop *old, VirtualDesktop *newDesktop, Output *output, Window *window) {
+    connect(ws, &Workspace::currentDesktopChanged, this, [this](VirtualDesktop *old, VirtualDesktop *newDesktop, LogicalOutput *output, Window *window) {
         Q_EMIT desktopChanged(old, newDesktop, output, window ? window->effectWindow() : nullptr);
     });
     connect(ws, &Workspace::currentDesktopChanging, this, [this](VirtualDesktop *currentDesktop, QPointF offset, KWin::Window *window) {
@@ -835,7 +835,7 @@ QString EffectsHandler::currentActivity() const
 #endif
 }
 
-VirtualDesktop *EffectsHandler::currentDesktop(Output *output) const
+VirtualDesktop *EffectsHandler::currentDesktop(LogicalOutput *output) const
 {
     return VirtualDesktopManager::self()->currentDesktop(output);
 }
@@ -845,7 +845,7 @@ QList<VirtualDesktop *> EffectsHandler::desktops() const
     return VirtualDesktopManager::self()->desktops();
 }
 
-void EffectsHandler::setCurrentDesktop(VirtualDesktop *desktop, Output *output)
+void EffectsHandler::setCurrentDesktop(VirtualDesktop *desktop, LogicalOutput *output)
 {
     VirtualDesktopManager::self()->setCurrent(desktop, output);
 }
diff --git a/src/effect/effecthandler.h b/src/effect/effecthandler.h
index f05fb09e95b..8fe1b298c25 100644
--- a/src/effect/effecthandler.h
+++ b/src/effect/effecthandler.h
@@ -323,7 +323,7 @@ public:
     /**
      * @returns The current desktop on screen @a output.
      */
-    VirtualDesktop *currentDesktop(KWin::Output *output = nullptr) const;
+    VirtualDesktop *currentDesktop(KWin::LogicalOutput *output = nullptr) const;
     /**
      * @returns Total number of desktops currently in existence.
      */
@@ -331,7 +331,7 @@ public:
     /**
      * Set the current desktop to @a desktop on screen @a output.
      */
-    void setCurrentDesktop(KWin::VirtualDesktop *desktop, KWin::Output *output = nullptr);
+    void setCurrentDesktop(KWin::VirtualDesktop *desktop, KWin::LogicalOutput *output = nullptr);
     /**
      * @returns The size of desktop layout in grid units.
      */
@@ -786,7 +786,7 @@ Q_SIGNALS:
      * @param with The window which is taken over to the new desktop, can be NULL
      * @since 4.9
      */
-    void desktopChanged(KWin::VirtualDesktop *oldDesktop, KWin::VirtualDesktop *newDesktop, KWin::Output *output, KWin::EffectWindow *with);
+    void desktopChanged(KWin::VirtualDesktop *oldDesktop, KWin::VirtualDesktop *newDesktop, KWin::LogicalOutput *output, KWin::EffectWindow *with);
 
     /**
      * Signal emitted while desktop is changing for animation.
diff --git a/src/plugins/slide/slide.cpp b/src/plugins/slide/slide.cpp
index 01bd1934fa4..dda784047b1 100644
--- a/src/plugins/slide/slide.cpp
+++ b/src/plugins/slide/slide.cpp
@@ -369,7 +369,7 @@ void SlideEffect::finishedSwitching()
     effects->setActiveFullScreenEffect(nullptr);
 }
 
-void SlideEffect::desktopChanged(VirtualDesktop *old, VirtualDesktop *current, Output *output, EffectWindow *with)
+void SlideEffect::desktopChanged(VirtualDesktop *old, VirtualDesktop *current, LogicalOutput *output, EffectWindow *with)
 {
     if (m_switchingActivity || (effects->hasActiveFullScreenEffect() && effects->activeFullScreenEffect() != this)) {
         return;
diff --git a/src/plugins/slide/slide.h b/src/plugins/slide/slide.h
index 14033e84e75..ced77d76d99 100644
--- a/src/plugins/slide/slide.h
+++ b/src/plugins/slide/slide.h
@@ -73,7 +73,7 @@ public:
     bool slideBackground() const;
 
 private Q_SLOTS:
-    void desktopChanged(VirtualDesktop *old, VirtualDesktop *current, Output *output, EffectWindow *with);
+    void desktopChanged(VirtualDesktop *old, VirtualDesktop *current, LogicalOutput *output, EffectWindow *with);
     void desktopChanging(VirtualDesktop *old, QPointF desktopOffset, EffectWindow *with);
     void desktopChangingCancelled();
     void windowAdded(EffectWindow *w);
diff --git a/src/scripting/workspace_wrapper.cpp b/src/scripting/workspace_wrapper.cpp
index 07e4cb9e966..ca2fa817ce4 100644
--- a/src/scripting/workspace_wrapper.cpp
+++ b/src/scripting/workspace_wrapper.cpp
@@ -80,12 +80,12 @@ void WorkspaceWrapper::setCurrentDesktop(VirtualDesktop *desktop)
     VirtualDesktopManager::self()->setCurrent(desktop);
 }
 
-KWin::VirtualDesktop *WorkspaceWrapper::currentDesktopForScreen(KWin::Output *output) const
+KWin::VirtualDesktop *WorkspaceWrapper::currentDesktopForScreen(KWin::LogicalOutput *output) const
 {
     return VirtualDesktopManager::self()->currentDesktop(output);
 }
 
-void WorkspaceWrapper::setCurrentDesktopForScreen(KWin::VirtualDesktop *desktop, KWin::Output *output)
+void WorkspaceWrapper::setCurrentDesktopForScreen(KWin::VirtualDesktop *desktop, KWin::LogicalOutput *output)
 {
     if (!desktop) {
         qCWarning(KWIN_SCRIPTING) << "Invalid desktop passed to setCurrentDesktopForScreen";
diff --git a/src/scripting/workspace_wrapper.h b/src/scripting/workspace_wrapper.h
index 0fcbeab3759..ed23e76d67f 100644
--- a/src/scripting/workspace_wrapper.h
+++ b/src/scripting/workspace_wrapper.h
@@ -142,7 +142,7 @@ Q_SIGNALS:
     /**
      * This signal is emitted when the current virtual desktop changes.
      */
-    void currentDesktopChanged(KWin::VirtualDesktop *previous, KWin::VirtualDesktop *current, KWin::Output *output);
+    void currentDesktopChanged(KWin::VirtualDesktop *previous, KWin::VirtualDesktop *current, KWin::LogicalOutput *output);
 
     /**
      * This signal is emitted when the cursor position changes.
diff --git a/src/virtualdesktops.cpp b/src/virtualdesktops.cpp
index 0b929748123..9cdaba19486 100644
--- a/src/virtualdesktops.cpp
+++ b/src/virtualdesktops.cpp
@@ -100,14 +100,14 @@ void VirtualDesktopManager::setVirtualDesktopManagement(PlasmaVirtualDesktopMana
         removeVirtualDesktop(id);
     });
 
-    connect(this, &VirtualDesktopManager::currentChanged, m_virtualDesktopManagement, [this](VirtualDesktop *oldDesktop, VirtualDesktop *newDesktop, Output *output) {
+    connect(this, &VirtualDesktopManager::currentChanged, m_virtualDesktopManagement, [this](VirtualDesktop *oldDesktop, VirtualDesktop *newDesktop, LogicalOutput *output) {
         // TODO: VD: is this correct?
         if (output != workspace()->activeOutput()) {
             return;
         }
         updatePlasmaVirtualDesktops(newDesktop);
     });
-    connect(workspace(), &Workspace::activeOutputChanged, this, [this](Output *output) {
+    connect(workspace(), &Workspace::activeOutputChanged, this, [this](LogicalOutput *output) {
         updatePlasmaVirtualDesktops(currentDesktop(output));
     });
 
@@ -575,7 +575,7 @@ uint VirtualDesktopManager::current() const
     return d ? d->x11DesktopNumber() : 0;
 }
 
-VirtualDesktop *VirtualDesktopManager::currentDesktop(Output *output) const
+VirtualDesktop *VirtualDesktopManager::currentDesktop(LogicalOutput *output) const
 {
     if (!output) {
         output = workspace()->activeOutput();
@@ -585,7 +585,7 @@ VirtualDesktop *VirtualDesktopManager::currentDesktop(Output *output) const
     return result ? result : m_desktops.at(0);
 }
 
-bool VirtualDesktopManager::setCurrent(uint newDesktop, Output *output)
+bool VirtualDesktopManager::setCurrent(uint newDesktop, LogicalOutput *output)
 {
     if (newDesktop < 1 || newDesktop > count()) {
         return false;
@@ -596,7 +596,7 @@ bool VirtualDesktopManager::setCurrent(uint newDesktop, Output *output)
 }
 
 // TODO: VD: Go through all usages and make sure that they're OK.
-bool VirtualDesktopManager::setCurrent(VirtualDesktop *newDesktop, Output *output)
+bool VirtualDesktopManager::setCurrent(VirtualDesktop *newDesktop, LogicalOutput *output)
 {
     Q_ASSERT(newDesktop);
     if (!output) {
diff --git a/src/virtualdesktops.h b/src/virtualdesktops.h
index 3cf3c0947eb..b87c406c4d0 100644
--- a/src/virtualdesktops.h
+++ b/src/virtualdesktops.h
@@ -201,7 +201,7 @@ public:
      * @see setCurrent
      * @see currentChanged
      */
-    VirtualDesktop *currentDesktop(Output *output = nullptr) const;
+    VirtualDesktop *currentDesktop(LogicalOutput *output = nullptr) const;
 
     /**
      * Moves to the desktop through the algorithm described by Direction.
@@ -359,7 +359,7 @@ public Q_SLOTS:
      * @see currentChanged
      * @see moveTo
      */
-    bool setCurrent(uint current, Output *output = nullptr);
+    bool setCurrent(uint current, LogicalOutput *output = nullptr);
 
     /**
      * Set the current desktop to @a current.
@@ -368,7 +368,7 @@ public Q_SLOTS:
      * @see currentChanged
      * @see moveTo
      */
-    bool setCurrent(VirtualDesktop *current, Output *output = nullptr);
+    bool setCurrent(VirtualDesktop *current, LogicalOutput *output = nullptr);
 
     /**
      * Updates the layout to a new number of rows. The number of columns will be calculated accordingly
@@ -442,7 +442,7 @@ Q_SIGNALS:
      * @param previousDesktop The virtual desktop changed from
      * @param newDesktop The virtual desktop changed to
      */
-    void currentChanged(KWin::VirtualDesktop *previousDesktop, KWin::VirtualDesktop *newDesktop, KWin::Output *output);
+    void currentChanged(KWin::VirtualDesktop *previousDesktop, KWin::VirtualDesktop *newDesktop, KWin::LogicalOutput *output);
 
     /**
      * Signal emitted for realtime desktop switching animations.
@@ -559,7 +559,7 @@ private:
     QAction *addAction(const QString &name, const QString &label, const QKeySequence &key, void (VirtualDesktopManager::*slot)());
 
     QList<VirtualDesktop *> m_desktops;
-    QHash<Output *, VirtualDesktop *> m_currentDesktops;
+    QHash<LogicalOutput *, VirtualDesktop *> m_currentDesktops;
     quint32 m_rows = 2;
     bool m_navigationWrapsAround;
     bool m_perOutputVirtualDesktops;
diff --git a/src/workspace.cpp b/src/workspace.cpp
index 6a6819daf5f..da0d4e8417d 100644
--- a/src/workspace.cpp
+++ b/src/workspace.cpp
@@ -1024,7 +1024,7 @@ void Workspace::slotReconfigure()
     }
 }
 
-void Workspace::slotCurrentDesktopChanged(VirtualDesktop *oldDesktop, VirtualDesktop *newDesktop, Output *output)
+void Workspace::slotCurrentDesktopChanged(VirtualDesktop *oldDesktop, VirtualDesktop *newDesktop, LogicalOutput *output)
 {
     updateWindowVisibilityAndActivateOnDesktopChange(newDesktop, output);
     Q_EMIT currentDesktopChanged(oldDesktop, newDesktop, output, m_moveResizeWindow);
@@ -1041,7 +1041,7 @@ void Workspace::slotCurrentDesktopChangingCancelled()
     Q_EMIT currentDesktopChangingCancelled();
 }
 
-void Workspace::updateWindowVisibilityOnDesktopChange(VirtualDesktop *newDesktop, Output *output)
+void Workspace::updateWindowVisibilityOnDesktopChange(VirtualDesktop *newDesktop, LogicalOutput *output)
 {
 #if KWIN_BUILD_X11
     for (auto it = stacking_order.constBegin(); it != stacking_order.constEnd(); ++it) {
@@ -1082,7 +1082,7 @@ void Workspace::updateWindowVisibilityOnDesktopChange(VirtualDesktop *newDesktop
     }
 }
 
-void Workspace::updateWindowVisibilityAndActivateOnDesktopChange(VirtualDesktop *newDesktop, Output *output)
+void Workspace::updateWindowVisibilityAndActivateOnDesktopChange(VirtualDesktop *newDesktop, LogicalOutput *output)
 {
     closeActivePopup();
     ++block_focus;
@@ -1180,7 +1180,7 @@ void Workspace::updateCurrentActivity(const QString &new_activity)
         return;
     }
 
-    for (Output *output : std::as_const(m_outputs)) {
+    for (LogicalOutput *output : std::as_const(m_outputs)) {
         updateWindowVisibilityAndActivateOnDesktopChange(VirtualDesktopManager::self()->currentDesktop(output), output);
     }
 
diff --git a/src/workspace.h b/src/workspace.h
index 741ba8c2e1a..4f3e940cb9e 100644
--- a/src/workspace.h
+++ b/src/workspace.h
@@ -531,7 +531,7 @@ private Q_SLOTS:
     void slotReloadConfig();
     void updateCurrentActivity(const QString &new_activity);
     // virtual desktop handling
-    void slotCurrentDesktopChanged(VirtualDesktop *previousDesktop, VirtualDesktop *newDesktop, Output *output);
+    void slotCurrentDesktopChanged(VirtualDesktop *previousDesktop, VirtualDesktop *newDesktop, LogicalOutput *output);
     void slotCurrentDesktopChanging(VirtualDesktop *currentDesktop, QPointF delta);
     void slotCurrentDesktopChangingCancelled();
     void slotDesktopAdded(VirtualDesktop *desktop);
@@ -548,7 +548,7 @@ Q_SIGNALS:
 
     // Signals required for the scripting interface
     void currentActivityChanged();
-    void currentDesktopChanged(KWin::VirtualDesktop *previousDesktop, KWin::VirtualDesktop *newDesktop, KWin::Output *output, KWin::Window *);
+    void currentDesktopChanged(KWin::VirtualDesktop *previousDesktop, KWin::VirtualDesktop *newDesktop, KWin::LogicalOutput *output, KWin::Window *);
     void currentDesktopChanging(KWin::VirtualDesktop *currentDesktop, QPointF delta, KWin::Window *); // for realtime animations
     void currentDesktopChangingCancelled();
     void windowAdded(KWin::Window *);
@@ -565,7 +565,7 @@ Q_SIGNALS:
     void outputAdded(KWin::LogicalOutput *);
     void outputRemoved(KWin::LogicalOutput *);
     void outputsChanged();
-    void activeOutputChanged(KWin::Output *);
+    void activeOutputChanged(KWin::LogicalOutput *);
     /**
      * This signal is emitted when the stacking order changed, i.e. a window is risen
      * or lowered
@@ -615,8 +615,8 @@ private:
     //---------------------------------------------------------------------
 
     void closeActivePopup();
-    void updateWindowVisibilityOnDesktopChange(VirtualDesktop *newDesktop, Output *output);
-    void updateWindowVisibilityAndActivateOnDesktopChange(VirtualDesktop *newDesktop, Output *output);
+    void updateWindowVisibilityOnDesktopChange(VirtualDesktop *newDesktop, LogicalOutput *output);
+    void updateWindowVisibilityAndActivateOnDesktopChange(VirtualDesktop *newDesktop, LogicalOutput *output);
     void activateWindowOnDesktop(VirtualDesktop *desktop);
     Window *findWindowToActivateOnDesktop(VirtualDesktop *desktop);
     void removeWindow(Window *window);
-- 
GitLab


From e3889b9680325229a7e9d82438633e8b53f08169 Mon Sep 17 00:00:00 2001
From: Hynek Schlindenbuch <schlindenbuch.h@seznam.cz>
Date: Fri, 28 Nov 2025 14:01:03 +0100
Subject: [PATCH 11/63] WIP: implement per-screen slide effect

---
 src/plugins/slide/slide.cpp | 211 ++++++++++++++++++++++++++++--------
 src/plugins/slide/slide.h   | 108 ++++++++++++------
 2 files changed, 244 insertions(+), 75 deletions(-)

diff --git a/src/plugins/slide/slide.cpp b/src/plugins/slide/slide.cpp
index dda784047b1..ec434b9de0d 100644
--- a/src/plugins/slide/slide.cpp
+++ b/src/plugins/slide/slide.cpp
@@ -22,6 +22,14 @@
 
 namespace KWin
 {
+SlideEffectScreen::SlideEffectScreen(SlideEffect *parent, LogicalOutput *screen)
+    : m_parent(parent)
+    , m_screen(screen)
+{
+    // Visible desktops have to be initialized to make sure that this screen is not blank while the slide effect
+    // is running on another screen. See paintWindow.
+    m_paintCtx.visibleDesktops << effects->currentDesktop(screen);
+}
 
 SlideEffect::SlideEffect()
 {
@@ -42,16 +50,24 @@ SlideEffect::SlideEffect()
             this, &SlideEffect::finishedSwitching);
     connect(effects, &EffectsHandler::desktopRemoved,
             this, &SlideEffect::finishedSwitching);
-    connect(effects, &EffectsHandler::screenAdded,
-            this, &SlideEffect::finishedSwitching);
-    connect(effects, &EffectsHandler::screenRemoved,
-            this, &SlideEffect::finishedSwitching);
+    connect(effects, &EffectsHandler::screenAdded, this, [this](LogicalOutput *screen) {
+        m_slideEffectScreens.tryEmplace(screen, this, screen);
+        finishedSwitching();
+    });
+    connect(effects, &EffectsHandler::screenRemoved, this, [this](LogicalOutput *screen) {
+        m_slideEffectScreens.remove(screen);
+        finishedSwitching();
+    });
     connect(effects, &EffectsHandler::currentActivityAboutToChange, this, [this]() {
         m_switchingActivity = true;
     });
     connect(effects, &EffectsHandler::currentActivityChanged, this, [this]() {
         m_switchingActivity = false;
     });
+
+    for (LogicalOutput *screen : effects->screens()) {
+        m_slideEffectScreens.emplace(screen, this, screen);
+    }
 }
 
 SlideEffect::~SlideEffect()
@@ -59,6 +75,11 @@ SlideEffect::~SlideEffect()
     finishedSwitching();
 }
 
+SlideEffectScreen::~SlideEffectScreen()
+{
+    finishedSwitching();
+}
+
 bool SlideEffect::supported()
 {
     return effects->animationsSupported();
@@ -71,6 +92,7 @@ void SlideEffect::reconfigure(ReconfigureFlags)
     const qreal springConstant = 300.0 / effects->animationTimeFactor();
     const qreal dampingRatio = 1.1;
 
+    // TODO: VD: Apply this to effect screens instead.
     m_motionX = SpringMotion(springConstant, dampingRatio);
     m_motionY = SpringMotion(springConstant, dampingRatio);
 
@@ -100,6 +122,16 @@ inline Region buildClipRegion(const QPoint &pos, int w, int h)
 
 void SlideEffect::prePaintScreen(ScreenPrePaintData &data, std::chrono::milliseconds presentTime)
 {
+    for (auto &effectScreen : m_slideEffectScreens) {
+        effectScreen.prePaintScreen(data, presentTime);
+    }
+}
+
+void SlideEffectScreen::prePaintScreen(ScreenPrePaintData &data, std::chrono::milliseconds presentTime)
+{
+    if (m_state == State::Inactive) {
+        return;
+    }
     const QList<VirtualDesktop *> desktops = effects->desktops();
     const int w = effects->desktopGridWidth();
     const int h = effects->desktopGridHeight();
@@ -149,22 +181,27 @@ void SlideEffect::prePaintScreen(ScreenPrePaintData &data, std::chrono::millisec
         }
     }
 
-    data.mask |= PAINT_SCREEN_TRANSFORMED | PAINT_SCREEN_BACKGROUND_FIRST;
+    data.mask |= Effect::PAINT_SCREEN_TRANSFORMED | Effect::PAINT_SCREEN_BACKGROUND_FIRST;
 
     effects->prePaintScreen(data, presentTime);
 }
 
 void SlideEffect::paintScreen(const RenderTarget &renderTarget, const RenderViewport &viewport, int mask, const Region &deviceRegion, LogicalOutput *screen)
+{
+    getOrCreateSlideEffectScreen(screen).paintScreen(renderTarget, viewport, mask, deviceRegion);
+}
+
+void SlideEffectScreen::paintScreen(const RenderTarget &renderTarget, const RenderViewport &viewport, int mask, const QRegion &deviceRegion)
 {
     m_paintCtx.wrap = effects->optionRollOverDesktops();
-    effects->paintScreen(renderTarget, viewport, mask, deviceRegion, screen);
+    effects->paintScreen(renderTarget, viewport, mask, deviceRegion, m_screen);
 }
 
-QPoint SlideEffect::getDrawCoords(QPointF pos, LogicalOutput *screen)
+QPoint SlideEffectScreen::getDrawCoords(QPointF pos, LogicalOutput *screen)
 {
     QPoint c = QPoint();
-    c.setX(pos.x() * (screen->geometry().width() + m_hGap));
-    c.setY(pos.y() * (screen->geometry().height() + m_vGap));
+    c.setX(pos.x() * (screen->geometry().width() + m_parent->horizontalGap()));
+    c.setY(pos.y() * (screen->geometry().height() + m_parent->verticalGap()));
     return c;
 }
 
@@ -172,11 +209,11 @@ QPoint SlideEffect::getDrawCoords(QPointF pos, LogicalOutput *screen)
  * Decide whether given window @p w should be transformed/translated.
  * @returns @c true if given window @p w should be transformed, otherwise @c false
  */
-bool SlideEffect::isTranslated(const EffectWindow *w) const
+bool SlideEffectScreen::isTranslated(const EffectWindow *w) const
 {
     if (w->isOnAllDesktops()) {
         if (w->isDesktop()) {
-            return m_slideBackground;
+            return m_parent->slideBackground();
         }
         return false;
     } else if (w == m_movingWindow) {
@@ -188,7 +225,7 @@ bool SlideEffect::isTranslated(const EffectWindow *w) const
 /**
  * Will a window be painted during this frame?
  */
-bool SlideEffect::willBePainted(const EffectWindow *w) const
+bool SlideEffectScreen::willBePainted(const EffectWindow *w) const
 {
     if (w->isOnAllDesktops()) {
         return true;
@@ -211,6 +248,11 @@ void SlideEffect::prePaintWindow(RenderView *view, EffectWindow *w, WindowPrePai
 }
 
 void SlideEffect::paintWindow(const RenderTarget &renderTarget, const RenderViewport &viewport, EffectWindow *w, int mask, const Region &deviceGeometry, WindowPaintData &data)
+{
+    getOrCreateSlideEffectScreen(w->screen()).paintWindow(renderTarget, viewport, w, mask, deviceGeometry, data);
+}
+
+void SlideEffectScreen::paintWindow(const RenderTarget &renderTarget, const RenderViewport &viewport, EffectWindow *w, int mask, const QRegion &deviceGeometry, WindowPaintData &data)
 {
     if (!willBePainted(w)) {
         return;
@@ -246,39 +288,55 @@ void SlideEffect::paintWindow(const RenderTarget &renderTarget, const RenderView
             desktopTranslation = QPointF(desktopTranslation.x(), desktopTranslation.y() + gridHeight);
         }
 
-        for (LogicalOutput *screen : screens) {
-            QPoint drawTranslation = getDrawCoords(desktopTranslation, screen);
-            data += drawTranslation;
+        QPoint drawTranslation = getDrawCoords(desktopTranslation, m_screen);
+        data += drawTranslation;
 
-            const Rect screenArea = screen->geometry();
-            const Rect logicalDamage = screenArea.translated(drawTranslation).intersected(screenArea);
+        const Rect screenArea = m_screen->geometry();
+        const Rect logicalDamage = screenArea.translated(drawTranslation).intersected(screenArea);
 
-            effects->paintWindow(
-                renderTarget, viewport, w, mask,
-                // Only paint the region that intersects the current screen and desktop.
-                deviceGeometry.intersected(viewport.mapToDeviceCoordinatesAligned(logicalDamage)),
-                data);
+        effects->paintWindow(
+            renderTarget, viewport, w, mask,
+            // Only paint the region that intersects the current screen and desktop.
+            deviceGeometry.intersected(viewport.mapToDeviceCoordinatesAligned(logicalDamage)),
+            data);
 
-            // Undo the translation for the next screen. I know, it hurts me too.
-            data += QPoint(-drawTranslation.x(), -drawTranslation.y());
-        }
+        // TODO: VD: Will this work, and is it even necessary anymore?
+        // Undo the translation for the next screen. I know, it hurts me too.
+        data += QPoint(-drawTranslation.x(), -drawTranslation.y());
     }
 }
 
 void SlideEffect::postPaintScreen()
 {
+    bool allInactive = true;
+    for (SlideEffectScreen &screenEffect : m_slideEffectScreens) {
+        screenEffect.postPaintScreen();
+        if (screenEffect.isActive()) {
+            allInactive = false;
+        }
+    }
+    if (allInactive) {
+        finishedSwitching();
+    }
+    effects->postPaintScreen();
+}
+
+void SlideEffectScreen::postPaintScreen()
+{
+    if (m_state == State::Inactive) {
+        return;
+    }
     if (m_state == State::ActiveAnimation && !m_motionX.isMoving() && !m_motionY.isMoving()) {
         finishedSwitching();
     }
 
-    effects->addRepaintFull();
-    effects->postPaintScreen();
+    effects->addRepaint(m_screen->geometry());
 }
 
 /*
  * Negative desktop positions aren't allowed.
  */
-QPointF SlideEffect::forcePositivePosition(QPointF p) const
+QPointF SlideEffectScreen::forcePositivePosition(QPointF p) const
 {
     if (p.x() < 0) {
         p.setX(p.x() + std::ceil(-p.x() / effects->desktopGridWidth()) * effects->desktopGridWidth());
@@ -289,7 +347,7 @@ QPointF SlideEffect::forcePositivePosition(QPointF p) const
     return p;
 }
 
-bool SlideEffect::shouldElevate(const EffectWindow *w) const
+bool SlideEffectScreen::shouldElevate(const EffectWindow *w) const
 {
     // Static docks(i.e. this effect doesn't slide docks) should be elevated
     // so they can properly animate themselves when an user enters or leaves
@@ -302,7 +360,7 @@ bool SlideEffect::shouldElevate(const EffectWindow *w) const
  * Called AFTER the gesture is released.
  * Sets up animation to round off to the new current desktop.
  */
-void SlideEffect::startAnimation(const QPointF &oldPos, VirtualDesktop *current, EffectWindow *movingWindow)
+void SlideEffectScreen::startAnimation(const QPointF &oldPos, VirtualDesktop *current, EffectWindow *movingWindow)
 {
     if (m_state == State::Inactive) {
         prepareSwitching();
@@ -324,17 +382,21 @@ void SlideEffect::startAnimation(const QPointF &oldPos, VirtualDesktop *current,
     m_motionY.setPosition(m_startPos.y() * virtualSpaceSize.height());
     m_lastPresentTime = std::chrono::milliseconds::zero();
 
-    effects->setActiveFullScreenEffect(this);
-    effects->addRepaintFull();
+    effects->addRepaint(m_screen->geometry());
 }
 
-void SlideEffect::prepareSwitching()
+void SlideEffectScreen::prepareSwitching()
 {
     const auto windows = effects->stackingOrder();
     m_windowData.reserve(windows.count());
 
     for (EffectWindow *w : windows) {
+        // TODO: VD: Is this correct?
+        if (w->screen() != m_screen) {
+            continue;
+        }
         m_windowData[w] = WindowData{
+            .deleteRef = EffectWindowDeletedRef(w),
             .visibilityRef = EffectWindowVisibleRef(w, EffectWindow::PAINT_DISABLED_BY_DESKTOP),
         };
 
@@ -348,12 +410,31 @@ void SlideEffect::prepareSwitching()
 }
 
 void SlideEffect::finishedSwitching()
+{
+    bool isAnyActive = false;
+    for (SlideEffectScreen &effectScreen : m_slideEffectScreens) {
+        if (!effectScreen.isActive()) {
+            continue;
+        }
+        isAnyActive = true;
+        effectScreen.finishedSwitching();
+    }
+    if (!isAnyActive) {
+        effects->setActiveFullScreenEffect(nullptr);
+    }
+}
+
+void SlideEffectScreen::finishedSwitching()
 {
     if (m_state == State::Inactive) {
         return;
     }
     const QList<EffectWindow *> windows = effects->stackingOrder();
     for (EffectWindow *w : windows) {
+        // TODO: VD: Is this correct?
+        if (w->screen() != m_screen) {
+            continue;
+        }
         w->setData(WindowForceBackgroundContrastRole, QVariant());
         w->setData(WindowForceBlurRole, QVariant());
     }
@@ -363,18 +444,27 @@ void SlideEffect::finishedSwitching()
     }
     m_elevatedWindows.clear();
 
-    m_windowData.clear();
+    // Clearing the window data may trigger windowDeleted which would try to delete the windows again. So it's necessary to reinitilize m_windowData first.
+    // TODO: VD: Now that I have the deleteRef, can't I just get rid of the problematic windowDeleted code and fix it that way?
+    QHash<EffectWindow *, WindowData> tmpWindowData;
+    tmpWindowData.swap(m_windowData);
+    tmpWindowData.clear();
     m_movingWindow = nullptr;
     m_state = State::Inactive;
-    effects->setActiveFullScreenEffect(nullptr);
 }
 
-void SlideEffect::desktopChanged(VirtualDesktop *old, VirtualDesktop *current, LogicalOutput *output, EffectWindow *with)
+void SlideEffect::desktopChanged(VirtualDesktop *old, VirtualDesktop *current, LogicalOutput *screen, EffectWindow *with)
 {
     if (m_switchingActivity || (effects->hasActiveFullScreenEffect() && effects->activeFullScreenEffect() != this)) {
         return;
     }
 
+    getOrCreateSlideEffectScreen(screen).desktopChanged(old, current, with);
+    effects->setActiveFullScreenEffect(this);
+}
+
+void SlideEffectScreen::desktopChanged(VirtualDesktop *old, VirtualDesktop *current, EffectWindow *with)
+{
     QPointF previousPos;
     switch (m_state) {
     case State::Inactive:
@@ -392,7 +482,6 @@ void SlideEffect::desktopChanged(VirtualDesktop *old, VirtualDesktop *current, L
         break;
     }
 
-    // TODO: VD: use output
     startAnimation(previousPos, current, with);
 }
 
@@ -402,6 +491,13 @@ void SlideEffect::desktopChanging(VirtualDesktop *old, QPointF desktopOffset, Ef
         return;
     }
 
+    // TODO: VD: Apply it to all/one screen depending on settings.
+    getOrCreateSlideEffectScreen(effects->activeScreen()).desktopChanging(old, desktopOffset, with);
+    effects->setActiveFullScreenEffect(this);
+}
+
+void SlideEffectScreen::desktopChanging(VirtualDesktop *old, QPointF desktopOffset, EffectWindow *with)
+{
     if (m_state == State::Inactive) {
         prepareSwitching();
     }
@@ -420,8 +516,7 @@ void SlideEffect::desktopChanging(VirtualDesktop *old, QPointF desktopOffset, Ef
         m_gesturePos = moveInsideDesktopGrid(m_gesturePos);
     }
 
-    effects->setActiveFullScreenEffect(this);
-    effects->addRepaintFull();
+    effects->addRepaint(m_screen->geometry());
 }
 
 void SlideEffect::desktopChangingCancelled()
@@ -429,12 +524,17 @@ void SlideEffect::desktopChangingCancelled()
     // If the fingers have been lifted and the current desktop didn't change, start animation
     // to move back to the original virtual desktop.
     if (effects->activeFullScreenEffect() == this) {
-        // TODO: VD: fix this
-        startAnimation(m_gesturePos, effects->currentDesktop(), nullptr);
+        // TODO: VD: Apply it to all/one screen depending on settings.
+        getOrCreateSlideEffectScreen(effects->activeScreen()).desktopChangingCancelled();
     }
 }
 
-QPointF SlideEffect::moveInsideDesktopGrid(QPointF p)
+void SlideEffectScreen::desktopChangingCancelled()
+{
+    startAnimation(m_gesturePos, effects->currentDesktop(m_screen), nullptr);
+}
+
+QPointF SlideEffectScreen::moveInsideDesktopGrid(QPointF p)
 {
     if (p.x() < 0) {
         p.setX(0);
@@ -452,6 +552,14 @@ QPointF SlideEffect::moveInsideDesktopGrid(QPointF p)
 }
 
 void SlideEffect::windowAdded(EffectWindow *w)
+{
+    if (!w) {
+        return;
+    }
+    getOrCreateSlideEffectScreen(w->screen()).windowAdded(w);
+}
+
+void SlideEffectScreen::windowAdded(EffectWindow *w)
 {
     if (m_state == State::Inactive) {
         return;
@@ -464,11 +572,20 @@ void SlideEffect::windowAdded(EffectWindow *w)
     w->setData(WindowForceBlurRole, QVariant(true));
 
     m_windowData[w] = WindowData{
+        .deleteRef = EffectWindowDeletedRef(w),
         .visibilityRef = EffectWindowVisibleRef(w, EffectWindow::PAINT_DISABLED_BY_DESKTOP),
     };
 }
 
 void SlideEffect::windowDeleted(EffectWindow *w)
+{
+    if (!w) {
+        return;
+    }
+    getOrCreateSlideEffectScreen(w->screen()).windowDeleted(w);
+}
+
+void SlideEffectScreen::windowDeleted(EffectWindow *w)
 {
     if (m_state == State::Inactive) {
         return;
@@ -485,7 +602,7 @@ void SlideEffect::windowDeleted(EffectWindow *w)
  * This function decides when it's better to wrap around the grid or not.
  * Only call if wrapping is enabled.
  */
-void SlideEffect::optimizePath()
+void SlideEffectScreen::optimizePath()
 {
     int w = effects->desktopGridWidth();
     int h = effects->desktopGridHeight();
@@ -545,13 +662,19 @@ void SlideEffect::optimizePath()
  * This function finds the true fastest path, regardless of which direction the animation is already going;
  * I was a little upset about this limitation until I realized that MacOS can't even wrap desktops :)
  */
-QPointF SlideEffect::constrainToDrawableRange(QPointF p)
+QPointF SlideEffectScreen::constrainToDrawableRange(QPointF p)
 {
     p.setX(fmod(p.x(), effects->desktopGridWidth()));
     p.setY(fmod(p.y(), effects->desktopGridHeight()));
     return p;
 }
 
+SlideEffectScreen &SlideEffect::getOrCreateSlideEffectScreen(LogicalOutput *screen)
+{
+    auto result = m_slideEffectScreens.tryEmplace(screen, this, screen);
+    return result.iterator.value();
+}
+
 } // namespace KWin
 
 #include "moc_slide.cpp"
diff --git a/src/plugins/slide/slide.h b/src/plugins/slide/slide.h
index ced77d76d99..02dcc10bc00 100644
--- a/src/plugins/slide/slide.h
+++ b/src/plugins/slide/slide.h
@@ -43,41 +43,27 @@ namespace KWin
  * Good luck :)
  */
 
-class SlideEffect : public Effect
-{
-    Q_OBJECT
-    Q_PROPERTY(int horizontalGap READ horizontalGap)
-    Q_PROPERTY(int verticalGap READ verticalGap)
-    Q_PROPERTY(bool slideBackground READ slideBackground)
+class SlideEffect;
 
+class SlideEffectScreen
+{
 public:
-    SlideEffect();
-    ~SlideEffect() override;
-
-    void reconfigure(ReconfigureFlags) override;
+    SlideEffectScreen(SlideEffect *parent, LogicalOutput *screen);
+    ~SlideEffectScreen();
 
-    void prePaintScreen(ScreenPrePaintData &data, std::chrono::milliseconds presentTime) override;
-    void paintScreen(const RenderTarget &renderTarget, const RenderViewport &viewport, int mask, const Region &deviceRegion, LogicalOutput *screen) override;
-    void postPaintScreen() override;
+    void prePaintScreen(ScreenPrePaintData &data, std::chrono::milliseconds presentTime);
+    void paintScreen(const RenderTarget &renderTarget, const RenderViewport &viewport, int mask, const QRegion &deviceRegion);
+    void postPaintScreen();
 
-    void prePaintWindow(RenderView *view, EffectWindow *w, WindowPrePaintData &data, std::chrono::milliseconds presentTime) override;
-    void paintWindow(const RenderTarget &renderTarget, const RenderViewport &viewport, EffectWindow *w, int mask, const Region &deviceGeometry, WindowPaintData &data) override;
+    void paintWindow(const RenderTarget &renderTarget, const RenderViewport &viewport, EffectWindow *w, int mask, const QRegion &deviceGeometry, WindowPaintData &data);
 
-    bool isActive() const override;
-    int requestedEffectChainPosition() const override;
-
-    static bool supported();
-
-    int horizontalGap() const;
-    int verticalGap() const;
-    bool slideBackground() const;
-
-private Q_SLOTS:
-    void desktopChanged(VirtualDesktop *old, VirtualDesktop *current, LogicalOutput *output, EffectWindow *with);
+    bool isActive() const;
+    void desktopChanged(VirtualDesktop *old, VirtualDesktop *current, EffectWindow *with);
     void desktopChanging(VirtualDesktop *old, QPointF desktopOffset, EffectWindow *with);
     void desktopChangingCancelled();
     void windowAdded(EffectWindow *w);
     void windowDeleted(EffectWindow *w);
+    void finishedSwitching();
 
 private:
     QPoint getDrawCoords(QPointF pos, LogicalOutput *screen);
@@ -91,13 +77,8 @@ private:
 
     void startAnimation(const QPointF &oldPos, VirtualDesktop *current, EffectWindow *movingWindow = nullptr);
     void prepareSwitching();
-    void finishedSwitching();
 
 private:
-    int m_hGap;
-    int m_vGap;
-    bool m_slideBackground;
-
     enum class State {
         Inactive,
         ActiveAnimation,
@@ -127,12 +108,67 @@ private:
 
     struct WindowData
     {
+        EffectWindowDeletedRef deleteRef;
         EffectWindowVisibleRef visibilityRef;
     };
 
     QList<EffectWindow *> m_elevatedWindows;
     QHash<EffectWindow *, WindowData> m_windowData;
     bool m_switchingActivity = false;
+    SlideEffect *m_parent;
+    LogicalOutput *m_screen;
+};
+
+class SlideEffect : public Effect
+{
+    Q_OBJECT
+    Q_PROPERTY(int horizontalGap READ horizontalGap)
+    Q_PROPERTY(int verticalGap READ verticalGap)
+    Q_PROPERTY(bool slideBackground READ slideBackground)
+
+public:
+    SlideEffect();
+    ~SlideEffect() override;
+
+    void reconfigure(ReconfigureFlags) override;
+
+    void prePaintScreen(ScreenPrePaintData &data, std::chrono::milliseconds presentTime) override;
+    void paintScreen(const RenderTarget &renderTarget, const RenderViewport &viewport, int mask, const Region &deviceRegion, LogicalOutput *screen) override;
+    void postPaintScreen() override;
+
+    void prePaintWindow(RenderView *view, EffectWindow *w, WindowPrePaintData &data, std::chrono::milliseconds presentTime) override;
+    void paintWindow(const RenderTarget &renderTarget, const RenderViewport &viewport, EffectWindow *w, int mask, const Region &deviceGeometry, WindowPaintData &data) override;
+
+    bool isActive() const override;
+    int requestedEffectChainPosition() const override;
+
+    static bool supported();
+
+    int horizontalGap() const;
+    int verticalGap() const;
+    bool slideBackground() const;
+
+private Q_SLOTS:
+    void desktopChanged(VirtualDesktop *old, VirtualDesktop *current, LogicalOutput *output, EffectWindow *with);
+    void desktopChanging(VirtualDesktop *old, QPointF desktopOffset, EffectWindow *with);
+    void desktopChangingCancelled();
+    void windowAdded(EffectWindow *w);
+    void windowDeleted(EffectWindow *w);
+
+private:
+    void finishedSwitching();
+    SlideEffectScreen &getOrCreateSlideEffectScreen(LogicalOutput *screen);
+
+private:
+    int m_hGap;
+    int m_vGap;
+    bool m_slideBackground;
+
+    SpringMotion m_motionX;
+    SpringMotion m_motionY;
+
+    bool m_switchingActivity = false;
+    QHash<LogicalOutput *, SlideEffectScreen> m_slideEffectScreens;
 };
 
 inline int SlideEffect::horizontalGap() const
@@ -151,6 +187,16 @@ inline bool SlideEffect::slideBackground() const
 }
 
 inline bool SlideEffect::isActive() const
+{
+    for (const SlideEffectScreen &effectScreen : std::as_const(m_slideEffectScreens)) {
+        if (effectScreen.isActive()) {
+            return true;
+        }
+    }
+    return false;
+}
+
+inline bool SlideEffectScreen::isActive() const
 {
     return m_state != State::Inactive;
 }
-- 
GitLab


From 3b3d0308543054ef8107138516242dc6f5d54e77 Mon Sep 17 00:00:00 2001
From: Hynek Schlindenbuch <schlindenbuch.h@seznam.cz>
Date: Fri, 28 Nov 2025 14:11:06 +0100
Subject: [PATCH 12/63] apply animation speed setting to per-screen slide
 effect

---
 src/plugins/slide/slide.cpp | 17 ++++++++++++-----
 src/plugins/slide/slide.h   |  1 +
 2 files changed, 13 insertions(+), 5 deletions(-)

diff --git a/src/plugins/slide/slide.cpp b/src/plugins/slide/slide.cpp
index ec434b9de0d..51ce8b69c37 100644
--- a/src/plugins/slide/slide.cpp
+++ b/src/plugins/slide/slide.cpp
@@ -29,6 +29,7 @@ SlideEffectScreen::SlideEffectScreen(SlideEffect *parent, LogicalOutput *screen)
     // Visible desktops have to be initialized to make sure that this screen is not blank while the slide effect
     // is running on another screen. See paintWindow.
     m_paintCtx.visibleDesktops << effects->currentDesktop(screen);
+    reconfigure();
 }
 
 SlideEffect::SlideEffect()
@@ -89,16 +90,22 @@ void SlideEffect::reconfigure(ReconfigureFlags)
 {
     SlideConfig::self()->read();
 
+    for (SlideEffectScreen &screen : m_slideEffectScreens) {
+        screen.reconfigure();
+    }
+
+    m_hGap = SlideConfig::horizontalGap();
+    m_vGap = SlideConfig::verticalGap();
+    m_slideBackground = SlideConfig::slideBackground();
+}
+
+void SlideEffectScreen::reconfigure()
+{
     const qreal springConstant = 300.0 / effects->animationTimeFactor();
     const qreal dampingRatio = 1.1;
 
-    // TODO: VD: Apply this to effect screens instead.
     m_motionX = SpringMotion(springConstant, dampingRatio);
     m_motionY = SpringMotion(springConstant, dampingRatio);
-
-    m_hGap = SlideConfig::horizontalGap();
-    m_vGap = SlideConfig::verticalGap();
-    m_slideBackground = SlideConfig::slideBackground();
 }
 
 inline Region buildClipRegion(const QPoint &pos, int w, int h)
diff --git a/src/plugins/slide/slide.h b/src/plugins/slide/slide.h
index 02dcc10bc00..29788146869 100644
--- a/src/plugins/slide/slide.h
+++ b/src/plugins/slide/slide.h
@@ -50,6 +50,7 @@ class SlideEffectScreen
 public:
     SlideEffectScreen(SlideEffect *parent, LogicalOutput *screen);
     ~SlideEffectScreen();
+    void reconfigure();
 
     void prePaintScreen(ScreenPrePaintData &data, std::chrono::milliseconds presentTime);
     void paintScreen(const RenderTarget &renderTarget, const RenderViewport &viewport, int mask, const QRegion &deviceRegion);
-- 
GitLab


From 92f4328e8b6857cbd5a46a0ff5c7732fcc86b939 Mon Sep 17 00:00:00 2001
From: Hynek Schlindenbuch <schlindenbuch.h@seznam.cz>
Date: Sat, 29 Nov 2025 09:23:52 +0100
Subject: [PATCH 13/63] remove resolved TODO

---
 src/keyboard_layout_switching.cpp   | 1 -
 src/scripting/workspace_wrapper.cpp | 1 -
 2 files changed, 2 deletions(-)

diff --git a/src/keyboard_layout_switching.cpp b/src/keyboard_layout_switching.cpp
index b88b9c26f44..63994bd8caa 100644
--- a/src/keyboard_layout_switching.cpp
+++ b/src/keyboard_layout_switching.cpp
@@ -150,7 +150,6 @@ quint32 getLayout(const T &layouts, const U &reference)
 
 void VirtualDesktopPolicy::desktopChanged()
 {
-    // TODO: VD: Fix this.
     auto d = VirtualDesktopManager::self()->currentDesktop();
     if (!d) {
         return;
diff --git a/src/scripting/workspace_wrapper.cpp b/src/scripting/workspace_wrapper.cpp
index ca2fa817ce4..9f64283399c 100644
--- a/src/scripting/workspace_wrapper.cpp
+++ b/src/scripting/workspace_wrapper.cpp
@@ -43,7 +43,6 @@ WorkspaceWrapper::WorkspaceWrapper(QObject *parent)
     connect(vds, &VirtualDesktopManager::desktopRemoved, this, &WorkspaceWrapper::desktopsChanged);
     connect(vds, &VirtualDesktopManager::desktopMoved, this, &WorkspaceWrapper::desktopsChanged);
     connect(vds, &VirtualDesktopManager::layoutChanged, this, &WorkspaceWrapper::desktopLayoutChanged);
-    // TODO: VD: What about this?
     connect(vds, &VirtualDesktopManager::currentChanged, this, &WorkspaceWrapper::currentDesktopChanged);
 #if KWIN_BUILD_ACTIVITIES
     if (KWin::Activities *activities = ws->activities()) {
-- 
GitLab


From 803209e9e415dd05bc82272ecba71a2d2d61bfb2 Mon Sep 17 00:00:00 2001
From: Hynek Schlindenbuch <schlindenbuch.h@seznam.cz>
Date: Sat, 29 Nov 2025 10:44:19 +0100
Subject: [PATCH 14/63] remove m_currentDesktop from FocusChain

I'm not sure whether this actually does anything. It seems to behave
exactly the same as in master, as far as I can tell.
---
 src/focuschain.cpp | 7 +++----
 src/focuschain.h   | 7 -------
 src/workspace.cpp  | 4 ----
 3 files changed, 3 insertions(+), 15 deletions(-)

diff --git a/src/focuschain.cpp b/src/focuschain.cpp
index 857e3a053c4..ade450f4e74 100644
--- a/src/focuschain.cpp
+++ b/src/focuschain.cpp
@@ -7,6 +7,7 @@
     SPDX-License-Identifier: GPL-2.0-or-later
 */
 #include "focuschain.h"
+#include "virtualdesktops.h"
 #include "window.h"
 #include "workspace.h"
 
@@ -30,9 +31,6 @@ void FocusChain::addDesktop(VirtualDesktop *desktop)
 
 void FocusChain::removeDesktop(VirtualDesktop *desktop)
 {
-    if (m_currentDesktop == desktop) {
-        m_currentDesktop = nullptr;
-    }
     m_desktopFocusChains.remove(desktop);
 }
 
@@ -68,13 +66,14 @@ void FocusChain::update(Window *window, FocusChain::Change change)
     }
 
     if (window->isOnAllDesktops()) {
+        const VirtualDesktop *currentDesktop = VirtualDesktopManager::self()->currentDesktop(window->output());
         // Now on all desktops, add it to focus chains it is not already in
         for (auto it = m_desktopFocusChains.begin();
              it != m_desktopFocusChains.end();
              ++it) {
             auto &chain = it.value();
             // Making first/last works only on current desktop, don't affect all desktops
-            if (it.key() == m_currentDesktop
+            if (it.key() == currentDesktop
                 && (change == MakeFirst || change == MakeLast)) {
                 if (change == MakeFirst) {
                     makeFirstInChain(window, chain);
diff --git a/src/focuschain.h b/src/focuschain.h
index c988555ed6f..f6baa471ea5 100644
--- a/src/focuschain.h
+++ b/src/focuschain.h
@@ -176,7 +176,6 @@ public Q_SLOTS:
     void remove(KWin::Window *window);
     void setSeparateScreenFocus(bool enabled);
     void setActiveWindow(KWin::Window *window);
-    void setCurrentDesktop(VirtualDesktop *desktop);
     void addDesktop(VirtualDesktop *desktop);
     void removeDesktop(VirtualDesktop *desktop);
 
@@ -212,7 +211,6 @@ private:
     QHash<VirtualDesktop *, Chain> m_desktopFocusChains;
     bool m_separateScreenFocus = false;
     Window *m_activeWindow = nullptr;
-    VirtualDesktop *m_currentDesktop = nullptr;
 };
 
 inline bool FocusChain::contains(Window *window) const
@@ -230,9 +228,4 @@ inline void FocusChain::setActiveWindow(Window *window)
     m_activeWindow = window;
 }
 
-inline void FocusChain::setCurrentDesktop(VirtualDesktop *desktop)
-{
-    m_currentDesktop = desktop;
-}
-
 } // namespace
diff --git a/src/workspace.cpp b/src/workspace.cpp
index da0d4e8417d..8acd4c4c3ee 100644
--- a/src/workspace.cpp
+++ b/src/workspace.cpp
@@ -199,10 +199,6 @@ void Workspace::init()
 
     connect(this, &Workspace::windowRemoved, m_focusChain.get(), &FocusChain::remove);
     connect(this, &Workspace::windowActivated, m_focusChain.get(), &FocusChain::setActiveWindow);
-    connect(VirtualDesktopManager::self(), &VirtualDesktopManager::currentChanged, m_focusChain.get(), [this](VirtualDesktop *oldDesktop, VirtualDesktop *newDesktop) {
-        // TODO: VD: What about this?
-        m_focusChain->setCurrentDesktop(newDesktop);
-    });
     connect(options, &Options::separateScreenFocusChanged, m_focusChain.get(), &FocusChain::setSeparateScreenFocus);
     m_focusChain->setSeparateScreenFocus(options->isSeparateScreenFocus());
 
-- 
GitLab


From 5f2eac090774372105c5ab0f6cd0acdb3a8bda67 Mon Sep 17 00:00:00 2001
From: Hynek Schlindenbuch <schlindenbuch.h@seznam.cz>
Date: Sat, 29 Nov 2025 13:33:53 +0100
Subject: [PATCH 15/63] save per-output desktops for activities

---
 src/activation.cpp                  |  2 +-
 src/activities.cpp                  | 53 ++++++++++++++++-------------
 src/activities.h                    |  8 ++---
 src/scripting/workspace_wrapper.cpp |  2 +-
 src/workspace.cpp                   |  4 +--
 5 files changed, 38 insertions(+), 31 deletions(-)

diff --git a/src/activation.cpp b/src/activation.cpp
index bcf87c68a56..d18611db670 100644
--- a/src/activation.cpp
+++ b/src/activation.cpp
@@ -317,7 +317,7 @@ void Workspace::activateWindow(Window *window, bool force)
         ++block_focus;
         // DBUS!
         // first isn't necessarily best, but it's easiest
-        m_activities->setCurrent(window->activities().constFirst(), window->isOnAllDesktops() ? nullptr : window->desktops().front());
+        m_activities->setCurrent(window->activities().constFirst(), window->isOnAllDesktops() ? nullptr : window->desktops().front(), window->output());
         --block_focus;
     }
 #endif
diff --git a/src/activities.cpp b/src/activities.cpp
index a506339ef4e..809bc0b2b77 100644
--- a/src/activities.cpp
+++ b/src/activities.cpp
@@ -35,15 +35,18 @@ Activities::Activities()
     connect(m_controller, &KActivities::Controller::serviceStatusChanged, this, &Activities::slotServiceStatusChanged);
 
     m_config = KSharedConfig::openStateConfig();
-    auto lastDesktopConfig = m_config->group("Activities").group("LastVirtualDesktop");
-
-    // migrate old config
-    kwinApp()->config()->group("Activities").group("LastVirtualDesktop").moveValuesTo(lastDesktopConfig);
-
-    for (const auto &activity : lastDesktopConfig.keyList()) {
-        const QString desktop = lastDesktopConfig.readEntry(activity);
-        if (!desktop.isEmpty()) {
-            m_lastVirtualDesktop[activity] = desktop;
+    // remove old config
+    m_config->group("Activities").deleteGroup("LastVirtualDesktop");
+    kwinApp()->config()->group("Activities").deleteGroup("LastVirtualDesktop");
+    auto perOutputLastDesktopConfig = m_config->group("Activities").group("PerOutputLastVirtualDesktop");
+
+    for (const auto &activity : perOutputLastDesktopConfig.groupList()) {
+        auto activityLastDesktopConfig = perOutputLastDesktopConfig.group(activity);
+        for (const auto &outputUuid : activityLastDesktopConfig.keyList()) {
+            const QString desktop = activityLastDesktopConfig.readEntry(outputUuid);
+            if (!desktop.isEmpty()) {
+                m_lastVirtualDesktop[activity][outputUuid] = desktop;
+            }
         }
     }
 
@@ -79,19 +82,19 @@ void Activities::slotServiceStatusChanged()
     }
 }
 
-void Activities::setCurrent(const QString &activity, VirtualDesktop *desktop)
+void Activities::setCurrent(const QString &activity, VirtualDesktop *desktop, LogicalOutput *output)
 {
-    if (desktop) {
-        m_lastVirtualDesktop[activity] = desktop->id();
+    if (desktop && output) {
+        m_lastVirtualDesktop[activity][output->uuid()] = desktop->id();
     }
     m_controller->setCurrentActivity(activity);
 }
 
-void Activities::notifyCurrentDesktopChanged(VirtualDesktop *desktop)
+void Activities::notifyCurrentDesktopChanged(VirtualDesktop *desktop, LogicalOutput *output)
 {
-    m_lastVirtualDesktop[m_current] = desktop->id();
-    auto lastDesktopConfig = m_config->group("Activities").group("LastVirtualDesktop");
-    lastDesktopConfig.writeEntry(m_current, desktop->id());
+    m_lastVirtualDesktop[m_current][output->uuid()] = desktop->id();
+    auto lastDesktopConfig = m_config->group("Activities").group("PerOutputLastVirtualDesktop").group(m_current);
+    lastDesktopConfig.writeEntry(output->uuid(), desktop->id());
 }
 
 void Activities::slotCurrentChanged(const QString &newActivity)
@@ -103,14 +106,18 @@ void Activities::slotCurrentChanged(const QString &newActivity)
     m_previous = m_current;
     m_current = newActivity;
 
-    if (m_previous != nullUuid()) {
-        m_lastVirtualDesktop[m_previous] = VirtualDesktopManager::self()->currentDesktop()->id();
-    }
+    // TODO: VD: What about when an output is added?
     const auto it = m_lastVirtualDesktop.find(m_current);
     if (it != m_lastVirtualDesktop.end()) {
-        VirtualDesktop *desktop = VirtualDesktopManager::self()->desktopForId(it->second);
-        if (desktop) {
-            VirtualDesktopManager::self()->setCurrent(desktop);
+        const auto &outputDesktops = it->second;
+        for (LogicalOutput *output : workspace()->outputs()) {
+            const auto outputDesktopIt = outputDesktops.find(output->uuid());
+            if (outputDesktopIt != outputDesktops.end()) {
+                VirtualDesktop *desktop = VirtualDesktopManager::self()->desktopForId(outputDesktopIt->second);
+                if (desktop) {
+                    VirtualDesktopManager::self()->setCurrent(desktop, output);
+                }
+            }
         }
     }
 
@@ -131,7 +138,7 @@ void Activities::slotRemoved(const QString &activity)
     }
 
     m_lastVirtualDesktop.erase(activity);
-    m_config->group("Activities").group("LastVirtualDesktop").deleteEntry(activity);
+    m_config->group("Activities").group("PerOutputLastVirtualDesktop").deleteGroup(activity);
 }
 
 void Activities::toggleWindowOnActivity(Window *window, const QString &activity, bool dont_activate)
diff --git a/src/activities.h b/src/activities.h
index d866e09bc54..4ba2494d7a7 100644
--- a/src/activities.h
+++ b/src/activities.h
@@ -25,6 +25,7 @@ namespace KWin
 {
 class Window;
 class VirtualDesktop;
+class LogicalOutput;
 
 class KWIN_EXPORT Activities : public QObject
 {
@@ -37,7 +38,7 @@ public:
      * Sets the current activity to @param activity, and if desktop isn't nullptr,
      * ensures that this doesn't interfere with virtual desktop switching
      */
-    void setCurrent(const QString &activity, VirtualDesktop *desktop);
+    void setCurrent(const QString &activity, VirtualDesktop *desktop, LogicalOutput *output);
     /**
      * Adds/removes window \a window to/from \a activity.
      *
@@ -77,7 +78,7 @@ Q_SIGNALS:
     void removed(const QString &id);
 
 public Q_SLOTS:
-    void notifyCurrentDesktopChanged(VirtualDesktop *desktop);
+    void notifyCurrentDesktopChanged(VirtualDesktop *desktop, LogicalOutput *output);
 
 private Q_SLOTS:
     void slotServiceStatusChanged();
@@ -88,8 +89,7 @@ private:
     QString m_previous;
     QString m_current;
     KActivities::Controller *m_controller;
-    // TODO: VD: store the last virtual desktop separately for each output
-    std::unordered_map<QString, QString> m_lastVirtualDesktop;
+    std::unordered_map<QString, std::unordered_map<QString, QString>> m_lastVirtualDesktop;
     KSharedConfig::Ptr m_config;
 };
 
diff --git a/src/scripting/workspace_wrapper.cpp b/src/scripting/workspace_wrapper.cpp
index 9f64283399c..35708f4f7fe 100644
--- a/src/scripting/workspace_wrapper.cpp
+++ b/src/scripting/workspace_wrapper.cpp
@@ -114,7 +114,7 @@ void WorkspaceWrapper::setCurrentActivity(const QString &activity)
 {
 #if KWIN_BUILD_ACTIVITIES
     if (Workspace::self()->activities()) {
-        Workspace::self()->activities()->setCurrent(activity, nullptr);
+        Workspace::self()->activities()->setCurrent(activity, nullptr, nullptr);
     }
 #endif
 }
diff --git a/src/workspace.cpp b/src/workspace.cpp
index 8acd4c4c3ee..5991cac7354 100644
--- a/src/workspace.cpp
+++ b/src/workspace.cpp
@@ -152,8 +152,8 @@ Workspace::Workspace()
 
 #if KWIN_BUILD_ACTIVITIES
     if (m_activities) {
-        connect(VirtualDesktopManager::self(), &VirtualDesktopManager::currentChanged, this, [this](VirtualDesktop *previous, VirtualDesktop *current) {
-            m_activities->notifyCurrentDesktopChanged(current);
+        connect(VirtualDesktopManager::self(), &VirtualDesktopManager::currentChanged, this, [this](VirtualDesktop *previous, VirtualDesktop *current, LogicalOutput *output) {
+            m_activities->notifyCurrentDesktopChanged(current, output);
         });
     }
 #endif
-- 
GitLab


From 5bbc70cb162522feee091786cbf1bab4c81b723d Mon Sep 17 00:00:00 2001
From: Hynek Schlindenbuch <schlindenbuch.h@seznam.cz>
Date: Sat, 13 Dec 2025 08:55:45 +0100
Subject: [PATCH 16/63] wayland: pass per-output active desktop through
 protocol

---
 src/virtualdesktops.cpp              |  31 +++++-
 src/virtualdesktops.h                |   2 +-
 src/wayland/plasmavirtualdesktop.cpp | 154 ++++++++++++++++++++++++++-
 src/wayland/plasmavirtualdesktop.h   |  64 +++++++++++
 4 files changed, 246 insertions(+), 5 deletions(-)

diff --git a/src/virtualdesktops.cpp b/src/virtualdesktops.cpp
index 9cdaba19486..44c7d944b12 100644
--- a/src/virtualdesktops.cpp
+++ b/src/virtualdesktops.cpp
@@ -101,18 +101,43 @@ void VirtualDesktopManager::setVirtualDesktopManagement(PlasmaVirtualDesktopMana
     });
 
     connect(this, &VirtualDesktopManager::currentChanged, m_virtualDesktopManagement, [this](VirtualDesktop *oldDesktop, VirtualDesktop *newDesktop, LogicalOutput *output) {
+        m_virtualDesktopManagement->setActiveDesktopForOutput(output, newDesktop->id());
+
         // TODO: VD: is this correct?
         if (output != workspace()->activeOutput()) {
             return;
         }
-        updatePlasmaVirtualDesktops(newDesktop);
+        updateLegacyPlasmaVirtualDesktops(newDesktop);
     });
     connect(workspace(), &Workspace::activeOutputChanged, this, [this](LogicalOutput *output) {
-        updatePlasmaVirtualDesktops(currentDesktop(output));
+        updateLegacyPlasmaVirtualDesktops(currentDesktop(output));
+    });
+
+    auto createPlasmaOutputVirtualDesktop = [this](LogicalOutput *output) {
+        // TODO: VD: How is the current desktop initialized for a new output?
+        PlasmaOutputVirtualDesktopInterface *outputVd = m_virtualDesktopManagement->addOutput(output, currentDesktop(output)->id());
+
+        connect(outputVd, &PlasmaOutputVirtualDesktopInterface::activateDesktopRequested, output, [this, output](QString desktopId) {
+            VirtualDesktop *desktop = desktopForId(desktopId);
+            if (!desktop) {
+                return;
+            }
+
+            setCurrent(desktop, output);
+        });
+    };
+
+    connect(workspace(), &Workspace::outputAdded, this, createPlasmaOutputVirtualDesktop);
+    connect(workspace(), &Workspace::outputRemoved, this, [this](LogicalOutput *output) {
+        m_virtualDesktopManagement->removeOutput(output);
     });
 
     std::for_each(m_desktops.constBegin(), m_desktops.constEnd(), createPlasmaVirtualDesktop);
 
+    for (LogicalOutput *output : workspace()->outputs()) {
+        createPlasmaOutputVirtualDesktop(output);
+    }
+
     m_virtualDesktopManagement->setRows(rows());
     m_virtualDesktopManagement->scheduleDone();
 }
@@ -947,7 +972,7 @@ void VirtualDesktopManager::initSwitchToShortcuts()
     }
 }
 
-void VirtualDesktopManager::updatePlasmaVirtualDesktops(VirtualDesktop *activeDesktop)
+void VirtualDesktopManager::updateLegacyPlasmaVirtualDesktops(VirtualDesktop *activeDesktop)
 {
     const QList<PlasmaVirtualDesktopInterface *> deskIfaces = m_virtualDesktopManagement->desktops();
     for (auto *deskInt : deskIfaces) {
diff --git a/src/virtualdesktops.h b/src/virtualdesktops.h
index b87c406c4d0..bc0f77f68a1 100644
--- a/src/virtualdesktops.h
+++ b/src/virtualdesktops.h
@@ -529,7 +529,7 @@ private:
     /**
      * Updates active status of plasma virtual desktops.
      */
-    void updatePlasmaVirtualDesktops(VirtualDesktop *activeDesktop);
+    void updateLegacyPlasmaVirtualDesktops(VirtualDesktop *activeDesktop);
 
     /**
      * Creates an action and connects it to the @p slot in this Manager. This method is
diff --git a/src/wayland/plasmavirtualdesktop.cpp b/src/wayland/plasmavirtualdesktop.cpp
index 9c90574d0e8..75bd49aaa2b 100644
--- a/src/wayland/plasmavirtualdesktop.cpp
+++ b/src/wayland/plasmavirtualdesktop.cpp
@@ -5,6 +5,7 @@
 */
 #include "plasmavirtualdesktop.h"
 #include "display.h"
+#include "output.h"
 #include "wayland/quirks.h"
 
 #include <QDebug>
@@ -15,7 +16,7 @@
 
 namespace KWin
 {
-static const quint32 s_version = 3;
+static const quint32 s_version = 4;
 
 class PlasmaVirtualDesktopInterfacePrivate : public QtWaylandServer::org_kde_plasma_virtual_desktop
 {
@@ -34,6 +35,21 @@ protected:
     void org_kde_plasma_virtual_desktop_request_activate(Resource *resource) override;
 };
 
+class PlasmaOutputVirtualDesktopInterfacePrivate : public QtWaylandServer::org_kde_plasma_output_virtual_desktop
+{
+public:
+    PlasmaOutputVirtualDesktopInterfacePrivate(PlasmaOutputVirtualDesktopInterface *q);
+    ~PlasmaOutputVirtualDesktopInterfacePrivate();
+
+    PlasmaOutputVirtualDesktopInterface *q;
+    QString uuid;
+    QString name;
+    QString activeDesktopId;
+
+protected:
+    void org_kde_plasma_output_virtual_desktop_request_activate_desktop(Resource *resource, const QString &desktop_id) override;
+};
+
 class PlasmaVirtualDesktopManagementInterfacePrivate : public QtWaylandServer::org_kde_plasma_virtual_desktop_management
 {
 public:
@@ -41,6 +57,7 @@ public:
 
     QTimer doneTimer;
     QList<PlasmaVirtualDesktopInterface *> desktops;
+    QHash<LogicalOutput *, PlasmaOutputVirtualDesktopInterface *> outputs;
     quint32 rows = 0;
     quint32 columns = 0;
     PlasmaVirtualDesktopManagementInterface *q;
@@ -48,6 +65,8 @@ public:
     inline QList<PlasmaVirtualDesktopInterface *>::const_iterator constFindDesktop(const QString &id);
     inline QList<PlasmaVirtualDesktopInterface *>::iterator findDesktop(const QString &id);
 
+    void sendOutputDesktopToClient(Resource *resource, PlasmaOutputVirtualDesktopInterface *outputVd);
+
 protected:
     void org_kde_plasma_virtual_desktop_management_get_virtual_desktop(Resource *resource, uint32_t id, const QString &desktop_id) override;
     void org_kde_plasma_virtual_desktop_management_request_create_virtual_desktop(Resource *resource, const QString &name, uint32_t position) override;
@@ -100,6 +119,20 @@ PlasmaVirtualDesktopManagementInterfacePrivate::PlasmaVirtualDesktopManagementIn
 {
 }
 
+void PlasmaVirtualDesktopManagementInterfacePrivate::sendOutputDesktopToClient(Resource *resource, PlasmaOutputVirtualDesktopInterface *outputVd)
+{
+    if (resource->version() < ORG_KDE_PLASMA_VIRTUAL_DESKTOP_MANAGEMENT_OUTPUT_ADDED_SINCE_VERSION) {
+        return;
+    }
+
+    auto outputVdClientResource = outputVd->d->add(resource->client(), resource->version());
+    send_output_added(resource->handle, outputVdClientResource->handle);
+    outputVd->d->send_uuid(outputVdClientResource->handle, outputVd->uuid());
+    outputVd->d->send_name(outputVdClientResource->handle, outputVd->name());
+    outputVd->d->send_active_desktop_id(outputVdClientResource->handle, outputVd->activeDesktop());
+    outputVd->d->send_done(outputVdClientResource->handle);
+}
+
 void PlasmaVirtualDesktopManagementInterfacePrivate::org_kde_plasma_virtual_desktop_management_bind_resource(Resource *resource)
 {
     quint32 i = 0;
@@ -111,6 +144,10 @@ void PlasmaVirtualDesktopManagementInterfacePrivate::org_kde_plasma_virtual_desk
         send_rows(resource->handle, rows);
     }
 
+    for (PlasmaOutputVirtualDesktopInterface *outputVd : outputs) {
+        sendOutputDesktopToClient(resource, outputVd);
+    }
+
     send_done(resource->handle);
 }
 
@@ -211,6 +248,54 @@ void PlasmaVirtualDesktopManagementInterface::removeDesktop(const QString &id)
     }
 }
 
+PlasmaOutputVirtualDesktopInterface *PlasmaVirtualDesktopManagementInterface::addOutput(LogicalOutput *output, const QString &activeDesktopId)
+{
+    auto i = d->outputs.constFind(output);
+    if (i != d->outputs.constEnd()) {
+        return *i;
+    }
+
+    auto outputVd = new PlasmaOutputVirtualDesktopInterface();
+    outputVd->d->uuid = output->uuid();
+    outputVd->d->name = output->name();
+    outputVd->d->activeDesktopId = activeDesktopId;
+
+    d->outputs.insert(output, outputVd);
+
+    const auto clientResources = d->resourceMap();
+    for (auto resource : clientResources) {
+        d->sendOutputDesktopToClient(resource, outputVd);
+        d->send_done(resource->handle);
+    }
+
+    return outputVd;
+}
+
+void PlasmaVirtualDesktopManagementInterface::setActiveDesktopForOutput(LogicalOutput *output, const QString &activeDesktopId)
+{
+    auto i = d->outputs.constFind(output);
+    if (i == d->outputs.constEnd()) {
+        addOutput(output, activeDesktopId);
+        return;
+    }
+
+    auto outputVd = *i;
+    outputVd->setActiveDesktop(activeDesktopId);
+    outputVd->sendDone();
+}
+
+void PlasmaVirtualDesktopManagementInterface::removeOutput(LogicalOutput *output)
+{
+    auto i = d->outputs.constFind(output);
+    if (i != d->outputs.constEnd()) {
+        return;
+    }
+
+    PlasmaOutputVirtualDesktopInterface *outputVd = *i;
+    d->outputs.erase(i);
+    delete outputVd;
+}
+
 QList<PlasmaVirtualDesktopInterface *> PlasmaVirtualDesktopManagementInterface::desktops() const
 {
     return d->desktops;
@@ -349,6 +434,73 @@ void PlasmaVirtualDesktopInterface::sendDone()
     }
 }
 
+//// PlasmaOutputVirtualDesktopInterface
+
+PlasmaOutputVirtualDesktopInterfacePrivate::PlasmaOutputVirtualDesktopInterfacePrivate(PlasmaOutputVirtualDesktopInterface *q)
+    : QtWaylandServer::org_kde_plasma_output_virtual_desktop()
+    , q(q)
+{
+}
+
+PlasmaOutputVirtualDesktopInterfacePrivate::~PlasmaOutputVirtualDesktopInterfacePrivate()
+{
+    const auto clientResources = resourceMap();
+    for (Resource *resource : clientResources) {
+        send_removed(resource->handle);
+        wl_resource_destroy(resource->handle);
+    }
+}
+
+void PlasmaOutputVirtualDesktopInterfacePrivate::org_kde_plasma_output_virtual_desktop_request_activate_desktop(Resource *resource, const QString &desktopId)
+{
+    Q_EMIT q->activateDesktopRequested(desktopId);
+}
+
+PlasmaOutputVirtualDesktopInterface::PlasmaOutputVirtualDesktopInterface()
+    : d(new PlasmaOutputVirtualDesktopInterfacePrivate(this))
+{
+}
+
+PlasmaOutputVirtualDesktopInterface::~PlasmaOutputVirtualDesktopInterface()
+{
+}
+
+QString PlasmaOutputVirtualDesktopInterface::uuid() const
+{
+    return d->uuid;
+}
+
+QString PlasmaOutputVirtualDesktopInterface::name() const
+{
+    return d->name;
+}
+
+QString PlasmaOutputVirtualDesktopInterface::activeDesktop() const
+{
+    return d->activeDesktopId;
+}
+
+void PlasmaOutputVirtualDesktopInterface::setActiveDesktop(const QString &desktopId)
+{
+    if (d->activeDesktopId == desktopId) {
+        return;
+    }
+
+    d->activeDesktopId = desktopId;
+
+    const auto clientResources = d->resourceMap();
+    for (auto resource : clientResources) {
+        d->send_active_desktop_id(resource->handle, desktopId);
+    }
+}
+
+void PlasmaOutputVirtualDesktopInterface::sendDone()
+{
+    const auto clientResources = d->resourceMap();
+    for (auto resource : clientResources) {
+        d->send_done(resource->handle);
+    }
+}
 }
 
 #include "moc_plasmavirtualdesktop.cpp"
diff --git a/src/wayland/plasmavirtualdesktop.h b/src/wayland/plasmavirtualdesktop.h
index 42da075edd5..3de373b2515 100644
--- a/src/wayland/plasmavirtualdesktop.h
+++ b/src/wayland/plasmavirtualdesktop.h
@@ -13,8 +13,11 @@
 namespace KWin
 {
 class Display;
+class LogicalOutput;
 class PlasmaVirtualDesktopInterface;
 class PlasmaVirtualDesktopInterfacePrivate;
+class PlasmaOutputVirtualDesktopInterface;
+class PlasmaOutputVirtualDesktopInterfacePrivate;
 class PlasmaVirtualDesktopManagementInterfacePrivate;
 
 /**
@@ -57,6 +60,21 @@ public:
      */
     void removeDesktop(const QString &id);
 
+    /**
+     * Adds output
+     */
+    PlasmaOutputVirtualDesktopInterface *addOutput(LogicalOutput *output, const QString &activeDesktopId);
+
+    /**
+     * Change active desktop for output
+     */
+    void setActiveDesktopForOutput(LogicalOutput *output, const QString &activeDesktopId);
+
+    /**
+     * Removes output
+     */
+    void removeOutput(LogicalOutput *output);
+
     /**
      * @returns All the desktops present.
      */
@@ -156,4 +174,50 @@ private:
     std::unique_ptr<PlasmaVirtualDesktopInterfacePrivate> d;
 };
 
+class KWIN_EXPORT PlasmaOutputVirtualDesktopInterface : public QObject
+{
+    Q_OBJECT
+public:
+    ~PlasmaOutputVirtualDesktopInterface() override;
+
+    /**
+     * @returns the UUID for this output
+     */
+    QString uuid() const;
+
+    /**
+     * @returns the name for this desktop
+     */
+    QString name() const;
+
+    /**
+     * Set active desktop for this output. It may affect other outputs as well.
+     */
+    void setActiveDesktop(const QString &desktopId);
+
+    /**
+     * @returns id of the desktop active on this output.
+     */
+    QString activeDesktop() const;
+
+    /**
+     * Inform the clients that all the properties have been sent, and
+     * their client-side representation is complete.
+     */
+    void sendDone();
+
+Q_SIGNALS:
+    /**
+     * Emitted when the client asked to activate another desktop on the output:
+     * it's the decision of the server whether to perform the activation or not.
+     */
+    void activateDesktopRequested(QString desktopId);
+
+private:
+    PlasmaOutputVirtualDesktopInterface();
+    friend class PlasmaVirtualDesktopManagementInterface;
+    friend class PlasmaVirtualDesktopManagementInterfacePrivate;
+
+    std::unique_ptr<PlasmaOutputVirtualDesktopInterfacePrivate> d;
+};
 }
-- 
GitLab


From 03a0a08c9ab32062664e6342f06c6bee6283feac Mon Sep 17 00:00:00 2001
From: Hynek Schlindenbuch <schlindenbuch.h@seznam.cz>
Date: Sun, 14 Dec 2025 08:53:06 +0100
Subject: [PATCH 17/63] fix slide animation during desktop switching

There were several bugs:

- When switching desktop on one screen, the other would show just the
  background.
- Occasionally, the windows on the other screen would pop in and out
  after the slide animation finished on the first screen.
- Windows overlaping multiple screens weren't handled correctly.

There's still an annoying effect for windows overlapping multiple
screens: When switching to the window's desktop on the window's screen
it takes a while until the parts of the window on the other screens pop
in (i.e. the animation has to finish).
---
 src/plugins/slide/slide.cpp | 19 ++++++++++++-------
 1 file changed, 12 insertions(+), 7 deletions(-)

diff --git a/src/plugins/slide/slide.cpp b/src/plugins/slide/slide.cpp
index 51ce8b69c37..87de5056a8d 100644
--- a/src/plugins/slide/slide.cpp
+++ b/src/plugins/slide/slide.cpp
@@ -129,9 +129,11 @@ inline Region buildClipRegion(const QPoint &pos, int w, int h)
 
 void SlideEffect::prePaintScreen(ScreenPrePaintData &data, std::chrono::milliseconds presentTime)
 {
-    for (auto &effectScreen : m_slideEffectScreens) {
-        effectScreen.prePaintScreen(data, presentTime);
+    if (data.screen) {
+        getOrCreateSlideEffectScreen(data.screen).prePaintScreen(data, presentTime);
     }
+
+    effects->prePaintScreen(data, presentTime);
 }
 
 void SlideEffectScreen::prePaintScreen(ScreenPrePaintData &data, std::chrono::milliseconds presentTime)
@@ -189,8 +191,6 @@ void SlideEffectScreen::prePaintScreen(ScreenPrePaintData &data, std::chrono::mi
     }
 
     data.mask |= Effect::PAINT_SCREEN_TRANSFORMED | Effect::PAINT_SCREEN_BACKGROUND_FIRST;
-
-    effects->prePaintScreen(data, presentTime);
 }
 
 void SlideEffect::paintScreen(const RenderTarget &renderTarget, const RenderViewport &viewport, int mask, const Region &deviceRegion, LogicalOutput *screen)
@@ -218,6 +218,9 @@ QPoint SlideEffectScreen::getDrawCoords(QPointF pos, LogicalOutput *screen)
  */
 bool SlideEffectScreen::isTranslated(const EffectWindow *w) const
 {
+    if (m_state == State::Inactive) {
+        return false;
+    }
     if (w->isOnAllDesktops()) {
         if (w->isDesktop()) {
             return m_parent->slideBackground();
@@ -234,6 +237,9 @@ bool SlideEffectScreen::isTranslated(const EffectWindow *w) const
  */
 bool SlideEffectScreen::willBePainted(const EffectWindow *w) const
 {
+    if (m_state == State::Inactive) {
+        return true;
+    }
     if (w->isOnAllDesktops()) {
         return true;
     }
@@ -330,13 +336,12 @@ void SlideEffect::postPaintScreen()
 
 void SlideEffectScreen::postPaintScreen()
 {
-    if (m_state == State::Inactive) {
-        return;
-    }
     if (m_state == State::ActiveAnimation && !m_motionX.isMoving() && !m_motionY.isMoving()) {
         finishedSwitching();
     }
 
+    // This is necessary even for screens where the effect isn't active. The active screen may have a window that's overlapping
+    // with the inactive screen.
     effects->addRepaint(m_screen->geometry());
 }
 
-- 
GitLab


From 0c9e9cbf1fd1e9f2cbf46e08373a787604b83873 Mon Sep 17 00:00:00 2001
From: Hynek Schlindenbuch <schlindenbuch.h@seznam.cz>
Date: Sun, 14 Dec 2025 09:35:33 +0100
Subject: [PATCH 18/63] fix fade animation for desktop switching

---
 src/plugins/fadedesktop/package/contents/code/main.js | 6 +++++-
 1 file changed, 5 insertions(+), 1 deletion(-)

diff --git a/src/plugins/fadedesktop/package/contents/code/main.js b/src/plugins/fadedesktop/package/contents/code/main.js
index ce186b3a655..5965b214488 100644
--- a/src/plugins/fadedesktop/package/contents/code/main.js
+++ b/src/plugins/fadedesktop/package/contents/code/main.js
@@ -65,7 +65,7 @@ var fadeDesktopEffect = {
             to: 0.0
         });
     },
-    slotDesktopChanged: function (oldDesktop, newDesktop, movingWindow) {
+    slotDesktopChanged: function (oldDesktop, newDesktop, screen, movingWindow) {
         if (effects.hasActiveFullScreenEffect && !effect.isActiveFullScreenEffect) {
             return;
         }
@@ -80,6 +80,10 @@ var fadeDesktopEffect = {
                 continue;
             }
 
+            if (w.screen !== screen) {
+                continue;
+            }
+
             // If the window is not on the old and the new desktop or it's
             // on both of them, then don't animate it.
             var onOldDesktop = w.isOnDesktop(oldDesktop);
-- 
GitLab


From 3801633446baab440c5384bf2173b22ca5a1e9bc Mon Sep 17 00:00:00 2001
From: Hynek Schlindenbuch <schlindenbuch.h@seznam.cz>
Date: Sun, 14 Dec 2025 13:11:45 +0100
Subject: [PATCH 19/63] re-assign desktops when moving window between outputs

---
 src/internalwindow.cpp |  6 ++----
 src/waylandwindow.cpp  |  6 ++----
 src/window.cpp         | 33 ++++++++++++++++++++++++++++++++-
 src/window.h           |  2 ++
 src/x11window.cpp      |  6 ++----
 5 files changed, 40 insertions(+), 13 deletions(-)

diff --git a/src/internalwindow.cpp b/src/internalwindow.cpp
index c03d03696b6..d5008c3c4d7 100644
--- a/src/internalwindow.cpp
+++ b/src/internalwindow.cpp
@@ -445,7 +445,7 @@ void InternalWindow::commitGeometry(const RectF &rect)
     // The client geometry and the buffer geometry are the same.
     const RectF oldClientGeometry = m_clientGeometry;
     const RectF oldFrameGeometry = m_frameGeometry;
-    const LogicalOutput *oldOutput = m_output;
+    LogicalOutput *oldOutput = m_output;
 
     Q_EMIT frameGeometryAboutToChange();
 
@@ -466,9 +466,7 @@ void InternalWindow::commitGeometry(const RectF &rect)
     if (oldFrameGeometry != m_frameGeometry) {
         Q_EMIT frameGeometryChanged(oldFrameGeometry);
     }
-    if (oldOutput != m_output) {
-        Q_EMIT outputChanged();
-    }
+    finishOutputChange(oldOutput);
 }
 
 void InternalWindow::setCaption(const QString &caption)
diff --git a/src/waylandwindow.cpp b/src/waylandwindow.cpp
index c1627acafef..9ca073bf6cd 100644
--- a/src/waylandwindow.cpp
+++ b/src/waylandwindow.cpp
@@ -215,7 +215,7 @@ void WaylandWindow::updateGeometry(const RectF &rect)
     const RectF oldClientGeometry = m_clientGeometry;
     const RectF oldFrameGeometry = m_frameGeometry;
     const RectF oldBufferGeometry = m_bufferGeometry;
-    const LogicalOutput *oldOutput = m_output;
+    LogicalOutput *oldOutput = m_output;
 
     m_clientGeometry = frameRectToClientRect(rect);
     m_frameGeometry = rect;
@@ -249,9 +249,7 @@ void WaylandWindow::updateGeometry(const RectF &rect)
     if (changedGeometries & WaylandGeometryFrame) {
         Q_EMIT frameGeometryChanged(oldFrameGeometry);
     }
-    if (oldOutput != m_output) {
-        Q_EMIT outputChanged();
-    }
+    finishOutputChange(oldOutput);
 }
 
 void WaylandWindow::markAsMapped()
diff --git a/src/window.cpp b/src/window.cpp
index 8cb0081a08d..ff6e9ace396 100644
--- a/src/window.cpp
+++ b/src/window.cpp
@@ -234,8 +234,9 @@ LogicalOutput *Window::output() const
 void Window::setOutput(LogicalOutput *output)
 {
     if (m_output != output) {
+        LogicalOutput *oldOutput = m_output;
         m_output = output;
-        Q_EMIT outputChanged();
+        finishOutputChange(oldOutput);
     }
 }
 
@@ -4701,6 +4702,36 @@ void Window::setDescription(const QString &description)
     }
 }
 
+void Window::finishOutputChange(LogicalOutput *prevOutput)
+{
+    if (m_output == prevOutput) {
+        return;
+    }
+
+    Q_EMIT outputChanged();
+
+    if (!prevOutput || m_desktops.empty()) {
+        return;
+    }
+
+    VirtualDesktop *currentOutputDesktop = VirtualDesktopManager::self()->currentDesktop(m_output);
+
+    if (m_desktops.contains(currentOutputDesktop)) {
+        return;
+    }
+
+    VirtualDesktop *prevOutputDesktop = VirtualDesktopManager::self()->currentDesktop(prevOutput);
+
+    if (currentOutputDesktop == prevOutputDesktop) {
+        return;
+    }
+
+    auto desktops = m_desktops;
+    desktops.removeOne(prevOutputDesktop);
+    desktops.append(currentOutputDesktop);
+    setDesktops(desktops);
+}
+
 void Window::setActivationToken(const QString &token)
 {
     m_activationToken = token;
diff --git a/src/window.h b/src/window.h
index 4f7e151c949..94c8791c09a 100644
--- a/src/window.h
+++ b/src/window.h
@@ -1806,6 +1806,8 @@ protected:
 
     void setDescription(const QString &description);
 
+    void finishOutputChange(LogicalOutput *prevOutput);
+
     LogicalOutput *m_output = nullptr;
     RectF m_frameGeometry;
     RectF m_clientGeometry;
diff --git a/src/x11window.cpp b/src/x11window.cpp
index 9566437ccdf..55786bf8b97 100644
--- a/src/x11window.cpp
+++ b/src/x11window.cpp
@@ -3599,7 +3599,7 @@ void X11Window::moveResizeInternal(const RectF &rect, MoveResizeMode mode)
     const RectF oldBufferGeometry = m_bufferGeometry;
     const RectF oldFrameGeometry = m_frameGeometry;
     const RectF oldClientGeometry = m_clientGeometry;
-    const LogicalOutput *oldOutput = m_output;
+    LogicalOutput *oldOutput = m_output;
 
     m_frameGeometry = frameGeometry;
     m_clientGeometry = clientGeometry;
@@ -3627,9 +3627,7 @@ void X11Window::moveResizeInternal(const RectF &rect, MoveResizeMode mode)
     if (oldFrameGeometry != m_frameGeometry) {
         Q_EMIT frameGeometryChanged(oldFrameGeometry);
     }
-    if (oldOutput != m_output) {
-        Q_EMIT outputChanged();
-    }
+    finishOutputChange(oldOutput);
     updateShapeRegion();
 }
 
-- 
GitLab


From 26bf4b4176665368651a5717fb692cbf341e2f51 Mon Sep 17 00:00:00 2001
From: Hynek Schlindenbuch <schlindenbuch.h@seznam.cz>
Date: Sun, 14 Dec 2025 13:47:16 +0100
Subject: [PATCH 20/63] fix per-output desktop slide for windows overlapping
 multiple screens

---
 src/plugins/slide/slide.cpp | 25 +++++++++++++------------
 1 file changed, 13 insertions(+), 12 deletions(-)

diff --git a/src/plugins/slide/slide.cpp b/src/plugins/slide/slide.cpp
index 87de5056a8d..324054a6a70 100644
--- a/src/plugins/slide/slide.cpp
+++ b/src/plugins/slide/slide.cpp
@@ -301,21 +301,22 @@ void SlideEffectScreen::paintWindow(const RenderTarget &renderTarget, const Rend
             desktopTranslation = QPointF(desktopTranslation.x(), desktopTranslation.y() + gridHeight);
         }
 
-        QPoint drawTranslation = getDrawCoords(desktopTranslation, m_screen);
-        data += drawTranslation;
+        for (LogicalOutput *screen : screens) {
+            QPoint drawTranslation = getDrawCoords(desktopTranslation, screen);
+            data += drawTranslation;
 
-        const Rect screenArea = m_screen->geometry();
-        const Rect logicalDamage = screenArea.translated(drawTranslation).intersected(screenArea);
+            const Rect screenArea = screen->geometry();
+            const Rect logicalDamage = screenArea.translated(drawTranslation).intersected(screenArea);
 
-        effects->paintWindow(
-            renderTarget, viewport, w, mask,
-            // Only paint the region that intersects the current screen and desktop.
-            deviceGeometry.intersected(viewport.mapToDeviceCoordinatesAligned(logicalDamage)),
-            data);
+            effects->paintWindow(
+                renderTarget, viewport, w, mask,
+                // Only paint the region that intersects the current screen and desktop.
+                deviceGeometry.intersected(viewport.mapToDeviceCoordinatesAligned(logicalDamage)),
+                data);
 
-        // TODO: VD: Will this work, and is it even necessary anymore?
-        // Undo the translation for the next screen. I know, it hurts me too.
-        data += QPoint(-drawTranslation.x(), -drawTranslation.y());
+            // Undo the translation for the next screen. I know, it hurts me too.
+            data += QPoint(-drawTranslation.x(), -drawTranslation.y());
+        }
     }
 }
 
-- 
GitLab


From 50f1d30b666ebe17c36022a4fe3dc5615b28bf1c Mon Sep 17 00:00:00 2001
From: Hynek Schlindenbuch <schlindenbuch.h@seznam.cz>
Date: Mon, 15 Dec 2025 10:46:51 +0100
Subject: [PATCH 21/63] fix slide for touchpad desktop switching

Previously it worked fine with per-output virtual desktops. But when it
was disabled, it still only shown the switching animation on the active
screen and the other screen then slided once the switch was completed.
---
 src/effect/effecthandler.cpp            |  4 ++--
 src/effect/effecthandler.h              |  7 ++++++-
 src/plugins/overview/overvieweffect.cpp |  3 ++-
 src/plugins/slide/slide.cpp             |  5 ++---
 src/plugins/slide/slide.h               |  2 +-
 src/virtualdesktops.cpp                 | 26 +++++++++++++++++--------
 src/virtualdesktops.h                   |  7 ++++++-
 src/workspace.cpp                       |  4 ++--
 src/workspace.h                         |  4 ++--
 9 files changed, 41 insertions(+), 21 deletions(-)

diff --git a/src/effect/effecthandler.cpp b/src/effect/effecthandler.cpp
index 3b4dc303192..c1ed6521d57 100644
--- a/src/effect/effecthandler.cpp
+++ b/src/effect/effecthandler.cpp
@@ -168,8 +168,8 @@ EffectsHandler::EffectsHandler(Compositor *compositor, WorkspaceScene *scene)
     connect(ws, &Workspace::currentDesktopChanged, this, [this](VirtualDesktop *old, VirtualDesktop *newDesktop, LogicalOutput *output, Window *window) {
         Q_EMIT desktopChanged(old, newDesktop, output, window ? window->effectWindow() : nullptr);
     });
-    connect(ws, &Workspace::currentDesktopChanging, this, [this](VirtualDesktop *currentDesktop, QPointF offset, KWin::Window *window) {
-        Q_EMIT desktopChanging(currentDesktop, offset, window ? window->effectWindow() : nullptr);
+    connect(ws, &Workspace::currentDesktopChanging, this, [this](VirtualDesktop *currentDesktop, QPointF offset, KWin::LogicalOutput *output, KWin::Window *window) {
+        Q_EMIT desktopChanging(currentDesktop, offset, output, window ? window->effectWindow() : nullptr);
     });
     connect(ws, &Workspace::currentDesktopChangingCancelled, this, [this]() {
         Q_EMIT desktopChangingCancelled();
diff --git a/src/effect/effecthandler.h b/src/effect/effecthandler.h
index 8fe1b298c25..d8742f771a0 100644
--- a/src/effect/effecthandler.h
+++ b/src/effect/effecthandler.h
@@ -794,8 +794,13 @@ Q_SIGNALS:
      * @param offset The current desktop offset.
      * offset.x() = .6 means 60% of the way to the desktop to the right.
      * Positive Values means Up and Right.
+     * @param output The affected screen. If it affects multiple screens, then a separate signal is emitted for each one.
+     */
+    void desktopChanging(KWin::VirtualDesktop *currentDesktop, QPointF offset, KWin::LogicalOutput *output, KWin::EffectWindow *with);
+
+    /**
+     * Signal emitted when realtime desktop switching animation is cancelled. It applies to all screens.
      */
-    void desktopChanging(KWin::VirtualDesktop *currentDesktop, QPointF offset, KWin::EffectWindow *with);
     void desktopChangingCancelled();
     void desktopAdded(KWin::VirtualDesktop *desktop);
     void desktopRemoved(KWin::VirtualDesktop *desktop);
diff --git a/src/plugins/overview/overvieweffect.cpp b/src/plugins/overview/overvieweffect.cpp
index bd3bfc78272..b96d797708c 100644
--- a/src/plugins/overview/overvieweffect.cpp
+++ b/src/plugins/overview/overvieweffect.cpp
@@ -105,7 +105,8 @@ OverviewEffect::OverviewEffect()
     connect(m_gridState, &EffectTogglableState::inProgressChanged, this, &OverviewEffect::gridGestureInProgressChanged);
     connect(m_gridState, &EffectTogglableState::partialActivationFactorChanged, this, &OverviewEffect::gridPartialActivationFactorChanged);
 
-    connect(effects, &EffectsHandler::desktopChanging, this, [this](VirtualDesktop *old, QPointF desktopOffset, EffectWindow *with) {
+    connect(effects, &EffectsHandler::desktopChanging, this, [this](VirtualDesktop *old, QPointF desktopOffset, LogicalOutput *output, EffectWindow *with) {
+        // TODO: VD: Fix this for per-output VDs (currently it affects all outputs).
         m_desktopOffset = desktopOffset;
         Q_EMIT desktopOffsetChanged();
     });
diff --git a/src/plugins/slide/slide.cpp b/src/plugins/slide/slide.cpp
index 324054a6a70..5a5a3d88a36 100644
--- a/src/plugins/slide/slide.cpp
+++ b/src/plugins/slide/slide.cpp
@@ -498,14 +498,13 @@ void SlideEffectScreen::desktopChanged(VirtualDesktop *old, VirtualDesktop *curr
     startAnimation(previousPos, current, with);
 }
 
-void SlideEffect::desktopChanging(VirtualDesktop *old, QPointF desktopOffset, EffectWindow *with)
+void SlideEffect::desktopChanging(VirtualDesktop *old, QPointF desktopOffset, LogicalOutput *output, EffectWindow *with)
 {
     if (effects->hasActiveFullScreenEffect() && effects->activeFullScreenEffect() != this) {
         return;
     }
 
-    // TODO: VD: Apply it to all/one screen depending on settings.
-    getOrCreateSlideEffectScreen(effects->activeScreen()).desktopChanging(old, desktopOffset, with);
+    getOrCreateSlideEffectScreen(output).desktopChanging(old, desktopOffset, with);
     effects->setActiveFullScreenEffect(this);
 }
 
diff --git a/src/plugins/slide/slide.h b/src/plugins/slide/slide.h
index 29788146869..c3256929447 100644
--- a/src/plugins/slide/slide.h
+++ b/src/plugins/slide/slide.h
@@ -151,7 +151,7 @@ public:
 
 private Q_SLOTS:
     void desktopChanged(VirtualDesktop *old, VirtualDesktop *current, LogicalOutput *output, EffectWindow *with);
-    void desktopChanging(VirtualDesktop *old, QPointF desktopOffset, EffectWindow *with);
+    void desktopChanging(VirtualDesktop *old, QPointF desktopOffset, LogicalOutput *output, EffectWindow *with);
     void desktopChangingCancelled();
     void windowAdded(EffectWindow *w);
     void windowDeleted(EffectWindow *w);
diff --git a/src/virtualdesktops.cpp b/src/virtualdesktops.cpp
index 44c7d944b12..58b791d1589 100644
--- a/src/virtualdesktops.cpp
+++ b/src/virtualdesktops.cpp
@@ -877,33 +877,43 @@ void VirtualDesktopManager::initShortcuts()
     // These connections decide which desktop to end on after gesture ends
     connect(m_swipeGestureReleasedX.get(), &QAction::triggered, this, &VirtualDesktopManager::gestureReleasedX);
     connect(m_swipeGestureReleasedY.get(), &QAction::triggered, this, &VirtualDesktopManager::gestureReleasedY);
+    const auto emitCurrentChanging = [this]() {
+        if (m_perOutputVirtualDesktops) {
+            LogicalOutput *output = workspace()->activeOutput();
+            Q_EMIT currentChanging(currentDesktop(output), m_currentDesktopOffset, output);
+        } else {
+            for (LogicalOutput *output : workspace()->outputs()) {
+                Q_EMIT currentChanging(currentDesktop(output), m_currentDesktopOffset, output);
+            }
+        }
+    };
 
-    const auto left = [this](qreal cb) {
+    const auto left = [this, emitCurrentChanging](qreal cb) {
         if (grid().width() > 1) {
             m_currentDesktopOffset.setX(cb);
-            Q_EMIT currentChanging(currentDesktop(), m_currentDesktopOffset);
+            emitCurrentChanging();
         }
     };
-    const auto right = [this](qreal cb) {
+    const auto right = [this, emitCurrentChanging](qreal cb) {
         if (grid().width() > 1) {
             m_currentDesktopOffset.setX(-cb);
-            Q_EMIT currentChanging(currentDesktop(), m_currentDesktopOffset);
+            emitCurrentChanging();
         }
     };
     input()->registerTouchpadSwipeShortcut(SwipeDirection::Left, 3, m_swipeGestureReleasedX.get(), left);
     input()->registerTouchpadSwipeShortcut(SwipeDirection::Right, 3, m_swipeGestureReleasedX.get(), right);
     input()->registerTouchpadSwipeShortcut(SwipeDirection::Left, 4, m_swipeGestureReleasedX.get(), left);
     input()->registerTouchpadSwipeShortcut(SwipeDirection::Right, 4, m_swipeGestureReleasedX.get(), right);
-    input()->registerTouchpadSwipeShortcut(SwipeDirection::Down, 3, m_swipeGestureReleasedY.get(), [this](qreal cb) {
+    input()->registerTouchpadSwipeShortcut(SwipeDirection::Down, 3, m_swipeGestureReleasedY.get(), [this, emitCurrentChanging](qreal cb) {
         if (grid().height() > 1) {
             m_currentDesktopOffset.setY(-cb);
-            Q_EMIT currentChanging(currentDesktop(), m_currentDesktopOffset);
+            emitCurrentChanging();
         }
     });
-    input()->registerTouchpadSwipeShortcut(SwipeDirection::Up, 3, m_swipeGestureReleasedY.get(), [this](qreal cb) {
+    input()->registerTouchpadSwipeShortcut(SwipeDirection::Up, 3, m_swipeGestureReleasedY.get(), [this, emitCurrentChanging](qreal cb) {
         if (grid().height() > 1) {
             m_currentDesktopOffset.setY(cb);
-            Q_EMIT currentChanging(currentDesktop(), m_currentDesktopOffset);
+            emitCurrentChanging();
         }
     });
     input()->registerTouchscreenSwipeShortcut(SwipeDirection::Left, 3, m_swipeGestureReleasedX.get(), left);
diff --git a/src/virtualdesktops.h b/src/virtualdesktops.h
index bc0f77f68a1..c16dc1738bb 100644
--- a/src/virtualdesktops.h
+++ b/src/virtualdesktops.h
@@ -450,8 +450,13 @@ Q_SIGNALS:
      * @param offset The current total change in desktop coordinate
      * Offset x and y are negative if switching Left and Down.
      * Example: x = 0.6 means 60% of the way to the desktop to the right.
+     * @param output The affected output. If it affects multiple outputs, then a separate signal is emitted for each one.
+     */
+    void currentChanging(KWin::VirtualDesktop *currentDesktop, QPointF offset, KWin::LogicalOutput *output);
+
+    /**
+     * Signal emitted when realtime desktop switching animation is cancelled. It applies to all outputs.
      */
-    void currentChanging(KWin::VirtualDesktop *currentDesktop, QPointF offset);
     void currentChangingCancelled();
 
     /**
diff --git a/src/workspace.cpp b/src/workspace.cpp
index 5991cac7354..f8a1a1669eb 100644
--- a/src/workspace.cpp
+++ b/src/workspace.cpp
@@ -1026,10 +1026,10 @@ void Workspace::slotCurrentDesktopChanged(VirtualDesktop *oldDesktop, VirtualDes
     Q_EMIT currentDesktopChanged(oldDesktop, newDesktop, output, m_moveResizeWindow);
 }
 
-void Workspace::slotCurrentDesktopChanging(VirtualDesktop *currentDesktop, QPointF offset)
+void Workspace::slotCurrentDesktopChanging(VirtualDesktop *currentDesktop, QPointF offset, LogicalOutput *output)
 {
     closeActivePopup();
-    Q_EMIT currentDesktopChanging(currentDesktop, offset, m_moveResizeWindow);
+    Q_EMIT currentDesktopChanging(currentDesktop, offset, output, m_moveResizeWindow);
 }
 
 void Workspace::slotCurrentDesktopChangingCancelled()
diff --git a/src/workspace.h b/src/workspace.h
index 4f3e940cb9e..b920c2dc251 100644
--- a/src/workspace.h
+++ b/src/workspace.h
@@ -532,7 +532,7 @@ private Q_SLOTS:
     void updateCurrentActivity(const QString &new_activity);
     // virtual desktop handling
     void slotCurrentDesktopChanged(VirtualDesktop *previousDesktop, VirtualDesktop *newDesktop, LogicalOutput *output);
-    void slotCurrentDesktopChanging(VirtualDesktop *currentDesktop, QPointF delta);
+    void slotCurrentDesktopChanging(VirtualDesktop *currentDesktop, QPointF delta, LogicalOutput *output);
     void slotCurrentDesktopChangingCancelled();
     void slotDesktopAdded(VirtualDesktop *desktop);
     void slotDesktopRemoved(VirtualDesktop *desktop);
@@ -549,7 +549,7 @@ Q_SIGNALS:
     // Signals required for the scripting interface
     void currentActivityChanged();
     void currentDesktopChanged(KWin::VirtualDesktop *previousDesktop, KWin::VirtualDesktop *newDesktop, KWin::LogicalOutput *output, KWin::Window *);
-    void currentDesktopChanging(KWin::VirtualDesktop *currentDesktop, QPointF delta, KWin::Window *); // for realtime animations
+    void currentDesktopChanging(KWin::VirtualDesktop *currentDesktop, QPointF delta, KWin::LogicalOutput *output, KWin::Window *); // for realtime animations
     void currentDesktopChangingCancelled();
     void windowAdded(KWin::Window *);
     void windowRemoved(KWin::Window *);
-- 
GitLab


From 4bf7026c087f23473b0303f05db54b999c6200d6 Mon Sep 17 00:00:00 2001
From: Hynek Schlindenbuch <schlindenbuch.h@seznam.cz>
Date: Mon, 15 Dec 2025 11:26:01 +0100
Subject: [PATCH 22/63] fix touchpad desktop switching for overview effect

Previously the touchpad desktop switching affected all screens
regardless of per-output virtual desktop switching. Now it should work
correctly in both cases.
---
 src/plugins/overview/overvieweffect.cpp | 22 +++++++++--------
 src/plugins/overview/overvieweffect.h   |  6 ++---
 src/plugins/overview/qml/Main.qml       | 32 ++++++++++++++++++-------
 3 files changed, 39 insertions(+), 21 deletions(-)

diff --git a/src/plugins/overview/overvieweffect.cpp b/src/plugins/overview/overvieweffect.cpp
index b96d797708c..62916cb3846 100644
--- a/src/plugins/overview/overvieweffect.cpp
+++ b/src/plugins/overview/overvieweffect.cpp
@@ -106,17 +106,19 @@ OverviewEffect::OverviewEffect()
     connect(m_gridState, &EffectTogglableState::partialActivationFactorChanged, this, &OverviewEffect::gridPartialActivationFactorChanged);
 
     connect(effects, &EffectsHandler::desktopChanging, this, [this](VirtualDesktop *old, QPointF desktopOffset, LogicalOutput *output, EffectWindow *with) {
-        // TODO: VD: Fix this for per-output VDs (currently it affects all outputs).
-        m_desktopOffset = desktopOffset;
-        Q_EMIT desktopOffsetChanged();
+        m_screenDesktopOffsets.insertOrAssign(output, desktopOffset);
+        Q_EMIT desktopOffsetChanged(output);
     });
-    connect(effects, &EffectsHandler::desktopChanged, this, [this]() {
-        m_desktopOffset = QPointF(0, 0);
-        Q_EMIT desktopOffsetChanged();
+    connect(effects, &EffectsHandler::desktopChanged, this, [this](KWin::VirtualDesktop *, KWin::VirtualDesktop *, KWin::LogicalOutput *output) {
+        m_screenDesktopOffsets.insertOrAssign(output, QPointF(0, 0));
+        Q_EMIT desktopOffsetChanged(output);
     });
     connect(effects, &EffectsHandler::desktopChangingCancelled, this, [this]() {
-        m_desktopOffset = QPointF(0, 0);
-        Q_EMIT desktopOffsetChanged();
+        m_screenDesktopOffsets.clear();
+
+        for (LogicalOutput *output : effects->screens()) {
+            Q_EMIT desktopOffsetChanged(output);
+        }
     });
 
     m_shutdownTimer->setSingleShot(true);
@@ -263,9 +265,9 @@ bool OverviewEffect::gridGestureInProgress() const
     return m_gridState->inProgress();
 }
 
-QPointF OverviewEffect::desktopOffset() const
+QPointF OverviewEffect::desktopOffsetForScreen(LogicalOutput *screen) const
 {
-    return m_desktopOffset;
+    return m_screenDesktopOffsets.value(screen, QPointF(0, 0));
 }
 
 bool OverviewEffect::ignoreMinimized() const
diff --git a/src/plugins/overview/overvieweffect.h b/src/plugins/overview/overvieweffect.h
index 99fbf7719bf..ed366fba7b0 100644
--- a/src/plugins/overview/overvieweffect.h
+++ b/src/plugins/overview/overvieweffect.h
@@ -28,7 +28,6 @@ class OverviewEffect : public QuickSceneEffect
     Q_PROPERTY(bool transitionGestureInProgress READ transitionGestureInProgress NOTIFY transitionGestureInProgressChanged)
     Q_PROPERTY(qreal gridPartialActivationFactor READ gridPartialActivationFactor NOTIFY gridPartialActivationFactorChanged)
     Q_PROPERTY(bool gridGestureInProgress READ gridGestureInProgress NOTIFY gridGestureInProgressChanged)
-    Q_PROPERTY(QPointF desktopOffset READ desktopOffset NOTIFY desktopOffsetChanged)
     Q_PROPERTY(QString searchText MEMBER m_searchText NOTIFY searchTextChanged)
 
 public:
@@ -51,6 +50,7 @@ public:
     qreal gridPartialActivationFactor() const;
     bool gridGestureInProgress() const;
     QPointF desktopOffset() const;
+    Q_INVOKABLE QPointF desktopOffsetForScreen(LogicalOutput *screen) const;
 
     int requestedEffectChainPosition() const override;
     bool borderActivated(ElectricBorder border) override;
@@ -67,7 +67,7 @@ Q_SIGNALS:
     void ignoreMinimizedChanged();
     void filterWindowsChanged();
     void organizedGridChanged();
-    void desktopOffsetChanged();
+    void desktopOffsetChanged(KWin::LogicalOutput *screen);
     void searchTextChanged();
 
 public Q_SLOTS:
@@ -92,8 +92,8 @@ private:
     QList<QKeySequence> m_gridShortcut;
     QList<ElectricBorder> m_borderActivate;
     QList<ElectricBorder> m_gridBorderActivate;
+    QHash<LogicalOutput *, QPointF> m_screenDesktopOffsets;
     QString m_searchText;
-    QPointF m_desktopOffset;
     bool m_filterWindows = true;
     int m_animationDuration = 400;
 };
diff --git a/src/plugins/overview/qml/Main.qml b/src/plugins/overview/qml/Main.qml
index c27e198a495..db4b73b37ad 100644
--- a/src/plugins/overview/qml/Main.qml
+++ b/src/plugins/overview/qml/Main.qml
@@ -438,14 +438,14 @@ FocusScope {
                 property real column: index % columns
                 // deltaX and deltaY are used to move all the desktops together to 1:1 animate the
                 // switching between different desktops
-                property real deltaX: (!current ? effect.desktopOffset.x :
-                                       column == 0 ? Math.max(0, effect.desktopOffset.x) :
-                                       column == columns - 1 ? Math.min(0, effect.desktopOffset.x) :
-                                       effect.desktopOffset.x)
-                property real deltaY: (!current ? effect.desktopOffset.y :
-                                       row == 0 ? Math.max(0, effect.desktopOffset.y) :
-                                       row == rows - 1 ? Math.min(0, effect.desktopOffset.y) :
-                                       effect.desktopOffset.y)
+                property real deltaX: (!current ? targetScreenDesktopOffset.desktopOffset.x :
+                                       column == 0 ? Math.max(0, targetScreenDesktopOffset.desktopOffset.x) :
+                                       column == columns - 1 ? Math.min(0, targetScreenDesktopOffset.desktopOffset.x) :
+                                           targetScreenDesktopOffset.desktopOffset.x)
+                property real deltaY: (!current ? targetScreenDesktopOffset.desktopOffset.y :
+                                       row == 0 ? Math.max(0, targetScreenDesktopOffset.desktopOffset.y) :
+                                       row == rows - 1 ? Math.min(0, targetScreenDesktopOffset.desktopOffset.y) :
+                                           targetScreenDesktopOffset.desktopOffset.y)
                 // deltaColumn and deltaRows are the difference between the column/row of this desktop
                 // compared to the column/row of the active one
                 property real deltaColumn: column - allDesktopHeaps.currentBackgroundItem.column - deltaX
@@ -800,6 +800,22 @@ FocusScope {
         organized = true
     }
 
+    Item {
+        id: targetScreenDesktopOffset
+        property point desktopOffset: effect.desktopOffsetForScreen(targetScreen)
+    }
+
+    Connections {
+        target: effect
+        onDesktopOffsetChanged: (screen) => {
+            if (screen !== container.targetScreen) {
+                return;
+            }
+
+            targetScreenDesktopOffset.desktopOffset = effect.desktopOffsetForScreen(screen);
+        }
+    }
+
     Connections {
         target: KWinComponents.Workspace
         onCurrentDesktopChanged: (previous, current, screen) => {
-- 
GitLab


From 7c76008ee77f14fea95e572bb726511590bd1598 Mon Sep 17 00:00:00 2001
From: Hynek Schlindenbuch <schlindenbuch.h@seznam.cz>
Date: Tue, 16 Dec 2025 13:03:08 +0100
Subject: [PATCH 23/63] remove unnecessary TODOs

---
 src/dbusinterface.cpp       | 1 -
 src/effect/effecthandler.h  | 1 -
 src/plugins/slide/slide.cpp | 2 --
 src/sm.cpp                  | 1 -
 src/virtualdesktops.cpp     | 4 ----
 src/workspace.cpp           | 1 -
 6 files changed, 10 deletions(-)

diff --git a/src/dbusinterface.cpp b/src/dbusinterface.cpp
index ad0ae455607..cdee5d09de3 100644
--- a/src/dbusinterface.cpp
+++ b/src/dbusinterface.cpp
@@ -298,7 +298,6 @@ VirtualDesktopManagerDBusInterface::VirtualDesktopManagerDBusInterface(VirtualDe
                                                  QStringLiteral("org.kde.KWin.VirtualDesktopManager"),
                                                  this);
 
-    // TODO: VD: What about this?
     connect(m_manager, &VirtualDesktopManager::currentChanged, this, [this](VirtualDesktop *oldDesktop, VirtualDesktop *newDesktop, LogicalOutput *output) {
         if (output != workspace()->activeOutput()) {
             return;
diff --git a/src/effect/effecthandler.h b/src/effect/effecthandler.h
index d8742f771a0..2e754a55560 100644
--- a/src/effect/effecthandler.h
+++ b/src/effect/effecthandler.h
@@ -129,7 +129,6 @@ class KWIN_EXPORT EffectsHandler : public QObject
     Q_PROPERTY(int workspaceWidth READ workspaceWidth)
     Q_PROPERTY(int workspaceHeight READ workspaceHeight)
     Q_PROPERTY(QList<KWin::VirtualDesktop *> desktops READ desktops)
-    // TODO: VD: Do we need this for perOutputVirtualDesktops as well?
     Q_PROPERTY(bool optionRollOverDesktops READ optionRollOverDesktops)
     Q_PROPERTY(KWin::LogicalOutput *activeScreen READ activeScreen)
     /**
diff --git a/src/plugins/slide/slide.cpp b/src/plugins/slide/slide.cpp
index 5a5a3d88a36..8ccbc467db0 100644
--- a/src/plugins/slide/slide.cpp
+++ b/src/plugins/slide/slide.cpp
@@ -404,7 +404,6 @@ void SlideEffectScreen::prepareSwitching()
     m_windowData.reserve(windows.count());
 
     for (EffectWindow *w : windows) {
-        // TODO: VD: Is this correct?
         if (w->screen() != m_screen) {
             continue;
         }
@@ -444,7 +443,6 @@ void SlideEffectScreen::finishedSwitching()
     }
     const QList<EffectWindow *> windows = effects->stackingOrder();
     for (EffectWindow *w : windows) {
-        // TODO: VD: Is this correct?
         if (w->screen() != m_screen) {
             continue;
         }
diff --git a/src/sm.cpp b/src/sm.cpp
index cd31b173fe5..04778cc7301 100644
--- a/src/sm.cpp
+++ b/src/sm.cpp
@@ -134,7 +134,6 @@ void SessionManager::storeSession(const QString &sessionName, SMSavePhase phase)
         // but both Qt and KDE treat phase1 and phase2 separately,
         // which results in different sessionkey and different config file :(
         m_sessionActiveClient = active_client;
-        // TODO: VD: Store VD for all outputs
         m_sessionDesktop = VirtualDesktopManager::self()->current();
     } else if (phase == SMSavePhase2) {
         cg.writeEntry("count", count);
diff --git a/src/virtualdesktops.cpp b/src/virtualdesktops.cpp
index 58b791d1589..d70dd266c85 100644
--- a/src/virtualdesktops.cpp
+++ b/src/virtualdesktops.cpp
@@ -103,7 +103,6 @@ void VirtualDesktopManager::setVirtualDesktopManagement(PlasmaVirtualDesktopMana
     connect(this, &VirtualDesktopManager::currentChanged, m_virtualDesktopManagement, [this](VirtualDesktop *oldDesktop, VirtualDesktop *newDesktop, LogicalOutput *output) {
         m_virtualDesktopManagement->setActiveDesktopForOutput(output, newDesktop->id());
 
-        // TODO: VD: is this correct?
         if (output != workspace()->activeOutput()) {
             return;
         }
@@ -257,7 +256,6 @@ void VirtualDesktopManager::setRootInfo(NETRootInfo *info)
     if (m_rootInfo) {
         if (RootInfo::desktopEnabled()) {
             updateRootInfo();
-            // TODO: VD: Figure out what to do with rootinfo
             m_rootInfo->setCurrentDesktop(currentDesktop()->x11DesktopNumber());
             for (auto *vd : std::as_const(m_desktops)) {
                 m_rootInfo->setDesktopName(vd->x11DesktopNumber(), vd->name().toUtf8().data());
@@ -620,7 +618,6 @@ bool VirtualDesktopManager::setCurrent(uint newDesktop, LogicalOutput *output)
     return setCurrent(d, output);
 }
 
-// TODO: VD: Go through all usages and make sure that they're OK.
 bool VirtualDesktopManager::setCurrent(VirtualDesktop *newDesktop, LogicalOutput *output)
 {
     Q_ASSERT(newDesktop);
@@ -987,7 +984,6 @@ void VirtualDesktopManager::updateLegacyPlasmaVirtualDesktops(VirtualDesktop *ac
     const QList<PlasmaVirtualDesktopInterface *> deskIfaces = m_virtualDesktopManagement->desktops();
     for (auto *deskInt : deskIfaces) {
         if (deskInt->id() == activeDesktop->id()) {
-            // TODO: VD: Figure out how to replace the bool
             deskInt->setActive(true);
         } else {
             deskInt->setActive(false);
diff --git a/src/workspace.cpp b/src/workspace.cpp
index f8a1a1669eb..7df95cef67e 100644
--- a/src/workspace.cpp
+++ b/src/workspace.cpp
@@ -1049,7 +1049,6 @@ void Workspace::updateWindowVisibilityOnDesktopChange(VirtualDesktop *newDesktop
             (c)->updateVisibility();
         }
     }
-    // TODO: VD: Is this correct?
     // Now propagate the change, after hiding, before showing
     if (rootInfo() && output == m_activeOutput) {
         rootInfo()->setCurrentDesktop(newDesktop->x11DesktopNumber());
-- 
GitLab


From 529d4e42ca5f217017ad5967d3e2c647fc8c4ba9 Mon Sep 17 00:00:00 2001
From: Hynek Schlindenbuch <schlindenbuch.h@seznam.cz>
Date: Tue, 16 Dec 2025 13:10:09 +0100
Subject: [PATCH 24/63] fix SlideEffect::desktopChangingCancelled

---
 src/plugins/slide/slide.cpp | 9 ++++++---
 1 file changed, 6 insertions(+), 3 deletions(-)

diff --git a/src/plugins/slide/slide.cpp b/src/plugins/slide/slide.cpp
index 8ccbc467db0..89bda86eae9 100644
--- a/src/plugins/slide/slide.cpp
+++ b/src/plugins/slide/slide.cpp
@@ -534,14 +534,17 @@ void SlideEffect::desktopChangingCancelled()
     // If the fingers have been lifted and the current desktop didn't change, start animation
     // to move back to the original virtual desktop.
     if (effects->activeFullScreenEffect() == this) {
-        // TODO: VD: Apply it to all/one screen depending on settings.
-        getOrCreateSlideEffectScreen(effects->activeScreen()).desktopChangingCancelled();
+        for (LogicalOutput *screen : effects->screens()) {
+            getOrCreateSlideEffectScreen(screen).desktopChangingCancelled();
+        }
     }
 }
 
 void SlideEffectScreen::desktopChangingCancelled()
 {
-    startAnimation(m_gesturePos, effects->currentDesktop(m_screen), nullptr);
+    if (m_state != State::Inactive) {
+        startAnimation(m_gesturePos, effects->currentDesktop(m_screen), nullptr);
+    }
 }
 
 QPointF SlideEffectScreen::moveInsideDesktopGrid(QPointF p)
-- 
GitLab


From 4d5c2c487c4d40c35a63b50a2b05322d25ad6913 Mon Sep 17 00:00:00 2001
From: Hynek Schlindenbuch <schlindenbuch.h@seznam.cz>
Date: Tue, 16 Dec 2025 13:20:54 +0100
Subject: [PATCH 25/63] remove complicated handling of deleted windows in slide
 animation

---
 src/plugins/slide/slide.cpp | 10 +++-------
 1 file changed, 3 insertions(+), 7 deletions(-)

diff --git a/src/plugins/slide/slide.cpp b/src/plugins/slide/slide.cpp
index 89bda86eae9..1f46c2a9497 100644
--- a/src/plugins/slide/slide.cpp
+++ b/src/plugins/slide/slide.cpp
@@ -455,11 +455,7 @@ void SlideEffectScreen::finishedSwitching()
     }
     m_elevatedWindows.clear();
 
-    // Clearing the window data may trigger windowDeleted which would try to delete the windows again. So it's necessary to reinitilize m_windowData first.
-    // TODO: VD: Now that I have the deleteRef, can't I just get rid of the problematic windowDeleted code and fix it that way?
-    QHash<EffectWindow *, WindowData> tmpWindowData;
-    tmpWindowData.swap(m_windowData);
-    tmpWindowData.clear();
+    m_windowData.clear();
     m_movingWindow = nullptr;
     m_state = State::Inactive;
 }
@@ -606,8 +602,8 @@ void SlideEffectScreen::windowDeleted(EffectWindow *w)
     if (w == m_movingWindow) {
         m_movingWindow = nullptr;
     }
-    m_elevatedWindows.removeAll(w);
-    m_windowData.remove(w);
+    // m_windowData holds a EffectWindowDeletedRef, so when this is called the window can no longer be in m_windowData.
+    // m_elevatedWindows is a subset of m_windowData, therefore the window cannot be in m_elevatedWindows either.
 }
 
 /*
-- 
GitLab


From 5e944d964125ae5af8960aa996ea4cabf056f6c7 Mon Sep 17 00:00:00 2001
From: Hynek Schlindenbuch <schlindenbuch.h@seznam.cz>
Date: Sat, 20 Dec 2025 10:58:19 +0100
Subject: [PATCH 26/63] fix crash in desktop slide animation after removing
 screen

The screenRemoved event is triggered before windowDeleted. So when a
screen was removed, the slide effect screen was correctly removed as
well. But then windowDeleted was triggered for that screen and it
immediatelly recreated the slide effect screen which caused a crash the
next time the slide animation was triggered.
---
 src/plugins/slide/slide.cpp | 8 +++++++-
 1 file changed, 7 insertions(+), 1 deletion(-)

diff --git a/src/plugins/slide/slide.cpp b/src/plugins/slide/slide.cpp
index 1f46c2a9497..0e0365fe839 100644
--- a/src/plugins/slide/slide.cpp
+++ b/src/plugins/slide/slide.cpp
@@ -591,7 +591,13 @@ void SlideEffect::windowDeleted(EffectWindow *w)
     if (!w) {
         return;
     }
-    getOrCreateSlideEffectScreen(w->screen()).windowDeleted(w);
+
+    // Don't use getOrCreateSlideEffectScreen: it could re-create SlideEffectScreen for removed screen.
+    auto it = m_slideEffectScreens.find(w->screen());
+    if (it == m_slideEffectScreens.end()) {
+        return;
+    }
+    it->windowDeleted(w);
 }
 
 void SlideEffectScreen::windowDeleted(EffectWindow *w)
-- 
GitLab


From 888b7ea200eae3311703cba342790a71d59e052c Mon Sep 17 00:00:00 2001
From: Hynek Schlindenbuch <schlindenbuch.h@seznam.cz>
Date: Sat, 20 Dec 2025 13:15:13 +0100
Subject: [PATCH 27/63] initialize virtual desktops for new output

---
 src/virtualdesktops.cpp | 29 +++++++++++++++++++++++------
 src/virtualdesktops.h   | 11 +++++++++++
 2 files changed, 34 insertions(+), 6 deletions(-)

diff --git a/src/virtualdesktops.cpp b/src/virtualdesktops.cpp
index d70dd266c85..eab89d4920c 100644
--- a/src/virtualdesktops.cpp
+++ b/src/virtualdesktops.cpp
@@ -112,8 +112,8 @@ void VirtualDesktopManager::setVirtualDesktopManagement(PlasmaVirtualDesktopMana
         updateLegacyPlasmaVirtualDesktops(currentDesktop(output));
     });
 
-    auto createPlasmaOutputVirtualDesktop = [this](LogicalOutput *output) {
-        // TODO: VD: How is the current desktop initialized for a new output?
+    auto initNewOutput = [this](LogicalOutput *output) {
+        initCurrentDesktopForOutput(output);
         PlasmaOutputVirtualDesktopInterface *outputVd = m_virtualDesktopManagement->addOutput(output, currentDesktop(output)->id());
 
         connect(outputVd, &PlasmaOutputVirtualDesktopInterface::activateDesktopRequested, output, [this, output](QString desktopId) {
@@ -126,15 +126,16 @@ void VirtualDesktopManager::setVirtualDesktopManagement(PlasmaVirtualDesktopMana
         });
     };
 
-    connect(workspace(), &Workspace::outputAdded, this, createPlasmaOutputVirtualDesktop);
+    connect(workspace(), &Workspace::outputAdded, this, initNewOutput);
     connect(workspace(), &Workspace::outputRemoved, this, [this](LogicalOutput *output) {
         m_virtualDesktopManagement->removeOutput(output);
+        m_currentDesktops.remove(output);
     });
 
     std::for_each(m_desktops.constBegin(), m_desktops.constEnd(), createPlasmaVirtualDesktop);
 
     for (LogicalOutput *output : workspace()->outputs()) {
-        createPlasmaOutputVirtualDesktop(output);
+        initNewOutput(output);
     }
 
     m_virtualDesktopManagement->setRows(rows());
@@ -604,8 +605,8 @@ VirtualDesktop *VirtualDesktopManager::currentDesktop(LogicalOutput *output) con
         output = workspace()->activeOutput();
     }
     VirtualDesktop *result = m_currentDesktops[output];
-    // TODO: VD: Should it fallback to 0?
-    return result ? result : m_desktops.at(0);
+    // Fallback is necessary because currentDesktop may be called before initialDesktopForNewOutput.
+    return result ? result : initialDesktopForNewOutput();
 }
 
 bool VirtualDesktopManager::setCurrent(uint newDesktop, LogicalOutput *output)
@@ -1027,6 +1028,22 @@ QAction *VirtualDesktopManager::addAction(const QString &name, const QString &la
     return a;
 }
 
+void VirtualDesktopManager::initCurrentDesktopForOutput(LogicalOutput *output)
+{
+    VirtualDesktop *current = m_currentDesktops.value(output, nullptr);
+    if (current) {
+        return;
+    }
+    m_currentDesktops[output] = initialDesktopForNewOutput();
+}
+
+VirtualDesktop *VirtualDesktopManager::initialDesktopForNewOutput() const
+{
+    return m_perOutputVirtualDesktops || m_currentDesktops.empty()
+        ? m_desktops.first()
+        : *m_currentDesktops.begin();
+}
+
 void VirtualDesktopManager::slotSwitchTo()
 {
     QAction *act = qobject_cast<QAction *>(sender());
diff --git a/src/virtualdesktops.h b/src/virtualdesktops.h
index c16dc1738bb..39ddbb22dcb 100644
--- a/src/virtualdesktops.h
+++ b/src/virtualdesktops.h
@@ -563,6 +563,17 @@ private:
      */
     QAction *addAction(const QString &name, const QString &label, const QKeySequence &key, void (VirtualDesktopManager::*slot)());
 
+    /**
+     * Initializes current desktop for @p output.
+     * @param output
+     */
+    void initCurrentDesktopForOutput(LogicalOutput *output);
+
+    /**
+     * Returns the desktop used to initialize a new output.
+     */
+    VirtualDesktop *initialDesktopForNewOutput() const;
+
     QList<VirtualDesktop *> m_desktops;
     QHash<LogicalOutput *, VirtualDesktop *> m_currentDesktops;
     quint32 m_rows = 2;
-- 
GitLab


From 07409dc43d00cd8537eef3fabfe2914a0d00b666 Mon Sep 17 00:00:00 2001
From: Hynek Schlindenbuch <schlindenbuch.h@seznam.cz>
Date: Sat, 20 Dec 2025 13:41:53 +0100
Subject: [PATCH 28/63] fix
 PlasmaVirtualDesktopManagementInterface::removeOutput

---
 src/wayland/plasmavirtualdesktop.cpp | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/wayland/plasmavirtualdesktop.cpp b/src/wayland/plasmavirtualdesktop.cpp
index 75bd49aaa2b..d5239fa2ebf 100644
--- a/src/wayland/plasmavirtualdesktop.cpp
+++ b/src/wayland/plasmavirtualdesktop.cpp
@@ -287,7 +287,7 @@ void PlasmaVirtualDesktopManagementInterface::setActiveDesktopForOutput(LogicalO
 void PlasmaVirtualDesktopManagementInterface::removeOutput(LogicalOutput *output)
 {
     auto i = d->outputs.constFind(output);
-    if (i != d->outputs.constEnd()) {
+    if (i == d->outputs.constEnd()) {
         return;
     }
 
-- 
GitLab


From 2b5d25101059b4c2be970e0c60bc6604b1111687 Mon Sep 17 00:00:00 2001
From: Hynek Schlindenbuch <schlindenbuch.h@seznam.cz>
Date: Sat, 20 Dec 2025 14:03:37 +0100
Subject: [PATCH 29/63] fix desktopchangeosd dialog

It always showed up on active screen, which is not always the best
behavior with per-output virtual desktops enabled. Consider this
scenario:

- Per-output desktops are enabled.
- There is a window on screen A which is not on it's current desktop.
- There is a task manager on screen B which includes tasks from all
  screens and desktops.
- The window is activated from the task manager on screen B, which
  causes screen A's desktop to change. However, B is the active screen,
  so the OSD previously showed up on screen B. Now it shows up on A.

There is still only one dialog shown at a time, even if desktops change
on multiple screens (e.g. per-output desktops are disabled). This is
handled by preferring the active screen.
---
 .../package/contents/ui/main.qml               |  4 ++--
 .../package/contents/ui/osd.qml                | 18 +++++++++---------
 2 files changed, 11 insertions(+), 11 deletions(-)

diff --git a/src/plugins/desktopchangeosd/package/contents/ui/main.qml b/src/plugins/desktopchangeosd/package/contents/ui/main.qml
index 41195fdfa5e..7d3f03c228a 100644
--- a/src/plugins/desktopchangeosd/package/contents/ui/main.qml
+++ b/src/plugins/desktopchangeosd/package/contents/ui/main.qml
@@ -14,11 +14,11 @@ Loader {
 
     Connections {
         target: Workspace
-        function onCurrentDesktopChanged(previous) {
+        function onCurrentDesktopChanged(previous, current, screen) {
             if (!mainItemLoader.item) {
                 mainItemLoader.source = "osd.qml";
             }
-            mainItemLoader.item.show(previous);
+            mainItemLoader.item.show(previous, current, screen);
         }
     }
 }
diff --git a/src/plugins/desktopchangeosd/package/contents/ui/osd.qml b/src/plugins/desktopchangeosd/package/contents/ui/osd.qml
index c758189fdf9..27fc8115abb 100644
--- a/src/plugins/desktopchangeosd/package/contents/ui/osd.qml
+++ b/src/plugins/desktopchangeosd/package/contents/ui/osd.qml
@@ -50,7 +50,6 @@ PlasmaCore.Window {
             horizontalAlignment: Text.AlignHCenter
             wrapMode: Text.NoWrap
             elide: Text.ElideRight
-            text: Workspace.currentDesktop.name
         }
 
         Grid {
@@ -261,25 +260,26 @@ PlasmaCore.Window {
         }
     }
 
-    function show(previous) {
-        if (Workspace.isEffectActive("overview")) {
+    function show(previous, current, screen) {
+        if (Workspace.isEffectActive("overview") || (dialog.visible && screen != Workspace.activeScreen)) {
             return;
         }
         dialogItem.previousIndex = Workspace.desktops.indexOf(previous);
-        dialogItem.currentIndex = Workspace.desktops.indexOf(Workspace.currentDesktop);
+        dialogItem.currentIndex = Workspace.desktops.indexOf(current);
         // screen geometry might have changed
-        var screen = Workspace.clientArea(KWin.FullScreenArea, Workspace.activeScreen, Workspace.currentDesktop);
-        dialogItem.screenWidth = screen.width;
-        dialogItem.screenHeight = screen.height;
+        var screenGeometry = Workspace.clientArea(KWin.FullScreenArea, screen, current);
+        dialogItem.screenWidth = screenGeometry.width;
+        dialogItem.screenHeight = screenGeometry.height;
         if (dialogItem.showGrid) {
             // non dependable properties might have changed
             view.columns = Workspace.desktopGridWidth;
             view.rows = Workspace.desktopGridHeight;
         }
+        textElement.text = current.name;
         dialog.visible = true;
         // position might have changed
-        dialog.x = screen.x + screen.width/2 - dialogItem.width/2;
-        dialog.y = screen.y + screen.height/2 - dialogItem.height/2;
+        dialog.x = screenGeometry.x + screenGeometry.width/2 - dialogItem.width/2;
+        dialog.y = screenGeometry.y + screenGeometry.height/2 - dialogItem.height/2;
         // start the hide timer
         timer.restart();
     }
-- 
GitLab


From 80f7094103a3e12ed116a0af1c24c7307e89a85d Mon Sep 17 00:00:00 2001
From: Hynek Schlindenbuch <schlindenbuch.h@seznam.cz>
Date: Sun, 21 Dec 2025 09:04:57 +0100
Subject: [PATCH 30/63] restore previous desktop for dynamically added output

It only makes sense with per-output desktops enabled. Without it, the
previous desktop was already restored on the initial screen(s) and may
have since changed.
---
 src/activities.cpp | 19 ++++++++++++++++++-
 src/activities.h   |  2 ++
 src/workspace.cpp  | 10 ++++++++++
 3 files changed, 30 insertions(+), 1 deletion(-)

diff --git a/src/activities.cpp b/src/activities.cpp
index 809bc0b2b77..7525c6d94cf 100644
--- a/src/activities.cpp
+++ b/src/activities.cpp
@@ -65,6 +65,24 @@ KActivities::Consumer::ServiceStatus Activities::serviceStatus() const
     return m_controller->serviceStatus();
 }
 
+void Activities::restoreDesktopForNewOutput(LogicalOutput *output) const
+{
+    const auto it = m_lastVirtualDesktop.find(m_current);
+    if (it == m_lastVirtualDesktop.end()) {
+        return;
+    }
+
+    const auto outputDesktopIt = it->second.find(output->uuid());
+    if (outputDesktopIt == it->second.end()) {
+        return;
+    }
+
+    VirtualDesktop *desktop = VirtualDesktopManager::self()->desktopForId(outputDesktopIt->second);
+    if (desktop) {
+        VirtualDesktopManager::self()->setCurrent(desktop, output);
+    }
+}
+
 void Activities::slotServiceStatusChanged()
 {
     if (m_controller->serviceStatus() != KActivities::Consumer::Running) {
@@ -106,7 +124,6 @@ void Activities::slotCurrentChanged(const QString &newActivity)
     m_previous = m_current;
     m_current = newActivity;
 
-    // TODO: VD: What about when an output is added?
     const auto it = m_lastVirtualDesktop.find(m_current);
     if (it != m_lastVirtualDesktop.end()) {
         const auto &outputDesktops = it->second;
diff --git a/src/activities.h b/src/activities.h
index 4ba2494d7a7..6dcfc538265 100644
--- a/src/activities.h
+++ b/src/activities.h
@@ -54,6 +54,8 @@ public:
 
     KActivities::Controller::ServiceStatus serviceStatus() const;
 
+    void restoreDesktopForNewOutput(LogicalOutput *output) const;
+
 Q_SIGNALS:
     /**
      * emitted before the current activity actually changes
diff --git a/src/workspace.cpp b/src/workspace.cpp
index 7df95cef67e..3c036f62604 100644
--- a/src/workspace.cpp
+++ b/src/workspace.cpp
@@ -361,6 +361,16 @@ void Workspace::init()
     connect(kwinApp()->outputBackend(), &OutputBackend::outputAdded, this, [this](BackendOutput *output) {
         connect(output, &BackendOutput::dpmsModeChanged, this, &Workspace::maybeUpdateDpmsState);
     });
+
+#if KWIN_BUILD_ACTIVITIES
+    if (m_activities) {
+        connect(this, &Workspace::outputAdded, m_activities.get(), [this, vds](LogicalOutput *output) {
+            if (vds->isPerOutputVirtualDesktops()) {
+                m_activities->restoreDesktopForNewOutput(output);
+            }
+        });
+    }
+#endif
 }
 
 QString Workspace::outputLayoutId() const
-- 
GitLab


From 73753e4b5a1e1339a82c4557c7a08407f905cf7c Mon Sep 17 00:00:00 2001
From: Hynek Schlindenbuch <schlindenbuch.h@seznam.cz>
Date: Sun, 21 Dec 2025 09:14:26 +0100
Subject: [PATCH 31/63] remove TODO

---
 src/workspace.cpp | 1 -
 1 file changed, 1 deletion(-)

diff --git a/src/workspace.cpp b/src/workspace.cpp
index 3c036f62604..bd8dbe613b1 100644
--- a/src/workspace.cpp
+++ b/src/workspace.cpp
@@ -1109,7 +1109,6 @@ void Workspace::updateWindowVisibilityAndActivateOnDesktopChange(VirtualDesktop
         window->requestTile(tile);
     }
 
-    // TODO: VD: is this correct?
     if (output == m_activeOutput) {
         activateWindowOnDesktop(newDesktop);
     }
-- 
GitLab


From 26775a976e38c6eeb0f64059dd61f20375109617 Mon Sep 17 00:00:00 2001
From: Hynek Schlindenbuch <schlindenbuch.h@seznam.cz>
Date: Sun, 21 Dec 2025 09:31:29 +0100
Subject: [PATCH 32/63] remove TODO

---
 src/plugins/private/qml/WindowHeapDelegate.qml | 2 --
 1 file changed, 2 deletions(-)

diff --git a/src/plugins/private/qml/WindowHeapDelegate.qml b/src/plugins/private/qml/WindowHeapDelegate.qml
index 7433aa4be1d..c8779877b04 100644
--- a/src/plugins/private/qml/WindowHeapDelegate.qml
+++ b/src/plugins/private/qml/WindowHeapDelegate.qml
@@ -29,8 +29,6 @@ ExpoCell {
     property Item contentItemParent: this
 
     // no desktops is a special value which means "All Desktops"
-    // TODO: VD: Does this need currentDesktopOnScreen? And if so, does it need to be live-updated as desktops change?
-    // It's used by overview and window view, but I can't see any difference in any of them.
     readonly property bool presentOnCurrentDesktop: !window.desktops.length || window.desktops.indexOf(KWinComponents.Workspace.currentDesktop) !== -1
     readonly property bool initialHidden: window.minimized
     readonly property bool activeHidden: {
-- 
GitLab


From 179e8f4c6d15a0ecd4d717a2efd413b484862929 Mon Sep 17 00:00:00 2001
From: Hynek Schlindenbuch <schlindenbuch.h@seznam.cz>
Date: Wed, 24 Dec 2025 10:46:56 +0100
Subject: [PATCH 33/63] fix compilation error after rebase

---
 src/plugins/slide/slide.cpp | 4 ++--
 src/plugins/slide/slide.h   | 4 ++--
 2 files changed, 4 insertions(+), 4 deletions(-)

diff --git a/src/plugins/slide/slide.cpp b/src/plugins/slide/slide.cpp
index 0e0365fe839..18f6c697f33 100644
--- a/src/plugins/slide/slide.cpp
+++ b/src/plugins/slide/slide.cpp
@@ -198,7 +198,7 @@ void SlideEffect::paintScreen(const RenderTarget &renderTarget, const RenderView
     getOrCreateSlideEffectScreen(screen).paintScreen(renderTarget, viewport, mask, deviceRegion);
 }
 
-void SlideEffectScreen::paintScreen(const RenderTarget &renderTarget, const RenderViewport &viewport, int mask, const QRegion &deviceRegion)
+void SlideEffectScreen::paintScreen(const RenderTarget &renderTarget, const RenderViewport &viewport, int mask, const Region &deviceRegion)
 {
     m_paintCtx.wrap = effects->optionRollOverDesktops();
     effects->paintScreen(renderTarget, viewport, mask, deviceRegion, m_screen);
@@ -265,7 +265,7 @@ void SlideEffect::paintWindow(const RenderTarget &renderTarget, const RenderView
     getOrCreateSlideEffectScreen(w->screen()).paintWindow(renderTarget, viewport, w, mask, deviceGeometry, data);
 }
 
-void SlideEffectScreen::paintWindow(const RenderTarget &renderTarget, const RenderViewport &viewport, EffectWindow *w, int mask, const QRegion &deviceGeometry, WindowPaintData &data)
+void SlideEffectScreen::paintWindow(const RenderTarget &renderTarget, const RenderViewport &viewport, EffectWindow *w, int mask, const Region &deviceGeometry, WindowPaintData &data)
 {
     if (!willBePainted(w)) {
         return;
diff --git a/src/plugins/slide/slide.h b/src/plugins/slide/slide.h
index c3256929447..216c370b1e2 100644
--- a/src/plugins/slide/slide.h
+++ b/src/plugins/slide/slide.h
@@ -53,10 +53,10 @@ public:
     void reconfigure();
 
     void prePaintScreen(ScreenPrePaintData &data, std::chrono::milliseconds presentTime);
-    void paintScreen(const RenderTarget &renderTarget, const RenderViewport &viewport, int mask, const QRegion &deviceRegion);
+    void paintScreen(const RenderTarget &renderTarget, const RenderViewport &viewport, int mask, const Region &deviceRegion);
     void postPaintScreen();
 
-    void paintWindow(const RenderTarget &renderTarget, const RenderViewport &viewport, EffectWindow *w, int mask, const QRegion &deviceGeometry, WindowPaintData &data);
+    void paintWindow(const RenderTarget &renderTarget, const RenderViewport &viewport, EffectWindow *w, int mask, const Region &deviceGeometry, WindowPaintData &data);
 
     bool isActive() const;
     void desktopChanged(VirtualDesktop *old, VirtualDesktop *current, EffectWindow *with);
-- 
GitLab


From 4810b735e9baae659b940dbb7fe7005cc5b114a3 Mon Sep 17 00:00:00 2001
From: Hynek Schlindenbuch <schlindenbuch.h@seznam.cz>
Date: Sat, 27 Dec 2025 10:11:53 +0100
Subject: [PATCH 34/63] remove useless code

---
 src/virtualdesktops.cpp | 6 ------
 1 file changed, 6 deletions(-)

diff --git a/src/virtualdesktops.cpp b/src/virtualdesktops.cpp
index eab89d4920c..b389317601c 100644
--- a/src/virtualdesktops.cpp
+++ b/src/virtualdesktops.cpp
@@ -696,12 +696,6 @@ void VirtualDesktopManager::setCount(uint count)
         }
     }
 
-    for (auto output : workspace()->outputs()) {
-        if (!m_currentDesktops.contains(output)) {
-            m_currentDesktops.insert(output, m_desktops.at(0));
-        }
-    }
-
     updateLayout();
     updateRootInfo();
 
-- 
GitLab


From 1f8e10b5f92c53857d0c8b9ee4232da0246ddb3f Mon Sep 17 00:00:00 2001
From: Hynek Schlindenbuch <schlindenbuch.h@seznam.cz>
Date: Sat, 27 Dec 2025 10:12:09 +0100
Subject: [PATCH 35/63] add output param to VirtualDesktopManager::current for
 consistency

---
 src/virtualdesktops.cpp | 4 ++--
 src/virtualdesktops.h   | 8 +++++---
 2 files changed, 7 insertions(+), 5 deletions(-)

diff --git a/src/virtualdesktops.cpp b/src/virtualdesktops.cpp
index b389317601c..4fd286eca91 100644
--- a/src/virtualdesktops.cpp
+++ b/src/virtualdesktops.cpp
@@ -593,9 +593,9 @@ void VirtualDesktopManager::moveVirtualDesktop(VirtualDesktop *desktop, int posi
     Q_EMIT desktopMoved(desktop, position);
 }
 
-uint VirtualDesktopManager::current() const
+uint VirtualDesktopManager::current(LogicalOutput *output) const
 {
-    VirtualDesktop *d = currentDesktop();
+    VirtualDesktop *d = currentDesktop(output);
     return d ? d->x11DesktopNumber() : 0;
 }
 
diff --git a/src/virtualdesktops.h b/src/virtualdesktops.h
index 39ddbb22dcb..445db011301 100644
--- a/src/virtualdesktops.h
+++ b/src/virtualdesktops.h
@@ -190,14 +190,16 @@ public:
     uint rows() const;
 
     /**
-     * @returns The ID of the current desktop on the active output.
+     * @returns The ID of the current desktop on @a output.
+     * @param output The output for which to return the current desktop (default = active output)
      * @see setCurrent
      * @see currentChanged
      */
-    uint current() const;
+    uint current(LogicalOutput *output = nullptr) const;
 
     /**
-     * @returns The current desktop on the active output.
+     * @returns The current desktop on @a output.
+     * @param output The output for which to return the current desktop (default = active output)
      * @see setCurrent
      * @see currentChanged
      */
-- 
GitLab


From 67055cc463dbc24e19077baeabd22390433646f3 Mon Sep 17 00:00:00 2001
From: Hynek Schlindenbuch <schlindenbuch.h@seznam.cz>
Date: Sat, 27 Dec 2025 10:12:35 +0100
Subject: [PATCH 36/63] move current desktop tests to integration test

The current desktop functionality now depends on Workspace.
---
 .../integration/virtual_desktop_test.cpp      | 280 ++++++++++++++++++
 autotests/test_virtual_desktops.cpp           | 279 -----------------
 2 files changed, 280 insertions(+), 279 deletions(-)

diff --git a/autotests/integration/virtual_desktop_test.cpp b/autotests/integration/virtual_desktop_test.cpp
index f44c0c090c7..470247bf6af 100644
--- a/autotests/integration/virtual_desktop_test.cpp
+++ b/autotests/integration/virtual_desktop_test.cpp
@@ -31,12 +31,33 @@ private Q_SLOTS:
     void initTestCase();
     void init();
     void cleanup();
+    void current_data();
+    void current();
+    void currentChangeOnCountChange_data();
+    void currentChangeOnCountChange();
+    void next_data();
+    void next();
+    void previous_data();
+    void previous();
+    void left_data();
+    void left();
+    void right_data();
+    void right();
+    void above_data();
+    void above();
+    void below_data();
+    void below();
+    void switchToShortcuts();
 #if KWIN_BUILD_X11
     void testNetCurrentDesktop();
 #endif
     void testLastDesktopRemoved();
     void testWindowOnMultipleDesktops();
     void testRemoveDesktopWithWindow();
+
+private:
+    void addDirectionColumns();
+    void testDirection(const QString &actionName, VirtualDesktopManager::Direction direction);
 };
 
 void VirtualDesktopTest::initTestCase()
@@ -262,5 +283,264 @@ void VirtualDesktopTest::testRemoveDesktopWithWindow()
     QCOMPARE(VirtualDesktopManager::self()->desktops()[1], window->desktops()[0]);
 }
 
+void VirtualDesktopTest::current_data()
+{
+    QTest::addColumn<uint>("count");
+    QTest::addColumn<uint>("init");
+    QTest::addColumn<uint>("request");
+    QTest::addColumn<uint>("result");
+    QTest::addColumn<bool>("signal");
+
+    QTest::newRow("lower") << (uint)4 << (uint)3 << (uint)2 << (uint)2 << true;
+    QTest::newRow("higher") << (uint)4 << (uint)1 << (uint)2 << (uint)2 << true;
+    QTest::newRow("maximum") << (uint)4 << (uint)1 << (uint)4 << (uint)4 << true;
+    QTest::newRow("above maximum") << (uint)4 << (uint)1 << (uint)5 << (uint)1 << false;
+    QTest::newRow("minimum") << (uint)4 << (uint)2 << (uint)1 << (uint)1 << true;
+    QTest::newRow("below minimum") << (uint)4 << (uint)2 << (uint)0 << (uint)2 << false;
+    QTest::newRow("unchanged") << (uint)4 << (uint)2 << (uint)2 << (uint)2 << false;
+}
+
+void VirtualDesktopTest::current()
+{
+    VirtualDesktopManager *vds = VirtualDesktopManager::self();
+    QCOMPARE(vds->current(), (uint)1);
+    QFETCH(uint, count);
+    QFETCH(uint, init);
+    vds->setCount(count);
+    vds->setCurrent(init);
+    QCOMPARE(vds->current(), init);
+
+    QSignalSpy spy(vds, &VirtualDesktopManager::currentChanged);
+
+    QFETCH(uint, request);
+    QFETCH(uint, result);
+    QFETCH(bool, signal);
+    QCOMPARE(vds->setCurrent(request), signal);
+    QCOMPARE(vds->current(), result);
+    QCOMPARE(spy.isEmpty(), !signal);
+    if (!spy.isEmpty()) {
+        QList<QVariant> arguments = spy.takeFirst();
+        QCOMPARE(arguments.count(), 3);
+
+        VirtualDesktop *previous = arguments.at(0).value<VirtualDesktop *>();
+        QCOMPARE(previous->x11DesktopNumber(), init);
+
+        VirtualDesktop *current = arguments.at(1).value<VirtualDesktop *>();
+        QCOMPARE(current->x11DesktopNumber(), result);
+    }
+}
+
+void VirtualDesktopTest::currentChangeOnCountChange_data()
+{
+    QTest::addColumn<uint>("initCount");
+    QTest::addColumn<uint>("initCurrent");
+    QTest::addColumn<uint>("request");
+    QTest::addColumn<uint>("current");
+    QTest::addColumn<bool>("signal");
+
+    QTest::newRow("increment") << (uint)4 << (uint)2 << (uint)5 << (uint)2 << false;
+    QTest::newRow("increment on last") << (uint)4 << (uint)4 << (uint)5 << (uint)4 << false;
+    QTest::newRow("decrement") << (uint)4 << (uint)2 << (uint)3 << (uint)2 << false;
+    QTest::newRow("decrement on second last") << (uint)4 << (uint)3 << (uint)3 << (uint)3 << false;
+    QTest::newRow("decrement on last") << (uint)4 << (uint)4 << (uint)3 << (uint)3 << true;
+    QTest::newRow("multiple decrement") << (uint)4 << (uint)2 << (uint)1 << (uint)1 << true;
+}
+
+void VirtualDesktopTest::currentChangeOnCountChange()
+{
+    VirtualDesktopManager *vds = VirtualDesktopManager::self();
+    QFETCH(uint, initCount);
+    QFETCH(uint, initCurrent);
+    vds->setCount(initCount);
+    vds->setCurrent(initCurrent);
+
+    QSignalSpy spy(vds, &VirtualDesktopManager::currentChanged);
+
+    QFETCH(uint, request);
+    QFETCH(uint, current);
+    QFETCH(bool, signal);
+
+    vds->setCount(request);
+    QCOMPARE(vds->current(), current);
+    QCOMPARE(spy.isEmpty(), !signal);
+}
+
+void VirtualDesktopTest::addDirectionColumns()
+{
+    QTest::addColumn<uint>("initCount");
+    QTest::addColumn<uint>("initCurrent");
+    QTest::addColumn<bool>("wrap");
+    QTest::addColumn<uint>("result");
+}
+
+void VirtualDesktopTest::testDirection(const QString &actionName, VirtualDesktopManager::Direction direction)
+{
+    VirtualDesktopManager *vds = VirtualDesktopManager::self();
+    QFETCH(uint, initCount);
+    QFETCH(uint, initCurrent);
+    vds->setCount(initCount);
+    vds->setCurrent(initCurrent);
+    vds->setRows(2);
+
+    QFETCH(bool, wrap);
+    QFETCH(uint, result);
+    QCOMPARE(vds->inDirection(nullptr, direction, wrap)->x11DesktopNumber(), result);
+
+    vds->setNavigationWrappingAround(wrap);
+    vds->initShortcuts();
+    QAction *action = vds->findChild<QAction *>(actionName);
+    QVERIFY(action);
+    action->trigger();
+    QCOMPARE(vds->current(), result);
+    QCOMPARE(vds->inDirection(initCurrent, direction, wrap), result);
+}
+
+void VirtualDesktopTest::next_data()
+{
+    addDirectionColumns();
+
+    QTest::newRow("one desktop, wrap") << (uint)1 << (uint)1 << true << (uint)1;
+    QTest::newRow("one desktop, no wrap") << (uint)1 << (uint)1 << false << (uint)1;
+    QTest::newRow("desktops, wrap") << (uint)4 << (uint)1 << true << (uint)2;
+    QTest::newRow("desktops, no wrap") << (uint)4 << (uint)1 << false << (uint)2;
+    QTest::newRow("desktops at end, wrap") << (uint)4 << (uint)4 << true << (uint)1;
+    QTest::newRow("desktops at end, no wrap") << (uint)4 << (uint)4 << false << (uint)4;
+}
+
+void VirtualDesktopTest::next()
+{
+    testDirection(QStringLiteral("Switch to Next Desktop"), VirtualDesktopManager::Direction::Next);
+}
+
+void VirtualDesktopTest::previous_data()
+{
+    addDirectionColumns();
+
+    QTest::newRow("one desktop, wrap") << (uint)1 << (uint)1 << true << (uint)1;
+    QTest::newRow("one desktop, no wrap") << (uint)1 << (uint)1 << false << (uint)1;
+    QTest::newRow("desktops, wrap") << (uint)4 << (uint)3 << true << (uint)2;
+    QTest::newRow("desktops, no wrap") << (uint)4 << (uint)3 << false << (uint)2;
+    QTest::newRow("desktops at start, wrap") << (uint)4 << (uint)1 << true << (uint)4;
+    QTest::newRow("desktops at start, no wrap") << (uint)4 << (uint)1 << false << (uint)1;
+}
+
+void VirtualDesktopTest::previous()
+{
+    testDirection(QStringLiteral("Switch to Previous Desktop"), VirtualDesktopManager::Direction::Previous);
+}
+
+void VirtualDesktopTest::left_data()
+{
+    addDirectionColumns();
+    QTest::newRow("one desktop, wrap") << (uint)1 << (uint)1 << true << (uint)1;
+    QTest::newRow("one desktop, no wrap") << (uint)1 << (uint)1 << false << (uint)1;
+    QTest::newRow("desktops, wrap, 1st row") << (uint)4 << (uint)2 << true << (uint)1;
+    QTest::newRow("desktops, no wrap, 1st row") << (uint)4 << (uint)2 << false << (uint)1;
+    QTest::newRow("desktops, wrap, 2nd row") << (uint)4 << (uint)4 << true << (uint)3;
+    QTest::newRow("desktops, no wrap, 2nd row") << (uint)4 << (uint)4 << false << (uint)3;
+
+    QTest::newRow("desktops at start, wrap, 1st row") << (uint)4 << (uint)1 << true << (uint)2;
+    QTest::newRow("desktops at start, no wrap, 1st row") << (uint)4 << (uint)1 << false << (uint)1;
+    QTest::newRow("desktops at start, wrap, 2nd row") << (uint)4 << (uint)3 << true << (uint)4;
+    QTest::newRow("desktops at start, no wrap, 2nd row") << (uint)4 << (uint)3 << false << (uint)3;
+
+    QTest::newRow("non symmetric, start") << (uint)5 << (uint)5 << false << (uint)4;
+    QTest::newRow("non symmetric, end, no wrap") << (uint)5 << (uint)4 << false << (uint)4;
+    QTest::newRow("non symmetric, end, wrap") << (uint)5 << (uint)4 << true << (uint)5;
+}
+
+void VirtualDesktopTest::left()
+{
+    testDirection(QStringLiteral("Switch One Desktop to the Left"), VirtualDesktopManager::Direction::Left);
+}
+
+void VirtualDesktopTest::right_data()
+{
+    addDirectionColumns();
+    QTest::newRow("one desktop, wrap") << (uint)1 << (uint)1 << true << (uint)1;
+    QTest::newRow("one desktop, no wrap") << (uint)1 << (uint)1 << false << (uint)1;
+    QTest::newRow("desktops, wrap, 1st row") << (uint)4 << (uint)1 << true << (uint)2;
+    QTest::newRow("desktops, no wrap, 1st row") << (uint)4 << (uint)1 << false << (uint)2;
+    QTest::newRow("desktops, wrap, 2nd row") << (uint)4 << (uint)3 << true << (uint)4;
+    QTest::newRow("desktops, no wrap, 2nd row") << (uint)4 << (uint)3 << false << (uint)4;
+
+    QTest::newRow("desktops at start, wrap, 1st row") << (uint)4 << (uint)2 << true << (uint)1;
+    QTest::newRow("desktops at start, no wrap, 1st row") << (uint)4 << (uint)2 << false << (uint)2;
+    QTest::newRow("desktops at start, wrap, 2nd row") << (uint)4 << (uint)4 << true << (uint)3;
+    QTest::newRow("desktops at start, no wrap, 2nd row") << (uint)4 << (uint)4 << false << (uint)4;
+
+    QTest::newRow("non symmetric, start") << (uint)5 << (uint)4 << false << (uint)5;
+    QTest::newRow("non symmetric, end, no wrap") << (uint)5 << (uint)5 << false << (uint)5;
+    QTest::newRow("non symmetric, end, wrap") << (uint)5 << (uint)5 << true << (uint)4;
+}
+
+void VirtualDesktopTest::right()
+{
+    testDirection(QStringLiteral("Switch One Desktop to the Right"), VirtualDesktopManager::Direction::Right);
+}
+
+void VirtualDesktopTest::above_data()
+{
+    addDirectionColumns();
+    QTest::newRow("one desktop, wrap") << (uint)1 << (uint)1 << true << (uint)1;
+    QTest::newRow("one desktop, no wrap") << (uint)1 << (uint)1 << false << (uint)1;
+    QTest::newRow("desktops, wrap, 1st column") << (uint)4 << (uint)3 << true << (uint)1;
+    QTest::newRow("desktops, no wrap, 1st column") << (uint)4 << (uint)3 << false << (uint)1;
+    QTest::newRow("desktops, wrap, 2nd column") << (uint)4 << (uint)4 << true << (uint)2;
+    QTest::newRow("desktops, no wrap, 2nd column") << (uint)4 << (uint)4 << false << (uint)2;
+
+    QTest::newRow("desktops at start, wrap, 1st column") << (uint)4 << (uint)1 << true << (uint)3;
+    QTest::newRow("desktops at start, no wrap, 1st column") << (uint)4 << (uint)1 << false << (uint)1;
+    QTest::newRow("desktops at start, wrap, 2nd column") << (uint)4 << (uint)2 << true << (uint)4;
+    QTest::newRow("desktops at start, no wrap, 2nd column") << (uint)4 << (uint)2 << false << (uint)2;
+}
+
+void VirtualDesktopTest::above()
+{
+    testDirection(QStringLiteral("Switch One Desktop Up"), VirtualDesktopManager::Direction::Up);
+}
+
+void VirtualDesktopTest::below_data()
+{
+    addDirectionColumns();
+    QTest::newRow("one desktop, wrap") << (uint)1 << (uint)1 << true << (uint)1;
+    QTest::newRow("one desktop, no wrap") << (uint)1 << (uint)1 << false << (uint)1;
+    QTest::newRow("desktops, wrap, 1st column") << (uint)4 << (uint)1 << true << (uint)3;
+    QTest::newRow("desktops, no wrap, 1st column") << (uint)4 << (uint)1 << false << (uint)3;
+    QTest::newRow("desktops, wrap, 2nd column") << (uint)4 << (uint)2 << true << (uint)4;
+    QTest::newRow("desktops, no wrap, 2nd column") << (uint)4 << (uint)2 << false << (uint)4;
+
+    QTest::newRow("desktops at start, wrap, 1st column") << (uint)4 << (uint)3 << true << (uint)1;
+    QTest::newRow("desktops at start, no wrap, 1st column") << (uint)4 << (uint)3 << false << (uint)3;
+    QTest::newRow("desktops at start, wrap, 2nd column") << (uint)4 << (uint)4 << true << (uint)2;
+    QTest::newRow("desktops at start, no wrap, 2nd column") << (uint)4 << (uint)4 << false << (uint)4;
+}
+
+void VirtualDesktopTest::below()
+{
+    testDirection(QStringLiteral("Switch One Desktop Down"), VirtualDesktopManager::Direction::Down);
+}
+
+void VirtualDesktopTest::switchToShortcuts()
+{
+    VirtualDesktopManager *vds = VirtualDesktopManager::self();
+    vds->setCount(vds->maximum());
+    vds->setCurrent(vds->maximum());
+    QCOMPARE(vds->current(), vds->maximum());
+    vds->initShortcuts();
+    const QString toDesktop = QStringLiteral("Switch to Desktop %1");
+    for (uint i = 1; i <= vds->maximum(); ++i) {
+        const QString desktop(toDesktop.arg(i));
+        QAction *action = vds->findChild<QAction *>(desktop);
+        QVERIFY2(action, desktop.toUtf8().constData());
+        action->trigger();
+        QCOMPARE(vds->current(), i);
+    }
+    // invoke switchTo not from a QAction
+    QMetaObject::invokeMethod(vds, "slotSwitchTo");
+    // should still be on max
+    QCOMPARE(vds->current(), vds->maximum());
+}
+
 WAYLANDTEST_MAIN(VirtualDesktopTest)
 #include "virtual_desktop_test.moc"
diff --git a/autotests/test_virtual_desktops.cpp b/autotests/test_virtual_desktops.cpp
index ec25c182466..4fc4283151e 100644
--- a/autotests/test_virtual_desktops.cpp
+++ b/autotests/test_virtual_desktops.cpp
@@ -52,36 +52,15 @@ private Q_SLOTS:
     void count();
     void navigationWrapsAround_data();
     void navigationWrapsAround();
-    void current_data();
-    void current();
-    void currentChangeOnCountChange_data();
-    void currentChangeOnCountChange();
-    void next_data();
-    void next();
-    void previous_data();
-    void previous();
-    void left_data();
-    void left();
-    void right_data();
-    void right();
-    void above_data();
-    void above();
-    void below_data();
-    void below();
     void updateGrid_data();
     void updateGrid();
     void updateLayout_data();
     void updateLayout();
     void name_data();
     void name();
-    void switchToShortcuts();
     void changeRows();
     void load();
     void save();
-
-private:
-    void addDirectionColumns();
-    void testDirection(const QString &actionName, VirtualDesktopManager::Direction direction);
 };
 
 void TestVirtualDesktops::init()
@@ -178,243 +157,6 @@ void TestVirtualDesktops::navigationWrapsAround()
     QCOMPARE(spy.isEmpty(), !signal);
 }
 
-void TestVirtualDesktops::current_data()
-{
-    QTest::addColumn<uint>("count");
-    QTest::addColumn<uint>("init");
-    QTest::addColumn<uint>("request");
-    QTest::addColumn<uint>("result");
-    QTest::addColumn<bool>("signal");
-
-    QTest::newRow("lower") << (uint)4 << (uint)3 << (uint)2 << (uint)2 << true;
-    QTest::newRow("higher") << (uint)4 << (uint)1 << (uint)2 << (uint)2 << true;
-    QTest::newRow("maximum") << (uint)4 << (uint)1 << (uint)4 << (uint)4 << true;
-    QTest::newRow("above maximum") << (uint)4 << (uint)1 << (uint)5 << (uint)1 << false;
-    QTest::newRow("minimum") << (uint)4 << (uint)2 << (uint)1 << (uint)1 << true;
-    QTest::newRow("below minimum") << (uint)4 << (uint)2 << (uint)0 << (uint)2 << false;
-    QTest::newRow("unchanged") << (uint)4 << (uint)2 << (uint)2 << (uint)2 << false;
-}
-
-void TestVirtualDesktops::current()
-{
-    VirtualDesktopManager *vds = VirtualDesktopManager::self();
-    QCOMPARE(vds->current(), (uint)0);
-    QFETCH(uint, count);
-    QFETCH(uint, init);
-    vds->setCount(count);
-    vds->setCurrent(init);
-    QCOMPARE(vds->current(), init);
-
-    QSignalSpy spy(vds, &VirtualDesktopManager::currentChanged);
-
-    QFETCH(uint, request);
-    QFETCH(uint, result);
-    QFETCH(bool, signal);
-    QCOMPARE(vds->setCurrent(request), signal);
-    QCOMPARE(vds->current(), result);
-    QCOMPARE(spy.isEmpty(), !signal);
-    if (!spy.isEmpty()) {
-        QList<QVariant> arguments = spy.takeFirst();
-        QCOMPARE(arguments.count(), 2);
-
-        VirtualDesktop *previous = arguments.at(0).value<VirtualDesktop *>();
-        QCOMPARE(previous->x11DesktopNumber(), init);
-
-        VirtualDesktop *current = arguments.at(1).value<VirtualDesktop *>();
-        QCOMPARE(current->x11DesktopNumber(), result);
-    }
-}
-
-void TestVirtualDesktops::currentChangeOnCountChange_data()
-{
-    QTest::addColumn<uint>("initCount");
-    QTest::addColumn<uint>("initCurrent");
-    QTest::addColumn<uint>("request");
-    QTest::addColumn<uint>("current");
-    QTest::addColumn<bool>("signal");
-
-    QTest::newRow("increment") << (uint)4 << (uint)2 << (uint)5 << (uint)2 << false;
-    QTest::newRow("increment on last") << (uint)4 << (uint)4 << (uint)5 << (uint)4 << false;
-    QTest::newRow("decrement") << (uint)4 << (uint)2 << (uint)3 << (uint)2 << false;
-    QTest::newRow("decrement on second last") << (uint)4 << (uint)3 << (uint)3 << (uint)3 << false;
-    QTest::newRow("decrement on last") << (uint)4 << (uint)4 << (uint)3 << (uint)3 << true;
-    QTest::newRow("multiple decrement") << (uint)4 << (uint)2 << (uint)1 << (uint)1 << true;
-}
-
-void TestVirtualDesktops::currentChangeOnCountChange()
-{
-    VirtualDesktopManager *vds = VirtualDesktopManager::self();
-    QFETCH(uint, initCount);
-    QFETCH(uint, initCurrent);
-    vds->setCount(initCount);
-    vds->setCurrent(initCurrent);
-
-    QSignalSpy spy(vds, &VirtualDesktopManager::currentChanged);
-
-    QFETCH(uint, request);
-    QFETCH(uint, current);
-    QFETCH(bool, signal);
-
-    vds->setCount(request);
-    QCOMPARE(vds->current(), current);
-    QCOMPARE(spy.isEmpty(), !signal);
-}
-
-void TestVirtualDesktops::addDirectionColumns()
-{
-    QTest::addColumn<uint>("initCount");
-    QTest::addColumn<uint>("initCurrent");
-    QTest::addColumn<bool>("wrap");
-    QTest::addColumn<uint>("result");
-}
-
-void TestVirtualDesktops::testDirection(const QString &actionName, VirtualDesktopManager::Direction direction)
-{
-    VirtualDesktopManager *vds = VirtualDesktopManager::self();
-    QFETCH(uint, initCount);
-    QFETCH(uint, initCurrent);
-    vds->setCount(initCount);
-    vds->setCurrent(initCurrent);
-
-    QFETCH(bool, wrap);
-    QFETCH(uint, result);
-    QCOMPARE(vds->inDirection(nullptr, direction, wrap)->x11DesktopNumber(), result);
-
-    vds->setNavigationWrappingAround(wrap);
-    vds->initShortcuts();
-    QAction *action = vds->findChild<QAction *>(actionName);
-    QVERIFY(action);
-    action->trigger();
-    QCOMPARE(vds->current(), result);
-    QCOMPARE(vds->inDirection(initCurrent, direction, wrap), result);
-}
-
-void TestVirtualDesktops::next_data()
-{
-    addDirectionColumns();
-
-    QTest::newRow("one desktop, wrap") << (uint)1 << (uint)1 << true << (uint)1;
-    QTest::newRow("one desktop, no wrap") << (uint)1 << (uint)1 << false << (uint)1;
-    QTest::newRow("desktops, wrap") << (uint)4 << (uint)1 << true << (uint)2;
-    QTest::newRow("desktops, no wrap") << (uint)4 << (uint)1 << false << (uint)2;
-    QTest::newRow("desktops at end, wrap") << (uint)4 << (uint)4 << true << (uint)1;
-    QTest::newRow("desktops at end, no wrap") << (uint)4 << (uint)4 << false << (uint)4;
-}
-
-void TestVirtualDesktops::next()
-{
-    testDirection(QStringLiteral("Switch to Next Desktop"), VirtualDesktopManager::Direction::Next);
-}
-
-void TestVirtualDesktops::previous_data()
-{
-    addDirectionColumns();
-
-    QTest::newRow("one desktop, wrap") << (uint)1 << (uint)1 << true << (uint)1;
-    QTest::newRow("one desktop, no wrap") << (uint)1 << (uint)1 << false << (uint)1;
-    QTest::newRow("desktops, wrap") << (uint)4 << (uint)3 << true << (uint)2;
-    QTest::newRow("desktops, no wrap") << (uint)4 << (uint)3 << false << (uint)2;
-    QTest::newRow("desktops at start, wrap") << (uint)4 << (uint)1 << true << (uint)4;
-    QTest::newRow("desktops at start, no wrap") << (uint)4 << (uint)1 << false << (uint)1;
-}
-
-void TestVirtualDesktops::previous()
-{
-    testDirection(QStringLiteral("Switch to Previous Desktop"), VirtualDesktopManager::Direction::Previous);
-}
-
-void TestVirtualDesktops::left_data()
-{
-    addDirectionColumns();
-    QTest::newRow("one desktop, wrap") << (uint)1 << (uint)1 << true << (uint)1;
-    QTest::newRow("one desktop, no wrap") << (uint)1 << (uint)1 << false << (uint)1;
-    QTest::newRow("desktops, wrap, 1st row") << (uint)4 << (uint)2 << true << (uint)1;
-    QTest::newRow("desktops, no wrap, 1st row") << (uint)4 << (uint)2 << false << (uint)1;
-    QTest::newRow("desktops, wrap, 2nd row") << (uint)4 << (uint)4 << true << (uint)3;
-    QTest::newRow("desktops, no wrap, 2nd row") << (uint)4 << (uint)4 << false << (uint)3;
-
-    QTest::newRow("desktops at start, wrap, 1st row") << (uint)4 << (uint)1 << true << (uint)2;
-    QTest::newRow("desktops at start, no wrap, 1st row") << (uint)4 << (uint)1 << false << (uint)1;
-    QTest::newRow("desktops at start, wrap, 2nd row") << (uint)4 << (uint)3 << true << (uint)4;
-    QTest::newRow("desktops at start, no wrap, 2nd row") << (uint)4 << (uint)3 << false << (uint)3;
-
-    QTest::newRow("non symmetric, start") << (uint)5 << (uint)5 << false << (uint)4;
-    QTest::newRow("non symmetric, end, no wrap") << (uint)5 << (uint)4 << false << (uint)4;
-    QTest::newRow("non symmetric, end, wrap") << (uint)5 << (uint)4 << true << (uint)5;
-}
-
-void TestVirtualDesktops::left()
-{
-    testDirection(QStringLiteral("Switch One Desktop to the Left"), VirtualDesktopManager::Direction::Left);
-}
-
-void TestVirtualDesktops::right_data()
-{
-    addDirectionColumns();
-    QTest::newRow("one desktop, wrap") << (uint)1 << (uint)1 << true << (uint)1;
-    QTest::newRow("one desktop, no wrap") << (uint)1 << (uint)1 << false << (uint)1;
-    QTest::newRow("desktops, wrap, 1st row") << (uint)4 << (uint)1 << true << (uint)2;
-    QTest::newRow("desktops, no wrap, 1st row") << (uint)4 << (uint)1 << false << (uint)2;
-    QTest::newRow("desktops, wrap, 2nd row") << (uint)4 << (uint)3 << true << (uint)4;
-    QTest::newRow("desktops, no wrap, 2nd row") << (uint)4 << (uint)3 << false << (uint)4;
-
-    QTest::newRow("desktops at start, wrap, 1st row") << (uint)4 << (uint)2 << true << (uint)1;
-    QTest::newRow("desktops at start, no wrap, 1st row") << (uint)4 << (uint)2 << false << (uint)2;
-    QTest::newRow("desktops at start, wrap, 2nd row") << (uint)4 << (uint)4 << true << (uint)3;
-    QTest::newRow("desktops at start, no wrap, 2nd row") << (uint)4 << (uint)4 << false << (uint)4;
-
-    QTest::newRow("non symmetric, start") << (uint)5 << (uint)4 << false << (uint)5;
-    QTest::newRow("non symmetric, end, no wrap") << (uint)5 << (uint)5 << false << (uint)5;
-    QTest::newRow("non symmetric, end, wrap") << (uint)5 << (uint)5 << true << (uint)4;
-}
-
-void TestVirtualDesktops::right()
-{
-    testDirection(QStringLiteral("Switch One Desktop to the Right"), VirtualDesktopManager::Direction::Right);
-}
-
-void TestVirtualDesktops::above_data()
-{
-    addDirectionColumns();
-    QTest::newRow("one desktop, wrap") << (uint)1 << (uint)1 << true << (uint)1;
-    QTest::newRow("one desktop, no wrap") << (uint)1 << (uint)1 << false << (uint)1;
-    QTest::newRow("desktops, wrap, 1st column") << (uint)4 << (uint)3 << true << (uint)1;
-    QTest::newRow("desktops, no wrap, 1st column") << (uint)4 << (uint)3 << false << (uint)1;
-    QTest::newRow("desktops, wrap, 2nd column") << (uint)4 << (uint)4 << true << (uint)2;
-    QTest::newRow("desktops, no wrap, 2nd column") << (uint)4 << (uint)4 << false << (uint)2;
-
-    QTest::newRow("desktops at start, wrap, 1st column") << (uint)4 << (uint)1 << true << (uint)3;
-    QTest::newRow("desktops at start, no wrap, 1st column") << (uint)4 << (uint)1 << false << (uint)1;
-    QTest::newRow("desktops at start, wrap, 2nd column") << (uint)4 << (uint)2 << true << (uint)4;
-    QTest::newRow("desktops at start, no wrap, 2nd column") << (uint)4 << (uint)2 << false << (uint)2;
-}
-
-void TestVirtualDesktops::above()
-{
-    testDirection(QStringLiteral("Switch One Desktop Up"), VirtualDesktopManager::Direction::Up);
-}
-
-void TestVirtualDesktops::below_data()
-{
-    addDirectionColumns();
-    QTest::newRow("one desktop, wrap") << (uint)1 << (uint)1 << true << (uint)1;
-    QTest::newRow("one desktop, no wrap") << (uint)1 << (uint)1 << false << (uint)1;
-    QTest::newRow("desktops, wrap, 1st column") << (uint)4 << (uint)1 << true << (uint)3;
-    QTest::newRow("desktops, no wrap, 1st column") << (uint)4 << (uint)1 << false << (uint)3;
-    QTest::newRow("desktops, wrap, 2nd column") << (uint)4 << (uint)2 << true << (uint)4;
-    QTest::newRow("desktops, no wrap, 2nd column") << (uint)4 << (uint)2 << false << (uint)4;
-
-    QTest::newRow("desktops at start, wrap, 1st column") << (uint)4 << (uint)3 << true << (uint)1;
-    QTest::newRow("desktops at start, no wrap, 1st column") << (uint)4 << (uint)3 << false << (uint)3;
-    QTest::newRow("desktops at start, wrap, 2nd column") << (uint)4 << (uint)4 << true << (uint)2;
-    QTest::newRow("desktops at start, no wrap, 2nd column") << (uint)4 << (uint)4 << false << (uint)4;
-}
-
-void TestVirtualDesktops::below()
-{
-    testDirection(QStringLiteral("Switch One Desktop Down"), VirtualDesktopManager::Direction::Down);
-}
-
 void TestVirtualDesktops::updateGrid_data()
 {
     QTest::addColumn<uint>("initCount");
@@ -541,27 +283,6 @@ void TestVirtualDesktops::name()
     QTEST(vd->name(), "desktopName");
 }
 
-void TestVirtualDesktops::switchToShortcuts()
-{
-    VirtualDesktopManager *vds = VirtualDesktopManager::self();
-    vds->setCount(vds->maximum());
-    vds->setCurrent(vds->maximum());
-    QCOMPARE(vds->current(), vds->maximum());
-    vds->initShortcuts();
-    const QString toDesktop = QStringLiteral("Switch to Desktop %1");
-    for (uint i = 1; i <= vds->maximum(); ++i) {
-        const QString desktop(toDesktop.arg(i));
-        QAction *action = vds->findChild<QAction *>(desktop);
-        QVERIFY2(action, desktop.toUtf8().constData());
-        action->trigger();
-        QCOMPARE(vds->current(), i);
-    }
-    // invoke switchTo not from a QAction
-    QMetaObject::invokeMethod(vds, "slotSwitchTo");
-    // should still be on max
-    QCOMPARE(vds->current(), vds->maximum());
-}
-
 void TestVirtualDesktops::changeRows()
 {
     VirtualDesktopManager *vds = VirtualDesktopManager::self();
-- 
GitLab


From 33903b1b925af5cfd6772ec09da90a5fb804654b Mon Sep 17 00:00:00 2001
From: Hynek Schlindenbuch <schlindenbuch.h@seznam.cz>
Date: Sat, 27 Dec 2025 10:40:13 +0100
Subject: [PATCH 37/63] test desktop switching with per-output desktops enabled

---
 .../integration/virtual_desktop_test.cpp      | 126 ++++++++++++++++++
 1 file changed, 126 insertions(+)

diff --git a/autotests/integration/virtual_desktop_test.cpp b/autotests/integration/virtual_desktop_test.cpp
index 470247bf6af..80032d25067 100644
--- a/autotests/integration/virtual_desktop_test.cpp
+++ b/autotests/integration/virtual_desktop_test.cpp
@@ -54,10 +54,13 @@ private Q_SLOTS:
     void testLastDesktopRemoved();
     void testWindowOnMultipleDesktops();
     void testRemoveDesktopWithWindow();
+    void testPerOutputDesktopSwitching();
+    void testTogglePerOutputDesktops();
 
 private:
     void addDirectionColumns();
     void testDirection(const QString &actionName, VirtualDesktopManager::Direction direction);
+    LogicalOutput *findInactiveOutput() const;
 };
 
 void VirtualDesktopTest::initTestCase()
@@ -317,6 +320,11 @@ void VirtualDesktopTest::current()
     QFETCH(bool, signal);
     QCOMPARE(vds->setCurrent(request), signal);
     QCOMPARE(vds->current(), result);
+
+    for (LogicalOutput *output : workspace()->outputs()) {
+        QCOMPARE(vds->current(output), result);
+    }
+
     QCOMPARE(spy.isEmpty(), !signal);
     if (!spy.isEmpty()) {
         QList<QVariant> arguments = spy.takeFirst();
@@ -542,5 +550,123 @@ void VirtualDesktopTest::switchToShortcuts()
     QCOMPARE(vds->current(), vds->maximum());
 }
 
+void VirtualDesktopTest::testPerOutputDesktopSwitching()
+{
+    VirtualDesktopManager *vds = VirtualDesktopManager::self();
+    vds->setPerOutputVirtualDesktops(true);
+    LogicalOutput *activeOutput = workspace()->activeOutput();
+    LogicalOutput *inactiveOutput = findInactiveOutput();
+
+    QCOMPARE(vds->current(activeOutput), (uint)1);
+    QCOMPARE(vds->current(inactiveOutput), (uint)1);
+    vds->setCount(4);
+    QSignalSpy spy(vds, &VirtualDesktopManager::currentChanged);
+    vds->setCurrent(2, inactiveOutput);
+    QCOMPARE(vds->current(activeOutput), (uint)1);
+    QCOMPARE(vds->current(inactiveOutput), (uint)2);
+    QCOMPARE(spy.size(), 1);
+    QList<QVariant> arguments = spy.takeFirst();
+    QCOMPARE(arguments.count(), 3);
+
+    VirtualDesktop *previous = arguments.at(0).value<VirtualDesktop *>();
+    QCOMPARE(previous->x11DesktopNumber(), (uint)1);
+
+    VirtualDesktop *current = arguments.at(1).value<VirtualDesktop *>();
+    QCOMPARE(current->x11DesktopNumber(), (uint)2);
+
+    LogicalOutput *output = arguments.at(2).value<LogicalOutput *>();
+    QCOMPARE(output, inactiveOutput);
+}
+
+void VirtualDesktopTest::testTogglePerOutputDesktops()
+{
+    VirtualDesktopManager *vds = VirtualDesktopManager::self();
+    vds->setPerOutputVirtualDesktops(false);
+    LogicalOutput *activeOutput = workspace()->activeOutput();
+    LogicalOutput *inactiveOutput = findInactiveOutput();
+
+    QCOMPARE(vds->current(activeOutput), (uint)1);
+    QCOMPARE(vds->current(inactiveOutput), (uint)1);
+    vds->setCount(4);
+    QSignalSpy spy(vds, &VirtualDesktopManager::currentChanged);
+    vds->setCurrent(2, inactiveOutput);
+    QCOMPARE(vds->current(activeOutput), (uint)2);
+    QCOMPARE(vds->current(inactiveOutput), (uint)2);
+
+    QCOMPARE(spy.size(), 2);
+
+    while (!spy.isEmpty()) {
+        QList<QVariant> arguments = spy.takeFirst();
+        QCOMPARE(arguments.count(), 3);
+
+        VirtualDesktop *previous = arguments.at(0).value<VirtualDesktop *>();
+        QCOMPARE(previous->x11DesktopNumber(), (uint)1);
+
+        VirtualDesktop *current = arguments.at(1).value<VirtualDesktop *>();
+        QCOMPARE(current->x11DesktopNumber(), (uint)2);
+        // ignore output
+    }
+
+    vds->setPerOutputVirtualDesktops(true);
+    QCOMPARE(spy.isEmpty(), true);
+    QCOMPARE(vds->current(activeOutput), (uint)2);
+    QCOMPARE(vds->current(inactiveOutput), (uint)2);
+
+    vds->setCurrent(3, inactiveOutput);
+    QCOMPARE(vds->current(activeOutput), (uint)2);
+    QCOMPARE(vds->current(inactiveOutput), (uint)3);
+
+    QCOMPARE(spy.size(), 1);
+
+    {
+        QList<QVariant> arguments = spy.takeFirst();
+        QCOMPARE(arguments.count(), 3);
+
+        VirtualDesktop *previous = arguments.at(0).value<VirtualDesktop *>();
+        QCOMPARE(previous->x11DesktopNumber(), (uint)2);
+
+        VirtualDesktop *current = arguments.at(1).value<VirtualDesktop *>();
+        QCOMPARE(current->x11DesktopNumber(), (uint)3);
+
+        LogicalOutput *output = arguments.at(2).value<LogicalOutput *>();
+        QCOMPARE(output, inactiveOutput);
+    }
+
+    vds->setPerOutputVirtualDesktops(false);
+    QCOMPARE(vds->current(activeOutput), (uint)2);
+    QCOMPARE(vds->current(inactiveOutput), (uint)2);
+
+    QCOMPARE(spy.size(), 1);
+
+    {
+        QList<QVariant> arguments = spy.takeFirst();
+        QCOMPARE(arguments.count(), 3);
+
+        VirtualDesktop *previous = arguments.at(0).value<VirtualDesktop *>();
+        QCOMPARE(previous->x11DesktopNumber(), (uint)3);
+
+        VirtualDesktop *current = arguments.at(1).value<VirtualDesktop *>();
+        QCOMPARE(current->x11DesktopNumber(), (uint)2);
+
+        LogicalOutput *output = arguments.at(2).value<LogicalOutput *>();
+        QCOMPARE(output, inactiveOutput);
+    }
+}
+
+LogicalOutput *VirtualDesktopTest::findInactiveOutput() const
+{
+    LogicalOutput *activeOutput = workspace()->activeOutput();
+
+    for (LogicalOutput *output : workspace()->outputs()) {
+        if (output != activeOutput) {
+            return output;
+        }
+    }
+
+    QTest::qFail("Expected to find inactive output, but didn't find one.", __FILE__, __LINE__);
+
+    return nullptr;
+}
+
 WAYLANDTEST_MAIN(VirtualDesktopTest)
 #include "virtual_desktop_test.moc"
-- 
GitLab


From 80fa0094d928c6723007569a42a27da8c584cacd Mon Sep 17 00:00:00 2001
From: Hynek Schlindenbuch <schlindenbuch.h@seznam.cz>
Date: Sat, 27 Dec 2025 13:40:56 +0100
Subject: [PATCH 38/63] fix kwin-testQuickTiling

---
 autotests/integration/quick_tiling_test.cpp | 79 +++++++++++++++++++++
 src/tiles/tilemanager.cpp                   |  5 +-
 src/workspace.cpp                           |  6 +-
 3 files changed, 87 insertions(+), 3 deletions(-)

diff --git a/autotests/integration/quick_tiling_test.cpp b/autotests/integration/quick_tiling_test.cpp
index 1026bd3755c..5a1518522d0 100644
--- a/autotests/integration/quick_tiling_test.cpp
+++ b/autotests/integration/quick_tiling_test.cpp
@@ -119,6 +119,7 @@ private Q_SLOTS:
     void testCloseTiledWindowX11();
     void testScript_data();
     void testScript();
+    void testDontChangeTileWhenDesktopChangesOnAnotherOutput();
     void testDontCrashWithMaximizeWindowRule();
 };
 
@@ -2304,6 +2305,84 @@ void QuickTilingTest::testScript()
     QCOMPARE(window->frameGeometry(), expectedGeometry);
 }
 
+void QuickTilingTest::testDontChangeTileWhenDesktopChangesOnAnotherOutput()
+{
+    auto vds = VirtualDesktopManager::self();
+    vds->setPerOutputVirtualDesktops(true);
+    const auto desktops = vds->desktops();
+    const auto outputs = workspace()->outputs();
+
+    std::unique_ptr<KWayland::Client::Surface> surface(Test::createSurface());
+    std::unique_ptr<Test::XdgToplevel> shellSurface(Test::createXdgToplevelSurface(surface.get()));
+    auto window = Test::renderAndWaitForShown(surface.get(), QSize(100, 100), Qt::blue);
+    window->setOnAllDesktops(true);
+
+    // We have to receive a configure event when the window becomes active.
+    QSignalSpy tileChangedSpy(window, &Window::tileChanged);
+    QSignalSpy toplevelConfigureRequestedSpy(shellSurface.get(), &Test::XdgToplevel::configureRequested);
+    QSignalSpy surfaceConfigureRequestedSpy(shellSurface->xdgSurface(), &Test::XdgSurface::configureRequested);
+    QVERIFY(surfaceConfigureRequestedSpy.wait());
+    QCOMPARE(surfaceConfigureRequestedSpy.count(), 1);
+
+    auto ackConfigure = [&]() {
+        QVERIFY(surfaceConfigureRequestedSpy.wait());
+        shellSurface->xdgSurface()->ack_configure(surfaceConfigureRequestedSpy.last().at(0).value<quint32>());
+        Test::render(surface.get(), toplevelConfigureRequestedSpy.last().at(0).toSize(), Qt::blue);
+        QVERIFY(tileChangedSpy.wait());
+    };
+
+    auto applyTileLayout = [](CustomTile *tile, qreal left, qreal right) {
+        const auto previousKiddos = tile->childTiles();
+        for (Tile *kiddo : previousKiddos) {
+            tile->destroyChild(kiddo);
+        }
+
+        tile->split(Tile::LayoutDirection::Horizontal);
+        tile->childTiles().at(0)->setRelativeGeometry(RectF(0, 0, left, 1.0));
+
+        QCOMPARE(tile->childTiles().at(0)->relativeGeometry(), RectF(0, 0, left, 1));
+        QCOMPARE(tile->childTiles().at(1)->relativeGeometry(), RectF(left, 0, right, 1));
+    };
+    applyTileLayout(workspace()->rootTile(outputs.at(0), desktops.at(0)), 0.4, 0.6);
+    applyTileLayout(workspace()->rootTile(outputs.at(0), desktops.at(1)), 0.35, 0.65);
+    applyTileLayout(workspace()->rootTile(outputs.at(1), desktops.at(0)), 0.3, 0.7);
+    applyTileLayout(workspace()->rootTile(outputs.at(1), desktops.at(1)), 0.25, 0.75);
+    LogicalOutput *tileOutput = outputs.at(0);
+    LogicalOutput *otherOutput = outputs.at(1);
+    VirtualDesktop *quickTileDesktop = vds->desktopForX11Id(1);
+    VirtualDesktop *customTileDesktop = vds->desktopForX11Id(2);
+    Tile *quickTile = workspace()->tileManager(tileOutput)->quickRootTile(quickTileDesktop)->tileForMode(QuickTileFlag::Left);
+    Tile *customTile = workspace()->rootTile(tileOutput, customTileDesktop)->childTile(1);
+    const RectF originalGeometry = window->frameGeometry();
+
+    vds->setCurrent(quickTileDesktop, tileOutput);
+    quickTile->manage(window);
+    QCOMPARE(window->tile(), nullptr);
+    QCOMPARE(window->requestedTile(), quickTile);
+    QCOMPARE(window->frameGeometry(), originalGeometry);
+    ackConfigure();
+    QCOMPARE(window->tile(), quickTile);
+    QCOMPARE(window->requestedTile(), quickTile);
+    QCOMPARE(window->frameGeometry(), quickTile->windowGeometry());
+
+    vds->setCurrent(customTileDesktop, tileOutput);
+    customTile->manage(window);
+    ackConfigure();
+    QCOMPARE(window->tile(), customTile);
+    QCOMPARE(window->requestedTile(), customTile);
+    QCOMPARE(window->frameGeometry(), customTile->windowGeometry());
+
+    vds->setCurrent(customTileDesktop, otherOutput);
+    QCOMPARE(window->tile(), customTile);
+    QCOMPARE(window->requestedTile(), customTile);
+    QCOMPARE(window->frameGeometry(), customTile->windowGeometry());
+
+    vds->setCurrent(quickTileDesktop, otherOutput);
+    QCOMPARE(window->tile(), customTile);
+    QCOMPARE(window->requestedTile(), customTile);
+    QCOMPARE(window->frameGeometry(), customTile->windowGeometry());
+}
+
 void QuickTilingTest::testDontCrashWithMaximizeWindowRule()
 {
     // this test verifies that a force-maximize window rule doesn't cause
diff --git a/src/tiles/tilemanager.cpp b/src/tiles/tilemanager.cpp
index f1506736adf..f2a3cd8a488 100644
--- a/src/tiles/tilemanager.cpp
+++ b/src/tiles/tilemanager.cpp
@@ -86,7 +86,10 @@ TileManager::TileManager(LogicalOutput *parent)
         delete m_quickRootTiles.take(desk);
     });
     connect(VirtualDesktopManager::self(), &VirtualDesktopManager::currentChanged,
-            this, [this](VirtualDesktop *oldDesk, VirtualDesktop *newDesk) {
+            this, [this](VirtualDesktop *oldDesk, VirtualDesktop *newDesk, LogicalOutput *output) {
+        if (output != m_output) {
+            return;
+        }
         Q_EMIT rootTileChanged(rootTile());
         Q_EMIT modelChanged(model());
     });
diff --git a/src/workspace.cpp b/src/workspace.cpp
index bd8dbe613b1..2b1c8274ff2 100644
--- a/src/workspace.cpp
+++ b/src/workspace.cpp
@@ -1102,8 +1102,10 @@ void Workspace::updateWindowVisibilityAndActivateOnDesktopChange(VirtualDesktop
         }
 
         Tile *tile = nullptr;
-        if (TileManager *manager = tileManager(output)) {
-            tile = manager->tileForWindow(window, newDesktop);
+        for (const auto &[tileOutput, manager] : m_tileManagers) {
+            if (Tile *candidate = manager->tileForWindow(window, newDesktop)) {
+                tile = candidate;
+            }
         }
 
         window->requestTile(tile);
-- 
GitLab


From 7b852ece72f77b440dba8850c61a3a62f3934c17 Mon Sep 17 00:00:00 2001
From: Hynek Schlindenbuch <schlindenbuch.h@seznam.cz>
Date: Sat, 27 Dec 2025 13:56:44 +0100
Subject: [PATCH 39/63] fix missing parenthesis in minimizeall script

---
 src/plugins/minimizeall/package/contents/code/main.js | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/plugins/minimizeall/package/contents/code/main.js b/src/plugins/minimizeall/package/contents/code/main.js
index b2602b469a1..9738d8b01b4 100644
--- a/src/plugins/minimizeall/package/contents/code/main.js
+++ b/src/plugins/minimizeall/package/contents/code/main.js
@@ -11,7 +11,7 @@ var registeredBorders = [];
 
 function isRelevant(window) {
     return window.minimizable &&
-        (!window.desktops.length || window.desktops.includes(workspace.currentDesktopForScreen(window.output)) &&
+        (!window.desktops.length || window.desktops.includes(workspace.currentDesktopForScreen(window.output))) &&
         (!window.activities.length || window.activities.includes(workspace.currentActivity));
 }
 
-- 
GitLab


From 1c8aa04a439ad35c6a25e1d297d0fd120537c610 Mon Sep 17 00:00:00 2001
From: Hynek Schlindenbuch <schlindenbuch.h@seznam.cz>
Date: Sun, 28 Dec 2025 09:47:03 +0100
Subject: [PATCH 40/63] add more tests per-output desktops

---
 .../integration/idle_inhibition_test.cpp      | 62 +++++++++++++++++++
 .../integration/keyboard_layout_test.cpp      | 56 +++++++++++++++++
 autotests/integration/quick_tiling_test.cpp   |  1 +
 .../integration/virtual_desktop_test.cpp      |  1 +
 4 files changed, 120 insertions(+)

diff --git a/autotests/integration/idle_inhibition_test.cpp b/autotests/integration/idle_inhibition_test.cpp
index d370c0bbb61..dfc216da756 100644
--- a/autotests/integration/idle_inhibition_test.cpp
+++ b/autotests/integration/idle_inhibition_test.cpp
@@ -31,6 +31,7 @@ private Q_SLOTS:
 
     void testInhibit();
     void testDontInhibitWhenNotOnCurrentDesktop();
+    void testDontInhibitWhenNotOnCurrentPerOutputDesktop();
     void testDontInhibitWhenMinimized();
     void testDontInhibitWhenUnmapped();
     void testDontInhibitWhenLeftCurrentDesktop();
@@ -61,6 +62,8 @@ void TestIdleInhibition::cleanup()
 
     VirtualDesktopManager::self()->setCount(1);
     QCOMPARE(VirtualDesktopManager::self()->count(), 1u);
+    VirtualDesktopManager::self()->setPerOutputVirtualDesktops(false);
+    QCOMPARE(VirtualDesktopManager::self()->isPerOutputVirtualDesktops(), false);
 }
 
 void TestIdleInhibition::testInhibit()
@@ -149,6 +152,65 @@ void TestIdleInhibition::testDontInhibitWhenNotOnCurrentDesktop()
     QCOMPARE(input()->idleInhibitors(), QList<Window *>{});
 }
 
+void TestIdleInhibition::testDontInhibitWhenNotOnCurrentPerOutputDesktop()
+{
+    // This test verifies that the idle inhibitor object is not honored when
+    // the associated surface is not on the current virtual desktop.
+
+    VirtualDesktopManager::self()->setCount(2);
+    QCOMPARE(VirtualDesktopManager::self()->count(), 2u);
+    VirtualDesktopManager::self()->setPerOutputVirtualDesktops(true);
+    QCOMPARE(VirtualDesktopManager::self()->isPerOutputVirtualDesktops(), true);
+
+    // Create the test window.
+    std::unique_ptr<KWayland::Client::Surface> surface(Test::createSurface());
+    QVERIFY(surface != nullptr);
+    std::unique_ptr<Test::XdgToplevel> shellSurface(Test::createXdgToplevelSurface(surface.get()));
+    QVERIFY(shellSurface != nullptr);
+
+    // Create the inhibitor object.
+    std::unique_ptr<Test::IdleInhibitorV1> inhibitor(Test::createIdleInhibitorV1(surface.get()));
+    QVERIFY(inhibitor);
+
+    // Render the window.
+    auto window = Test::renderAndWaitForShown(surface.get(), QSize(100, 50), Qt::blue);
+    QVERIFY(window);
+    window->setOutput(workspace()->outputs().at(0));
+    QCOMPARE(window->output(), workspace()->outputs().at(0));
+
+    // The test window should be only on the first virtual desktop.
+    QCOMPARE(window->desktops().count(), 1);
+    QCOMPARE(window->desktops().first(), VirtualDesktopManager::self()->desktops().first());
+
+    // This should inhibit our server object.
+    QCOMPARE(input()->idleInhibitors(), QList<Window *>{window});
+
+    // Switch to the second virtual desktop on another output.
+    VirtualDesktopManager::self()->setCurrent(2, workspace()->outputs().at(1));
+
+    // The surface is still visible, so it should still inhibit.
+    QCOMPARE(input()->idleInhibitors(), QList<Window *>{window});
+
+    // Switch to the second virtual desktop on window's output.
+    VirtualDesktopManager::self()->setCurrent(2, workspace()->outputs().at(0));
+
+    // The surface is no longer visible, so the compositor don't have to honor the
+    // idle inhibitor object.
+    QCOMPARE(input()->idleInhibitors(), QList<Window *>{});
+
+    // Switch back to the first virtual desktop.
+    VirtualDesktopManager::self()->setCurrent(1, workspace()->outputs().at(0));
+
+    // The test window became visible again, so the compositor has to honor the idle
+    // inhibitor object back again.
+    QCOMPARE(input()->idleInhibitors(), QList<Window *>{window});
+
+    // Destroy the test window.
+    shellSurface.reset();
+    QVERIFY(Test::waitForWindowClosed(window));
+    QCOMPARE(input()->idleInhibitors(), QList<Window *>{});
+}
+
 void TestIdleInhibition::testDontInhibitWhenMinimized()
 {
     // This test verifies that the idle inhibitor object is not honored when the
diff --git a/autotests/integration/keyboard_layout_test.cpp b/autotests/integration/keyboard_layout_test.cpp
index 0af9806f605..ae490ae58b0 100644
--- a/autotests/integration/keyboard_layout_test.cpp
+++ b/autotests/integration/keyboard_layout_test.cpp
@@ -60,6 +60,7 @@ private Q_SLOTS:
     void testChangeLayoutThroughDBus();
     void testPerLayoutShortcut();
     void testVirtualDesktopPolicy();
+    void testVirtualDesktopPolicyWithPerOutputDesktops();
     void testWindowPolicy();
     void testApplicationPolicy();
     void testNumLock();
@@ -157,6 +158,7 @@ void KeyboardLayoutTest::init()
 void KeyboardLayoutTest::cleanup()
 {
     Test::destroyWaylandConnection();
+    VirtualDesktopManager::self()->setPerOutputVirtualDesktops(false);
 }
 
 void KeyboardLayoutTest::testReconfigure()
@@ -370,6 +372,60 @@ void KeyboardLayoutTest::testVirtualDesktopPolicy()
     QCOMPARE(layoutGroup.keyList().filter(QStringLiteral("LayoutDefault")).count(), 1);
 }
 
+void KeyboardLayoutTest::testVirtualDesktopPolicyWithPerOutputDesktops()
+{
+    layoutGroup.writeEntry("LayoutList", QStringLiteral("us,de(neo),de"), KConfig::Notify);
+    layoutGroup.writeEntry("SwitchMode", QStringLiteral("Desktop"), KConfig::Notify);
+    layoutGroup.sync();
+
+    reconfigureLayouts();
+    auto xkb = input()->keyboard()->xkb();
+    QCOMPARE(xkb->numberOfLayouts(), 3u);
+    QCOMPARE(xkb->layoutName(), QStringLiteral("English (US)"));
+
+    VirtualDesktopManager::self()->setCount(4);
+    QCOMPARE(VirtualDesktopManager::self()->count(), 4u);
+    VirtualDesktopManager::self()->setPerOutputVirtualDesktops(true);
+    QCOMPARE(VirtualDesktopManager::self()->isPerOutputVirtualDesktops(), true);
+    auto desktops = VirtualDesktopManager::self()->desktops();
+    QCOMPARE(desktops.count(), 4);
+    workspace()->setActiveOutput(workspace()->outputs().at(0));
+
+    // give desktops different layouts
+    uint desktop, layout;
+    for (desktop = 0; desktop < VirtualDesktopManager::self()->count(); ++desktop) {
+        // switch to another virtual desktop
+        VirtualDesktopManager::self()->setCurrent(desktops.at(desktop));
+        QCOMPARE(desktops.at(desktop), VirtualDesktopManager::self()->currentDesktop());
+        // should be reset to English
+        QCOMPARE(xkb->currentLayout(), 0);
+        // change first desktop to German
+        layout = (desktop + 1) % xkb->numberOfLayouts();
+        changeLayout(layout).waitForFinished();
+        QCOMPARE(xkb->currentLayout(), layout);
+    }
+
+    // imitate app restart to test layouts saving feature
+    resetLayouts();
+
+    desktop = 1;
+    layout = (desktop + 1) % xkb->numberOfLayouts();
+
+    // check that layout changes with desktop on active output.
+    VirtualDesktopManager::self()->setCurrent(desktops.at(desktop), workspace()->outputs().at(0));
+    QCOMPARE(xkb->currentLayout(), layout);
+
+    // check that layout is unaffected by desktop changes on inactive output.
+    desktop = 2;
+    VirtualDesktopManager::self()->setCurrent(desktops.at(desktop), workspace()->outputs().at(1));
+    QCOMPARE(xkb->currentLayout(), layout);
+
+    // check that layout is changed with active output change.
+    workspace()->setActiveOutput(workspace()->outputs().at(1));
+    layout = (desktop + 1) % xkb->numberOfLayouts();
+    QCOMPARE(xkb->currentLayout(), layout);
+}
+
 void KeyboardLayoutTest::testWindowPolicy()
 {
     enum Layout {
diff --git a/autotests/integration/quick_tiling_test.cpp b/autotests/integration/quick_tiling_test.cpp
index 5a1518522d0..0394ce3b868 100644
--- a/autotests/integration/quick_tiling_test.cpp
+++ b/autotests/integration/quick_tiling_test.cpp
@@ -167,6 +167,7 @@ void QuickTilingTest::cleanup()
 
     // discard window rules
     workspace()->rulebook()->load();
+    VirtualDesktopManager::self()->setPerOutputVirtualDesktops(false);
 }
 
 void QuickTilingTest::testQuickTiling_data()
diff --git a/autotests/integration/virtual_desktop_test.cpp b/autotests/integration/virtual_desktop_test.cpp
index 80032d25067..b607c89e894 100644
--- a/autotests/integration/virtual_desktop_test.cpp
+++ b/autotests/integration/virtual_desktop_test.cpp
@@ -99,6 +99,7 @@ void VirtualDesktopTest::init()
 void VirtualDesktopTest::cleanup()
 {
     Test::destroyWaylandConnection();
+    VirtualDesktopManager::self()->setPerOutputVirtualDesktops(false);
 }
 
 #if KWIN_BUILD_X11
-- 
GitLab


From 2ed326f6fecbd73a3c5e8c5a79f9dc7150e70f92 Mon Sep 17 00:00:00 2001
From: Hynek Schlindenbuch <schlindenbuch.h@seznam.cz>
Date: Sun, 28 Dec 2025 11:48:07 +0100
Subject: [PATCH 41/63] add more tests for per-output desktops

---
 .../desktop_switching_animation_test.cpp      | 87 +++++++++++++++++++
 .../effects/scripted_effects_test.cpp         | 34 +++++++-
 .../effectsHandlerPerOutputDesktops.js        |  3 +
 autotests/integration/outputchanges_test.cpp  | 54 ++++++++++++
 4 files changed, 176 insertions(+), 2 deletions(-)
 create mode 100644 autotests/integration/effects/scripts/effectsHandlerPerOutputDesktops.js

diff --git a/autotests/integration/effects/desktop_switching_animation_test.cpp b/autotests/integration/effects/desktop_switching_animation_test.cpp
index fa24c17b3c2..ee60c8a1fbd 100644
--- a/autotests/integration/effects/desktop_switching_animation_test.cpp
+++ b/autotests/integration/effects/desktop_switching_animation_test.cpp
@@ -33,6 +33,8 @@ private Q_SLOTS:
 
     void testSwitchDesktops_data();
     void testSwitchDesktops();
+    void testSwitchPerOutputDesktops_data();
+    void testSwitchPerOutputDesktops();
 };
 
 void DesktopSwitchingAnimationTest::initTestCase()
@@ -77,6 +79,7 @@ void DesktopSwitchingAnimationTest::cleanup()
     QVERIFY(effects->loadedEffects().isEmpty());
 
     VirtualDesktopManager::self()->setCount(1);
+    VirtualDesktopManager::self()->setPerOutputVirtualDesktops(false);
 
     Test::destroyWaylandConnection();
 }
@@ -135,5 +138,89 @@ void DesktopSwitchingAnimationTest::testSwitchDesktops()
     QVERIFY(Test::waitForWindowClosed(window));
 }
 
+void DesktopSwitchingAnimationTest::testSwitchPerOutputDesktops_data()
+{
+    QTest::addColumn<QString>("effectName");
+
+    QTest::newRow("Fade Desktop") << QStringLiteral("fadedesktop");
+    QTest::newRow("Slide") << QStringLiteral("slide");
+}
+
+void DesktopSwitchingAnimationTest::testSwitchPerOutputDesktops()
+{
+    // This test verifies that virtual desktop switching animation effects actually
+    // try to animate switching between desktops.
+
+    LogicalOutput *output1 = workspace()->outputs().at(0);
+    LogicalOutput *output2 = workspace()->outputs().at(1);
+    VirtualDesktopManager::self()->setPerOutputVirtualDesktops(true);
+    VirtualDesktopManager::self()->setCount(3);
+    QCOMPARE(VirtualDesktopManager::self()->current(output1), 1u);
+    QCOMPARE(VirtualDesktopManager::self()->current(output2), 1u);
+    QCOMPARE(VirtualDesktopManager::self()->count(), 3u);
+    QCOMPARE(VirtualDesktopManager::self()->isPerOutputVirtualDesktops(), true);
+
+    // The Fade Desktop effect will do nothing if there are no windows to fade,
+    // so we have to create a dummy test windows.
+    std::unique_ptr<KWayland::Client::Surface> surface1(Test::createSurface());
+    QVERIFY(surface1 != nullptr);
+    std::unique_ptr<Test::XdgToplevel> shellSurface1(Test::createXdgToplevelSurface(surface1.get()));
+    QVERIFY(shellSurface1 != nullptr);
+    Window *window1 = Test::renderAndWaitForShown(surface1.get(), QSize(100, 50), Qt::blue);
+    QVERIFY(window1);
+    window1->setOutput(output1);
+    QCOMPARE(window1->desktops().count(), 1);
+    QCOMPARE(window1->desktops().first(), VirtualDesktopManager::self()->desktops().first());
+    QCOMPARE(window1->output(), output1);
+
+    std::unique_ptr<KWayland::Client::Surface> surface2(Test::createSurface());
+    QVERIFY(surface2 != nullptr);
+    std::unique_ptr<Test::XdgToplevel> shellSurface2(Test::createXdgToplevelSurface(surface2.get()));
+    QVERIFY(shellSurface2 != nullptr);
+    Window *window2 = Test::renderAndWaitForShown(surface2.get(), QSize(100, 50), Qt::blue);
+    QVERIFY(window2);
+    window2->setOutput(output2);
+    QCOMPARE(window2->desktops().count(), 1);
+    QCOMPARE(window2->desktops().first(), VirtualDesktopManager::self()->desktops().first());
+    QCOMPARE(window2->output(), output2);
+
+    // Load effect that will be tested.
+    QFETCH(QString, effectName);
+    QVERIFY(effects);
+    QVERIFY(effects->loadEffect(effectName));
+    QCOMPARE(effects->loadedEffects().count(), 1);
+    QCOMPARE(effects->loadedEffects().first(), effectName);
+    Effect *effect = effects->findEffect(effectName);
+    QVERIFY(effect);
+    QVERIFY(!effect->isActive());
+
+    // Switch to the second virtual desktop on first output.
+    VirtualDesktopManager::self()->setCurrent(2u, output1);
+    QCOMPARE(VirtualDesktopManager::self()->current(output1), 2u);
+    QVERIFY(effect->isActive());
+    QCOMPARE(effects->activeFullScreenEffect(), effect);
+
+    // Eventually, the animation will be complete.
+    QTRY_VERIFY(!effect->isActive());
+    QTRY_COMPARE(effects->activeFullScreenEffect(), nullptr);
+
+    // Switch to the second virtual desktop on second output.
+    VirtualDesktopManager::self()->setCurrent(3u, output2);
+    QCOMPARE(VirtualDesktopManager::self()->current(output2), 3u);
+    QVERIFY(effect->isActive());
+    QCOMPARE(effects->activeFullScreenEffect(), effect);
+
+    // Eventually, the animation will be complete.
+    QTRY_VERIFY(!effect->isActive());
+    QTRY_COMPARE(effects->activeFullScreenEffect(), nullptr);
+
+    // Destroy the test windows.
+    surface1.reset();
+    QVERIFY(Test::waitForWindowClosed(window1));
+
+    surface2.reset();
+    QVERIFY(Test::waitForWindowClosed(window2));
+}
+
 WAYLANDTEST_MAIN(DesktopSwitchingAnimationTest)
 #include "desktop_switching_animation_test.moc"
diff --git a/autotests/integration/effects/scripted_effects_test.cpp b/autotests/integration/effects/scripted_effects_test.cpp
index fb1dd2844a0..5b89abda68b 100644
--- a/autotests/integration/effects/scripted_effects_test.cpp
+++ b/autotests/integration/effects/scripted_effects_test.cpp
@@ -43,6 +43,7 @@ private Q_SLOTS:
     void cleanup();
 
     void testEffectsHandler();
+    void testEffectsHandlerPerOutputDesktops();
     void testEffectsContext();
     void testShortcuts();
     void testAnimations_data();
@@ -143,7 +144,10 @@ void ScriptedEffectsTest::initTestCase()
     qputenv("KWIN_COMPOSE", QByteArrayLiteral("O2"));
     qputenv("KWIN_EFFECTS_FORCE_ANIMATIONS", "1");
     kwinApp()->start();
-    Test::setOutputConfig({Rect(0, 0, 1280, 1024)});
+    Test::setOutputConfig({
+        Rect(0, 0, 1280, 1024),
+        Rect(1280, 0, 1280, 1024),
+    });
 
     KWin::VirtualDesktopManager::self()->setCount(2);
 }
@@ -161,6 +165,7 @@ void ScriptedEffectsTest::cleanup()
     QVERIFY(effects->loadedEffects().isEmpty());
 
     KWin::VirtualDesktopManager::self()->setCurrent(1);
+    KWin::VirtualDesktopManager::self()->setPerOutputVirtualDesktops(false);
 }
 
 void ScriptedEffectsTest::testEffectsHandler()
@@ -205,6 +210,31 @@ void ScriptedEffectsTest::testEffectsHandler()
     waitFor("desktopChanged - 1 2");
 }
 
+void ScriptedEffectsTest::testEffectsHandlerPerOutputDesktops()
+{
+    KWin::VirtualDesktopManager::self()->setPerOutputVirtualDesktops(true);
+    const auto outputs = workspace()->outputs();
+    QCOMPARE_GE(outputs.size(), 2);
+    KWin::VirtualDesktopManager::self()->setCurrent(1, outputs[0]);
+    KWin::VirtualDesktopManager::self()->setCurrent(1, outputs[1]);
+    // this triggers and tests some of the signals in EffectHandler, which is exposed to JS as context property "effects"
+    auto *effect = new ScriptedEffectWithDebugSpy; // cleaned up in ::clean
+    QSignalSpy effectOutputSpy(effect, &ScriptedEffectWithDebugSpy::testOutput);
+    auto waitFor = [&effectOutputSpy](const QString &expected) {
+        QVERIFY(effectOutputSpy.count() > 0 || effectOutputSpy.wait());
+        QCOMPARE(effectOutputSpy.first().first(), expected);
+        effectOutputSpy.removeFirst();
+    };
+    QVERIFY(effect->load("effectsHandlerPerOutputDesktops"));
+
+    // desktop management
+    KWin::VirtualDesktopManager::self()->setCurrent(2, outputs[0]);
+    waitFor("desktopChanged - 1 2 Virtual-0");
+
+    KWin::VirtualDesktopManager::self()->setCurrent(2, outputs[1]);
+    waitFor("desktopChanged - 1 2 Virtual-1");
+}
+
 void ScriptedEffectsTest::testEffectsContext()
 {
     // this tests misc non-objects exposed to the script engine: animationTime, displaySize, use of external enums
@@ -212,7 +242,7 @@ void ScriptedEffectsTest::testEffectsContext()
     auto *effect = new ScriptedEffectWithDebugSpy; // cleaned up in ::clean
     QSignalSpy effectOutputSpy(effect, &ScriptedEffectWithDebugSpy::testOutput);
     QVERIFY(effect->load("effectContext"));
-    QCOMPARE(effectOutputSpy[0].first(), "1280x1024");
+    QCOMPARE(effectOutputSpy[0].first(), "2560x1024");
     QCOMPARE(effectOutputSpy[1].first(), "100");
     QCOMPARE(effectOutputSpy[2].first(), "2");
     QCOMPARE(effectOutputSpy[3].first(), "0");
diff --git a/autotests/integration/effects/scripts/effectsHandlerPerOutputDesktops.js b/autotests/integration/effects/scripts/effectsHandlerPerOutputDesktops.js
new file mode 100644
index 00000000000..883dc3445dc
--- /dev/null
+++ b/autotests/integration/effects/scripts/effectsHandlerPerOutputDesktops.js
@@ -0,0 +1,3 @@
+effects.desktopChanged.connect(function(old, current, output) {
+    sendTestResponse("desktopChanged - " + old.x11DesktopNumber + " " + current.x11DesktopNumber + " " + output.name);
+});
diff --git a/autotests/integration/outputchanges_test.cpp b/autotests/integration/outputchanges_test.cpp
index f578850a5f8..5c3ed247a0e 100644
--- a/autotests/integration/outputchanges_test.cpp
+++ b/autotests/integration/outputchanges_test.cpp
@@ -149,6 +149,8 @@ private Q_SLOTS:
     void testMirroring();
 
     void testAutoBrightness();
+
+    void testPerOutputDesktopsOutputToggle();
 };
 
 void OutputChangesTest::initTestCase()
@@ -188,6 +190,8 @@ void OutputChangesTest::init()
 void OutputChangesTest::cleanup()
 {
     Test::destroyWaylandConnection();
+    VirtualDesktopManager::self()->setPerOutputVirtualDesktops(false);
+    VirtualDesktopManager::self()->setCount(1);
 }
 
 void OutputChangesTest::testWindowSticksToOutputAfterOutputIsDisabled()
@@ -2371,6 +2375,56 @@ void OutputChangesTest::testAutoBrightness()
     COMPARE_RANGE(curve.sample(50), 0.75, eta);
 }
 
+void OutputChangesTest::testPerOutputDesktopsOutputToggle()
+{
+    // This test verifies per-output desktops work with output hotplugging/unplugging.
+
+    const auto outputs = kwinApp()->outputBackend()->outputs();
+    VirtualDesktopManager::self()->setPerOutputVirtualDesktops(true);
+    VirtualDesktopManager::self()->setCount(2);
+    const auto desktops = VirtualDesktopManager::self()->desktops();
+    VirtualDesktopManager::self()->setCurrent(desktops[0], workspace()->outputs()[0]);
+    VirtualDesktopManager::self()->setCurrent(desktops[1], workspace()->outputs()[1]);
+
+    // Create a window.
+    std::unique_ptr<KWayland::Client::Surface> surface(Test::createSurface());
+    std::unique_ptr<Test::XdgToplevel> shellSurface(Test::createXdgToplevelSurface(surface.get()));
+    auto window = Test::renderAndWaitForShown(surface.get(), QSize(100, 50), Qt::blue);
+    QVERIFY(window);
+
+    // Move the window to the right output.
+    window->move(QPointF(1280 + 50, 100));
+    QCOMPARE(window->output()->backendOutput(), outputs[1]);
+    QCOMPARE(window->frameGeometry(), RectF(1280 + 50, 100, 100, 50));
+    QCOMPARE(window->desktops(), {desktops[1]});
+
+    // Disable the right output.
+    OutputConfiguration config1;
+    {
+        auto changeSet = config1.changeSet(outputs[1]);
+        changeSet->enabled = false;
+    }
+    workspace()->applyOutputConfiguration(config1);
+
+    // The window will be moved to the left monitor.
+    QCOMPARE(window->output()->backendOutput(), outputs[0]);
+    QCOMPARE(window->frameGeometry(), RectF(50, 100, 100, 50));
+    QCOMPARE(window->desktops(), {desktops[1]});
+
+    // Enable the right monitor.
+    OutputConfiguration config2;
+    {
+        auto changeSet = config2.changeSet(outputs[1]);
+        changeSet->enabled = true;
+    }
+    workspace()->applyOutputConfiguration(config2);
+
+    // The window will be moved back to the right monitor.
+    QCOMPARE(window->output()->backendOutput(), outputs[1]);
+    QCOMPARE(window->frameGeometry(), RectF(1280 + 50, 100, 100, 50));
+    QCOMPARE(window->desktops(), {desktops[1]});
+}
+
 } // namespace KWin
 
 WAYLANDTEST_MAIN(KWin::OutputChangesTest)
-- 
GitLab


From 658d2479f082586d7091c055c97f2c5fdd4d0ef9 Mon Sep 17 00:00:00 2001
From: Hynek Schlindenbuch <schlindenbuch.h@seznam.cz>
Date: Thu, 1 Jan 2026 09:57:41 +0100
Subject: [PATCH 42/63] make effecthandler change less BC breaking

---
 .../effects/scripts/effectsHandlerPerOutputDesktops.js |  2 +-
 src/effect/effecthandler.cpp                           |  4 ++--
 src/effect/effecthandler.h                             | 10 +++++-----
 src/plugins/fadedesktop/package/contents/code/main.js  |  2 +-
 src/plugins/overview/overvieweffect.cpp                |  4 ++--
 src/plugins/slide/slide.cpp                            |  4 ++--
 src/plugins/slide/slide.h                              |  4 ++--
 7 files changed, 15 insertions(+), 15 deletions(-)

diff --git a/autotests/integration/effects/scripts/effectsHandlerPerOutputDesktops.js b/autotests/integration/effects/scripts/effectsHandlerPerOutputDesktops.js
index 883dc3445dc..ef450f14402 100644
--- a/autotests/integration/effects/scripts/effectsHandlerPerOutputDesktops.js
+++ b/autotests/integration/effects/scripts/effectsHandlerPerOutputDesktops.js
@@ -1,3 +1,3 @@
-effects.desktopChanged.connect(function(old, current, output) {
+effects.desktopChanged.connect(function(old, current, w, output) {
     sendTestResponse("desktopChanged - " + old.x11DesktopNumber + " " + current.x11DesktopNumber + " " + output.name);
 });
diff --git a/src/effect/effecthandler.cpp b/src/effect/effecthandler.cpp
index c1ed6521d57..9e46f39a299 100644
--- a/src/effect/effecthandler.cpp
+++ b/src/effect/effecthandler.cpp
@@ -166,10 +166,10 @@ EffectsHandler::EffectsHandler(Compositor *compositor, WorkspaceScene *scene)
         }
     });
     connect(ws, &Workspace::currentDesktopChanged, this, [this](VirtualDesktop *old, VirtualDesktop *newDesktop, LogicalOutput *output, Window *window) {
-        Q_EMIT desktopChanged(old, newDesktop, output, window ? window->effectWindow() : nullptr);
+        Q_EMIT desktopChanged(old, newDesktop, window ? window->effectWindow() : nullptr, output);
     });
     connect(ws, &Workspace::currentDesktopChanging, this, [this](VirtualDesktop *currentDesktop, QPointF offset, KWin::LogicalOutput *output, KWin::Window *window) {
-        Q_EMIT desktopChanging(currentDesktop, offset, output, window ? window->effectWindow() : nullptr);
+        Q_EMIT desktopChanging(currentDesktop, offset, window ? window->effectWindow() : nullptr, output);
     });
     connect(ws, &Workspace::currentDesktopChangingCancelled, this, [this]() {
         Q_EMIT desktopChangingCancelled();
diff --git a/src/effect/effecthandler.h b/src/effect/effecthandler.h
index 2e754a55560..abf8510eaa1 100644
--- a/src/effect/effecthandler.h
+++ b/src/effect/effecthandler.h
@@ -320,7 +320,7 @@ public:
     QString currentActivity() const;
     // Desktops
     /**
-     * @returns The current desktop on screen @a output.
+     * @returns The current desktop on screen @a output (default = active screen).
      */
     VirtualDesktop *currentDesktop(KWin::LogicalOutput *output = nullptr) const;
     /**
@@ -328,7 +328,7 @@ public:
      */
     QList<VirtualDesktop *> desktops() const;
     /**
-     * Set the current desktop to @a desktop on screen @a output.
+     * Set the current desktop to @a desktop on screen @a output (default = active screen).
      */
     void setCurrentDesktop(KWin::VirtualDesktop *desktop, KWin::LogicalOutput *output = nullptr);
     /**
@@ -781,11 +781,11 @@ Q_SIGNALS:
      * Signal emitted when the current desktop changed.
      * @param oldDesktop The previously current desktop
      * @param newDesktop The new current desktop
-     * @param output The screen where the desktop was changed
      * @param with The window which is taken over to the new desktop, can be NULL
+     * @param output The screen where the desktop was changed
      * @since 4.9
      */
-    void desktopChanged(KWin::VirtualDesktop *oldDesktop, KWin::VirtualDesktop *newDesktop, KWin::LogicalOutput *output, KWin::EffectWindow *with);
+    void desktopChanged(KWin::VirtualDesktop *oldDesktop, KWin::VirtualDesktop *newDesktop, KWin::EffectWindow *with, KWin::LogicalOutput *output);
 
     /**
      * Signal emitted while desktop is changing for animation.
@@ -795,7 +795,7 @@ Q_SIGNALS:
      * Positive Values means Up and Right.
      * @param output The affected screen. If it affects multiple screens, then a separate signal is emitted for each one.
      */
-    void desktopChanging(KWin::VirtualDesktop *currentDesktop, QPointF offset, KWin::LogicalOutput *output, KWin::EffectWindow *with);
+    void desktopChanging(KWin::VirtualDesktop *currentDesktop, QPointF offset, KWin::EffectWindow *with, KWin::LogicalOutput *output);
 
     /**
      * Signal emitted when realtime desktop switching animation is cancelled. It applies to all screens.
diff --git a/src/plugins/fadedesktop/package/contents/code/main.js b/src/plugins/fadedesktop/package/contents/code/main.js
index 5965b214488..f9c4cfc5c75 100644
--- a/src/plugins/fadedesktop/package/contents/code/main.js
+++ b/src/plugins/fadedesktop/package/contents/code/main.js
@@ -65,7 +65,7 @@ var fadeDesktopEffect = {
             to: 0.0
         });
     },
-    slotDesktopChanged: function (oldDesktop, newDesktop, screen, movingWindow) {
+    slotDesktopChanged: function (oldDesktop, newDesktop, movingWindow, screen) {
         if (effects.hasActiveFullScreenEffect && !effect.isActiveFullScreenEffect) {
             return;
         }
diff --git a/src/plugins/overview/overvieweffect.cpp b/src/plugins/overview/overvieweffect.cpp
index 62916cb3846..b95b8181f5d 100644
--- a/src/plugins/overview/overvieweffect.cpp
+++ b/src/plugins/overview/overvieweffect.cpp
@@ -105,11 +105,11 @@ OverviewEffect::OverviewEffect()
     connect(m_gridState, &EffectTogglableState::inProgressChanged, this, &OverviewEffect::gridGestureInProgressChanged);
     connect(m_gridState, &EffectTogglableState::partialActivationFactorChanged, this, &OverviewEffect::gridPartialActivationFactorChanged);
 
-    connect(effects, &EffectsHandler::desktopChanging, this, [this](VirtualDesktop *old, QPointF desktopOffset, LogicalOutput *output, EffectWindow *with) {
+    connect(effects, &EffectsHandler::desktopChanging, this, [this](VirtualDesktop *old, QPointF desktopOffset, EffectWindow *, LogicalOutput *output) {
         m_screenDesktopOffsets.insertOrAssign(output, desktopOffset);
         Q_EMIT desktopOffsetChanged(output);
     });
-    connect(effects, &EffectsHandler::desktopChanged, this, [this](KWin::VirtualDesktop *, KWin::VirtualDesktop *, KWin::LogicalOutput *output) {
+    connect(effects, &EffectsHandler::desktopChanged, this, [this](KWin::VirtualDesktop *, KWin::VirtualDesktop *, EffectWindow *, KWin::LogicalOutput *output) {
         m_screenDesktopOffsets.insertOrAssign(output, QPointF(0, 0));
         Q_EMIT desktopOffsetChanged(output);
     });
diff --git a/src/plugins/slide/slide.cpp b/src/plugins/slide/slide.cpp
index 18f6c697f33..a2c5f62c29a 100644
--- a/src/plugins/slide/slide.cpp
+++ b/src/plugins/slide/slide.cpp
@@ -460,7 +460,7 @@ void SlideEffectScreen::finishedSwitching()
     m_state = State::Inactive;
 }
 
-void SlideEffect::desktopChanged(VirtualDesktop *old, VirtualDesktop *current, LogicalOutput *screen, EffectWindow *with)
+void SlideEffect::desktopChanged(VirtualDesktop *old, VirtualDesktop *current, EffectWindow *with, LogicalOutput *screen)
 {
     if (m_switchingActivity || (effects->hasActiveFullScreenEffect() && effects->activeFullScreenEffect() != this)) {
         return;
@@ -492,7 +492,7 @@ void SlideEffectScreen::desktopChanged(VirtualDesktop *old, VirtualDesktop *curr
     startAnimation(previousPos, current, with);
 }
 
-void SlideEffect::desktopChanging(VirtualDesktop *old, QPointF desktopOffset, LogicalOutput *output, EffectWindow *with)
+void SlideEffect::desktopChanging(VirtualDesktop *old, QPointF desktopOffset, EffectWindow *with, LogicalOutput *output)
 {
     if (effects->hasActiveFullScreenEffect() && effects->activeFullScreenEffect() != this) {
         return;
diff --git a/src/plugins/slide/slide.h b/src/plugins/slide/slide.h
index 216c370b1e2..fd1469243d6 100644
--- a/src/plugins/slide/slide.h
+++ b/src/plugins/slide/slide.h
@@ -150,8 +150,8 @@ public:
     bool slideBackground() const;
 
 private Q_SLOTS:
-    void desktopChanged(VirtualDesktop *old, VirtualDesktop *current, LogicalOutput *output, EffectWindow *with);
-    void desktopChanging(VirtualDesktop *old, QPointF desktopOffset, LogicalOutput *output, EffectWindow *with);
+    void desktopChanged(VirtualDesktop *old, VirtualDesktop *current, EffectWindow *with, LogicalOutput *output);
+    void desktopChanging(VirtualDesktop *old, QPointF desktopOffset, EffectWindow *with, LogicalOutput *output);
     void desktopChangingCancelled();
     void windowAdded(EffectWindow *w);
     void windowDeleted(EffectWindow *w);
-- 
GitLab


From 056dbf82d975128ae7631fc73beb744d260a2523 Mon Sep 17 00:00:00 2001
From: Hynek Schlindenbuch <schlindenbuch.h@seznam.cz>
Date: Thu, 1 Jan 2026 09:58:50 +0100
Subject: [PATCH 43/63] pass desktop explicitly to directional methods

---
 src/screenedge.cpp  | 11 ++++++-----
 src/useractions.cpp |  2 +-
 2 files changed, 7 insertions(+), 6 deletions(-)

diff --git a/src/screenedge.cpp b/src/screenedge.cpp
index 468b8d60c39..669549615b4 100644
--- a/src/screenedge.cpp
+++ b/src/screenedge.cpp
@@ -242,10 +242,11 @@ bool Edge::activatesForPointer() const
     }
     const bool isMovingWindow = Workspace::self()->moveResizeWindow() && !Workspace::self()->moveResizeWindow()->isInteractiveResize();
     if (m_edges->isDesktopSwitching() || (m_edges->isDesktopSwitchingMovingClients() && isMovingWindow)) {
-        const bool canSwitch = (isLeft() && VirtualDesktopManager::self()->toLeft(nullptr, options->isRollOverDesktops()) != VirtualDesktopManager::self()->currentDesktop())
-            || (isRight() && VirtualDesktopManager::self()->toRight(nullptr, options->isRollOverDesktops()) != VirtualDesktopManager::self()->currentDesktop())
-            || (isBottom() && VirtualDesktopManager::self()->below(nullptr, options->isRollOverDesktops()) != VirtualDesktopManager::self()->currentDesktop())
-            || (isTop() && VirtualDesktopManager::self()->above(nullptr, options->isRollOverDesktops()) != VirtualDesktopManager::self()->currentDesktop());
+        VirtualDesktop *currentDesktop = VirtualDesktopManager::self()->currentDesktop(m_output);
+        const bool canSwitch = (isLeft() && VirtualDesktopManager::self()->toLeft(currentDesktop, options->isRollOverDesktops()) != currentDesktop)
+            || (isRight() && VirtualDesktopManager::self()->toRight(currentDesktop, options->isRollOverDesktops()) != currentDesktop)
+            || (isBottom() && VirtualDesktopManager::self()->below(currentDesktop, options->isRollOverDesktops()) != currentDesktop)
+            || (isTop() && VirtualDesktopManager::self()->above(currentDesktop, options->isRollOverDesktops()) != currentDesktop);
         if (canSwitch) {
             return true;
         }
@@ -471,7 +472,7 @@ void Edge::switchDesktop(const QPoint &cursorPos)
 {
     QPoint pos(cursorPos);
     VirtualDesktopManager *vds = VirtualDesktopManager::self();
-    VirtualDesktop *oldDesktop = vds->currentDesktop();
+    VirtualDesktop *oldDesktop = vds->currentDesktop(m_output);
     VirtualDesktop *desktop = oldDesktop;
     const int OFFSET = 2;
     if (isLeft()) {
diff --git a/src/useractions.cpp b/src/useractions.cpp
index 217d9908619..df453cfd421 100644
--- a/src/useractions.cpp
+++ b/src/useractions.cpp
@@ -1409,7 +1409,7 @@ void windowToDesktop(Window *window, VirtualDesktopManager::Direction direction)
     VirtualDesktopManager *vds = VirtualDesktopManager::self();
     Workspace *ws = Workspace::self();
     // TODO: why is options->isRollOverDesktops() not honored?
-    const auto desktop = vds->inDirection(nullptr, direction, true);
+    const auto desktop = vds->inDirection(vds->currentDesktop(window->output()), direction, true);
     if (ws->moveResizeWindow()) {
         if (ws->moveResizeWindow() == window) {
             vds->setCurrent(desktop, window->output());
-- 
GitLab


From 9cedaae479bb3bc84019dcdf422f611b9d20bb4a Mon Sep 17 00:00:00 2001
From: Hynek Schlindenbuch <schlindenbuch.h@seznam.cz>
Date: Thu, 1 Jan 2026 11:47:59 +0100
Subject: [PATCH 44/63] fix doc comments

---
 src/options.h                      | 2 +-
 src/scripting/workspace_wrapper.h  | 4 ++--
 src/virtualdesktops.h              | 7 +++++--
 src/wayland/plasmavirtualdesktop.h | 2 +-
 4 files changed, 9 insertions(+), 6 deletions(-)

diff --git a/src/options.h b/src/options.h
index e2694c04ad2..8b92ee38f80 100644
--- a/src/options.h
+++ b/src/options.h
@@ -382,7 +382,7 @@ public:
     }
 
     /**
-     * Whether or not we roll over to the other edge when switching desktops past the edge.
+     * Whether virtual desktops are switched independently for each screen.
      */
     bool isPerOutputVirtualDesktops() const
     {
diff --git a/src/scripting/workspace_wrapper.h b/src/scripting/workspace_wrapper.h
index ed23e76d67f..08b57cce262 100644
--- a/src/scripting/workspace_wrapper.h
+++ b/src/scripting/workspace_wrapper.h
@@ -221,14 +221,14 @@ public:
     /**
      * Returns current desktop on screen given by @p output.
      *
-     * @since 6.6
+     * @since 6.7
      */
     Q_INVOKABLE KWin::VirtualDesktop *currentDesktopForScreen(KWin::LogicalOutput *output) const;
 
     /**
      * Sets current desktop on @p output.
      *
-     * @since 6.6
+     * @since 6.7
      */
     Q_INVOKABLE void setCurrentDesktopForScreen(KWin::VirtualDesktop *desktop, KWin::LogicalOutput *output);
 
diff --git a/src/virtualdesktops.h b/src/virtualdesktops.h
index 445db011301..2492aaf98fe 100644
--- a/src/virtualdesktops.h
+++ b/src/virtualdesktops.h
@@ -355,7 +355,8 @@ public Q_SLOTS:
     void setCount(uint count);
 
     /**
-     * Set the current desktop to @a current.
+     * Set the current desktop to @a current on @a output.
+     * @param output The output for which to return the current desktop (default = active output)
      * @returns True on success, false otherwise.
      * @see current
      * @see currentChanged
@@ -364,7 +365,8 @@ public Q_SLOTS:
     bool setCurrent(uint current, LogicalOutput *output = nullptr);
 
     /**
-     * Set the current desktop to @a current.
+     * Set the current desktop to @a current on @a output.
+     * @param output The output for which to return the current desktop (default = active output)
      * @returns True on success, false otherwise.
      * @see current
      * @see currentChanged
@@ -443,6 +445,7 @@ Q_SIGNALS:
      * Signal emitted whenever the current desktop changes.
      * @param previousDesktop The virtual desktop changed from
      * @param newDesktop The virtual desktop changed to
+     * @param output The affected output. If it affects multiple outputs, then a separate signal is emitted for each one.
      */
     void currentChanged(KWin::VirtualDesktop *previousDesktop, KWin::VirtualDesktop *newDesktop, KWin::LogicalOutput *output);
 
diff --git a/src/wayland/plasmavirtualdesktop.h b/src/wayland/plasmavirtualdesktop.h
index 3de373b2515..07ea7775c05 100644
--- a/src/wayland/plasmavirtualdesktop.h
+++ b/src/wayland/plasmavirtualdesktop.h
@@ -186,7 +186,7 @@ public:
     QString uuid() const;
 
     /**
-     * @returns the name for this desktop
+     * @returns the name for this output
      */
     QString name() const;
 
-- 
GitLab


From 6f5852d128eb171045ec66e93df8940ddc482ea2 Mon Sep 17 00:00:00 2001
From: Hynek Schlindenbuch <schlindenbuch.h@seznam.cz>
Date: Thu, 1 Jan 2026 11:48:26 +0100
Subject: [PATCH 45/63] pass output explicitly to currentDesktop

---
 src/screenedge.cpp | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/src/screenedge.cpp b/src/screenedge.cpp
index 669549615b4..3e09be3c5a3 100644
--- a/src/screenedge.cpp
+++ b/src/screenedge.cpp
@@ -509,7 +509,7 @@ void Edge::switchDesktop(const QPoint &cursorPos)
         }
     }
     vds->setCurrent(desktop, m_output);
-    if (vds->currentDesktop() != oldDesktop) {
+    if (vds->currentDesktop(m_output) != oldDesktop) {
         m_pushBackBlocked = true;
         input()->pointer()->warp(pos);
         auto unblockPush = [this] {
-- 
GitLab


From 35e61b3b4712d2952041dfa6e85773ecb4a06761 Mon Sep 17 00:00:00 2001
From: Hynek Schlindenbuch <schlindenbuch.h@seznam.cz>
Date: Thu, 1 Jan 2026 11:49:00 +0100
Subject: [PATCH 46/63] use forward declaration instead of include

---
 src/virtualdesktops.cpp | 1 +
 src/virtualdesktops.h   | 2 +-
 2 files changed, 2 insertions(+), 1 deletion(-)

diff --git a/src/virtualdesktops.cpp b/src/virtualdesktops.cpp
index 4fd286eca91..36b908a09a9 100644
--- a/src/virtualdesktops.cpp
+++ b/src/virtualdesktops.cpp
@@ -8,6 +8,7 @@
     SPDX-License-Identifier: GPL-2.0-or-later
 */
 #include "virtualdesktops.h"
+#include "core/output.h"
 #include "input.h"
 #include "wayland/plasmavirtualdesktop.h"
 #include "workspace.h"
diff --git a/src/virtualdesktops.h b/src/virtualdesktops.h
index 2492aaf98fe..fe5e3677023 100644
--- a/src/virtualdesktops.h
+++ b/src/virtualdesktops.h
@@ -8,7 +8,6 @@
 */
 #pragma once
 // KWin
-#include "core/output.h"
 #include "effect/globals.h"
 #include <kwin_export.h>
 // Qt includes
@@ -29,6 +28,7 @@ class QAction;
 namespace KWin
 {
 
+class LogicalOutput;
 class Options;
 class PlasmaVirtualDesktopManagementInterface;
 
-- 
GitLab


From 3df8051d7d0122f9092bec54d7f0d9116c5d23a7 Mon Sep 17 00:00:00 2001
From: Hynek Schlindenbuch <schlindenbuch.h@seznam.cz>
Date: Thu, 1 Jan 2026 13:05:32 +0100
Subject: [PATCH 47/63] fix frozenapp effect flicker

If a frozen app was shown on one screen and a desktop was switched on
another, the frozenapp effect would sometimes flicker off and back on.
---
 src/plugins/frozenapp/package/contents/code/main.js | 7 ++++++-
 1 file changed, 6 insertions(+), 1 deletion(-)

diff --git a/src/plugins/frozenapp/package/contents/code/main.js b/src/plugins/frozenapp/package/contents/code/main.js
index 7ca4ac62ddd..257529a69c1 100644
--- a/src/plugins/frozenapp/package/contents/code/main.js
+++ b/src/plugins/frozenapp/package/contents/code/main.js
@@ -88,10 +88,15 @@ var frozenAppEffect = {
             window.unresponsiveAnimation = undefined;
         }
     },
-    desktopChanged: function () {
+    desktopChanged: function (oldDesktop, newDesktop, withWindow, screen) {
         var windows = effects.stackingOrder;
         for (var i = 0, length = windows.length; i < length; ++i) {
             var window = windows[i];
+
+            if (window.screen !== screen) {
+                continue;
+            }
+
             frozenAppEffect.cancelAnimation(window);
             frozenAppEffect.restartAnimation(window);
         }
-- 
GitLab


From e06fbe9cec14094c5c6984dbbc8e201382bce978 Mon Sep 17 00:00:00 2001
From: Hynek Schlindenbuch <schlindenbuch.h@seznam.cz>
Date: Thu, 1 Jan 2026 13:11:08 +0100
Subject: [PATCH 48/63] fix translucency effect flicker

It flickered if the translucent window was on one screen and a desktop
was changed on another screen.
---
 src/plugins/translucency/package/contents/code/main.js | 5 ++++-
 1 file changed, 4 insertions(+), 1 deletion(-)

diff --git a/src/plugins/translucency/package/contents/code/main.js b/src/plugins/translucency/package/contents/code/main.js
index a743597e5b4..3b73106c1be 100644
--- a/src/plugins/translucency/package/contents/code/main.js
+++ b/src/plugins/translucency/package/contents/code/main.js
@@ -193,10 +193,13 @@ var translucencyEffect = {
             window.translucencyInactiveAnimation = ids;
         }
     },
-    desktopChanged: function () {
+    desktopChanged: function (oldDesktop, newDesktop, withWindow, screen) {
         var i, windows;
         windows = effects.stackingOrder;
         for (i = 0; i < windows.length; i += 1) {
+            if (windows[i].screen !== screen) {
+                continue;
+            }
             translucencyEffect.cancelAnimations(windows[i]);
             translucencyEffect.startAnimation(windows[i]);
             if (windows[i] !== effects.activeWindow) {
-- 
GitLab


From 531d532d955b9e5c0d8a29afc23db738dc92ca7d Mon Sep 17 00:00:00 2001
From: Hynek Schlindenbuch <schlindenbuch.h@seznam.cz>
Date: Thu, 1 Jan 2026 13:48:51 +0100
Subject: [PATCH 49/63] send done event after active desktop changes

---
 src/virtualdesktops.cpp | 9 +++++----
 1 file changed, 5 insertions(+), 4 deletions(-)

diff --git a/src/virtualdesktops.cpp b/src/virtualdesktops.cpp
index 36b908a09a9..eafa9ba9cf5 100644
--- a/src/virtualdesktops.cpp
+++ b/src/virtualdesktops.cpp
@@ -104,13 +104,15 @@ void VirtualDesktopManager::setVirtualDesktopManagement(PlasmaVirtualDesktopMana
     connect(this, &VirtualDesktopManager::currentChanged, m_virtualDesktopManagement, [this](VirtualDesktop *oldDesktop, VirtualDesktop *newDesktop, LogicalOutput *output) {
         m_virtualDesktopManagement->setActiveDesktopForOutput(output, newDesktop->id());
 
-        if (output != workspace()->activeOutput()) {
-            return;
+        if (output == workspace()->activeOutput()) {
+            updateLegacyPlasmaVirtualDesktops(newDesktop);
         }
-        updateLegacyPlasmaVirtualDesktops(newDesktop);
+
+        m_virtualDesktopManagement->scheduleDone();
     });
     connect(workspace(), &Workspace::activeOutputChanged, this, [this](LogicalOutput *output) {
         updateLegacyPlasmaVirtualDesktops(currentDesktop(output));
+        m_virtualDesktopManagement->scheduleDone();
     });
 
     auto initNewOutput = [this](LogicalOutput *output) {
@@ -985,7 +987,6 @@ void VirtualDesktopManager::updateLegacyPlasmaVirtualDesktops(VirtualDesktop *ac
             deskInt->setActive(false);
         }
     }
-    m_virtualDesktopManagement->scheduleDone();
 }
 
 QAction *VirtualDesktopManager::addAction(const QString &name, const KLocalizedString &label, uint value, const QList<QKeySequence> &key, void (VirtualDesktopManager::*slot)())
-- 
GitLab


From 1f19e951c046bf37bfa8921907851f608c4365fc Mon Sep 17 00:00:00 2001
From: Hynek Schlindenbuch <schlindenbuch.h@seznam.cz>
Date: Fri, 2 Jan 2026 13:54:24 +0100
Subject: [PATCH 50/63] prevent flicker in dialogparent effect

I didn't manage to simulate it in a VM, but I suspect that it would
flicker if desktops were changed on another screen.
---
 src/plugins/dialogparent/package/contents/code/main.js | 5 ++++-
 1 file changed, 4 insertions(+), 1 deletion(-)

diff --git a/src/plugins/dialogparent/package/contents/code/main.js b/src/plugins/dialogparent/package/contents/code/main.js
index b6d20bd2803..0f8d55ca6b4 100644
--- a/src/plugins/dialogparent/package/contents/code/main.js
+++ b/src/plugins/dialogparent/package/contents/code/main.js
@@ -90,7 +90,7 @@ var dialogParentEffect = {
         cancel(window.dialogParentAnimation);
         delete window.dialogParentEffect;
     },
-    desktopChanged: function () {
+    desktopChanged: function (oldDesktop, newDesktop, withWindow, screen) {
         "use strict";
         // If there is an active full screen effect, then try smoothly dim/brighten
         // the main windows. Keep in mind that in order for this to work properly, this
@@ -103,6 +103,9 @@ var dialogParentEffect = {
 
         const windows = effects.stackingOrder;
         for (const window of windows) {
+            if (window.screen !== screen) {
+                continue;
+            }
             dialogParentEffect.cancelAnimationInstant(window);
             dialogParentEffect.restartAnimation(window);
         }
-- 
GitLab


From 91cd47e3a3ef4187287cf0a6ebe9d05c20f66673 Mon Sep 17 00:00:00 2001
From: Hynek Schlindenbuch <schlindenbuch.h@seznam.cz>
Date: Sat, 3 Jan 2026 11:06:07 +0100
Subject: [PATCH 51/63] fix move/resize for window with forced desktop

This isn't a problem when all outputs have the same desktop. But with
per-output desktops, it could happen that the desktop on the other
screen is different. So when the window's output changed, it stayed
visible, but it wasn't really there. Now it disappears when it's dropped
on the other output with different desktop.
---
 .../integration/xdgshellwindow_rules_test.cpp | 84 +++++++++++++++++++
 src/scene/windowitem.cpp                      |  1 +
 src/workspace.cpp                             |  7 ++
 3 files changed, 92 insertions(+)

diff --git a/autotests/integration/xdgshellwindow_rules_test.cpp b/autotests/integration/xdgshellwindow_rules_test.cpp
index 28f827e1192..00d6b528f85 100644
--- a/autotests/integration/xdgshellwindow_rules_test.cpp
+++ b/autotests/integration/xdgshellwindow_rules_test.cpp
@@ -16,6 +16,7 @@
 #include "core/outputconfiguration.h"
 #include "cursor.h"
 #include "rules.h"
+#include "scene/windowitem.h"
 #include "virtualdesktops.h"
 #include "wayland_server.h"
 #include "window.h"
@@ -71,6 +72,7 @@ private Q_SLOTS:
     void testDesktopsApply();
     void testDesktopsRemember();
     void testDesktopsForce();
+    void testDesktopsForceWithPerOutputDesktopsAndInteractiveMove();
     void testDesktopsApplyNow();
     void testDesktopsForceTemporarily();
 
@@ -228,6 +230,8 @@ void TestXdgShellWindowRules::cleanup()
     // Restore virtual desktops to the initial state.
     VirtualDesktopManager::self()->setCount(1);
     QCOMPARE(VirtualDesktopManager::self()->count(), 1u);
+    VirtualDesktopManager::self()->setPerOutputVirtualDesktops(false);
+    QCOMPARE(VirtualDesktopManager::self()->isPerOutputVirtualDesktops(), false);
 }
 
 void TestXdgShellWindowRules::createTestWindow(ClientFlags flags)
@@ -1436,6 +1440,86 @@ void TestXdgShellWindowRules::testDesktopsForce()
     destroyTestWindow();
 }
 
+void TestXdgShellWindowRules::testDesktopsForceWithPerOutputDesktopsAndInteractiveMove()
+{
+    // We need at least two virtual desktop for this test.
+    VirtualDesktopManager::self()->setCount(2);
+    QCOMPARE(VirtualDesktopManager::self()->count(), 2u);
+    VirtualDesktopManager::self()->setPerOutputVirtualDesktops(true);
+    QVERIFY(VirtualDesktopManager::self()->isPerOutputVirtualDesktops());
+    VirtualDesktop *vd1 = VirtualDesktopManager::self()->desktops().at(0);
+    VirtualDesktop *vd2 = VirtualDesktopManager::self()->desktops().at(1);
+
+    setWindowRule("desktops", QStringList{vd2->id()}, int(Rules::Force));
+
+    const auto outputs = workspace()->outputs();
+    QCOMPARE_GE(outputs.size(), 2);
+    LogicalOutput *startOutput = outputs[0];
+    LogicalOutput *endOutput = outputs[1];
+    VirtualDesktopManager::self()->setCurrent(vd2, startOutput);
+    VirtualDesktopManager::self()->setCurrent(vd1, endOutput);
+    workspace()->setActiveOutput(startOutput);
+
+    createTestWindow();
+
+    QVERIFY(m_window);
+    QCOMPARE(workspace()->activeWindow(), m_window);
+    QCOMPARE(m_window->frameGeometry(), RectF(0, 0, 100, 50));
+    m_window->sendToOutput(startOutput);
+    QCOMPARE(m_window->output(), startOutput);
+    workspace()->setActiveWindow(m_window);
+
+    QSignalSpy interactiveMoveResizeStartedSpy(m_window, &Window::interactiveMoveResizeStarted);
+    QSignalSpy moveResizedChangedSpy(m_window, &Window::moveResizedChanged);
+    QSignalSpy interactiveMoveResizeSteppedSpy(m_window, &Window::interactiveMoveResizeStepped);
+    QSignalSpy interactiveMoveResizeFinishedSpy(m_window, &Window::interactiveMoveResizeFinished);
+
+    // begin move
+    QVERIFY(workspace()->moveResizeWindow() == nullptr);
+    QVERIFY(m_window->isActive());
+    QVERIFY(m_window->windowItem()->isVisible());
+    QCOMPARE(m_window->isInteractiveMove(), false);
+    workspace()->slotWindowMove();
+    QCOMPARE(workspace()->moveResizeWindow(), m_window);
+    QCOMPARE(interactiveMoveResizeStartedSpy.count(), 1);
+    QCOMPARE(moveResizedChangedSpy.count(), 1);
+    QCOMPARE(m_window->isInteractiveMove(), true);
+    QCOMPARE(m_window->geometryRestore(), RectF());
+
+    // move the window to endOutput
+    const QPointF cursorPos = Cursors::self()->mouse()->pos();
+
+    for (int i = 0; i < 40; i++) {
+        m_window->keyPressEvent(Qt::Key_Right | Qt::ALT);
+        m_window->updateInteractiveMoveResize(Cursors::self()->mouse()->pos(), Qt::KeyboardModifiers());
+    }
+
+    QCOMPARE(Cursors::self()->mouse()->pos(), cursorPos + QPoint(40 * 32, 0));
+    QCOMPARE(interactiveMoveResizeSteppedSpy.count(), 40);
+    QVERIFY(m_window->isActive());
+    QVERIFY(m_window->windowItem()->isVisible());
+    interactiveMoveResizeSteppedSpy.clear();
+
+    // end move
+    QCOMPARE(interactiveMoveResizeFinishedSpy.count(), 0);
+    m_window->keyPressEvent(Qt::Key_Enter);
+    QCOMPARE(interactiveMoveResizeFinishedSpy.count(), 1);
+    QCOMPARE(moveResizedChangedSpy.count(), 2);
+    QCOMPARE(m_window->isInteractiveMove(), false);
+    QVERIFY(workspace()->moveResizeWindow() == nullptr);
+
+    // The window should appear on the second screen. It should keep its desktop, so it should disappear.
+    QCOMPARE(m_window->desktops(), {vd2});
+    QCOMPARE(m_window->output(), endOutput);
+    QCOMPARE(VirtualDesktopManager::self()->currentDesktop(endOutput), vd1);
+    QCOMPARE(m_window->isOnCurrentDesktop(), false);
+    // It's not immediate due to scale effect.
+    QTRY_COMPARE_WITH_TIMEOUT(m_window->windowItem()->isVisible(), false, 1000);
+    QCOMPARE(m_window->isActive(), false);
+
+    destroyTestWindow();
+}
+
 void TestXdgShellWindowRules::testDesktopsApplyNow()
 {
     // We need at least two virtual desktop for this test.
diff --git a/src/scene/windowitem.cpp b/src/scene/windowitem.cpp
index 5451a2ffb3f..beba237521e 100644
--- a/src/scene/windowitem.cpp
+++ b/src/scene/windowitem.cpp
@@ -47,6 +47,7 @@ WindowItem::WindowItem(Window *window, Item *parent)
     connect(window, &Window::activitiesChanged, this, &WindowItem::updateVisibility);
     connect(window, &Window::desktopsChanged, this, &WindowItem::updateVisibility);
     connect(window, &Window::offscreenRenderingChanged, this, &WindowItem::updateVisibility);
+    connect(window, &Window::interactiveMoveResizeFinished, this, &WindowItem::updateVisibility);
     connect(waylandServer(), &WaylandServer::lockStateChanged, this, &WindowItem::updateVisibility);
     connect(workspace(), &Workspace::currentActivityChanged, this, &WindowItem::updateVisibility);
     connect(workspace(), &Workspace::currentDesktopChanged, this, &WindowItem::updateVisibility);
diff --git a/src/workspace.cpp b/src/workspace.cpp
index 2b1c8274ff2..91d5ddd4f74 100644
--- a/src/workspace.cpp
+++ b/src/workspace.cpp
@@ -702,6 +702,13 @@ void Workspace::setupWindowConnections(Window *window)
 {
     connect(window, &Window::minimizedChanged, this, std::bind(&Workspace::windowMinimizedChanged, this, window));
     connect(window, &Window::fullScreenChanged, m_screenEdges.get(), &ScreenEdges::checkBlocking);
+    connect(window, &Window::interactiveMoveResizeFinished, this, [this, window]() {
+        if (!window->isActive() || window->isOnCurrentDesktop()) {
+            return;
+        }
+
+        activateWindowOnDesktop(VirtualDesktopManager::self()->currentDesktop());
+    });
 }
 
 void Workspace::constrain(Window *below, Window *above)
-- 
GitLab


From 6931a3503ab551f7e989f40ce2027a084fbc2f1d Mon Sep 17 00:00:00 2001
From: Hynek Schlindenbuch <schlindenbuch.h@seznam.cz>
Date: Sat, 3 Jan 2026 14:37:56 +0100
Subject: [PATCH 52/63] remove overview effect's desktop offset for removes
 creens

---
 src/plugins/overview/overvieweffect.cpp | 4 ++++
 1 file changed, 4 insertions(+)

diff --git a/src/plugins/overview/overvieweffect.cpp b/src/plugins/overview/overvieweffect.cpp
index b95b8181f5d..fec157de879 100644
--- a/src/plugins/overview/overvieweffect.cpp
+++ b/src/plugins/overview/overvieweffect.cpp
@@ -120,6 +120,10 @@ OverviewEffect::OverviewEffect()
             Q_EMIT desktopOffsetChanged(output);
         }
     });
+    connect(effects, &EffectsHandler::screenRemoved, this, [this](KWin::LogicalOutput *output) {
+        m_screenDesktopOffsets.remove(output);
+        Q_EMIT desktopOffsetChanged(output);
+    });
 
     m_shutdownTimer->setSingleShot(true);
     connect(m_shutdownTimer, &QTimer::timeout, this, &OverviewEffect::realDeactivate);
-- 
GitLab


From 6efc251c14e9ffe0a5a706457a63f389c2ae706e Mon Sep 17 00:00:00 2001
From: Hynek Schlindenbuch <schlindenbuch.h@seznam.cz>
Date: Fri, 9 Jan 2026 12:48:20 +0100
Subject: [PATCH 53/63] fix output detkop's version

I previously incorrectly assumed that
org_kde_plasma_virtual_desktop_management,
org_kde_plasma_output_virtual_desktop and org_kde_plasma_virtual_desktop
will all have the same version number. But management's version should
instead be the maximum of the two other versions.
---
 src/wayland/plasmavirtualdesktop.cpp | 3 ++-
 1 file changed, 2 insertions(+), 1 deletion(-)

diff --git a/src/wayland/plasmavirtualdesktop.cpp b/src/wayland/plasmavirtualdesktop.cpp
index d5239fa2ebf..ba7a078d898 100644
--- a/src/wayland/plasmavirtualdesktop.cpp
+++ b/src/wayland/plasmavirtualdesktop.cpp
@@ -17,6 +17,7 @@
 namespace KWin
 {
 static const quint32 s_version = 4;
+static const int s_output_desktop_version = 4;
 
 class PlasmaVirtualDesktopInterfacePrivate : public QtWaylandServer::org_kde_plasma_virtual_desktop
 {
@@ -125,7 +126,7 @@ void PlasmaVirtualDesktopManagementInterfacePrivate::sendOutputDesktopToClient(R
         return;
     }
 
-    auto outputVdClientResource = outputVd->d->add(resource->client(), resource->version());
+    auto outputVdClientResource = outputVd->d->add(resource->client(), std::min(resource->version(), s_output_desktop_version));
     send_output_added(resource->handle, outputVdClientResource->handle);
     outputVd->d->send_uuid(outputVdClientResource->handle, outputVd->uuid());
     outputVd->d->send_name(outputVdClientResource->handle, outputVd->name());
-- 
GitLab


From fef1479f58cee0d32f2288f2982bf4d04e27c336 Mon Sep 17 00:00:00 2001
From: Hynek Schlindenbuch <schlindenbuch.h@seznam.cz>
Date: Sat, 10 Jan 2026 08:32:16 +0100
Subject: [PATCH 54/63] add since 6.7 for scripting API changes

---
 src/effect/effecthandler.h        | 10 ++++++----
 src/scripting/workspace_wrapper.h |  1 +
 2 files changed, 7 insertions(+), 4 deletions(-)

diff --git a/src/effect/effecthandler.h b/src/effect/effecthandler.h
index abf8510eaa1..0a649860ee2 100644
--- a/src/effect/effecthandler.h
+++ b/src/effect/effecthandler.h
@@ -320,7 +320,8 @@ public:
     QString currentActivity() const;
     // Desktops
     /**
-     * @returns The current desktop on screen @a output (default = active screen).
+     * @param output The output to return the current desktop for (default = active screen). @since 6.7
+     * @returns The current desktop on screen @a output.
      */
     VirtualDesktop *currentDesktop(KWin::LogicalOutput *output = nullptr) const;
     /**
@@ -328,7 +329,8 @@ public:
      */
     QList<VirtualDesktop *> desktops() const;
     /**
-     * Set the current desktop to @a desktop on screen @a output (default = active screen).
+     * @param output The output to set the current desktop for (default = active screen). @since 6.7
+     * Set the current desktop to @a desktop on screen @a output.
      */
     void setCurrentDesktop(KWin::VirtualDesktop *desktop, KWin::LogicalOutput *output = nullptr);
     /**
@@ -782,7 +784,7 @@ Q_SIGNALS:
      * @param oldDesktop The previously current desktop
      * @param newDesktop The new current desktop
      * @param with The window which is taken over to the new desktop, can be NULL
-     * @param output The screen where the desktop was changed
+     * @param output The screen where the desktop was changed. @since 6.7
      * @since 4.9
      */
     void desktopChanged(KWin::VirtualDesktop *oldDesktop, KWin::VirtualDesktop *newDesktop, KWin::EffectWindow *with, KWin::LogicalOutput *output);
@@ -793,7 +795,7 @@ Q_SIGNALS:
      * @param offset The current desktop offset.
      * offset.x() = .6 means 60% of the way to the desktop to the right.
      * Positive Values means Up and Right.
-     * @param output The affected screen. If it affects multiple screens, then a separate signal is emitted for each one.
+     * @param output The affected screen. If it affects multiple screens, then a separate signal is emitted for each one. @since 6.7
      */
     void desktopChanging(KWin::VirtualDesktop *currentDesktop, QPointF offset, KWin::EffectWindow *with, KWin::LogicalOutput *output);
 
diff --git a/src/scripting/workspace_wrapper.h b/src/scripting/workspace_wrapper.h
index 08b57cce262..6a705a9ac05 100644
--- a/src/scripting/workspace_wrapper.h
+++ b/src/scripting/workspace_wrapper.h
@@ -140,6 +140,7 @@ Q_SIGNALS:
     void virtualScreenGeometryChanged();
 
     /**
+     * @param output The screen where the desktop was changed. @since 6.7
      * This signal is emitted when the current virtual desktop changes.
      */
     void currentDesktopChanged(KWin::VirtualDesktop *previous, KWin::VirtualDesktop *current, KWin::LogicalOutput *output);
-- 
GitLab


From 5b7a04353a418d418a47c99076f630277ed10219 Mon Sep 17 00:00:00 2001
From: Hynek Schlindenbuch <schlindenbuch.h@seznam.cz>
Date: Sat, 24 Jan 2026 09:45:51 +0100
Subject: [PATCH 55/63] fix CR issues

---
 src/activities.cpp                      |  9 ++++--
 src/dbusinterface.cpp                   | 11 ++++++-
 src/dbusinterface.h                     |  1 +
 src/plugins/overview/overvieweffect.cpp |  3 +-
 src/plugins/slide/slide.cpp             |  6 ++--
 src/virtualdesktops.cpp                 | 41 +++++++++++++------------
 src/virtualdesktops.h                   | 14 +--------
 src/wayland/plasmavirtualdesktop.h      | 22 -------------
 src/workspace.cpp                       |  1 +
 9 files changed, 46 insertions(+), 62 deletions(-)

diff --git a/src/activities.cpp b/src/activities.cpp
index 7525c6d94cf..1708b4f940a 100644
--- a/src/activities.cpp
+++ b/src/activities.cpp
@@ -40,9 +40,11 @@ Activities::Activities()
     kwinApp()->config()->group("Activities").deleteGroup("LastVirtualDesktop");
     auto perOutputLastDesktopConfig = m_config->group("Activities").group("PerOutputLastVirtualDesktop");
 
-    for (const auto &activity : perOutputLastDesktopConfig.groupList()) {
+    const auto &activities = perOutputLastDesktopConfig.groupList();
+    for (const auto &activity : activities) {
         auto activityLastDesktopConfig = perOutputLastDesktopConfig.group(activity);
-        for (const auto &outputUuid : activityLastDesktopConfig.keyList()) {
+        const auto &outputUuids = activityLastDesktopConfig.keyList();
+        for (const auto &outputUuid : outputUuids) {
             const QString desktop = activityLastDesktopConfig.readEntry(outputUuid);
             if (!desktop.isEmpty()) {
                 m_lastVirtualDesktop[activity][outputUuid] = desktop;
@@ -127,7 +129,8 @@ void Activities::slotCurrentChanged(const QString &newActivity)
     const auto it = m_lastVirtualDesktop.find(m_current);
     if (it != m_lastVirtualDesktop.end()) {
         const auto &outputDesktops = it->second;
-        for (LogicalOutput *output : workspace()->outputs()) {
+        const auto &outputs = workspace()->outputs();
+        for (LogicalOutput *output : outputs) {
             const auto outputDesktopIt = outputDesktops.find(output->uuid());
             if (outputDesktopIt != outputDesktops.end()) {
                 VirtualDesktop *desktop = VirtualDesktopManager::self()->desktopForId(outputDesktopIt->second);
diff --git a/src/dbusinterface.cpp b/src/dbusinterface.cpp
index cdee5d09de3..ca9aa924a37 100644
--- a/src/dbusinterface.cpp
+++ b/src/dbusinterface.cpp
@@ -302,7 +302,16 @@ VirtualDesktopManagerDBusInterface::VirtualDesktopManagerDBusInterface(VirtualDe
         if (output != workspace()->activeOutput()) {
             return;
         }
-        Q_EMIT currentChanged(newDesktop->id());
+        m_lastDesktopId = newDesktop->id();
+        Q_EMIT currentChanged(m_lastDesktopId);
+    });
+    connect(workspace(), &Workspace::activeOutputChanged, this, [this](LogicalOutput *output) {
+        QString newDesktopId = m_manager->currentDesktop(output)->id();
+        if (m_lastDesktopId == newDesktopId) {
+            return;
+        }
+        m_lastDesktopId = newDesktopId;
+        Q_EMIT currentChanged(m_lastDesktopId);
     });
 
     connect(m_manager, &VirtualDesktopManager::countChanged, this, [this](uint previousCount, uint newCount) {
diff --git a/src/dbusinterface.h b/src/dbusinterface.h
index b128e2270b9..bba6478b9d9 100644
--- a/src/dbusinterface.h
+++ b/src/dbusinterface.h
@@ -243,6 +243,7 @@ public Q_SLOTS:
 
 private:
     VirtualDesktopManager *m_manager;
+    QString m_lastDesktopId;
 };
 
 class PluginManagerDBusInterface : public QObject
diff --git a/src/plugins/overview/overvieweffect.cpp b/src/plugins/overview/overvieweffect.cpp
index fec157de879..295f61fd99c 100644
--- a/src/plugins/overview/overvieweffect.cpp
+++ b/src/plugins/overview/overvieweffect.cpp
@@ -115,8 +115,9 @@ OverviewEffect::OverviewEffect()
     });
     connect(effects, &EffectsHandler::desktopChangingCancelled, this, [this]() {
         m_screenDesktopOffsets.clear();
+        const auto &screens = effects->screens();
 
-        for (LogicalOutput *output : effects->screens()) {
+        for (LogicalOutput *output : screens) {
             Q_EMIT desktopOffsetChanged(output);
         }
     });
diff --git a/src/plugins/slide/slide.cpp b/src/plugins/slide/slide.cpp
index a2c5f62c29a..fa116b8ab7d 100644
--- a/src/plugins/slide/slide.cpp
+++ b/src/plugins/slide/slide.cpp
@@ -66,7 +66,8 @@ SlideEffect::SlideEffect()
         m_switchingActivity = false;
     });
 
-    for (LogicalOutput *screen : effects->screens()) {
+    const auto &screens = effects->screens();
+    for (LogicalOutput *screen : screens) {
         m_slideEffectScreens.emplace(screen, this, screen);
     }
 }
@@ -530,7 +531,8 @@ void SlideEffect::desktopChangingCancelled()
     // If the fingers have been lifted and the current desktop didn't change, start animation
     // to move back to the original virtual desktop.
     if (effects->activeFullScreenEffect() == this) {
-        for (LogicalOutput *screen : effects->screens()) {
+        const auto &screens = effects->screens();
+        for (LogicalOutput *screen : screens) {
             getOrCreateSlideEffectScreen(screen).desktopChangingCancelled();
         }
     }
diff --git a/src/virtualdesktops.cpp b/src/virtualdesktops.cpp
index eafa9ba9cf5..761b6c7cee3 100644
--- a/src/virtualdesktops.cpp
+++ b/src/virtualdesktops.cpp
@@ -137,7 +137,8 @@ void VirtualDesktopManager::setVirtualDesktopManagement(PlasmaVirtualDesktopMana
 
     std::for_each(m_desktops.constBegin(), m_desktops.constEnd(), createPlasmaVirtualDesktop);
 
-    for (LogicalOutput *output : workspace()->outputs()) {
+    const auto &outputs = workspace()->outputs();
+    for (LogicalOutput *output : outputs) {
         initNewOutput(output);
     }
 
@@ -237,7 +238,6 @@ KWIN_SINGLETON_FACTORY_VARIABLE(VirtualDesktopManager, s_manager)
 VirtualDesktopManager::VirtualDesktopManager(QObject *parent)
     : QObject(parent)
     , m_navigationWrapsAround(false)
-    , m_perOutputVirtualDesktops(false)
 #if KWIN_BUILD_X11
     , m_rootInfo(nullptr)
 #endif
@@ -544,12 +544,12 @@ void VirtualDesktopManager::removeVirtualDesktop(VirtualDesktop *desktop)
 
     VirtualDesktop *newDesktop = (i < m_desktops.count()) ? m_desktops.at(i) : m_desktops.constLast();
 
-    for (auto [key, value] : m_currentDesktops.asKeyValueRange()) {
-        if (value != desktop) {
+    for (auto [output, currentDesktop] : m_currentDesktops.asKeyValueRange()) {
+        if (currentDesktop != desktop) {
             continue;
         }
-        value = newDesktop;
-        Q_EMIT currentChanged(desktop, newDesktop, key);
+        currentDesktop = newDesktop;
+        Q_EMIT currentChanged(desktop, newDesktop, output);
     }
 
     updateLayout();
@@ -607,9 +607,8 @@ VirtualDesktop *VirtualDesktopManager::currentDesktop(LogicalOutput *output) con
     if (!output) {
         output = workspace()->activeOutput();
     }
-    VirtualDesktop *result = m_currentDesktops[output];
     // Fallback is necessary because currentDesktop may be called before initialDesktopForNewOutput.
-    return result ? result : initialDesktopForNewOutput();
+    return m_currentDesktops.value(output, initialDesktopForNewOutput());
 }
 
 bool VirtualDesktopManager::setCurrent(uint newDesktop, LogicalOutput *output)
@@ -637,13 +636,13 @@ bool VirtualDesktopManager::setCurrent(VirtualDesktop *newDesktop, LogicalOutput
         Q_EMIT currentChanged(oldDesktop, newDesktop, output);
         return true;
     }
-    for (auto [key, value] : m_currentDesktops.asKeyValueRange()) {
-        if (value == newDesktop) {
+    for (auto [otherOutput, otherDesktop] : m_currentDesktops.asKeyValueRange()) {
+        if (otherDesktop == newDesktop) {
             continue;
         }
-        auto oldDesktop = value;
-        value = newDesktop;
-        Q_EMIT currentChanged(oldDesktop, newDesktop, key);
+        auto oldDesktop = otherDesktop;
+        otherDesktop = newDesktop;
+        Q_EMIT currentChanged(oldDesktop, newDesktop, otherOutput);
     }
     return true;
 }
@@ -661,13 +660,13 @@ void VirtualDesktopManager::setCount(uint count)
     if ((uint)m_desktops.count() > count) {
         const auto desktopsToRemove = m_desktops.mid(count);
         m_desktops.resize(count);
-        for (auto [key, value] : m_currentDesktops.asKeyValueRange()) {
-            if (!desktopsToRemove.contains(value)) {
+        for (auto [output, currentDesktop] : m_currentDesktops.asKeyValueRange()) {
+            if (!desktopsToRemove.contains(currentDesktop)) {
                 continue;
             }
-            VirtualDesktop *oldDesktop = value;
-            value = m_desktops.last();
-            Q_EMIT currentChanged(oldDesktop, value, key);
+            VirtualDesktop *oldDesktop = currentDesktop;
+            currentDesktop = m_desktops.last();
+            Q_EMIT currentChanged(oldDesktop, currentDesktop, output);
         }
         for (auto desktop : desktopsToRemove) {
             Q_EMIT desktopRemoved(desktop);
@@ -877,7 +876,8 @@ void VirtualDesktopManager::initShortcuts()
             LogicalOutput *output = workspace()->activeOutput();
             Q_EMIT currentChanging(currentDesktop(output), m_currentDesktopOffset, output);
         } else {
-            for (LogicalOutput *output : workspace()->outputs()) {
+            const auto &outputs = workspace()->outputs();
+            for (LogicalOutput *output : outputs) {
                 Q_EMIT currentChanging(currentDesktop(output), m_currentDesktopOffset, output);
             }
         }
@@ -1071,7 +1071,8 @@ void VirtualDesktopManager::setPerOutputVirtualDesktops(bool enabled)
     m_perOutputVirtualDesktops = enabled;
     if (!enabled) {
         VirtualDesktop *newDesktop = currentDesktop(workspace()->activeOutput());
-        for (const auto output : workspace()->outputs()) {
+        const auto &outputs = workspace()->outputs();
+        for (const auto output : outputs) {
             setCurrent(newDesktop, output);
         }
     }
diff --git a/src/virtualdesktops.h b/src/virtualdesktops.h
index fe5e3677023..ec5b3e87c60 100644
--- a/src/virtualdesktops.h
+++ b/src/virtualdesktops.h
@@ -535,10 +535,6 @@ private:
      * Creates all the global keyboard shortcuts for "Switch To Desktop n" actions.
      */
     void initSwitchToShortcuts();
-
-    /**
-     * Updates active status of plasma virtual desktops.
-     */
     void updateLegacyPlasmaVirtualDesktops(VirtualDesktop *activeDesktop);
 
     /**
@@ -568,22 +564,14 @@ private:
      */
     QAction *addAction(const QString &name, const QString &label, const QKeySequence &key, void (VirtualDesktopManager::*slot)());
 
-    /**
-     * Initializes current desktop for @p output.
-     * @param output
-     */
     void initCurrentDesktopForOutput(LogicalOutput *output);
-
-    /**
-     * Returns the desktop used to initialize a new output.
-     */
     VirtualDesktop *initialDesktopForNewOutput() const;
 
     QList<VirtualDesktop *> m_desktops;
     QHash<LogicalOutput *, VirtualDesktop *> m_currentDesktops;
     quint32 m_rows = 2;
     bool m_navigationWrapsAround;
-    bool m_perOutputVirtualDesktops;
+    bool m_perOutputVirtualDesktops = false;
     VirtualDesktopGrid m_grid;
     // TODO: QPointer
 #if KWIN_BUILD_X11
diff --git a/src/wayland/plasmavirtualdesktop.h b/src/wayland/plasmavirtualdesktop.h
index 07ea7775c05..715c193614d 100644
--- a/src/wayland/plasmavirtualdesktop.h
+++ b/src/wayland/plasmavirtualdesktop.h
@@ -60,19 +60,8 @@ public:
      */
     void removeDesktop(const QString &id);
 
-    /**
-     * Adds output
-     */
     PlasmaOutputVirtualDesktopInterface *addOutput(LogicalOutput *output, const QString &activeDesktopId);
-
-    /**
-     * Change active desktop for output
-     */
     void setActiveDesktopForOutput(LogicalOutput *output, const QString &activeDesktopId);
-
-    /**
-     * Removes output
-     */
     void removeOutput(LogicalOutput *output);
 
     /**
@@ -180,24 +169,13 @@ class KWIN_EXPORT PlasmaOutputVirtualDesktopInterface : public QObject
 public:
     ~PlasmaOutputVirtualDesktopInterface() override;
 
-    /**
-     * @returns the UUID for this output
-     */
     QString uuid() const;
-
-    /**
-     * @returns the name for this output
-     */
     QString name() const;
 
     /**
      * Set active desktop for this output. It may affect other outputs as well.
      */
     void setActiveDesktop(const QString &desktopId);
-
-    /**
-     * @returns id of the desktop active on this output.
-     */
     QString activeDesktop() const;
 
     /**
diff --git a/src/workspace.cpp b/src/workspace.cpp
index 91d5ddd4f74..851f64c6dd7 100644
--- a/src/workspace.cpp
+++ b/src/workspace.cpp
@@ -703,6 +703,7 @@ void Workspace::setupWindowConnections(Window *window)
     connect(window, &Window::minimizedChanged, this, std::bind(&Workspace::windowMinimizedChanged, this, window));
     connect(window, &Window::fullScreenChanged, m_screenEdges.get(), &ScreenEdges::checkBlocking);
     connect(window, &Window::interactiveMoveResizeFinished, this, [this, window]() {
+        // Activate another window if the resized/moved window "disappeared" (e.g. due to force desktop window rule).
         if (!window->isActive() || window->isOnCurrentDesktop()) {
             return;
         }
-- 
GitLab


From b267d199d8c0700a2f12b49e4e830129246deb35 Mon Sep 17 00:00:00 2001
From: Hynek Schlindenbuch <schlindenbuch.h@seznam.cz>
Date: Sat, 24 Jan 2026 10:34:42 +0100
Subject: [PATCH 56/63] use const auto &[k, v] with asKeyValueRange()

---
 src/virtualdesktops.cpp | 6 +++---
 1 file changed, 3 insertions(+), 3 deletions(-)

diff --git a/src/virtualdesktops.cpp b/src/virtualdesktops.cpp
index 761b6c7cee3..250f87c70e2 100644
--- a/src/virtualdesktops.cpp
+++ b/src/virtualdesktops.cpp
@@ -544,7 +544,7 @@ void VirtualDesktopManager::removeVirtualDesktop(VirtualDesktop *desktop)
 
     VirtualDesktop *newDesktop = (i < m_desktops.count()) ? m_desktops.at(i) : m_desktops.constLast();
 
-    for (auto [output, currentDesktop] : m_currentDesktops.asKeyValueRange()) {
+    for (const auto &[output, currentDesktop] : m_currentDesktops.asKeyValueRange()) {
         if (currentDesktop != desktop) {
             continue;
         }
@@ -636,7 +636,7 @@ bool VirtualDesktopManager::setCurrent(VirtualDesktop *newDesktop, LogicalOutput
         Q_EMIT currentChanged(oldDesktop, newDesktop, output);
         return true;
     }
-    for (auto [otherOutput, otherDesktop] : m_currentDesktops.asKeyValueRange()) {
+    for (const auto &[otherOutput, otherDesktop] : m_currentDesktops.asKeyValueRange()) {
         if (otherDesktop == newDesktop) {
             continue;
         }
@@ -660,7 +660,7 @@ void VirtualDesktopManager::setCount(uint count)
     if ((uint)m_desktops.count() > count) {
         const auto desktopsToRemove = m_desktops.mid(count);
         m_desktops.resize(count);
-        for (auto [output, currentDesktop] : m_currentDesktops.asKeyValueRange()) {
+        for (const auto &[output, currentDesktop] : m_currentDesktops.asKeyValueRange()) {
             if (!desktopsToRemove.contains(currentDesktop)) {
                 continue;
             }
-- 
GitLab


From a21fa6efca57e481665c79697554c36bed7a6a09 Mon Sep 17 00:00:00 2001
From: Hynek Schlindenbuch <schlindenbuch.h@seznam.cz>
Date: Sat, 31 Jan 2026 07:59:03 +0100
Subject: [PATCH 57/63] don't reassign desktops when output is removed

The desktop reassignment was intended only for interactive operations.
There was already a test for it, but it didn't work, because the removed
output no longer had its desktop assigned by the VirtualDesktopManager
at the time it got to finishOutputChange, so it got desktop 1 as a
fallback, which was the same desktop as the other remaining output which
prevented the desktop reassignment.
---
 .../integration/move_resize_window_test.cpp   | 81 +++++++++++++++++++
 autotests/integration/outputchanges_test.cpp  | 35 ++++----
 src/internalwindow.cpp                        |  6 +-
 src/scene/windowitem.cpp                      |  9 +++
 src/waylandwindow.cpp                         |  6 +-
 src/window.cpp                                | 21 ++---
 src/window.h                                  |  2 +-
 src/workspace.cpp                             | 10 +++
 src/workspace.h                               |  1 +
 src/x11window.cpp                             |  6 +-
 10 files changed, 144 insertions(+), 33 deletions(-)

diff --git a/autotests/integration/move_resize_window_test.cpp b/autotests/integration/move_resize_window_test.cpp
index 090f9a80a00..bbc10c2d799 100644
--- a/autotests/integration/move_resize_window_test.cpp
+++ b/autotests/integration/move_resize_window_test.cpp
@@ -13,6 +13,8 @@
 #include "cursor.h"
 #include "placement.h"
 #include "pointer_input.h"
+#include "scene/windowitem.h"
+#include "virtualdesktops.h"
 #include "wayland_server.h"
 #include "window.h"
 #include "workspace.h"
@@ -65,6 +67,7 @@ private Q_SLOTS:
     void testRestrictedMoveMultiMonitor();
     void testRestrictedResizeUp();
     void testRestrictedResizeRight();
+    void testMoveWithPerOutputVirtualDesktops();
 
 private:
     std::tuple<Window *, std::unique_ptr<KWayland::Client::Surface>, std::unique_ptr<Test::XdgToplevel>> showWindow();
@@ -97,6 +100,8 @@ void MoveResizeWindowTest::init()
     m_compositor = Test::waylandCompositor();
 
     workspace()->setActiveOutput(QPoint(640, 512));
+    VirtualDesktopManager::self()->setCount(1);
+    VirtualDesktopManager::self()->setPerOutputVirtualDesktops(false);
 }
 
 void MoveResizeWindowTest::cleanup()
@@ -1255,6 +1260,82 @@ void MoveResizeWindowTest::testRestrictedResizeRight()
     surface.reset();
     QVERIFY(Test::waitForWindowClosed(window));
 }
+
+void MoveResizeWindowTest::testMoveWithPerOutputVirtualDesktops()
+{
+
+    Test::setOutputConfig({
+        Rect(0, 0, 1280, 1024),
+        Rect(1280, 0, 1280, 1024),
+    });
+    const auto outputs = workspace()->outputs();
+    QCOMPARE(outputs.count(), 2);
+    VirtualDesktopManager::self()->setCount(2);
+    VirtualDesktopManager::self()->setPerOutputVirtualDesktops(true);
+    const auto desktops = VirtualDesktopManager::self()->desktops();
+    VirtualDesktopManager::self()->setCurrent(desktops[0], outputs[0]);
+    VirtualDesktopManager::self()->setCurrent(desktops[1], outputs[1]);
+
+    std::unique_ptr<KWayland::Client::Surface> surface(Test::createSurface());
+    QVERIFY(surface != nullptr);
+
+    std::unique_ptr<Test::XdgToplevel> shellSurface(Test::createXdgToplevelSurface(surface.get()));
+    QVERIFY(shellSurface != nullptr);
+    // let's render
+    auto window = Test::renderAndWaitForShown(surface.get(), QSize(100, 50), Qt::blue);
+
+    QVERIFY(window);
+    QCOMPARE(workspace()->activeWindow(), window);
+    QCOMPARE(window->frameGeometry(), RectF(0, 0, 100, 50));
+    QCOMPARE(window->output(), outputs[0]);
+    QCOMPARE(window->desktops(), {desktops[0]});
+    QSignalSpy interactiveMoveResizeStartedSpy(window, &Window::interactiveMoveResizeStarted);
+    QSignalSpy moveResizedChangedSpy(window, &Window::moveResizedChanged);
+    QSignalSpy interactiveMoveResizeSteppedSpy(window, &Window::interactiveMoveResizeStepped);
+    QSignalSpy interactiveMoveResizeFinishedSpy(window, &Window::interactiveMoveResizeFinished);
+
+    // begin move
+    QVERIFY(workspace()->moveResizeWindow() == nullptr);
+    QCOMPARE(window->isInteractiveMove(), false);
+    workspace()->slotWindowMove();
+    QCOMPARE(workspace()->moveResizeWindow(), window);
+    QCOMPARE(interactiveMoveResizeStartedSpy.count(), 1);
+    QCOMPARE(moveResizedChangedSpy.count(), 1);
+    QCOMPARE(window->isInteractiveMove(), true);
+    QCOMPARE(window->geometryRestore(), RectF());
+
+    // move to other output
+    const QPointF cursorPos = Cursors::self()->mouse()->pos();
+    for (int i = 0; i < 40; i++) {
+        window->keyPressEvent(Qt::Key_Right | Qt::ALT);
+        window->updateInteractiveMoveResize(Cursors::self()->mouse()->pos(), Qt::KeyboardModifiers());
+    }
+
+    QCOMPARE(Cursors::self()->mouse()->pos(), cursorPos + QPoint(40 * 32, 0));
+    QCOMPARE(interactiveMoveResizeSteppedSpy.count(), 40);
+    QVERIFY(window->isActive());
+    QVERIFY(window->windowItem()->isVisible());
+    interactiveMoveResizeSteppedSpy.clear();
+
+    // let's end
+    QCOMPARE(interactiveMoveResizeFinishedSpy.count(), 0);
+    window->keyPressEvent(Qt::Key_Enter);
+    QCOMPARE(interactiveMoveResizeFinishedSpy.count(), 1);
+    QCOMPARE(moveResizedChangedSpy.count(), 2);
+    QCOMPARE(window->isInteractiveMove(), false);
+    QVERIFY(workspace()->moveResizeWindow() == nullptr);
+
+    // The window should appear on the second screen and its desktop
+    QCOMPARE(window->desktops(), {desktops[1]});
+    QCOMPARE(window->output(), outputs[1]);
+    QCOMPARE(VirtualDesktopManager::self()->currentDesktop(outputs[1]), desktops[1]);
+    QCOMPARE(window->isOnCurrentDesktop(), true);
+    QCOMPARE(window->windowItem()->isVisible(), true);
+    QCOMPARE(window->isActive(), true);
+
+    surface.reset();
+    QVERIFY(Test::waitForWindowClosed(window));
+}
 }
 
 WAYLANDTEST_MAIN(KWin::MoveResizeWindowTest)
diff --git a/autotests/integration/outputchanges_test.cpp b/autotests/integration/outputchanges_test.cpp
index 5c3ed247a0e..2fff6142bce 100644
--- a/autotests/integration/outputchanges_test.cpp
+++ b/autotests/integration/outputchanges_test.cpp
@@ -2381,22 +2381,23 @@ void OutputChangesTest::testPerOutputDesktopsOutputToggle()
 
     const auto outputs = kwinApp()->outputBackend()->outputs();
     VirtualDesktopManager::self()->setPerOutputVirtualDesktops(true);
-    VirtualDesktopManager::self()->setCount(2);
+    VirtualDesktopManager::self()->setCount(3);
     const auto desktops = VirtualDesktopManager::self()->desktops();
-    VirtualDesktopManager::self()->setCurrent(desktops[0], workspace()->outputs()[0]);
-    VirtualDesktopManager::self()->setCurrent(desktops[1], workspace()->outputs()[1]);
+    VirtualDesktopManager::self()->setCurrent(desktops[1], workspace()->outputs()[0]);
+    VirtualDesktopManager::self()->setCurrent(desktops[2], workspace()->outputs()[1]);
+    QCOMPARE(VirtualDesktopManager::self()->current(workspace()->outputs()[0]), desktops[1]->x11DesktopNumber());
+    QCOMPARE(VirtualDesktopManager::self()->current(workspace()->outputs()[1]), desktops[2]->x11DesktopNumber());
 
     // Create a window.
-    std::unique_ptr<KWayland::Client::Surface> surface(Test::createSurface());
-    std::unique_ptr<Test::XdgToplevel> shellSurface(Test::createXdgToplevelSurface(surface.get()));
-    auto window = Test::renderAndWaitForShown(surface.get(), QSize(100, 50), Qt::blue);
-    QVERIFY(window);
+    Test::XdgToplevelWindow window;
+    QVERIFY(window.show());
 
     // Move the window to the right output.
-    window->move(QPointF(1280 + 50, 100));
-    QCOMPARE(window->output()->backendOutput(), outputs[1]);
-    QCOMPARE(window->frameGeometry(), RectF(1280 + 50, 100, 100, 50));
-    QCOMPARE(window->desktops(), {desktops[1]});
+    window.m_window->move(QPointF(1280 + 50, 100));
+    window.m_window->setDesktops({desktops[2]});
+    QCOMPARE(window.m_window->output()->backendOutput(), outputs[1]);
+    QCOMPARE(window.m_window->frameGeometry(), RectF(1280 + 50, 100, 100, 100));
+    QCOMPARE(window.m_window->desktops(), {desktops[2]});
 
     // Disable the right output.
     OutputConfiguration config1;
@@ -2407,9 +2408,9 @@ void OutputChangesTest::testPerOutputDesktopsOutputToggle()
     workspace()->applyOutputConfiguration(config1);
 
     // The window will be moved to the left monitor.
-    QCOMPARE(window->output()->backendOutput(), outputs[0]);
-    QCOMPARE(window->frameGeometry(), RectF(50, 100, 100, 50));
-    QCOMPARE(window->desktops(), {desktops[1]});
+    QCOMPARE(window.m_window->output()->backendOutput(), outputs[0]);
+    QCOMPARE(window.m_window->frameGeometry(), RectF(50, 100, 100, 100));
+    QCOMPARE(window.m_window->desktops(), {desktops[2]});
 
     // Enable the right monitor.
     OutputConfiguration config2;
@@ -2420,9 +2421,9 @@ void OutputChangesTest::testPerOutputDesktopsOutputToggle()
     workspace()->applyOutputConfiguration(config2);
 
     // The window will be moved back to the right monitor.
-    QCOMPARE(window->output()->backendOutput(), outputs[1]);
-    QCOMPARE(window->frameGeometry(), RectF(1280 + 50, 100, 100, 50));
-    QCOMPARE(window->desktops(), {desktops[1]});
+    QCOMPARE(window.m_window->output()->backendOutput(), outputs[1]);
+    QCOMPARE(window.m_window->frameGeometry(), RectF(1280 + 50, 100, 100, 100));
+    QCOMPARE(window.m_window->desktops(), {desktops[2]});
 }
 
 } // namespace KWin
diff --git a/src/internalwindow.cpp b/src/internalwindow.cpp
index d5008c3c4d7..c03d03696b6 100644
--- a/src/internalwindow.cpp
+++ b/src/internalwindow.cpp
@@ -445,7 +445,7 @@ void InternalWindow::commitGeometry(const RectF &rect)
     // The client geometry and the buffer geometry are the same.
     const RectF oldClientGeometry = m_clientGeometry;
     const RectF oldFrameGeometry = m_frameGeometry;
-    LogicalOutput *oldOutput = m_output;
+    const LogicalOutput *oldOutput = m_output;
 
     Q_EMIT frameGeometryAboutToChange();
 
@@ -466,7 +466,9 @@ void InternalWindow::commitGeometry(const RectF &rect)
     if (oldFrameGeometry != m_frameGeometry) {
         Q_EMIT frameGeometryChanged(oldFrameGeometry);
     }
-    finishOutputChange(oldOutput);
+    if (oldOutput != m_output) {
+        Q_EMIT outputChanged();
+    }
 }
 
 void InternalWindow::setCaption(const QString &caption)
diff --git a/src/scene/windowitem.cpp b/src/scene/windowitem.cpp
index beba237521e..de58350d843 100644
--- a/src/scene/windowitem.cpp
+++ b/src/scene/windowitem.cpp
@@ -48,6 +48,15 @@ WindowItem::WindowItem(Window *window, Item *parent)
     connect(window, &Window::desktopsChanged, this, &WindowItem::updateVisibility);
     connect(window, &Window::offscreenRenderingChanged, this, &WindowItem::updateVisibility);
     connect(window, &Window::interactiveMoveResizeFinished, this, &WindowItem::updateVisibility);
+    connect(window, &Window::outputChanged, this, [this, window] {
+        // When window's output changes, its visibility may need to change as well, because of different virtual desktops between the previous and new output.
+        // However, the window must remain visible during interactive move/resize, even though it's not (yet) on the target desktop.
+        if (window->isInteractiveResize() || window->isInteractiveMove()) {
+            return;
+        }
+
+        updateVisibility();
+    });
     connect(waylandServer(), &WaylandServer::lockStateChanged, this, &WindowItem::updateVisibility);
     connect(workspace(), &Workspace::currentActivityChanged, this, &WindowItem::updateVisibility);
     connect(workspace(), &Workspace::currentDesktopChanged, this, &WindowItem::updateVisibility);
diff --git a/src/waylandwindow.cpp b/src/waylandwindow.cpp
index 9ca073bf6cd..c1627acafef 100644
--- a/src/waylandwindow.cpp
+++ b/src/waylandwindow.cpp
@@ -215,7 +215,7 @@ void WaylandWindow::updateGeometry(const RectF &rect)
     const RectF oldClientGeometry = m_clientGeometry;
     const RectF oldFrameGeometry = m_frameGeometry;
     const RectF oldBufferGeometry = m_bufferGeometry;
-    LogicalOutput *oldOutput = m_output;
+    const LogicalOutput *oldOutput = m_output;
 
     m_clientGeometry = frameRectToClientRect(rect);
     m_frameGeometry = rect;
@@ -249,7 +249,9 @@ void WaylandWindow::updateGeometry(const RectF &rect)
     if (changedGeometries & WaylandGeometryFrame) {
         Q_EMIT frameGeometryChanged(oldFrameGeometry);
     }
-    finishOutputChange(oldOutput);
+    if (oldOutput != m_output) {
+        Q_EMIT outputChanged();
+    }
 }
 
 void WaylandWindow::markAsMapped()
diff --git a/src/window.cpp b/src/window.cpp
index ff6e9ace396..695481c7223 100644
--- a/src/window.cpp
+++ b/src/window.cpp
@@ -234,9 +234,8 @@ LogicalOutput *Window::output() const
 void Window::setOutput(LogicalOutput *output)
 {
     if (m_output != output) {
-        LogicalOutput *oldOutput = m_output;
         m_output = output;
-        finishOutputChange(oldOutput);
+        Q_EMIT outputChanged();
     }
 }
 
@@ -1106,6 +1105,7 @@ void Window::finishInteractiveMoveResize(bool cancel)
         if (isRequestedFullScreen() || requestedMaximizeMode() != MaximizeRestore) {
             checkWorkspacePosition();
         }
+        finishInteractiveOutputChange(workspace()->findOutputByUuid(m_interactiveMoveResize.initialOutputId));
     }
 
     if (isElectricBorderMaximizing()) {
@@ -3931,6 +3931,7 @@ void Window::sendToOutput(LogicalOutput *newOutput)
         return;
     }
 
+    LogicalOutput *prevOutput = m_output;
     const RectF oldGeom = moveResizeGeometry();
     const RectF oldScreenArea = workspace()->clientArea(MaximizeArea, this, moveResizeOutput());
     const RectF screenArea = workspace()->clientArea(MaximizeArea, this, newOutput);
@@ -3956,6 +3957,7 @@ void Window::sendToOutput(LogicalOutput *newOutput)
     for (auto it = tso.constBegin(), end = tso.constEnd(); it != end; ++it) {
         (*it)->sendToOutput(newOutput);
     }
+    finishInteractiveOutputChange(prevOutput);
 }
 
 void Window::checkWorkspacePosition(RectF oldGeometry, const VirtualDesktop *oldDesktop)
@@ -4702,14 +4704,15 @@ void Window::setDescription(const QString &description)
     }
 }
 
-void Window::finishOutputChange(LogicalOutput *prevOutput)
+void Window::finishInteractiveOutputChange(LogicalOutput *prevOutput)
 {
-    if (m_output == prevOutput) {
-        return;
-    }
-
-    Q_EMIT outputChanged();
-
+    // We want to preserve the window being on current desktop during interactive output changes:
+    // - interactive move/resize
+    // - "Move to screen" user action
+    // - Move window to another screen via keyboard shortcut.
+    // ...
+    // On the other hand, this behavior doesn't make sense for non-interactive output changes (e.g. if the output is disabled and all the windows are placed
+    // on another output).
     if (!prevOutput || m_desktops.empty()) {
         return;
     }
diff --git a/src/window.h b/src/window.h
index 94c8791c09a..b04c83871df 100644
--- a/src/window.h
+++ b/src/window.h
@@ -1806,7 +1806,7 @@ protected:
 
     void setDescription(const QString &description);
 
-    void finishOutputChange(LogicalOutput *prevOutput);
+    void finishInteractiveOutputChange(LogicalOutput *prevOutput);
 
     LogicalOutput *m_output = nullptr;
     RectF m_frameGeometry;
diff --git a/src/workspace.cpp b/src/workspace.cpp
index 851f64c6dd7..3377b2c9380 100644
--- a/src/workspace.cpp
+++ b/src/workspace.cpp
@@ -1297,6 +1297,16 @@ LogicalOutput *Workspace::findOutput(BackendOutput *backendOutput) const
     return it == m_outputs.end() ? nullptr : *it;
 }
 
+LogicalOutput *Workspace::findOutputByUuid(const QString &uuid) const
+{
+    for (LogicalOutput *output : std::as_const(m_outputs)) {
+        if (output->uuid() == uuid) {
+            return output;
+        }
+    }
+    return nullptr;
+}
+
 void Workspace::slotOutputBackendOutputsQueried()
 {
     updateOutputConfiguration();
diff --git a/src/workspace.h b/src/workspace.h
index b920c2dc251..12cc262c538 100644
--- a/src/workspace.h
+++ b/src/workspace.h
@@ -316,6 +316,7 @@ public:
     LogicalOutput *findOutput(LogicalOutput *reference, Direction direction, bool wrapAround = false) const;
     LogicalOutput *findOutput(const QString &name) const;
     LogicalOutput *findOutput(BackendOutput *backendOutput) const;
+    LogicalOutput *findOutputByUuid(const QString &uuid) const;
     void switchToOutput(LogicalOutput *output);
 
     QString outputLayoutId() const;
diff --git a/src/x11window.cpp b/src/x11window.cpp
index 55786bf8b97..9566437ccdf 100644
--- a/src/x11window.cpp
+++ b/src/x11window.cpp
@@ -3599,7 +3599,7 @@ void X11Window::moveResizeInternal(const RectF &rect, MoveResizeMode mode)
     const RectF oldBufferGeometry = m_bufferGeometry;
     const RectF oldFrameGeometry = m_frameGeometry;
     const RectF oldClientGeometry = m_clientGeometry;
-    LogicalOutput *oldOutput = m_output;
+    const LogicalOutput *oldOutput = m_output;
 
     m_frameGeometry = frameGeometry;
     m_clientGeometry = clientGeometry;
@@ -3627,7 +3627,9 @@ void X11Window::moveResizeInternal(const RectF &rect, MoveResizeMode mode)
     if (oldFrameGeometry != m_frameGeometry) {
         Q_EMIT frameGeometryChanged(oldFrameGeometry);
     }
-    finishOutputChange(oldOutput);
+    if (oldOutput != m_output) {
+        Q_EMIT outputChanged();
+    }
     updateShapeRegion();
 }
 
-- 
GitLab


From e746ae3c5c9e0e58bfd6470fecaba8a2409c2bbe Mon Sep 17 00:00:00 2001
From: Hynek Schlindenbuch <schlindenbuch.h@seznam.cz>
Date: Sat, 31 Jan 2026 08:53:37 +0100
Subject: [PATCH 58/63] add QuickSceneView::currentDesktop property for
 convenience

---
 src/effect/quickeffect.cpp              | 18 ++++++++++++++++++
 src/effect/quickeffect.h                |  8 ++++++++
 src/plugins/overview/qml/DesktopBar.qml |  4 ++--
 src/plugins/overview/qml/Main.qml       | 19 ++++---------------
 src/plugins/tileseditor/qml/Main.qml    | 15 ++-------------
 src/plugins/windowview/qml/Main.qml     | 18 +++---------------
 6 files changed, 37 insertions(+), 45 deletions(-)

diff --git a/src/effect/quickeffect.cpp b/src/effect/quickeffect.cpp
index b8e771f2c15..3c32646f375 100644
--- a/src/effect/quickeffect.cpp
+++ b/src/effect/quickeffect.cpp
@@ -7,6 +7,7 @@
 #include "effect/quickeffect.h"
 #include "core/output.h"
 #include "effect/effecthandler.h"
+#include "virtualdesktops.h"
 
 #include "logging_p.h"
 
@@ -99,6 +100,13 @@ QuickSceneView::QuickSceneView(QuickSceneEffect *effect, LogicalOutput *screen)
     connect(screen, &LogicalOutput::geometryChanged, this, [this, screen]() {
         setGeometry(screen->geometry());
     });
+    connect(VirtualDesktopManager::self(), &VirtualDesktopManager::currentChanged, this, [this](VirtualDesktop *, VirtualDesktop *newDesktop, LogicalOutput *screen) {
+        if (m_screen != screen) {
+            return;
+        }
+
+        Q_EMIT currentDesktopChanged(newDesktop);
+    });
 
     s_views.insert(window(), this);
 }
@@ -158,6 +166,16 @@ void QuickSceneView::scheduleRepaint()
     effects->addRepaint(geometry());
 }
 
+VirtualDesktop *QuickSceneView::currentDesktop() const
+{
+    return VirtualDesktopManager::self()->currentDesktop(m_screen);
+}
+
+void QuickSceneView::setCurrentDesktop(VirtualDesktop *desktop)
+{
+    VirtualDesktopManager::self()->setCurrent(desktop, m_screen);
+}
+
 QuickSceneView *QuickSceneView::findView(QQuickItem *item)
 {
     return s_views.value(item->window());
diff --git a/src/effect/quickeffect.h b/src/effect/quickeffect.h
index 0a6e976bf6c..4c29ffd35ad 100644
--- a/src/effect/quickeffect.h
+++ b/src/effect/quickeffect.h
@@ -17,6 +17,7 @@ namespace KWin
 
 class QuickSceneEffect;
 class QuickSceneEffectPrivate;
+class VirtualDesktop;
 
 /**
  * The QuickSceneView represents a QtQuick scene view on a particular screen.
@@ -32,6 +33,7 @@ class KWIN_EXPORT QuickSceneView : public OffscreenQuickView
     Q_PROPERTY(QuickSceneEffect *effect READ effect CONSTANT)
     Q_PROPERTY(LogicalOutput *screen READ screen CONSTANT)
     Q_PROPERTY(QQuickItem *rootItem READ rootItem CONSTANT)
+    Q_PROPERTY(VirtualDesktop *currentDesktop READ currentDesktop WRITE setCurrentDesktop NOTIFY currentDesktopChanged)
 
 public:
     explicit QuickSceneView(QuickSceneEffect *effect, LogicalOutput *screen);
@@ -43,6 +45,9 @@ public:
     QQuickItem *rootItem() const;
     void setRootItem(QQuickItem *item);
 
+    VirtualDesktop *currentDesktop() const;
+    void setCurrentDesktop(VirtualDesktop *desktop);
+
     bool isDirty() const;
     void markDirty();
     void resetDirty();
@@ -53,6 +58,9 @@ public:
 public Q_SLOTS:
     void scheduleRepaint();
 
+Q_SIGNALS:
+    void currentDesktopChanged(VirtualDesktop *newDesktop);
+
 private:
     QuickSceneEffect *m_effect;
     LogicalOutput *m_screen;
diff --git a/src/plugins/overview/qml/DesktopBar.qml b/src/plugins/overview/qml/DesktopBar.qml
index b36c693ed87..eef94818e7d 100644
--- a/src/plugins/overview/qml/DesktopBar.qml
+++ b/src/plugins/overview/qml/DesktopBar.qml
@@ -75,10 +75,10 @@ Item {
                     Keys.onRightPressed: nextItemInFocusChain(!LayoutMirroring.enabled).forceActiveFocus(Qt.TabFocusReason);
 
                     function activate() {
-                        if (KWinComponents.Workspace.currentDesktop === delegate.desktop) {
+                        if (KWinComponents.SceneView.currentDesktop === delegate.desktop) {
                             effect.deactivate()
                         } else {
-                            KWinComponents.Workspace.currentDesktop = delegate.desktop;
+                            KWinComponents.SceneView.currentDesktop = delegate.desktop;
                         }
                     }
 
diff --git a/src/plugins/overview/qml/Main.qml b/src/plugins/overview/qml/Main.qml
index db4b73b37ad..0fbcdb0044c 100644
--- a/src/plugins/overview/qml/Main.qml
+++ b/src/plugins/overview/qml/Main.qml
@@ -32,7 +32,7 @@ FocusScope {
     property bool organized: false
 
     property bool verticalDesktopBar: KWinComponents.Workspace.desktopGridHeight >= bar.desktopCount && KWinComponents.Workspace.desktopGridHeight != 1
-    property QtObject currentDesktop: KWinComponents.Workspace.currentDesktopForScreen(targetScreen)
+    property QtObject currentDesktop: KWinComponents.SceneView.currentDesktop
 
     // The values of overviewVal and gridVal might not be 0 on startup,
     // but we always want to animate from 0 to those values. So, we initially
@@ -125,7 +125,7 @@ FocusScope {
     }
 
     function switchTo(desktop) {
-        KWinComponents.Workspace.setCurrentDesktopForScreen(desktop, targetScreen);
+        KWinComponents.SceneView.currentDesktop = desktop;
         effect.deactivate();
     }
 
@@ -172,7 +172,7 @@ FocusScope {
         }
         let newIndex = y * container.columns + x;
 
-        KWinComponents.Workspace.setCurrentDesktopForScreen(allDesktopHeaps.itemAt(newIndex).desktop, targetScreen);
+        KWinComponents.SceneView.currentDesktop = allDesktopHeaps.itemAt(newIndex).desktop;
         allDesktopHeaps.itemAt(newIndex).nestedHeap.focus = true
         allDesktopHeaps.itemAt(newIndex).selectLastItem(invertedDirection);
         return true;
@@ -584,7 +584,7 @@ FocusScope {
                     TapHandler {
                         acceptedButtons: Qt.LeftButton
                         onTapped: {
-                            KWinComponents.Workspace.setCurrentDesktopForScreen(mainBackground.desktop, container.targetScreen);
+                            container.KWinComponents.SceneView.currentDesktop = mainBackground.desktop;
                             container.effect.deactivate();
                         }
                     }
@@ -815,15 +815,4 @@ FocusScope {
             targetScreenDesktopOffset.desktopOffset = effect.desktopOffsetForScreen(screen);
         }
     }
-
-    Connections {
-        target: KWinComponents.Workspace
-        onCurrentDesktopChanged: (previous, current, screen) => {
-            if (screen !== container.targetScreen) {
-                return;
-            }
-
-            container.currentDesktop = current;
-        }
-    }
 }
diff --git a/src/plugins/tileseditor/qml/Main.qml b/src/plugins/tileseditor/qml/Main.qml
index 3f3162b4ec1..f8bfe6790b1 100644
--- a/src/plugins/tileseditor/qml/Main.qml
+++ b/src/plugins/tileseditor/qml/Main.qml
@@ -39,8 +39,7 @@ FocusScope {
 
     property bool active: false
 
-    property QtObject currentDesktop: KWinComponents.Workspace.currentDesktopForScreen(targetScreen)
-    readonly property QtObject rootTile: KWinComponents.Workspace.rootTile(root.targetScreen, currentDesktop)
+    readonly property QtObject rootTile: KWinComponents.Workspace.rootTile(root.targetScreen, KWinComponents.SceneView.currentDesktop)
 
     Component.onCompleted: {
         root.active = true;
@@ -62,7 +61,7 @@ FocusScope {
         Repeater {
             model: KWinComponents.WindowFilterModel {
                 activity: KWinComponents.Workspace.currentActivity
-                desktop: root.currentDesktop
+                desktop: root.KWinComponents.SceneView.currentDesktop
                 screenName: targetScreen.name
                 windowModel: KWinComponents.WindowModel {}
             }
@@ -273,14 +272,4 @@ FocusScope {
             }
         }
     }
-    Connections {
-        target: KWinComponents.Workspace
-        onCurrentDesktopChanged: (previous, current, screen) => {
-            if (screen !== root.targetScreen) {
-                return;
-            }
-
-            root.currentDesktop = current;
-        }
-    }
 }
diff --git a/src/plugins/windowview/qml/Main.qml b/src/plugins/windowview/qml/Main.qml
index 0038424b0bb..2d4373b19e4 100644
--- a/src/plugins/windowview/qml/Main.qml
+++ b/src/plugins/windowview/qml/Main.qml
@@ -19,7 +19,6 @@ Item {
 
     readonly property QtObject effect: KWinComponents.SceneView.effect
     readonly property QtObject targetScreen: KWinComponents.SceneView.screen
-    property QtObject currentDesktop: KWinComponents.Workspace.currentDesktopForScreen(targetScreen)
 
     property bool animationEnabled: false
     property bool organized: false
@@ -64,7 +63,7 @@ Item {
 
     KWinComponents.DesktopBackground {
         activity: KWinComponents.Workspace.currentActivity
-        desktop: currentDesktop
+        desktop: container.KWinComponents.SceneView.currentDesktop
         outputName: targetScreen.name
 
         layer.enabled: true
@@ -186,7 +185,7 @@ Item {
                     switch (container.effect.mode) {
                         case WindowView.ModeCurrentDesktop:
                         case WindowView.ModeWindowClassCurrentDesktop:
-                            return container.currentDesktop;
+                            return container.KWinComponents.SceneView.currentDesktop;
                         default:
                             return undefined;
                     }
@@ -240,7 +239,7 @@ Item {
         asynchronous: true
 
         model: KWinComponents.WindowFilterModel {
-            desktop: currentDesktop
+            desktop: container.KWinComponents.SceneView.currentDesktop
             screenName: targetScreen.name
             windowModel: stackModel
             windowType: KWinComponents.WindowFilterModel.Dock
@@ -270,15 +269,4 @@ Item {
     }
 
     Component.onCompleted: start();
-
-    Connections {
-        target: KWinComponents.Workspace
-        onCurrentDesktopChanged: (previous, current, screen) => {
-            if (screen !== container.targetScreen) {
-                return;
-            }
-
-            container.currentDesktop = current;
-        }
-    }
 }
-- 
GitLab


From 85531dbd3411f757f6a773eb5c0f1335b54f32bb Mon Sep 17 00:00:00 2001
From: Hynek Schlindenbuch <schlindenbuch.h@seznam.cz>
Date: Sat, 31 Jan 2026 10:00:02 +0100
Subject: [PATCH 59/63] fix straightforward CR issues

---
 .../desktop_switching_animation_test.cpp      | 41 +++++---------
 .../effects/scripted_effects_test.cpp         |  4 +-
 autotests/integration/quick_tiling_test.cpp   | 53 +++++++++----------
 src/activities.cpp                            |  2 +-
 src/plugins/overview/overvieweffect.cpp       |  2 +-
 src/plugins/overview/qml/Main.qml             | 24 ++++-----
 src/plugins/slide/slide.cpp                   |  4 +-
 src/plugins/slide/slide.h                     |  7 +--
 src/virtualdesktops.cpp                       | 10 ++--
 9 files changed, 59 insertions(+), 88 deletions(-)

diff --git a/autotests/integration/effects/desktop_switching_animation_test.cpp b/autotests/integration/effects/desktop_switching_animation_test.cpp
index ee60c8a1fbd..9196fbba30d 100644
--- a/autotests/integration/effects/desktop_switching_animation_test.cpp
+++ b/autotests/integration/effects/desktop_switching_animation_test.cpp
@@ -162,27 +162,19 @@ void DesktopSwitchingAnimationTest::testSwitchPerOutputDesktops()
 
     // The Fade Desktop effect will do nothing if there are no windows to fade,
     // so we have to create a dummy test windows.
-    std::unique_ptr<KWayland::Client::Surface> surface1(Test::createSurface());
-    QVERIFY(surface1 != nullptr);
-    std::unique_ptr<Test::XdgToplevel> shellSurface1(Test::createXdgToplevelSurface(surface1.get()));
-    QVERIFY(shellSurface1 != nullptr);
-    Window *window1 = Test::renderAndWaitForShown(surface1.get(), QSize(100, 50), Qt::blue);
-    QVERIFY(window1);
-    window1->setOutput(output1);
-    QCOMPARE(window1->desktops().count(), 1);
-    QCOMPARE(window1->desktops().first(), VirtualDesktopManager::self()->desktops().first());
-    QCOMPARE(window1->output(), output1);
-
-    std::unique_ptr<KWayland::Client::Surface> surface2(Test::createSurface());
-    QVERIFY(surface2 != nullptr);
-    std::unique_ptr<Test::XdgToplevel> shellSurface2(Test::createXdgToplevelSurface(surface2.get()));
-    QVERIFY(shellSurface2 != nullptr);
-    Window *window2 = Test::renderAndWaitForShown(surface2.get(), QSize(100, 50), Qt::blue);
-    QVERIFY(window2);
-    window2->setOutput(output2);
-    QCOMPARE(window2->desktops().count(), 1);
-    QCOMPARE(window2->desktops().first(), VirtualDesktopManager::self()->desktops().first());
-    QCOMPARE(window2->output(), output2);
+    Test::XdgToplevelWindow window1;
+    QVERIFY(window1.show());
+    window1.m_window->setOutput(output1);
+    QCOMPARE(window1.m_window->desktops().count(), 1);
+    QCOMPARE(window1.m_window->desktops().first(), VirtualDesktopManager::self()->desktops().first());
+    QCOMPARE(window1.m_window->output(), output1);
+
+    Test::XdgToplevelWindow window2;
+    QVERIFY(window2.show());
+    window2.m_window->setOutput(output2);
+    QCOMPARE(window2.m_window->desktops().count(), 1);
+    QCOMPARE(window2.m_window->desktops().first(), VirtualDesktopManager::self()->desktops().first());
+    QCOMPARE(window2.m_window->output(), output2);
 
     // Load effect that will be tested.
     QFETCH(QString, effectName);
@@ -213,13 +205,6 @@ void DesktopSwitchingAnimationTest::testSwitchPerOutputDesktops()
     // Eventually, the animation will be complete.
     QTRY_VERIFY(!effect->isActive());
     QTRY_COMPARE(effects->activeFullScreenEffect(), nullptr);
-
-    // Destroy the test windows.
-    surface1.reset();
-    QVERIFY(Test::waitForWindowClosed(window1));
-
-    surface2.reset();
-    QVERIFY(Test::waitForWindowClosed(window2));
 }
 
 WAYLANDTEST_MAIN(DesktopSwitchingAnimationTest)
diff --git a/autotests/integration/effects/scripted_effects_test.cpp b/autotests/integration/effects/scripted_effects_test.cpp
index 5b89abda68b..2cb306364dc 100644
--- a/autotests/integration/effects/scripted_effects_test.cpp
+++ b/autotests/integration/effects/scripted_effects_test.cpp
@@ -229,10 +229,10 @@ void ScriptedEffectsTest::testEffectsHandlerPerOutputDesktops()
 
     // desktop management
     KWin::VirtualDesktopManager::self()->setCurrent(2, outputs[0]);
-    waitFor("desktopChanged - 1 2 Virtual-0");
+    waitFor(QStringLiteral("desktopChanged - 1 2 %1").arg(outputs[0]->name()));
 
     KWin::VirtualDesktopManager::self()->setCurrent(2, outputs[1]);
-    waitFor("desktopChanged - 1 2 Virtual-1");
+    waitFor(QStringLiteral("desktopChanged - 1 2 %1").arg(outputs[1]->name()));
 }
 
 void ScriptedEffectsTest::testEffectsContext()
diff --git a/autotests/integration/quick_tiling_test.cpp b/autotests/integration/quick_tiling_test.cpp
index 0394ce3b868..5fec122be76 100644
--- a/autotests/integration/quick_tiling_test.cpp
+++ b/autotests/integration/quick_tiling_test.cpp
@@ -2313,22 +2313,21 @@ void QuickTilingTest::testDontChangeTileWhenDesktopChangesOnAnotherOutput()
     const auto desktops = vds->desktops();
     const auto outputs = workspace()->outputs();
 
-    std::unique_ptr<KWayland::Client::Surface> surface(Test::createSurface());
-    std::unique_ptr<Test::XdgToplevel> shellSurface(Test::createXdgToplevelSurface(surface.get()));
-    auto window = Test::renderAndWaitForShown(surface.get(), QSize(100, 100), Qt::blue);
-    window->setOnAllDesktops(true);
+    Test::XdgToplevelWindow window;
+    QVERIFY(window.show());
+    window.m_window->setOnAllDesktops(true);
 
     // We have to receive a configure event when the window becomes active.
-    QSignalSpy tileChangedSpy(window, &Window::tileChanged);
-    QSignalSpy toplevelConfigureRequestedSpy(shellSurface.get(), &Test::XdgToplevel::configureRequested);
-    QSignalSpy surfaceConfigureRequestedSpy(shellSurface->xdgSurface(), &Test::XdgSurface::configureRequested);
+    QSignalSpy tileChangedSpy(window.m_window, &Window::tileChanged);
+    QSignalSpy toplevelConfigureRequestedSpy(window.m_toplevel.get(), &Test::XdgToplevel::configureRequested);
+    QSignalSpy surfaceConfigureRequestedSpy(window.m_toplevel->xdgSurface(), &Test::XdgSurface::configureRequested);
     QVERIFY(surfaceConfigureRequestedSpy.wait());
     QCOMPARE(surfaceConfigureRequestedSpy.count(), 1);
 
     auto ackConfigure = [&]() {
         QVERIFY(surfaceConfigureRequestedSpy.wait());
-        shellSurface->xdgSurface()->ack_configure(surfaceConfigureRequestedSpy.last().at(0).value<quint32>());
-        Test::render(surface.get(), toplevelConfigureRequestedSpy.last().at(0).toSize(), Qt::blue);
+        window.m_toplevel->xdgSurface()->ack_configure(surfaceConfigureRequestedSpy.last().at(0).value<quint32>());
+        Test::render(window.m_surface.get(), toplevelConfigureRequestedSpy.last().at(0).toSize(), Qt::blue);
         QVERIFY(tileChangedSpy.wait());
     };
 
@@ -2354,34 +2353,34 @@ void QuickTilingTest::testDontChangeTileWhenDesktopChangesOnAnotherOutput()
     VirtualDesktop *customTileDesktop = vds->desktopForX11Id(2);
     Tile *quickTile = workspace()->tileManager(tileOutput)->quickRootTile(quickTileDesktop)->tileForMode(QuickTileFlag::Left);
     Tile *customTile = workspace()->rootTile(tileOutput, customTileDesktop)->childTile(1);
-    const RectF originalGeometry = window->frameGeometry();
+    const RectF originalGeometry = window.m_window->frameGeometry();
 
     vds->setCurrent(quickTileDesktop, tileOutput);
-    quickTile->manage(window);
-    QCOMPARE(window->tile(), nullptr);
-    QCOMPARE(window->requestedTile(), quickTile);
-    QCOMPARE(window->frameGeometry(), originalGeometry);
+    quickTile->manage(window.m_window);
+    QCOMPARE(window.m_window->tile(), nullptr);
+    QCOMPARE(window.m_window->requestedTile(), quickTile);
+    QCOMPARE(window.m_window->frameGeometry(), originalGeometry);
     ackConfigure();
-    QCOMPARE(window->tile(), quickTile);
-    QCOMPARE(window->requestedTile(), quickTile);
-    QCOMPARE(window->frameGeometry(), quickTile->windowGeometry());
+    QCOMPARE(window.m_window->tile(), quickTile);
+    QCOMPARE(window.m_window->requestedTile(), quickTile);
+    QCOMPARE(window.m_window->frameGeometry(), quickTile->windowGeometry());
 
     vds->setCurrent(customTileDesktop, tileOutput);
-    customTile->manage(window);
+    customTile->manage(window.m_window);
     ackConfigure();
-    QCOMPARE(window->tile(), customTile);
-    QCOMPARE(window->requestedTile(), customTile);
-    QCOMPARE(window->frameGeometry(), customTile->windowGeometry());
+    QCOMPARE(window.m_window->tile(), customTile);
+    QCOMPARE(window.m_window->requestedTile(), customTile);
+    QCOMPARE(window.m_window->frameGeometry(), customTile->windowGeometry());
 
     vds->setCurrent(customTileDesktop, otherOutput);
-    QCOMPARE(window->tile(), customTile);
-    QCOMPARE(window->requestedTile(), customTile);
-    QCOMPARE(window->frameGeometry(), customTile->windowGeometry());
+    QCOMPARE(window.m_window->tile(), customTile);
+    QCOMPARE(window.m_window->requestedTile(), customTile);
+    QCOMPARE(window.m_window->frameGeometry(), customTile->windowGeometry());
 
     vds->setCurrent(quickTileDesktop, otherOutput);
-    QCOMPARE(window->tile(), customTile);
-    QCOMPARE(window->requestedTile(), customTile);
-    QCOMPARE(window->frameGeometry(), customTile->windowGeometry());
+    QCOMPARE(window.m_window->tile(), customTile);
+    QCOMPARE(window.m_window->requestedTile(), customTile);
+    QCOMPARE(window.m_window->frameGeometry(), customTile->windowGeometry());
 }
 
 void QuickTilingTest::testDontCrashWithMaximizeWindowRule()
diff --git a/src/activities.cpp b/src/activities.cpp
index 1708b4f940a..02833b622a5 100644
--- a/src/activities.cpp
+++ b/src/activities.cpp
@@ -129,7 +129,7 @@ void Activities::slotCurrentChanged(const QString &newActivity)
     const auto it = m_lastVirtualDesktop.find(m_current);
     if (it != m_lastVirtualDesktop.end()) {
         const auto &outputDesktops = it->second;
-        const auto &outputs = workspace()->outputs();
+        const auto outputs = workspace()->outputs();
         for (LogicalOutput *output : outputs) {
             const auto outputDesktopIt = outputDesktops.find(output->uuid());
             if (outputDesktopIt != outputDesktops.end()) {
diff --git a/src/plugins/overview/overvieweffect.cpp b/src/plugins/overview/overvieweffect.cpp
index 295f61fd99c..3336f1c1560 100644
--- a/src/plugins/overview/overvieweffect.cpp
+++ b/src/plugins/overview/overvieweffect.cpp
@@ -115,7 +115,7 @@ OverviewEffect::OverviewEffect()
     });
     connect(effects, &EffectsHandler::desktopChangingCancelled, this, [this]() {
         m_screenDesktopOffsets.clear();
-        const auto &screens = effects->screens();
+        const auto screens = effects->screens();
 
         for (LogicalOutput *output : screens) {
             Q_EMIT desktopOffsetChanged(output);
diff --git a/src/plugins/overview/qml/Main.qml b/src/plugins/overview/qml/Main.qml
index 0fbcdb0044c..90275f87415 100644
--- a/src/plugins/overview/qml/Main.qml
+++ b/src/plugins/overview/qml/Main.qml
@@ -33,6 +33,7 @@ FocusScope {
 
     property bool verticalDesktopBar: KWinComponents.Workspace.desktopGridHeight >= bar.desktopCount && KWinComponents.Workspace.desktopGridHeight != 1
     property QtObject currentDesktop: KWinComponents.SceneView.currentDesktop
+    property point targetScreenDesktopOffset: effect.desktopOffsetForScreen(targetScreen);
 
     // The values of overviewVal and gridVal might not be 0 on startup,
     // but we always want to animate from 0 to those values. So, we initially
@@ -438,14 +439,14 @@ FocusScope {
                 property real column: index % columns
                 // deltaX and deltaY are used to move all the desktops together to 1:1 animate the
                 // switching between different desktops
-                property real deltaX: (!current ? targetScreenDesktopOffset.desktopOffset.x :
-                                       column == 0 ? Math.max(0, targetScreenDesktopOffset.desktopOffset.x) :
-                                       column == columns - 1 ? Math.min(0, targetScreenDesktopOffset.desktopOffset.x) :
-                                           targetScreenDesktopOffset.desktopOffset.x)
-                property real deltaY: (!current ? targetScreenDesktopOffset.desktopOffset.y :
-                                       row == 0 ? Math.max(0, targetScreenDesktopOffset.desktopOffset.y) :
-                                       row == rows - 1 ? Math.min(0, targetScreenDesktopOffset.desktopOffset.y) :
-                                           targetScreenDesktopOffset.desktopOffset.y)
+                property real deltaX: (!current ? targetScreenDesktopOffset.x :
+                                       column == 0 ? Math.max(0, targetScreenDesktopOffset.x) :
+                                       column == columns - 1 ? Math.min(0, targetScreenDesktopOffset.x) :
+                                           targetScreenDesktopOffset.x)
+                property real deltaY: (!current ? targetScreenDesktopOffset.y :
+                                       row == 0 ? Math.max(0, targetScreenDesktopOffset.y) :
+                                       row == rows - 1 ? Math.min(0, targetScreenDesktopOffset.y) :
+                                           targetScreenDesktopOffset.y)
                 // deltaColumn and deltaRows are the difference between the column/row of this desktop
                 // compared to the column/row of the active one
                 property real deltaColumn: column - allDesktopHeaps.currentBackgroundItem.column - deltaX
@@ -800,11 +801,6 @@ FocusScope {
         organized = true
     }
 
-    Item {
-        id: targetScreenDesktopOffset
-        property point desktopOffset: effect.desktopOffsetForScreen(targetScreen)
-    }
-
     Connections {
         target: effect
         onDesktopOffsetChanged: (screen) => {
@@ -812,7 +808,7 @@ FocusScope {
                 return;
             }
 
-            targetScreenDesktopOffset.desktopOffset = effect.desktopOffsetForScreen(screen);
+            container.targetScreenDesktopOffset = effect.desktopOffsetForScreen(screen);
         }
     }
 }
diff --git a/src/plugins/slide/slide.cpp b/src/plugins/slide/slide.cpp
index fa116b8ab7d..8b64e6368b0 100644
--- a/src/plugins/slide/slide.cpp
+++ b/src/plugins/slide/slide.cpp
@@ -66,7 +66,7 @@ SlideEffect::SlideEffect()
         m_switchingActivity = false;
     });
 
-    const auto &screens = effects->screens();
+    const auto screens = effects->screens();
     for (LogicalOutput *screen : screens) {
         m_slideEffectScreens.emplace(screen, this, screen);
     }
@@ -531,7 +531,7 @@ void SlideEffect::desktopChangingCancelled()
     // If the fingers have been lifted and the current desktop didn't change, start animation
     // to move back to the original virtual desktop.
     if (effects->activeFullScreenEffect() == this) {
-        const auto &screens = effects->screens();
+        const auto screens = effects->screens();
         for (LogicalOutput *screen : screens) {
             getOrCreateSlideEffectScreen(screen).desktopChangingCancelled();
         }
diff --git a/src/plugins/slide/slide.h b/src/plugins/slide/slide.h
index fd1469243d6..77c2bd830d4 100644
--- a/src/plugins/slide/slide.h
+++ b/src/plugins/slide/slide.h
@@ -189,12 +189,7 @@ inline bool SlideEffect::slideBackground() const
 
 inline bool SlideEffect::isActive() const
 {
-    for (const SlideEffectScreen &effectScreen : std::as_const(m_slideEffectScreens)) {
-        if (effectScreen.isActive()) {
-            return true;
-        }
-    }
-    return false;
+    return std::ranges::any_of(m_slideEffectScreens, &SlideEffectScreen::isActive);
 }
 
 inline bool SlideEffectScreen::isActive() const
diff --git a/src/virtualdesktops.cpp b/src/virtualdesktops.cpp
index 250f87c70e2..cce56df65c4 100644
--- a/src/virtualdesktops.cpp
+++ b/src/virtualdesktops.cpp
@@ -136,11 +136,7 @@ void VirtualDesktopManager::setVirtualDesktopManagement(PlasmaVirtualDesktopMana
     });
 
     std::for_each(m_desktops.constBegin(), m_desktops.constEnd(), createPlasmaVirtualDesktop);
-
-    const auto &outputs = workspace()->outputs();
-    for (LogicalOutput *output : outputs) {
-        initNewOutput(output);
-    }
+    std::ranges::for_each(workspace()->outputs(), initNewOutput);
 
     m_virtualDesktopManagement->setRows(rows());
     m_virtualDesktopManagement->scheduleDone();
@@ -876,7 +872,7 @@ void VirtualDesktopManager::initShortcuts()
             LogicalOutput *output = workspace()->activeOutput();
             Q_EMIT currentChanging(currentDesktop(output), m_currentDesktopOffset, output);
         } else {
-            const auto &outputs = workspace()->outputs();
+            const auto outputs = workspace()->outputs();
             for (LogicalOutput *output : outputs) {
                 Q_EMIT currentChanging(currentDesktop(output), m_currentDesktopOffset, output);
             }
@@ -1071,7 +1067,7 @@ void VirtualDesktopManager::setPerOutputVirtualDesktops(bool enabled)
     m_perOutputVirtualDesktops = enabled;
     if (!enabled) {
         VirtualDesktop *newDesktop = currentDesktop(workspace()->activeOutput());
-        const auto &outputs = workspace()->outputs();
+        const auto outputs = workspace()->outputs();
         for (const auto output : outputs) {
             setCurrent(newDesktop, output);
         }
-- 
GitLab


From 4ea10707ecabeaeecd44fe3713f3c20168c35b2e Mon Sep 17 00:00:00 2001
From: Hynek Schlindenbuch <schlindenbuch.h@seznam.cz>
Date: Sat, 31 Jan 2026 11:00:03 +0100
Subject: [PATCH 60/63] don't trigger currentChanged too early

---
 src/workspace.cpp | 4 +++-
 1 file changed, 3 insertions(+), 1 deletion(-)

diff --git a/src/workspace.cpp b/src/workspace.cpp
index 3377b2c9380..7356f61242e 100644
--- a/src/workspace.cpp
+++ b/src/workspace.cpp
@@ -364,11 +364,13 @@ void Workspace::init()
 
 #if KWIN_BUILD_ACTIVITIES
     if (m_activities) {
+        // This needs QueuedConnection, because it triggers VirtualDesktopManager::currentChanged which includes the output as a parameter. Therefore, if it was
+        // run directly it might cause some objects to receive currentChanged before outputAdded and violate their expections (e.g. SlideEffect).
         connect(this, &Workspace::outputAdded, m_activities.get(), [this, vds](LogicalOutput *output) {
             if (vds->isPerOutputVirtualDesktops()) {
                 m_activities->restoreDesktopForNewOutput(output);
             }
-        });
+        }, Qt::QueuedConnection);
     }
 #endif
 }
-- 
GitLab


From 4f2ba377be02e7d618c5c1ac6abb79f3c2541010 Mon Sep 17 00:00:00 2001
From: Hynek Schlindenbuch <schlindenbuch.h@seznam.cz>
Date: Sat, 31 Jan 2026 11:09:24 +0100
Subject: [PATCH 61/63] rely on SlideEffectScreen being created in advance

---
 src/plugins/slide/slide.cpp | 22 ++++++++++------------
 src/plugins/slide/slide.h   |  2 +-
 2 files changed, 11 insertions(+), 13 deletions(-)

diff --git a/src/plugins/slide/slide.cpp b/src/plugins/slide/slide.cpp
index 8b64e6368b0..4e87f23f585 100644
--- a/src/plugins/slide/slide.cpp
+++ b/src/plugins/slide/slide.cpp
@@ -52,7 +52,7 @@ SlideEffect::SlideEffect()
     connect(effects, &EffectsHandler::desktopRemoved,
             this, &SlideEffect::finishedSwitching);
     connect(effects, &EffectsHandler::screenAdded, this, [this](LogicalOutput *screen) {
-        m_slideEffectScreens.tryEmplace(screen, this, screen);
+        m_slideEffectScreens.emplace(screen, this, screen);
         finishedSwitching();
     });
     connect(effects, &EffectsHandler::screenRemoved, this, [this](LogicalOutput *screen) {
@@ -131,7 +131,7 @@ inline Region buildClipRegion(const QPoint &pos, int w, int h)
 void SlideEffect::prePaintScreen(ScreenPrePaintData &data, std::chrono::milliseconds presentTime)
 {
     if (data.screen) {
-        getOrCreateSlideEffectScreen(data.screen).prePaintScreen(data, presentTime);
+        getSlideEffectScreen(data.screen).prePaintScreen(data, presentTime);
     }
 
     effects->prePaintScreen(data, presentTime);
@@ -196,7 +196,7 @@ void SlideEffectScreen::prePaintScreen(ScreenPrePaintData &data, std::chrono::mi
 
 void SlideEffect::paintScreen(const RenderTarget &renderTarget, const RenderViewport &viewport, int mask, const Region &deviceRegion, LogicalOutput *screen)
 {
-    getOrCreateSlideEffectScreen(screen).paintScreen(renderTarget, viewport, mask, deviceRegion);
+    getSlideEffectScreen(screen).paintScreen(renderTarget, viewport, mask, deviceRegion);
 }
 
 void SlideEffectScreen::paintScreen(const RenderTarget &renderTarget, const RenderViewport &viewport, int mask, const Region &deviceRegion)
@@ -263,7 +263,7 @@ void SlideEffect::prePaintWindow(RenderView *view, EffectWindow *w, WindowPrePai
 
 void SlideEffect::paintWindow(const RenderTarget &renderTarget, const RenderViewport &viewport, EffectWindow *w, int mask, const Region &deviceGeometry, WindowPaintData &data)
 {
-    getOrCreateSlideEffectScreen(w->screen()).paintWindow(renderTarget, viewport, w, mask, deviceGeometry, data);
+    getSlideEffectScreen(w->screen()).paintWindow(renderTarget, viewport, w, mask, deviceGeometry, data);
 }
 
 void SlideEffectScreen::paintWindow(const RenderTarget &renderTarget, const RenderViewport &viewport, EffectWindow *w, int mask, const Region &deviceGeometry, WindowPaintData &data)
@@ -467,7 +467,7 @@ void SlideEffect::desktopChanged(VirtualDesktop *old, VirtualDesktop *current, E
         return;
     }
 
-    getOrCreateSlideEffectScreen(screen).desktopChanged(old, current, with);
+    getSlideEffectScreen(screen).desktopChanged(old, current, with);
     effects->setActiveFullScreenEffect(this);
 }
 
@@ -499,7 +499,7 @@ void SlideEffect::desktopChanging(VirtualDesktop *old, QPointF desktopOffset, Ef
         return;
     }
 
-    getOrCreateSlideEffectScreen(output).desktopChanging(old, desktopOffset, with);
+    getSlideEffectScreen(output).desktopChanging(old, desktopOffset, with);
     effects->setActiveFullScreenEffect(this);
 }
 
@@ -533,7 +533,7 @@ void SlideEffect::desktopChangingCancelled()
     if (effects->activeFullScreenEffect() == this) {
         const auto screens = effects->screens();
         for (LogicalOutput *screen : screens) {
-            getOrCreateSlideEffectScreen(screen).desktopChangingCancelled();
+            getSlideEffectScreen(screen).desktopChangingCancelled();
         }
     }
 }
@@ -567,7 +567,7 @@ void SlideEffect::windowAdded(EffectWindow *w)
     if (!w) {
         return;
     }
-    getOrCreateSlideEffectScreen(w->screen()).windowAdded(w);
+    getSlideEffectScreen(w->screen()).windowAdded(w);
 }
 
 void SlideEffectScreen::windowAdded(EffectWindow *w)
@@ -594,7 +594,6 @@ void SlideEffect::windowDeleted(EffectWindow *w)
         return;
     }
 
-    // Don't use getOrCreateSlideEffectScreen: it could re-create SlideEffectScreen for removed screen.
     auto it = m_slideEffectScreens.find(w->screen());
     if (it == m_slideEffectScreens.end()) {
         return;
@@ -686,10 +685,9 @@ QPointF SlideEffectScreen::constrainToDrawableRange(QPointF p)
     return p;
 }
 
-SlideEffectScreen &SlideEffect::getOrCreateSlideEffectScreen(LogicalOutput *screen)
+SlideEffectScreen &SlideEffect::getSlideEffectScreen(LogicalOutput *screen)
 {
-    auto result = m_slideEffectScreens.tryEmplace(screen, this, screen);
-    return result.iterator.value();
+    return m_slideEffectScreens.find(screen).value();
 }
 
 } // namespace KWin
diff --git a/src/plugins/slide/slide.h b/src/plugins/slide/slide.h
index 77c2bd830d4..3e926d24dfb 100644
--- a/src/plugins/slide/slide.h
+++ b/src/plugins/slide/slide.h
@@ -158,7 +158,7 @@ private Q_SLOTS:
 
 private:
     void finishedSwitching();
-    SlideEffectScreen &getOrCreateSlideEffectScreen(LogicalOutput *screen);
+    SlideEffectScreen &getSlideEffectScreen(LogicalOutput *screen);
 
 private:
     int m_hGap;
-- 
GitLab


From 51dfe083d1cd63c9d2e92daffe264a1a11d9f86f Mon Sep 17 00:00:00 2001
From: Hynek Schlindenbuch <schlindenbuch.h@seznam.cz>
Date: Sat, 31 Jan 2026 13:57:14 +0100
Subject: [PATCH 62/63] remove deleteRef from slide effect

It doesn't seem to be necessary anymore. It used to crash without it,
but now it doesn't.
---
 src/plugins/slide/slide.cpp | 7 +++----
 src/plugins/slide/slide.h   | 1 -
 2 files changed, 3 insertions(+), 5 deletions(-)

diff --git a/src/plugins/slide/slide.cpp b/src/plugins/slide/slide.cpp
index 4e87f23f585..124117fb4c2 100644
--- a/src/plugins/slide/slide.cpp
+++ b/src/plugins/slide/slide.cpp
@@ -409,7 +409,6 @@ void SlideEffectScreen::prepareSwitching()
             continue;
         }
         m_windowData[w] = WindowData{
-            .deleteRef = EffectWindowDeletedRef(w),
             .visibilityRef = EffectWindowVisibleRef(w, EffectWindow::PAINT_DISABLED_BY_DESKTOP),
         };
 
@@ -583,7 +582,6 @@ void SlideEffectScreen::windowAdded(EffectWindow *w)
     w->setData(WindowForceBlurRole, QVariant(true));
 
     m_windowData[w] = WindowData{
-        .deleteRef = EffectWindowDeletedRef(w),
         .visibilityRef = EffectWindowVisibleRef(w, EffectWindow::PAINT_DISABLED_BY_DESKTOP),
     };
 }
@@ -594,6 +592,7 @@ void SlideEffect::windowDeleted(EffectWindow *w)
         return;
     }
 
+    // This cannot use getSlideEffectScreen, because the screen may be removed before the window.
     auto it = m_slideEffectScreens.find(w->screen());
     if (it == m_slideEffectScreens.end()) {
         return;
@@ -609,8 +608,8 @@ void SlideEffectScreen::windowDeleted(EffectWindow *w)
     if (w == m_movingWindow) {
         m_movingWindow = nullptr;
     }
-    // m_windowData holds a EffectWindowDeletedRef, so when this is called the window can no longer be in m_windowData.
-    // m_elevatedWindows is a subset of m_windowData, therefore the window cannot be in m_elevatedWindows either.
+    m_elevatedWindows.removeAll(w);
+    m_windowData.remove(w);
 }
 
 /*
diff --git a/src/plugins/slide/slide.h b/src/plugins/slide/slide.h
index 3e926d24dfb..ece88dbc8cc 100644
--- a/src/plugins/slide/slide.h
+++ b/src/plugins/slide/slide.h
@@ -109,7 +109,6 @@ private:
 
     struct WindowData
     {
-        EffectWindowDeletedRef deleteRef;
         EffectWindowVisibleRef visibilityRef;
     };
 
-- 
GitLab


From f0fe5ed05c9284968b011e272d896001b3da72ee Mon Sep 17 00:00:00 2001
From: Hynek Schlindenbuch <schlindenbuch.h@seznam.cz>
Date: Sat, 31 Jan 2026 15:04:06 +0100
Subject: [PATCH 63/63] remove useless check from windowAdded

windowAdded seems to only be called after m_windowItem is set.

Unfortunately, windowDeleted currently crashes without the check,
because KWin doesn't make sure that it's not called with nullptr like it
does for windowAdded.
---
 src/plugins/slide/slide.cpp | 4 +---
 1 file changed, 1 insertion(+), 3 deletions(-)

diff --git a/src/plugins/slide/slide.cpp b/src/plugins/slide/slide.cpp
index 124117fb4c2..59d32054910 100644
--- a/src/plugins/slide/slide.cpp
+++ b/src/plugins/slide/slide.cpp
@@ -563,9 +563,6 @@ QPointF SlideEffectScreen::moveInsideDesktopGrid(QPointF p)
 
 void SlideEffect::windowAdded(EffectWindow *w)
 {
-    if (!w) {
-        return;
-    }
     getSlideEffectScreen(w->screen()).windowAdded(w);
 }
 
@@ -588,6 +585,7 @@ void SlideEffectScreen::windowAdded(EffectWindow *w)
 
 void SlideEffect::windowDeleted(EffectWindow *w)
 {
+    // FIXME: effectWindow should never be nullptr
     if (!w) {
         return;
     }
-- 
GitLab

openSUSE Build Service is sponsored by