From 09d0847ea4358f256948f4ef831c73c90ddea1f7 Mon Sep 17 00:00:00 2001 From: Toast Date: Fri, 19 Sep 2025 18:00:15 +0200 Subject: [PATCH] Desktop/kde: add patches --- .../patches/frameworkintegration/patches.txt | 2 + .../patches/frameworkintegration/pr54.patch | 50 + roles/kde/patches/kwin/patches.txt | 2 + roles/kde/patches/kwin/pr7927.patch | 669 +++++++++++++ roles/kde/patches/kwin/pr8005.patch | 71 ++ .../kde/patches/plasma-workspace/patches.txt | 9 + .../kde/patches/plasma-workspace/pr5609.patch | 456 +++++++++ .../kde/patches/plasma-workspace/pr5657.patch | 177 ++++ .../kde/patches/plasma-workspace/pr5673.patch | 392 ++++++++ .../patches/plasma-workspace/pr5678.9.patch | 913 ++++++++++++++++++ .../kde/patches/plasma-workspace/pr5734.patch | 133 +++ .../kde/patches/plasma-workspace/pr5746.patch | 33 + .../kde/patches/plasma-workspace/pr5782.patch | 32 + .../kde/patches/plasma-workspace/pr5788.patch | 42 + 14 files changed, 2981 insertions(+) create mode 100644 roles/kde/patches/frameworkintegration/patches.txt create mode 100644 roles/kde/patches/frameworkintegration/pr54.patch create mode 100644 roles/kde/patches/kwin/pr7927.patch create mode 100644 roles/kde/patches/kwin/pr8005.patch create mode 100644 roles/kde/patches/plasma-workspace/pr5609.patch create mode 100644 roles/kde/patches/plasma-workspace/pr5657.patch create mode 100644 roles/kde/patches/plasma-workspace/pr5673.patch create mode 100644 roles/kde/patches/plasma-workspace/pr5678.9.patch create mode 100644 roles/kde/patches/plasma-workspace/pr5734.patch create mode 100644 roles/kde/patches/plasma-workspace/pr5746.patch create mode 100644 roles/kde/patches/plasma-workspace/pr5782.patch create mode 100644 roles/kde/patches/plasma-workspace/pr5788.patch diff --git a/roles/kde/patches/frameworkintegration/patches.txt b/roles/kde/patches/frameworkintegration/patches.txt new file mode 100644 index 0000000..09818df --- /dev/null +++ b/roles/kde/patches/frameworkintegration/patches.txt @@ -0,0 +1,2 @@ +Frameworks 6.18: +Pr 54 https://invent.kde.org/frameworks/frameworkintegration/-/merge_requests/54 diff --git a/roles/kde/patches/frameworkintegration/pr54.patch b/roles/kde/patches/frameworkintegration/pr54.patch new file mode 100644 index 0000000..aee5303 --- /dev/null +++ b/roles/kde/patches/frameworkintegration/pr54.patch @@ -0,0 +1,50 @@ +From 516a3642796563bcc7a13cd02795de3077a861b7 Mon Sep 17 00:00:00 2001 +From: Nate Graham +Date: Tue, 5 Aug 2025 09:27:36 -0600 +Subject: [PATCH] Turn on popups for device notifications + +Part of https://invent.kde.org/plasma/plasma-desktop/-/issues/149 + +These notifications are a useful way to communicate to the user that +their device isn't dead and was actuallt detected. Currently we have +only sounds turned on, and no popups. This problematic for +accessibility because deaf people won't hear the sound. + +For this reason, EU Directive 2019/882 requires that all sound-based +notifications be accompanied by something visual as well. + +Let's turn on popups to comply with that. We did some work to make the +popups not annoying, including: +- Not appearing in the history +- Removing the connection notification when disconnected (and vice + versa) +- Removing the notification when it's for a USB disk and the Disks & + Devices widget appears automatically. +--- + plasma_workspace.notifyrc | 4 ++-- + 1 file changed, 2 insertions(+), 2 deletions(-) + +diff --git a/plasma_workspace.notifyrc b/plasma_workspace.notifyrc +index 7b9311d..4394192 100644 +--- a/plasma_workspace.notifyrc ++++ b/plasma_workspace.notifyrc +@@ -2380,7 +2380,7 @@ Comment[uk]=Було з'єднано пристрій + Comment[x-test]=xxA device was plugged inxx + Comment[zh_CN]=已插入一个设备 + Comment[zh_TW]=有裝置剛被插入 +-Action=Sound ++Action=Popup|Sound + Sound=device-added + Urgency=Low + +@@ -2465,6 +2465,6 @@ Comment[uk]=Було від'єднано пристрій + Comment[x-test]=xxA device was unpluggedxx + Comment[zh_CN]=已拔出一个设备 + Comment[zh_TW]=有裝置剛被拔出 +-Action=Sound ++Action=Popup|Sound + Sound=device-removed + Urgency=Low +-- +GitLab + diff --git a/roles/kde/patches/kwin/patches.txt b/roles/kde/patches/kwin/patches.txt index cf1766b..f860b5d 100644 --- a/roles/kde/patches/kwin/patches.txt +++ b/roles/kde/patches/kwin/patches.txt @@ -1,3 +1,5 @@ Plasma 6.5.0: Pr 3612 https://invent.kde.org/plasma/kwin/-/merge_requests/3612 Pr 7823 https://invent.kde.org/plasma/kwin/-/merge_requests/7823 +Pr 8005 https://invent.kde.org/plasma/kwin/-/merge_requests/8005 + Depends on Pr 7927 https://invent.kde.org/plasma/kwin/-/merge_requests/7927 diff --git a/roles/kde/patches/kwin/pr7927.patch b/roles/kde/patches/kwin/pr7927.patch new file mode 100644 index 0000000..d35ef4b --- /dev/null +++ b/roles/kde/patches/kwin/pr7927.patch @@ -0,0 +1,669 @@ +From 9f6c92806490d662117575a766f9fcb01e253344 Mon Sep 17 00:00:00 2001 +From: Xaver Hugl +Date: Tue, 22 Jul 2025 23:24:51 +0200 +Subject: [PATCH 1/4] xdgactivation: move the activation token to workspace + +--- + src/activation.cpp | 14 ++++++++++++ + src/workspace.h | 6 +++++ + src/xdgactivationv1.cpp | 50 +++++++++-------------------------------- + src/xdgactivationv1.h | 14 ++---------- + 4 files changed, 32 insertions(+), 52 deletions(-) + +diff --git a/src/activation.cpp b/src/activation.cpp +index 60b921247ce..9e743b8b098 100644 +--- a/src/activation.cpp ++++ b/src/activation.cpp +@@ -630,4 +630,18 @@ void Workspace::windowAttentionChanged(Window *window, bool set) + } + } + ++void Workspace::setActivationToken(const QString &token, uint32_t serial) ++{ ++ m_activationToken = token; ++ m_activationTokenSerial = serial; ++} ++ ++bool Workspace::mayActivate(const QString &token) const ++{ ++ if (!m_activeWindow) { ++ return true; ++ } ++ return !m_activationToken.isEmpty() && token == m_activationToken && m_activeWindow->lastUsageSerial() <= m_activationTokenSerial; ++} ++ + } // namespace +diff --git a/src/workspace.h b/src/workspace.h +index 2082bbe148d..03c54e06750 100644 +--- a/src/workspace.h ++++ b/src/workspace.h +@@ -436,6 +436,9 @@ public: + OutputConfigurationError applyOutputConfiguration(OutputConfiguration &config, const std::optional> &outputOrder = std::nullopt); + void updateXwaylandScale(); + ++ void setActivationToken(const QString &token, uint32_t serial); ++ bool mayActivate(const QString &token) const; ++ + public Q_SLOTS: + void performWindowOperation(KWin::Window *window, Options::WindowOperation op); + // Keybindings +@@ -729,6 +732,9 @@ private: + std::unique_ptr m_dpmsFilter; + KConfigWatcher::Ptr m_kdeglobalsWatcher; + ++ QString m_activationToken; ++ uint32_t m_activationTokenSerial = 0; ++ + private: + friend bool performTransiencyCheck(); + friend Workspace *workspace(); +diff --git a/src/xdgactivationv1.cpp b/src/xdgactivationv1.cpp +index 39dade95332..a6be350399a 100644 +--- a/src/xdgactivationv1.cpp ++++ b/src/xdgactivationv1.cpp +@@ -33,22 +33,6 @@ static bool isPrivilegedInWindowManagement(const ClientConnection *client) + XdgActivationV1Integration::XdgActivationV1Integration(XdgActivationV1Interface *activation, QObject *parent) + : QObject(parent) + { +- Workspace *ws = Workspace::self(); +- connect(ws, &Workspace::windowActivated, this, [this](Window *window) { +- if (!m_currentActivationToken || !window || window->property("token").toString() == m_currentActivationToken->token) { +- return; +- } +- +- // We check that it's not the app that we are trying to activate +- if (window->desktopFileName() != m_currentActivationToken->applicationId) { +- // But also that the new one has been requested after the token was requested +- if (window->lastUsageSerial() < m_currentActivationToken->serial) { +- return; +- } +- } +- +- clear(); +- }); + activation->setActivationTokenCreator([this](ClientConnection *client, SurfaceInterface *surface, uint serial, SeatInterface *seat, const QString &appId) -> QString { + Workspace *ws = Workspace::self(); + Q_ASSERT(client); // Should always be available as it's coming straight from the wayland implementation +@@ -69,9 +53,6 @@ QString XdgActivationV1Integration::requestToken(bool isPrivileged, SurfaceInter + static int i = 0; + const auto newToken = QStringLiteral("kwin-%1").arg(++i); + +- if (m_currentActivationToken) { +- clear(); +- } + bool showNotify = false; + QIcon icon = QIcon::fromTheme(QStringLiteral("system-run")); + if (const QString desktopFilePath = Window::findDesktopFile(appId); !desktopFilePath.isEmpty()) { +@@ -83,13 +64,13 @@ QString XdgActivationV1Integration::requestToken(bool isPrivileged, SurfaceInter + } + icon = QIcon::fromTheme(df.readIcon(), icon); + } +- std::unique_ptr activation; + if (showNotify) { +- activation = waylandServer()->plasmaActivationFeedback()->createActivation(appId); ++ m_lastToken = newToken; ++ m_activation = waylandServer()->plasmaActivationFeedback()->createActivation(appId); + } +- m_currentActivationToken = std::make_unique(ActivationToken{newToken, isPrivileged, surface, serial, seat, appId, showNotify, std::move(activation)}); ++ workspace()->setActivationToken(newToken, serial); + if (showNotify) { +- Q_EMIT effects->startupAdded(m_currentActivationToken->token, icon); ++ Q_EMIT effects->startupAdded(newToken, icon); + } + return newToken; + } +@@ -103,31 +84,20 @@ void XdgActivationV1Integration::activateSurface(SurfaceInterface *surface, cons + return; + } + +- if (!m_currentActivationToken || m_currentActivationToken->token != token) { +- qCDebug(KWIN_CORE) << "Refusing to activate " << window << " (provided token: " << token << ", current token:" << (m_currentActivationToken ? m_currentActivationToken->token : QStringLiteral("null")) << ")"; ++ if (!ws->mayActivate(token)) { + window->demandAttention(); + return; + } +- +- auto ownerWindow = waylandServer()->findWindow(m_currentActivationToken->surface); +- qCDebug(KWIN_CORE) << "activating" << window << surface << "on behalf of" << m_currentActivationToken->surface << "into" << ownerWindow; +- if (!ws->activeWindow() || ws->activeWindow() == ownerWindow || ws->activeWindow()->lastUsageSerial() < m_currentActivationToken->serial || m_currentActivationToken->isPrivileged) { +- ws->activateWindow(window); +- } else { +- qCWarning(KWIN_CORE) << "Activation requested while owner isn't active" << (ownerWindow ? ownerWindow->desktopFileName() : "null") +- << m_currentActivationToken->applicationId; +- window->demandAttention(); +- clear(); +- } ++ ws->activateWindow(window); ++ clear(); + } + + void XdgActivationV1Integration::clear() + { +- Q_ASSERT(m_currentActivationToken); +- if (m_currentActivationToken->showNotify) { +- Q_EMIT effects->startupRemoved(m_currentActivationToken->token); ++ if (m_activation) { ++ Q_EMIT effects->startupRemoved(m_lastToken); ++ m_activation.reset(); + } +- m_currentActivationToken.reset(); + } + + } +diff --git a/src/xdgactivationv1.h b/src/xdgactivationv1.h +index 98835def8aa..77d21856095 100644 +--- a/src/xdgactivationv1.h ++++ b/src/xdgactivationv1.h +@@ -38,18 +38,8 @@ private: + QString requestToken(bool isPrivileged, SurfaceInterface *surface, uint serial, SeatInterface *seat, const QString &appId); + void clear(); + +- struct ActivationToken +- { +- QString token; +- bool isPrivileged; +- QPointer surface; +- uint serial; +- SeatInterface *seat; +- QString applicationId; +- bool showNotify; +- std::unique_ptr activation; +- }; +- std::unique_ptr m_currentActivationToken; ++ QString m_lastToken; ++ std::unique_ptr m_activation; + }; + + } +-- +GitLab + + +From 6c673a479412902a14c06046199f976e2192dc65 Mon Sep 17 00:00:00 2001 +From: Xaver Hugl +Date: Tue, 22 Jul 2025 23:43:14 +0200 +Subject: [PATCH 2/4] xdgactivation: also allow using activation tokens before + the window is mapped + +We just store the token in the window, and then Workspace uses it to decide whether +or not to activate the window when it's actually shown. +--- + src/window.cpp | 10 ++++++++++ + src/window.h | 5 +++++ + src/workspace.cpp | 4 ++-- + src/xdgactivationv1.cpp | 6 +++++- + 4 files changed, 22 insertions(+), 3 deletions(-) + +diff --git a/src/window.cpp b/src/window.cpp +index 4a97a54aa74..879147ec673 100644 +--- a/src/window.cpp ++++ b/src/window.cpp +@@ -4675,6 +4675,16 @@ void Window::setDescription(const QString &description) + } + } + ++void Window::setActivationToken(const QString &token) ++{ ++ m_activationToken = token; ++} ++ ++QString Window::activationToken() const ++{ ++ return m_activationToken; ++} ++ + } // namespace KWin + + #include "moc_window.cpp" +diff --git a/src/window.h b/src/window.h +index 1eb371018ca..91addb46819 100644 +--- a/src/window.h ++++ b/src/window.h +@@ -1357,6 +1357,9 @@ public: + QString tag() const; + QString description() const; + ++ void setActivationToken(const QString &token); ++ QString activationToken() const; ++ + public Q_SLOTS: + virtual void closeWindow() = 0; + +@@ -1880,6 +1883,8 @@ protected: + + QString m_tag; + QString m_description; ++ ++ QString m_activationToken; + }; + + inline QRectF Window::bufferGeometry() const +diff --git a/src/workspace.cpp b/src/workspace.cpp +index 3a5cb12677b..866abb8080d 100644 +--- a/src/workspace.cpp ++++ b/src/workspace.cpp +@@ -781,8 +781,8 @@ void Workspace::addWaylandWindow(Window *window) + rearrange(); + } + if (window->wantsInput() && !window->isMinimized()) { +- // Never activate a window on its own in "Extreme" mode. +- if (options->focusStealingPreventionLevel() < 4) { ++ // In "Extreme" mode, require an activation token to activate new windows ++ if (options->focusStealingPreventionLevel() < 4 || (!m_activationToken.isEmpty() && window->activationToken() == m_activationToken)) { + if (!window->isDesktop() + // If there's no active window, make this desktop the active one. + || (activeWindow() == nullptr && should_get_focus.count() == 0)) { +diff --git a/src/xdgactivationv1.cpp b/src/xdgactivationv1.cpp +index a6be350399a..11c95c84b32 100644 +--- a/src/xdgactivationv1.cpp ++++ b/src/xdgactivationv1.cpp +@@ -88,7 +88,11 @@ void XdgActivationV1Integration::activateSurface(SurfaceInterface *surface, cons + window->demandAttention(); + return; + } +- ws->activateWindow(window); ++ if (window->readyForPainting()) { ++ ws->activateWindow(window); ++ } else { ++ window->setActivationToken(token); ++ } + clear(); + } + +-- +GitLab + + +From 2436625a66257f586a0934c3d678c910bbdb3705 Mon Sep 17 00:00:00 2001 +From: Xaver Hugl +Date: Wed, 23 Jul 2025 13:18:39 +0200 +Subject: [PATCH 3/4] xdgactivation: move the "not granted" token to + requestToken + +Having some but not all checks in that method is a bit confusing +--- + src/xdgactivationv1.cpp | 14 ++++++-------- + 1 file changed, 6 insertions(+), 8 deletions(-) + +diff --git a/src/xdgactivationv1.cpp b/src/xdgactivationv1.cpp +index 11c95c84b32..e818466f8ad 100644 +--- a/src/xdgactivationv1.cpp ++++ b/src/xdgactivationv1.cpp +@@ -34,15 +34,8 @@ XdgActivationV1Integration::XdgActivationV1Integration(XdgActivationV1Interface + : QObject(parent) + { + activation->setActivationTokenCreator([this](ClientConnection *client, SurfaceInterface *surface, uint serial, SeatInterface *seat, const QString &appId) -> QString { +- Workspace *ws = Workspace::self(); + Q_ASSERT(client); // Should always be available as it's coming straight from the wayland implementation +- const bool isPrivileged = isPrivilegedInWindowManagement(client); +- if (!isPrivileged && ws->activeWindow() && ws->activeWindow()->surface() != surface) { +- qCDebug(KWIN_CORE) << "Cannot grant a token to" << client; +- return QStringLiteral("not-granted-666"); +- } +- +- return requestToken(isPrivileged, surface, serial, seat, appId); ++ return requestToken(isPrivilegedInWindowManagement(client), surface, serial, seat, appId); + }); + + connect(activation, &XdgActivationV1Interface::activateRequested, this, &XdgActivationV1Integration::activateSurface); +@@ -50,6 +43,11 @@ XdgActivationV1Integration::XdgActivationV1Integration(XdgActivationV1Interface + + QString XdgActivationV1Integration::requestToken(bool isPrivileged, SurfaceInterface *surface, uint serial, SeatInterface *seat, const QString &appId) + { ++ auto window = waylandServer()->findWindow(surface); ++ if (!isPrivileged && workspace()->activeWindow() && workspace()->activeWindow()->surface() != surface) { ++ qCWarning(KWIN_CORE) << "Cannot grant a token to" << window; ++ return QStringLiteral("not-granted-666"); ++ } + static int i = 0; + const auto newToken = QStringLiteral("kwin-%1").arg(++i); + +-- +GitLab + + +From 8508b8060813c07fe035801381bad1f9a375acf0 Mon Sep 17 00:00:00 2001 +From: Xaver Hugl +Date: Thu, 24 Jul 2025 20:43:46 +0200 +Subject: [PATCH 4/4] autotests/integration: add a test case for xdg activation + +--- + autotests/integration/CMakeLists.txt | 3 +- + autotests/integration/activation_test.cpp | 126 +++++++++++++++++++++- + autotests/integration/kwin_wayland_test.h | 31 ++++++ + autotests/integration/test_helpers.cpp | 50 +++++++++ + 4 files changed, 208 insertions(+), 2 deletions(-) + +diff --git a/autotests/integration/CMakeLists.txt b/autotests/integration/CMakeLists.txt +index 13b0cde04f5..408af6ac8b8 100644 +--- a/autotests/integration/CMakeLists.txt ++++ b/autotests/integration/CMakeLists.txt +@@ -16,13 +16,14 @@ qt6_generate_wayland_protocol_client_sources(KWinIntegrationTestFramework + FILES + ${CMAKE_SOURCE_DIR}/src/wayland/protocols/wlr-layer-shell-unstable-v1.xml + ${WaylandProtocols_DATADIR}/stable/presentation-time/presentation-time.xml ++ ${WaylandProtocols_DATADIR}/stable/tablet/tablet-v2.xml + ${WaylandProtocols_DATADIR}/stable/xdg-shell/xdg-shell.xml + ${WaylandProtocols_DATADIR}/staging/color-management/color-management-v1.xml +- ${WaylandProtocols_DATADIR}/stable/tablet/tablet-v2.xml + ${WaylandProtocols_DATADIR}/staging/cursor-shape/cursor-shape-v1.xml + ${WaylandProtocols_DATADIR}/staging/fifo/fifo-v1.xml + ${WaylandProtocols_DATADIR}/staging/fractional-scale/fractional-scale-v1.xml + ${WaylandProtocols_DATADIR}/staging/security-context/security-context-v1.xml ++ ${WaylandProtocols_DATADIR}/staging/xdg-activation/xdg-activation-v1.xml + ${WaylandProtocols_DATADIR}/staging/xdg-dialog/xdg-dialog-v1.xml + ${WaylandProtocols_DATADIR}/unstable/idle-inhibit/idle-inhibit-unstable-v1.xml + ${WaylandProtocols_DATADIR}/unstable/text-input/text-input-unstable-v3.xml +diff --git a/autotests/integration/activation_test.cpp b/autotests/integration/activation_test.cpp +index 75c9e7e8c7b..30f671d6fe2 100644 +--- a/autotests/integration/activation_test.cpp ++++ b/autotests/integration/activation_test.cpp +@@ -15,6 +15,7 @@ + #include "workspace.h" + #include "x11window.h" + ++#include + #include + #include + #include +@@ -40,6 +41,7 @@ private Q_SLOTS: + void testSwitchToWindowMaximized(); + void testSwitchToWindowFullScreen(); + void testActiveFullscreen(); ++ void testXdgActivation(); + + private: + void stackScreensHorizontally(); +@@ -64,7 +66,7 @@ void ActivationTest::initTestCase() + + void ActivationTest::init() + { +- QVERIFY(Test::setupWaylandConnection()); ++ QVERIFY(Test::setupWaylandConnection(Test::AdditionalWaylandInterface::XdgActivation | Test::AdditionalWaylandInterface::Seat)); + + workspace()->setActiveOutput(QPoint(640, 512)); + input()->pointer()->warp(QPoint(640, 512)); +@@ -592,6 +594,128 @@ void ActivationTest::testActiveFullscreen() + QCOMPARE(workspace()->activeWindow(), waylandWindow); + QCOMPARE(x11Window->layer(), Layer::NormalLayer); + } ++ ++void ActivationTest::testXdgActivation() ++{ ++ Test::setOutputConfig({QRect(0, 0, 1280, 1024)}); ++ ++ uint32_t time = 0; ++ ++ std::vector> surfaces; ++ std::vector> shellSurfaces; ++ std::vector windows; ++ const auto setupWindows = [&]() { ++ windows.clear(); ++ shellSurfaces.clear(); ++ surfaces.clear(); ++ ++ // re-create the same setup every time for reduced confusion ++ for (int i = 0; i < 3; i++) { ++ surfaces.push_back(Test::createSurface()); ++ shellSurfaces.push_back(Test::createXdgToplevelSurface(surfaces.back().get())); ++ windows.push_back(Test::renderAndWaitForShown(surfaces.back().get(), QSize(100, 50), Qt::blue)); ++ windows.back()->move(QPoint(150 * i, 0)); ++ ++ Test::pointerMotion(windows.back()->frameGeometry().center(), time++); ++ Test::pointerButtonPressed(1, time++); ++ Test::pointerButtonReleased(1, time++); ++ } ++ }; ++ setupWindows(); ++ ++ QSignalSpy activationSpy(workspace(), &Workspace::windowActivated); ++ ++ // activating a window without a valid token should fail ++ Test::xdgActivation()->activate(QString(), *surfaces[1]); ++ QVERIFY(!activationSpy.wait(10)); ++ ++ // activating it without a surface should fail as well, even if a serial is present ++ auto token = Test::xdgActivation()->createToken(); ++ token->set_serial(windows.back()->lastUsageSerial(), *Test::waylandSeat()); ++ Test::xdgActivation()->activate(token->commitAndWait(), *surfaces[1]); ++ QVERIFY(!activationSpy.wait(10)); ++ ++ // adding the surface should make it work ++ token = Test::xdgActivation()->createToken(); ++ token->set_surface(*surfaces.back()); ++ token->set_serial(windows.back()->lastUsageSerial(), *Test::waylandSeat()); ++ Test::xdgActivation()->activate(token->commitAndWait(), *surfaces[1]); ++ QVERIFY(activationSpy.wait(10)); ++ QCOMPARE(workspace()->activeWindow(), windows[1]); ++ ++ // activation should still work if the window is closed after creating the token ++ setupWindows(); ++ token = Test::xdgActivation()->createToken(); ++ token->set_surface(*surfaces[2]); ++ token->set_serial(windows[2]->lastUsageSerial(), *Test::waylandSeat()); ++ QString result = token->commitAndWait(); ++ ++ surfaces[2]->attachBuffer((wl_buffer *)nullptr); ++ surfaces[2]->commit(KWayland::Client::Surface::CommitFlag::None); ++ QVERIFY(activationSpy.wait(10)); ++ QCOMPARE(workspace()->activeWindow(), windows[1]); ++ ++ Test::xdgActivation()->activate(result, *surfaces[0]); ++ QVERIFY(activationSpy.wait(10)); ++ QCOMPARE(workspace()->activeWindow(), windows[0]); ++ ++ // ...unless the user interacted with another window in between ++ setupWindows(); ++ token = Test::xdgActivation()->createToken(); ++ token->set_surface(*surfaces[2]); ++ token->set_serial(windows[2]->lastUsageSerial(), *Test::waylandSeat()); ++ result = token->commitAndWait(); ++ ++ surfaces[2]->attachBuffer((wl_buffer *)nullptr); ++ surfaces[2]->commit(KWayland::Client::Surface::CommitFlag::None); ++ QVERIFY(activationSpy.wait(10)); ++ QCOMPARE(workspace()->activeWindow(), windows[1]); ++ ++ Test::pointerMotion(windows[1]->frameGeometry().center(), time++); ++ Test::pointerButtonPressed(1, time++); ++ Test::pointerButtonReleased(1, time++); ++ ++ Test::xdgActivation()->activate(result, *surfaces[0]); ++ QVERIFY(!activationSpy.wait(10)); ++ QCOMPARE(workspace()->activeWindow(), windows[1]); ++ ++ // ensure that windows are only activated on show with a valid activation token ++ options->setFocusStealingPreventionLevel(4); ++ ++ // creating a new window and immediately activating it should work ++ setupWindows(); ++ token = Test::xdgActivation()->createToken(); ++ token->set_surface(*surfaces[2]); ++ token->set_serial(windows[2]->lastUsageSerial(), *Test::waylandSeat()); ++ result = token->commitAndWait(); ++ surfaces.push_back(Test::createSurface()); ++ shellSurfaces.push_back(Test::createXdgToplevelSurface(surfaces.back().get(), [&](Test::XdgToplevel *toplevel) { ++ Test::xdgActivation()->activate(result, *surfaces.back()); ++ })); ++ windows.push_back(Test::renderAndWaitForShown(surfaces.back().get(), QSize(100, 50), Qt::blue)); ++ QCOMPARE(workspace()->activeWindow(), windows.back()); ++ windows.back()->move(QPoint(150 * 3, 0)); ++ ++ // activation should fail if the user clicks on another window in between ++ // creating the activation token and using it ++ setupWindows(); ++ token = Test::xdgActivation()->createToken(); ++ token->set_surface(*surfaces[2]); ++ token->set_serial(windows[2]->lastUsageSerial(), *Test::waylandSeat()); ++ result = token->commitAndWait(); ++ ++ Test::pointerMotion(windows[1]->frameGeometry().center(), time++); ++ Test::pointerButtonPressed(1, time++); ++ Test::pointerButtonReleased(1, time++); ++ ++ surfaces.push_back(Test::createSurface()); ++ shellSurfaces.push_back(Test::createXdgToplevelSurface(surfaces.back().get(), [&](Test::XdgToplevel *toplevel) { ++ Test::xdgActivation()->activate(result, *surfaces.back()); ++ })); ++ windows.push_back(Test::renderAndWaitForShown(surfaces.back().get(), QSize(100, 50), Qt::blue)); ++ QCOMPARE(workspace()->activeWindow(), windows[1]); ++ windows.back()->move(QPoint(150 * 3, 0)); ++} + } + + WAYLANDTEST_MAIN(KWin::ActivationTest) +diff --git a/autotests/integration/kwin_wayland_test.h b/autotests/integration/kwin_wayland_test.h +index b0f63dadbc5..a6616189617 100644 +--- a/autotests/integration/kwin_wayland_test.h ++++ b/autotests/integration/kwin_wayland_test.h +@@ -34,6 +34,7 @@ + #include "qwayland-security-context-v1.h" + #include "qwayland-text-input-unstable-v3.h" + #include "qwayland-wlr-layer-shell-unstable-v1.h" ++#include "qwayland-xdg-activation-v1.h" + #include "qwayland-xdg-decoration-unstable-v1.h" + #include "qwayland-xdg-dialog-v1.h" + #include "qwayland-xdg-shell.h" +@@ -611,6 +612,7 @@ enum class AdditionalWaylandInterface { + ColorManagement = 1 << 22, + FifoV1 = 1 << 23, + PresentationTime = 1 << 24, ++ XdgActivation = 1 << 25, + }; + Q_DECLARE_FLAGS(AdditionalWaylandInterfaces, AdditionalWaylandInterface) + +@@ -717,6 +719,33 @@ private: + void wp_presentation_feedback_discarded() override; + }; + ++class XdgActivationToken : public QObject, public QtWayland::xdg_activation_token_v1 ++{ ++ Q_OBJECT ++public: ++ explicit XdgActivationToken(::xdg_activation_token_v1 *object); ++ ~XdgActivationToken() override; ++ ++ QString commitAndWait(); ++ ++Q_SIGNALS: ++ void tokenReceived(); ++ ++private: ++ void xdg_activation_token_v1_done(const QString &token) override; ++ ++ QString m_token; ++}; ++ ++class XdgActivation : public QtWayland::xdg_activation_v1 ++{ ++public: ++ explicit XdgActivation(::wl_registry *registry, uint32_t id, int version); ++ ~XdgActivation() override; ++ ++ std::unique_ptr createToken(); ++}; ++ + struct Connection + { + static std::unique_ptr setup(AdditionalWaylandInterfaces interfaces = AdditionalWaylandInterfaces()); +@@ -757,6 +786,7 @@ struct Connection + std::unique_ptr colorManager; + std::unique_ptr fifoManager; + std::unique_ptr presentationTime; ++ std::unique_ptr xdgActivation; + }; + + void keyboardKeyPressed(quint32 key, quint32 time); +@@ -821,6 +851,7 @@ SecurityContextManagerV1 *waylandSecurityContextManagerV1(); + ColorManagerV1 *colorManager(); + FifoManagerV1 *fifoManager(); + PresentationTime *presentationTime(); ++XdgActivation *xdgActivation(); + + bool waitForWaylandSurface(Window *window); + +diff --git a/autotests/integration/test_helpers.cpp b/autotests/integration/test_helpers.cpp +index 22380c947d3..e524f10826d 100644 +--- a/autotests/integration/test_helpers.cpp ++++ b/autotests/integration/test_helpers.cpp +@@ -535,6 +535,11 @@ std::unique_ptr Connection::setup(AdditionalWaylandInterfaces flags) + c->presentationTime = std::make_unique(*c->registry, name, version); + } + } ++ if (flags & AdditionalWaylandInterface::XdgActivation) { ++ if (interface == xdg_activation_v1_interface.name) { ++ c->xdgActivation = std::make_unique(*c->registry, name, version); ++ } ++ } + }); + + QSignalSpy allAnnounced(registry, &KWayland::Client::Registry::interfacesAnnounced); +@@ -665,6 +670,7 @@ Connection::~Connection() + colorManager.reset(); + fifoManager.reset(); + presentationTime.reset(); ++ xdgActivation.reset(); + + delete queue; // Must be destroyed last + queue = nullptr; +@@ -796,6 +802,11 @@ PresentationTime *presentationTime() + return s_waylandConnection->presentationTime.get(); + } + ++XdgActivation *xdgActivation() ++{ ++ return s_waylandConnection->xdgActivation.get(); ++} ++ + bool waitForWaylandSurface(Window *window) + { + if (window->surface()) { +@@ -1817,6 +1828,45 @@ void WpPresentationFeedback::wp_presentation_feedback_discarded() + Q_EMIT discarded(); + } + ++XdgActivationToken::XdgActivationToken(::xdg_activation_token_v1 *object) ++ : QtWayland::xdg_activation_token_v1(object) ++{ ++} ++ ++XdgActivationToken::~XdgActivationToken() ++{ ++ destroy(); ++} ++ ++QString XdgActivationToken::commitAndWait() ++{ ++ QSignalSpy received(this, &XdgActivationToken::tokenReceived); ++ commit(); ++ received.wait(); ++ return m_token; ++} ++ ++void XdgActivationToken::xdg_activation_token_v1_done(const QString &token) ++{ ++ m_token = token; ++ Q_EMIT tokenReceived(); ++} ++ ++XdgActivation::XdgActivation(::wl_registry *registry, uint32_t id, int version) ++ : QtWayland::xdg_activation_v1(registry, id, version) ++{ ++} ++ ++XdgActivation::~XdgActivation() ++{ ++ destroy(); ++} ++ ++std::unique_ptr XdgActivation::createToken() ++{ ++ return std::make_unique(get_activation_token()); ++} ++ + void keyboardKeyPressed(quint32 key, quint32 time) + { + auto virtualKeyboard = static_cast(kwinApp())->virtualKeyboard(); +-- +GitLab + diff --git a/roles/kde/patches/kwin/pr8005.patch b/roles/kde/patches/kwin/pr8005.patch new file mode 100644 index 0000000..b95d140 --- /dev/null +++ b/roles/kde/patches/kwin/pr8005.patch @@ -0,0 +1,71 @@ +From dc692e89f101a47b9049b1f6ae4cc3cebef46edb Mon Sep 17 00:00:00 2001 +From: Xaver Hugl +Date: Tue, 12 Aug 2025 15:59:16 +0200 +Subject: [PATCH] xdgactivation: clear activation feedback if no token is + provided too + +If the window is activated, the user expectation is that feedback stops. The underlying +reason for why it's activated doesn't matter. +--- + src/xdgactivationv1.cpp | 12 ++++++++++-- + src/xdgactivationv1.h | 3 ++- + 2 files changed, 12 insertions(+), 3 deletions(-) + +diff --git a/src/xdgactivationv1.cpp b/src/xdgactivationv1.cpp +index 360ca9b8743..567b792025f 100644 +--- a/src/xdgactivationv1.cpp ++++ b/src/xdgactivationv1.cpp +@@ -33,6 +33,13 @@ static bool isPrivilegedInWindowManagement(const ClientConnection *client) + XdgActivationV1Integration::XdgActivationV1Integration(XdgActivationV1Interface *activation, QObject *parent) + : QObject(parent) + { ++ connect(Workspace::self(), &Workspace::windowActivated, this, [this](Window *window) { ++ if (!m_activation || !window || m_lastTokenAppId != window->desktopFileName()) { ++ return; ++ } ++ clearFeedback(); ++ }); ++ + activation->setActivationTokenCreator([this](ClientConnection *client, SurfaceInterface *surface, uint serial, SeatInterface *seat, const QString &appId) -> QString { + Q_ASSERT(client); // Should always be available as it's coming straight from the wayland implementation + return requestToken(isPrivilegedInWindowManagement(client), surface, serial, seat, appId); +@@ -64,6 +71,7 @@ QString XdgActivationV1Integration::requestToken(bool isPrivileged, SurfaceInter + } + if (showNotify) { + m_lastToken = newToken; ++ m_lastTokenAppId = appId; + m_activation = waylandServer()->plasmaActivationFeedback()->createActivation(appId); + } + if (isPrivileged && workspace()->activeWindow()) { +@@ -95,10 +103,10 @@ void XdgActivationV1Integration::activateSurface(SurfaceInterface *surface, cons + } else { + window->setActivationToken(token); + } +- clear(); ++ clearFeedback(); + } + +-void XdgActivationV1Integration::clear() ++void XdgActivationV1Integration::clearFeedback() + { + if (m_activation) { + Q_EMIT effects->startupRemoved(m_lastToken); +diff --git a/src/xdgactivationv1.h b/src/xdgactivationv1.h +index 77d21856095..ad007c088b6 100644 +--- a/src/xdgactivationv1.h ++++ b/src/xdgactivationv1.h +@@ -36,9 +36,10 @@ public: + + private: + QString requestToken(bool isPrivileged, SurfaceInterface *surface, uint serial, SeatInterface *seat, const QString &appId); +- void clear(); ++ void clearFeedback(); + + QString m_lastToken; ++ QString m_lastTokenAppId; + std::unique_ptr m_activation; + }; + +-- +GitLab + diff --git a/roles/kde/patches/plasma-workspace/patches.txt b/roles/kde/patches/plasma-workspace/patches.txt index 56ecbad..26a8d42 100644 --- a/roles/kde/patches/plasma-workspace/patches.txt +++ b/roles/kde/patches/plasma-workspace/patches.txt @@ -5,3 +5,12 @@ Pr 5626 https://invent.kde.org/plasma/plasma-workspace/-/merge_requests/5626 Pr 5627 https://invent.kde.org/plasma/plasma-workspace/-/merge_requests/5627 Commit 8202ba92 https://invent.kde.org/plasma/plasma-workspace/-/commit/8202ba92b610c691b8bc6bab8ad5a1c3b9ac73da Part of https://invent.kde.org/plasma/plasma-workspace/-/merge_requests/5628, the other part got cherry picked on 6.4.2 +Pr 5657 https://invent.kde.org/plasma/plasma-workspace/-/merge_requests/5657 +Pr 5734 https://invent.kde.org/plasma/plasma-workspace/-/merge_requests/5734 + Depends on Pr 5609 https://invent.kde.org/plasma/plasma-workspace/-/merge_requests/5609 +Pr 5746 https://invent.kde.org/plasma/plasma-workspace/-/merge_requests/5746 +Pr 5782 https://invent.kde.org/plasma/plasma-workspace/-/merge_requests/5782 +Pr 5678 https://invent.kde.org/plasma/plasma-workspace/-/merge_requests/5678 + Depends on Pr 5673 https://invent.kde.org/plasma/plasma-workspace/-/merge_requests/5673 +Pr 5788 https://invent.kde.org/plasma/plasma-workspace/-/merge_requests/5788 + Allows compiling on gcc 14 after applying pr 5678 diff --git a/roles/kde/patches/plasma-workspace/pr5609.patch b/roles/kde/patches/plasma-workspace/pr5609.patch new file mode 100644 index 0000000..74c9d39 --- /dev/null +++ b/roles/kde/patches/plasma-workspace/pr5609.patch @@ -0,0 +1,456 @@ +From 439b251bcb3ea24f52c052e7244fb7ced04503aa Mon Sep 17 00:00:00 2001 +From: Bohdan Onofriichuk +Date: Thu, 19 Jun 2025 14:51:30 +0000 +Subject: [PATCH] applets/devicenotifier: port to plasma_add_applet + +--- + applets/devicenotifier/CMakeLists.txt | 58 ++++++++++++++++--- + .../{plugin => }/actioninterface.cpp | 0 + .../{plugin => }/actioninterface.h | 0 + .../{plugin => }/actions/defaultaction.cpp | 0 + .../{plugin => }/actions/defaultaction.h | 0 + .../{plugin => }/actions/mountaction.cpp | 0 + .../{plugin => }/actions/mountaction.h | 0 + .../actions/mountandopenaction.cpp | 0 + .../{plugin => }/actions/mountandopenaction.h | 0 + .../actions/openwithfilemanageraction.cpp | 0 + .../actions/openwithfilemanageraction.h | 0 + .../{plugin => }/actions/unmountaction.cpp | 0 + .../{plugin => }/actions/unmountaction.h | 0 + .../{plugin => }/actionscontrol.cpp | 0 + .../{plugin => }/actionscontrol.h | 0 + .../{plugin => }/devicecontrol.cpp | 0 + .../{plugin => }/devicecontrol.h | 0 + .../{plugin => }/deviceerrormonitor_p.cpp | 0 + .../{plugin => }/deviceerrormonitor_p.h | 0 + .../{plugin => }/devicefiltercontrol.cpp | 0 + .../{plugin => }/devicefiltercontrol.h | 0 + .../{plugin => }/devicenotifications.notifyrc | 0 + .../{plugin => }/deviceserviceaction.cpp | 0 + .../{plugin => }/deviceserviceaction.h | 0 + .../{plugin => }/devicestatemonitor_p.cpp | 0 + .../{plugin => }/devicestatemonitor_p.h | 0 + .../{package/contents/config => }/main.xml | 0 + .../{package => }/metadata.json | 1 - + applets/devicenotifier/plugin/CMakeLists.txt | 51 ---------------- + .../{plugin => }/predicatesmonitor_p.cpp | 0 + .../{plugin => }/predicatesmonitor_p.h | 0 + .../contents/ui => qml}/DeviceItem.qml | 22 ++++--- + .../ui => qml}/FullRepresentation.qml | 0 + .../{package/contents/ui => qml}/main.qml | 9 ++- + .../{plugin => }/spacemonitor_p.cpp | 0 + .../{plugin => }/spacemonitor_p.h | 0 + 36 files changed, 65 insertions(+), 76 deletions(-) + rename applets/devicenotifier/{plugin => }/actioninterface.cpp (100%) + rename applets/devicenotifier/{plugin => }/actioninterface.h (100%) + rename applets/devicenotifier/{plugin => }/actions/defaultaction.cpp (100%) + rename applets/devicenotifier/{plugin => }/actions/defaultaction.h (100%) + rename applets/devicenotifier/{plugin => }/actions/mountaction.cpp (100%) + rename applets/devicenotifier/{plugin => }/actions/mountaction.h (100%) + rename applets/devicenotifier/{plugin => }/actions/mountandopenaction.cpp (100%) + rename applets/devicenotifier/{plugin => }/actions/mountandopenaction.h (100%) + rename applets/devicenotifier/{plugin => }/actions/openwithfilemanageraction.cpp (100%) + rename applets/devicenotifier/{plugin => }/actions/openwithfilemanageraction.h (100%) + rename applets/devicenotifier/{plugin => }/actions/unmountaction.cpp (100%) + rename applets/devicenotifier/{plugin => }/actions/unmountaction.h (100%) + rename applets/devicenotifier/{plugin => }/actionscontrol.cpp (100%) + rename applets/devicenotifier/{plugin => }/actionscontrol.h (100%) + rename applets/devicenotifier/{plugin => }/devicecontrol.cpp (100%) + rename applets/devicenotifier/{plugin => }/devicecontrol.h (100%) + rename applets/devicenotifier/{plugin => }/deviceerrormonitor_p.cpp (100%) + rename applets/devicenotifier/{plugin => }/deviceerrormonitor_p.h (100%) + rename applets/devicenotifier/{plugin => }/devicefiltercontrol.cpp (100%) + rename applets/devicenotifier/{plugin => }/devicefiltercontrol.h (100%) + rename applets/devicenotifier/{plugin => }/devicenotifications.notifyrc (100%) + rename applets/devicenotifier/{plugin => }/deviceserviceaction.cpp (100%) + rename applets/devicenotifier/{plugin => }/deviceserviceaction.h (100%) + rename applets/devicenotifier/{plugin => }/devicestatemonitor_p.cpp (100%) + rename applets/devicenotifier/{plugin => }/devicestatemonitor_p.h (100%) + rename applets/devicenotifier/{package/contents/config => }/main.xml (100%) + rename applets/devicenotifier/{package => }/metadata.json (99%) + delete mode 100644 applets/devicenotifier/plugin/CMakeLists.txt + rename applets/devicenotifier/{plugin => }/predicatesmonitor_p.cpp (100%) + rename applets/devicenotifier/{plugin => }/predicatesmonitor_p.h (100%) + rename applets/devicenotifier/{package/contents/ui => qml}/DeviceItem.qml (81%) + rename applets/devicenotifier/{package/contents/ui => qml}/FullRepresentation.qml (100%) + rename applets/devicenotifier/{package/contents/ui => qml}/main.qml (96%) + rename applets/devicenotifier/{plugin => }/spacemonitor_p.cpp (100%) + rename applets/devicenotifier/{plugin => }/spacemonitor_p.h (100%) + +diff --git a/applets/devicenotifier/CMakeLists.txt b/applets/devicenotifier/CMakeLists.txt +index bde4a38dd30..f336db13a69 100644 +--- a/applets/devicenotifier/CMakeLists.txt ++++ b/applets/devicenotifier/CMakeLists.txt +@@ -1,11 +1,55 @@ +-add_subdirectory(plugin) ++# SPDX-FileCopyrightText: 2024 Fushan Wen ++# SPDX-License-Identifier: BSD-3-Clause + +-ecm_qt_install_logging_categories( +- EXPORT APPLETS::DEVICENOTIFIER +- FILE applets/devicenotifier.categories +- DESTINATION ${KDE_INSTALL_LOGGINGCATEGORIESDIR} ++add_definitions(-DTRANSLATION_DOMAIN=\"plasma_applet_org.kde.plasma.devicenotifier\") ++ ++plasma_add_applet(org.kde.plasma.devicenotifier ++ QML_SOURCES ++ qml/DeviceItem.qml ++ qml/FullRepresentation.qml ++ qml/main.qml ++ CPP_SOURCES ++ actionscontrol.cpp ++ devicecontrol.cpp ++ spacemonitor_p.cpp ++ devicestatemonitor_p.cpp ++ deviceserviceaction.cpp ++ predicatesmonitor_p.cpp ++ deviceerrormonitor_p.cpp ++ actioninterface.cpp ++ devicefiltercontrol.cpp ++ actions/defaultaction.cpp ++ actions/mountandopenaction.cpp ++ actions/mountaction.cpp ++ actions/unmountaction.cpp ++ actions/openwithfilemanageraction.cpp ++ RESOURCES ++ main.xml ++ GENERATE_APPLET_CLASS ++) ++ ++target_link_libraries(org.kde.plasma.devicenotifier ++ PRIVATE ++ Qt::Qml ++ Plasma::Plasma ++ KF6::Solid ++ KF6::I18n ++ KF6::CoreAddons ++ KF6::Service ++ KF6::KIOCore ++ KF6::KIOGui # KIO::CommandLauncherJob ++ KF6::JobWidgets # KNotificationJobUiDelegate ++ KSysGuard::ProcessCore ++ KF6::Notifications + ) + +-plasma_install_package(package org.kde.plasma.devicenotifier) ++ecm_qt_declare_logging_category(org.kde.plasma.devicenotifier ++ HEADER "devicenotifier_debug.h" ++ IDENTIFIER "APPLETS::DEVICENOTIFIER" ++ CATEGORY_NAME org.kde.applets.devicenotifier ++ DEFAULT_SEVERITY Warning ++ DESCRIPTION "Device Notifier applet" EXPORT "APPLETS::DEVICENOTIFIER" ++) + +-install(FILES openWithFileManager.desktop DESTINATION ${KDE_INSTALL_DATADIR}/solid/actions ) ++install(FILES devicenotifications.notifyrc DESTINATION ${KDE_INSTALL_KNOTIFYRCDIR}) ++install(FILES openWithFileManager.desktop DESTINATION ${KDE_INSTALL_DATADIR}/solid/actions) +diff --git a/applets/devicenotifier/plugin/actioninterface.cpp b/applets/devicenotifier/actioninterface.cpp +similarity index 100% +rename from applets/devicenotifier/plugin/actioninterface.cpp +rename to applets/devicenotifier/actioninterface.cpp +diff --git a/applets/devicenotifier/plugin/actioninterface.h b/applets/devicenotifier/actioninterface.h +similarity index 100% +rename from applets/devicenotifier/plugin/actioninterface.h +rename to applets/devicenotifier/actioninterface.h +diff --git a/applets/devicenotifier/plugin/actions/defaultaction.cpp b/applets/devicenotifier/actions/defaultaction.cpp +similarity index 100% +rename from applets/devicenotifier/plugin/actions/defaultaction.cpp +rename to applets/devicenotifier/actions/defaultaction.cpp +diff --git a/applets/devicenotifier/plugin/actions/defaultaction.h b/applets/devicenotifier/actions/defaultaction.h +similarity index 100% +rename from applets/devicenotifier/plugin/actions/defaultaction.h +rename to applets/devicenotifier/actions/defaultaction.h +diff --git a/applets/devicenotifier/plugin/actions/mountaction.cpp b/applets/devicenotifier/actions/mountaction.cpp +similarity index 100% +rename from applets/devicenotifier/plugin/actions/mountaction.cpp +rename to applets/devicenotifier/actions/mountaction.cpp +diff --git a/applets/devicenotifier/plugin/actions/mountaction.h b/applets/devicenotifier/actions/mountaction.h +similarity index 100% +rename from applets/devicenotifier/plugin/actions/mountaction.h +rename to applets/devicenotifier/actions/mountaction.h +diff --git a/applets/devicenotifier/plugin/actions/mountandopenaction.cpp b/applets/devicenotifier/actions/mountandopenaction.cpp +similarity index 100% +rename from applets/devicenotifier/plugin/actions/mountandopenaction.cpp +rename to applets/devicenotifier/actions/mountandopenaction.cpp +diff --git a/applets/devicenotifier/plugin/actions/mountandopenaction.h b/applets/devicenotifier/actions/mountandopenaction.h +similarity index 100% +rename from applets/devicenotifier/plugin/actions/mountandopenaction.h +rename to applets/devicenotifier/actions/mountandopenaction.h +diff --git a/applets/devicenotifier/plugin/actions/openwithfilemanageraction.cpp b/applets/devicenotifier/actions/openwithfilemanageraction.cpp +similarity index 100% +rename from applets/devicenotifier/plugin/actions/openwithfilemanageraction.cpp +rename to applets/devicenotifier/actions/openwithfilemanageraction.cpp +diff --git a/applets/devicenotifier/plugin/actions/openwithfilemanageraction.h b/applets/devicenotifier/actions/openwithfilemanageraction.h +similarity index 100% +rename from applets/devicenotifier/plugin/actions/openwithfilemanageraction.h +rename to applets/devicenotifier/actions/openwithfilemanageraction.h +diff --git a/applets/devicenotifier/plugin/actions/unmountaction.cpp b/applets/devicenotifier/actions/unmountaction.cpp +similarity index 100% +rename from applets/devicenotifier/plugin/actions/unmountaction.cpp +rename to applets/devicenotifier/actions/unmountaction.cpp +diff --git a/applets/devicenotifier/plugin/actions/unmountaction.h b/applets/devicenotifier/actions/unmountaction.h +similarity index 100% +rename from applets/devicenotifier/plugin/actions/unmountaction.h +rename to applets/devicenotifier/actions/unmountaction.h +diff --git a/applets/devicenotifier/plugin/actionscontrol.cpp b/applets/devicenotifier/actionscontrol.cpp +similarity index 100% +rename from applets/devicenotifier/plugin/actionscontrol.cpp +rename to applets/devicenotifier/actionscontrol.cpp +diff --git a/applets/devicenotifier/plugin/actionscontrol.h b/applets/devicenotifier/actionscontrol.h +similarity index 100% +rename from applets/devicenotifier/plugin/actionscontrol.h +rename to applets/devicenotifier/actionscontrol.h +diff --git a/applets/devicenotifier/plugin/devicecontrol.cpp b/applets/devicenotifier/devicecontrol.cpp +similarity index 100% +rename from applets/devicenotifier/plugin/devicecontrol.cpp +rename to applets/devicenotifier/devicecontrol.cpp +diff --git a/applets/devicenotifier/plugin/devicecontrol.h b/applets/devicenotifier/devicecontrol.h +similarity index 100% +rename from applets/devicenotifier/plugin/devicecontrol.h +rename to applets/devicenotifier/devicecontrol.h +diff --git a/applets/devicenotifier/plugin/deviceerrormonitor_p.cpp b/applets/devicenotifier/deviceerrormonitor_p.cpp +similarity index 100% +rename from applets/devicenotifier/plugin/deviceerrormonitor_p.cpp +rename to applets/devicenotifier/deviceerrormonitor_p.cpp +diff --git a/applets/devicenotifier/plugin/deviceerrormonitor_p.h b/applets/devicenotifier/deviceerrormonitor_p.h +similarity index 100% +rename from applets/devicenotifier/plugin/deviceerrormonitor_p.h +rename to applets/devicenotifier/deviceerrormonitor_p.h +diff --git a/applets/devicenotifier/plugin/devicefiltercontrol.cpp b/applets/devicenotifier/devicefiltercontrol.cpp +similarity index 100% +rename from applets/devicenotifier/plugin/devicefiltercontrol.cpp +rename to applets/devicenotifier/devicefiltercontrol.cpp +diff --git a/applets/devicenotifier/plugin/devicefiltercontrol.h b/applets/devicenotifier/devicefiltercontrol.h +similarity index 100% +rename from applets/devicenotifier/plugin/devicefiltercontrol.h +rename to applets/devicenotifier/devicefiltercontrol.h +diff --git a/applets/devicenotifier/plugin/devicenotifications.notifyrc b/applets/devicenotifier/devicenotifications.notifyrc +similarity index 100% +rename from applets/devicenotifier/plugin/devicenotifications.notifyrc +rename to applets/devicenotifier/devicenotifications.notifyrc +diff --git a/applets/devicenotifier/plugin/deviceserviceaction.cpp b/applets/devicenotifier/deviceserviceaction.cpp +similarity index 100% +rename from applets/devicenotifier/plugin/deviceserviceaction.cpp +rename to applets/devicenotifier/deviceserviceaction.cpp +diff --git a/applets/devicenotifier/plugin/deviceserviceaction.h b/applets/devicenotifier/deviceserviceaction.h +similarity index 100% +rename from applets/devicenotifier/plugin/deviceserviceaction.h +rename to applets/devicenotifier/deviceserviceaction.h +diff --git a/applets/devicenotifier/plugin/devicestatemonitor_p.cpp b/applets/devicenotifier/devicestatemonitor_p.cpp +similarity index 100% +rename from applets/devicenotifier/plugin/devicestatemonitor_p.cpp +rename to applets/devicenotifier/devicestatemonitor_p.cpp +diff --git a/applets/devicenotifier/plugin/devicestatemonitor_p.h b/applets/devicenotifier/devicestatemonitor_p.h +similarity index 100% +rename from applets/devicenotifier/plugin/devicestatemonitor_p.h +rename to applets/devicenotifier/devicestatemonitor_p.h +diff --git a/applets/devicenotifier/package/contents/config/main.xml b/applets/devicenotifier/main.xml +similarity index 100% +rename from applets/devicenotifier/package/contents/config/main.xml +rename to applets/devicenotifier/main.xml +diff --git a/applets/devicenotifier/package/metadata.json b/applets/devicenotifier/metadata.json +similarity index 99% +rename from applets/devicenotifier/package/metadata.json +rename to applets/devicenotifier/metadata.json +index 0a330dfc189..77d7feac1cb 100644 +--- a/applets/devicenotifier/package/metadata.json ++++ b/applets/devicenotifier/metadata.json +@@ -115,7 +115,6 @@ + "desktop" + ], + "Icon": "device-notifier", +- "Id": "org.kde.plasma.devicenotifier", + "License": "GPL-2.0+", + "Name": "Disks & Devices", + "Name[ar]": "الأجهزة والأقراص", +diff --git a/applets/devicenotifier/plugin/CMakeLists.txt b/applets/devicenotifier/plugin/CMakeLists.txt +deleted file mode 100644 +index 34a3456d690..00000000000 +--- a/applets/devicenotifier/plugin/CMakeLists.txt ++++ /dev/null +@@ -1,51 +0,0 @@ +-# SPDX-FileCopyrightText: 2024 Fushan Wen +-# SPDX-License-Identifier: BSD-3-Clause +- +-add_definitions(-DTRANSLATION_DOMAIN=\"plasma_applet_org.kde.plasma.devicenotifier\") +- +-ecm_add_qml_module(devicenotifierplugin URI org.kde.plasma.private.devicenotifier GENERATE_PLUGIN_SOURCE) +- +-target_sources(devicenotifierplugin +- PRIVATE +- actionscontrol.cpp actionscontrol.h +- devicecontrol.cpp devicecontrol.h +- spacemonitor_p.cpp spacemonitor_p.h +- devicestatemonitor_p.cpp devicestatemonitor_p.h +- deviceserviceaction.cpp deviceserviceaction.h +- predicatesmonitor_p.cpp predicatesmonitor_p.h +- deviceerrormonitor_p.cpp deviceerrormonitor_p.h +- actioninterface.cpp actioninterface.h +- devicefiltercontrol.cpp devicefiltercontrol.h +- actions/defaultaction.cpp actions/defaultaction.h +- actions/mountandopenaction.cpp actions/mountandopenaction.h +- actions/mountaction.cpp actions/mountaction.h +- actions/unmountaction.cpp actions/unmountaction.h +- actions/openwithfilemanageraction.cpp actions/openwithfilemanageraction.h +-) +- +-target_link_libraries(devicenotifierplugin +- PRIVATE +- Qt::Qml +- Plasma::Plasma +- KF6::Solid +- KF6::I18n +- KF6::CoreAddons +- KF6::Service +- KF6::KIOCore +- KF6::KIOGui # KIO::CommandLauncherJob +- KF6::JobWidgets # KNotificationJobUiDelegate +- KSysGuard::ProcessCore +- KF6::Notifications +-) +- +-ecm_qt_declare_logging_category(devicenotifierplugin +- HEADER "devicenotifier_debug.h" +- IDENTIFIER "APPLETS::DEVICENOTIFIER" +- CATEGORY_NAME org.kde.applets.devicenotifier +- DEFAULT_SEVERITY Warning +- DESCRIPTION "Device Notifier applet" EXPORT "APPLETS::DEVICENOTIFIER" +-) +- +-ecm_finalize_qml_module(devicenotifierplugin) +- +-install(FILES devicenotifications.notifyrc DESTINATION ${KDE_INSTALL_KNOTIFYRCDIR}) +diff --git a/applets/devicenotifier/plugin/predicatesmonitor_p.cpp b/applets/devicenotifier/predicatesmonitor_p.cpp +similarity index 100% +rename from applets/devicenotifier/plugin/predicatesmonitor_p.cpp +rename to applets/devicenotifier/predicatesmonitor_p.cpp +diff --git a/applets/devicenotifier/plugin/predicatesmonitor_p.h b/applets/devicenotifier/predicatesmonitor_p.h +similarity index 100% +rename from applets/devicenotifier/plugin/predicatesmonitor_p.h +rename to applets/devicenotifier/predicatesmonitor_p.h +diff --git a/applets/devicenotifier/package/contents/ui/DeviceItem.qml b/applets/devicenotifier/qml/DeviceItem.qml +similarity index 81% +rename from applets/devicenotifier/package/contents/ui/DeviceItem.qml +rename to applets/devicenotifier/qml/DeviceItem.qml +index c19c9535b04..861996af45c 100644 +--- a/applets/devicenotifier/package/contents/ui/DeviceItem.qml ++++ b/applets/devicenotifier/qml/DeviceItem.qml +@@ -19,8 +19,6 @@ import org.kde.kirigami as Kirigami + + import org.kde.kquickcontrolsaddons + +-import org.kde.plasma.private.devicenotifier as DN +- + PlasmaExtras.ExpandableListItem { + id: deviceItem + +@@ -41,18 +39,18 @@ PlasmaExtras.ExpandableListItem { + + property bool hasMessage: deviceItem.deviceErrorMessage !== "" + +- property bool isFree: deviceItem.deviceOperationResult !== DN.DevicesStateMonitor.Working && deviceItem.deviceOperationResult !== DN.DevicesStateMonitor.Checking && deviceItem.deviceOperationResult !== DN.DevicesStateMonitor.Repairing && deviceItem.deviceOperationResult !== DN.DevicesStateMonitor.NotPresent && !(deviceItem.deviceMounted === false && deviceItem.deviceOperationResult === DN.DevicesStateMonitor.Successful) ++ property bool isFree: deviceItem.deviceOperationResult !== DevicesStateMonitor.Working && deviceItem.deviceOperationResult !== DevicesStateMonitor.Checking && deviceItem.deviceOperationResult !== DevicesStateMonitor.Repairing && deviceItem.deviceOperationResult !== DevicesStateMonitor.NotPresent && !(deviceItem.deviceMounted === false && deviceItem.deviceOperationResult === DevicesStateMonitor.Successful) + + onDeviceOperationResultChanged: { + if (!popupIconTimer.running) { +- if (deviceItem.deviceOperationResult === DN.DevicesStateMonitor.Working) { ++ if (deviceItem.deviceOperationResult === DevicesStateMonitor.Working) { + if(deviceMounted){ + unmountTimer.restart(); + } +- } else if (deviceItem.deviceOperationResult === DN.DevicesStateMonitor.Successful) { ++ } else if (deviceItem.deviceOperationResult === DevicesStateMonitor.Successful) { + devicenotifier.popupIcon = "dialog-ok" + popupIconTimer.restart() +- } else if (deviceItem.deviceOperationResult === DN.DevicesStateMonitor.Unsuccessful) { ++ } else if (deviceItem.deviceOperationResult === DevicesStateMonitor.Unsuccessful) { + devicenotifier.popupIcon = "dialog-error" + popupIconTimer.restart() + } +@@ -80,7 +78,7 @@ PlasmaExtras.ExpandableListItem { + } else { + return "emblem-error" + } +- } else if (deviceItem.deviceOperationResult !== DN.DevicesStateMonitor.Working && deviceItem.deviceEmblems[0]) { ++ } else if (deviceItem.deviceOperationResult !== DevicesStateMonitor.Working && deviceItem.deviceEmblems[0]) { + return deviceItem.deviceEmblems[0] + } else { + return "" +@@ -93,16 +91,16 @@ PlasmaExtras.ExpandableListItem { + if (deviceItem.hasMessage) { + return deviceItem.deviceErrorMessage + } +- if (deviceItem.deviceOperationResult === DN.DevicesStateMonitor.Checking) { ++ if (deviceItem.deviceOperationResult === DevicesStateMonitor.Checking) { + return i18nc("Accessing is a less technical word for Mounting; translation should be short and mean \'Currently mounting this device\'", "Checking…") +- } else if (deviceItem.deviceOperationResult === DN.DevicesStateMonitor.Repairing) { ++ } else if (deviceItem.deviceOperationResult === DevicesStateMonitor.Repairing) { + return i18nc("Accessing is a less technical word for Mounting; translation should be short and mean \'Currently mounting this device\'", "Repairing…") +- } else if (deviceItem.deviceOperationResult !== DN.DevicesStateMonitor.Working) { ++ } else if (deviceItem.deviceOperationResult !== DevicesStateMonitor.Working) { + if (deviceItem.deviceFreeSpace > 0 && deviceItem.deviceSize > 0) { + return i18nc("@info:status Free disk space", "%1 free of %2", deviceItem.deviceFreeSpaceText, deviceItem.deviceSizeText) + } + return "" +- } else if (!deviceItem.deviceMounted && deviceItem.deviceOperationResult === DN.DevicesStateMonitor.Working) { ++ } else if (!deviceItem.deviceMounted && deviceItem.deviceOperationResult === DevicesStateMonitor.Working) { + return i18nc("Accessing is a less technical word for Mounting; translation should be short and mean \'Currently mounting this device\'", "Accessing…") + } else if (unmountTimer.running) { + // Unmounting; shown if unmount takes less than 1 second +@@ -139,7 +137,7 @@ PlasmaExtras.ExpandableListItem { + } + } + +- isBusy: deviceItem.deviceOperationResult === DN.DevicesStateMonitor.Working || deviceItem.deviceOperationResult === DN.DevicesStateMonitor.Checking || deviceItem.deviceOperationResult === DN.DevicesStateMonitor.Repairing ++ isBusy: deviceItem.deviceOperationResult === DevicesStateMonitor.Working || deviceItem.deviceOperationResult === DevicesStateMonitor.Checking || deviceItem.deviceOperationResult === DevicesStateMonitor.Repairing + + customExpandedViewContent: deviceActions !== undefined && deviceActions.rowCount() !== 0 && isFree ? actionComponent : null + +diff --git a/applets/devicenotifier/package/contents/ui/FullRepresentation.qml b/applets/devicenotifier/qml/FullRepresentation.qml +similarity index 100% +rename from applets/devicenotifier/package/contents/ui/FullRepresentation.qml +rename to applets/devicenotifier/qml/FullRepresentation.qml +diff --git a/applets/devicenotifier/package/contents/ui/main.qml b/applets/devicenotifier/qml/main.qml +similarity index 96% +rename from applets/devicenotifier/package/contents/ui/main.qml +rename to applets/devicenotifier/qml/main.qml +index 4061480becc..7fcd76a6d16 100644 +--- a/applets/devicenotifier/package/contents/ui/main.qml ++++ b/applets/devicenotifier/qml/main.qml +@@ -15,21 +15,20 @@ import org.kde.kirigami as Kirigami + + import org.kde.kcmutils // For KCMLauncher + import org.kde.config // KAuthorized +-import org.kde.plasma.private.devicenotifier as DN + + PlasmoidItem { + id: devicenotifier + +- DN.DeviceFilterControl { ++ DeviceFilterControl { + id: filterModel + + filterType: { + if (Plasmoid.configuration.allDevices) { +- return DN.DeviceFilterControl.All ++ return DeviceFilterControl.All + } else if (Plasmoid.configuration.removableDevices) { +- return DN.DeviceFilterControl.Removable ++ return DeviceFilterControl.Removable + } else { +- return DN.DeviceFilterControl.Unremovable ++ return DeviceFilterControl.Unremovable + } + } + +diff --git a/applets/devicenotifier/plugin/spacemonitor_p.cpp b/applets/devicenotifier/spacemonitor_p.cpp +similarity index 100% +rename from applets/devicenotifier/plugin/spacemonitor_p.cpp +rename to applets/devicenotifier/spacemonitor_p.cpp +diff --git a/applets/devicenotifier/plugin/spacemonitor_p.h b/applets/devicenotifier/spacemonitor_p.h +similarity index 100% +rename from applets/devicenotifier/plugin/spacemonitor_p.h +rename to applets/devicenotifier/spacemonitor_p.h +-- +GitLab + diff --git a/roles/kde/patches/plasma-workspace/pr5657.patch b/roles/kde/patches/plasma-workspace/pr5657.patch new file mode 100644 index 0000000..71620a0 --- /dev/null +++ b/roles/kde/patches/plasma-workspace/pr5657.patch @@ -0,0 +1,177 @@ +From aa1e466e5f7684a8b624c34d466dda9d10a331d2 Mon Sep 17 00:00:00 2001 +From: Nate Graham +Date: Sun, 6 Jul 2025 23:20:47 -0400 +Subject: [PATCH 1/2] Improve UX of USB plug/unplug notifications when popup is + shown + +1. When plugged or unplugged, revoke the opposite notification if it's + visible. +2. Set the urgency to low so it won't clutter up the notification + history. +--- + devicenotifications/devicenotifications.cpp | 55 ++++++++++++++++----- + devicenotifications/devicenotifications.h | 5 ++ + 2 files changed, 48 insertions(+), 12 deletions(-) + +diff --git a/devicenotifications/devicenotifications.cpp b/devicenotifications/devicenotifications.cpp +index cc462f58f5..f326691c11 100644 +--- a/devicenotifications/devicenotifications.cpp ++++ b/devicenotifications/devicenotifications.cpp +@@ -15,6 +15,7 @@ + + #include + ++#include + #include + + K_PLUGIN_CLASS_WITH_JSON(KdedDeviceNotifications, "devicenotifications.json") +@@ -375,13 +376,28 @@ void KdedDeviceNotifications::onDeviceAdded(const UdevDevice &device) + return; + } + +- const QString text = !displayName.isEmpty() ? i18n("%1 has been plugged in.", displayName.toHtmlEscaped()) : i18n("A USB device has been plugged in."); ++ // If the user unplugged something and then immediately plugged it in again, ++ // there's no need to keep the unplug notification around. ++ if (m_usbDeviceRemovedNotification) { ++ m_usbDeviceRemovedNotification->close(); ++ } ++ ++ // Only show one of these at a time. We already suppressed creating a bunch ++ // in quick succession for the dock/hub use case, so any that are created ++ // over that time limit anyway are not necessary to stack up. ++ if (m_usbDeviceAddedNotification) { ++ m_usbDeviceAddedNotification->close(); ++ } ++ ++ const QString text = !displayName.isEmpty() ? i18n("%1 has been connected.", displayName.toHtmlEscaped()) : i18n("A USB device has been connected."); ++ ++ m_usbDeviceAddedNotification = new KNotification(QStringLiteral("deviceAdded")); ++ m_usbDeviceAddedNotification->setFlags(KNotification::DefaultEvent); ++ m_usbDeviceAddedNotification->setIconName(QStringLiteral("drive-removable-media-usb")); ++ m_usbDeviceAddedNotification->setTitle(i18nc("@title:notifications", "USB Device Detected")); ++ m_usbDeviceAddedNotification->setText(text); ++ m_usbDeviceAddedNotification->sendEvent(); + +- KNotification::event(QStringLiteral("deviceAdded"), +- i18nc("@title:notifications", "USB Device Detected"), +- text, +- QStringLiteral("drive-removable-media-usb"), +- KNotification::DefaultEvent); + m_deviceAddedTimer.start(); + } + +@@ -401,13 +417,28 @@ void KdedDeviceNotifications::onDeviceRemoved(const UdevDevice &device) + return; + } + +- const QString text = !displayName.isEmpty() ? i18n("%1 has been unplugged.", displayName.toHtmlEscaped()) : i18n("A USB device has been unplugged."); ++ // If the user plugged something in and then immediately unplugged it again, ++ // there's no need to keep the plug notification around. ++ if (m_usbDeviceAddedNotification) { ++ m_usbDeviceAddedNotification->close(); ++ } ++ ++ // Only show one of these at a time. We already suppressed removing a bunch ++ // in quick succession for the dock/hub use case, so any that are removed ++ // over that time limit anyway are not necessary to stack up. ++ if (m_usbDeviceRemovedNotification) { ++ m_usbDeviceRemovedNotification->close(); ++ } ++ ++ const QString text = !displayName.isEmpty() ? i18n("%1 has been disconnected.", displayName.toHtmlEscaped()) : i18n("A USB device has been disconnected."); ++ ++ m_usbDeviceRemovedNotification = new KNotification(QStringLiteral("deviceRemoved")); ++ m_usbDeviceRemovedNotification->setFlags(KNotification::DefaultEvent); ++ m_usbDeviceRemovedNotification->setIconName(QStringLiteral("drive-removable-media-usb")); ++ m_usbDeviceRemovedNotification->setTitle(i18nc("@title:notifications", "USB Device Went Away")); ++ m_usbDeviceRemovedNotification->setText(text); ++ m_usbDeviceRemovedNotification->sendEvent(); + +- KNotification::event(QStringLiteral("deviceRemoved"), +- i18nc("@title:notifications", "USB Device Removed"), +- text, +- QStringLiteral("drive-removable-media-usb"), +- KNotification::DefaultEvent); + m_deviceRemovedTimer.start(); + } + +diff --git a/devicenotifications/devicenotifications.h b/devicenotifications/devicenotifications.h +index 11334008b0..ab7e6b3ff9 100644 +--- a/devicenotifications/devicenotifications.h ++++ b/devicenotifications/devicenotifications.h +@@ -8,11 +8,13 @@ + + #include + #include ++#include + #include + #include + #include + + #include ++#include + + #include + +@@ -98,4 +100,7 @@ private: + + QTimer m_deviceAddedTimer; + QTimer m_deviceRemovedTimer; ++ ++ QPointer m_usbDeviceAddedNotification; ++ QPointer m_usbDeviceRemovedNotification; + }; +-- +2.51.0 + + +From 05f72383fd0b29105f3b5494759500d26b38ffc2 Mon Sep 17 00:00:00 2001 +From: Nate Graham +Date: Fri, 11 Jul 2025 11:25:23 -0600 +Subject: [PATCH 2/2] Delete closed notifications too + +Closing is async; make sure we actually delete them when we want them +gone. +--- + devicenotifications/devicenotifications.cpp | 4 ++++ + 1 file changed, 4 insertions(+) + +diff --git a/devicenotifications/devicenotifications.cpp b/devicenotifications/devicenotifications.cpp +index f326691c11..987d65d805 100644 +--- a/devicenotifications/devicenotifications.cpp ++++ b/devicenotifications/devicenotifications.cpp +@@ -380,6 +380,7 @@ void KdedDeviceNotifications::onDeviceAdded(const UdevDevice &device) + // there's no need to keep the unplug notification around. + if (m_usbDeviceRemovedNotification) { + m_usbDeviceRemovedNotification->close(); ++ m_usbDeviceRemovedNotification = nullptr; + } + + // Only show one of these at a time. We already suppressed creating a bunch +@@ -387,6 +388,7 @@ void KdedDeviceNotifications::onDeviceAdded(const UdevDevice &device) + // over that time limit anyway are not necessary to stack up. + if (m_usbDeviceAddedNotification) { + m_usbDeviceAddedNotification->close(); ++ m_usbDeviceAddedNotification = nullptr; + } + + const QString text = !displayName.isEmpty() ? i18n("%1 has been connected.", displayName.toHtmlEscaped()) : i18n("A USB device has been connected."); +@@ -421,6 +423,7 @@ void KdedDeviceNotifications::onDeviceRemoved(const UdevDevice &device) + // there's no need to keep the plug notification around. + if (m_usbDeviceAddedNotification) { + m_usbDeviceAddedNotification->close(); ++ m_usbDeviceAddedNotification = nullptr; + } + + // Only show one of these at a time. We already suppressed removing a bunch +@@ -428,6 +431,7 @@ void KdedDeviceNotifications::onDeviceRemoved(const UdevDevice &device) + // over that time limit anyway are not necessary to stack up. + if (m_usbDeviceRemovedNotification) { + m_usbDeviceRemovedNotification->close(); ++ m_usbDeviceRemovedNotification = nullptr; + } + + const QString text = !displayName.isEmpty() ? i18n("%1 has been disconnected.", displayName.toHtmlEscaped()) : i18n("A USB device has been disconnected."); +-- +2.51.0 + diff --git a/roles/kde/patches/plasma-workspace/pr5673.patch b/roles/kde/patches/plasma-workspace/pr5673.patch new file mode 100644 index 0000000..c78dca1 --- /dev/null +++ b/roles/kde/patches/plasma-workspace/pr5673.patch @@ -0,0 +1,392 @@ +From 97c77a8e3259d77cb615dadd1c92185545513ebb Mon Sep 17 00:00:00 2001 +From: Harald Sitter +Date: Sun, 13 Jul 2025 16:06:08 +0200 +Subject: [PATCH 1/7] servicerunner: en_US spelling please + +--- + runners/services/servicerunner.cpp | 14 +++++++------- + runners/services/servicerunner.h | 4 ++-- + 2 files changed, 9 insertions(+), 9 deletions(-) + +diff --git a/runners/services/servicerunner.cpp b/runners/services/servicerunner.cpp +index 454cf4e99f..357558a77d 100644 +--- a/runners/services/servicerunner.cpp ++++ b/runners/services/servicerunner.cpp +@@ -296,7 +296,7 @@ private: + relevance += .09; + } + +- if (const auto foundIt = m_runner->m_favourites.constFind(service->desktopEntryName()); foundIt != m_runner->m_favourites.cend()) { ++ if (const auto foundIt = m_runner->m_favorites.constFind(service->desktopEntryName()); foundIt != m_runner->m_favorites.cend()) { + if (foundIt->isGlobal || foundIt->linkedActivities.contains(m_currentActivity)) { + qCDebug(RUNNER_SERVICES) << "entry is a favorite" << id << match.subtext() << relevance; + relevance *= 1.25; // Give favorites a relative boost, +@@ -423,7 +423,7 @@ ServiceRunner::ServiceRunner(QObject *parent, const KPluginMetaData &metaData) + }); + + connect(&m_kactivitiesWatcher, &ResultWatcher::resultUnlinked, [this](QString resource) { +- m_favourites.remove(resource.remove(".desktop"_L1)); ++ m_favorites.remove(resource.remove(".desktop"_L1)); + // In case it was only unlinked from one activity + processActivitiesResults(ResultSet(m_kactivitiesQuery | Terms::Url::contains(resource))); + }); +@@ -466,11 +466,11 @@ void ServiceRunner::processActivitiesResults(const ResultSet &results) + const static QLatin1String applicationScheme("applications"); + for (const ResultSet::Result &result : results) { + if (result.url().scheme() == applicationScheme) { +- m_favourites.insert(result.url().path().remove(QLatin1String(".desktop")), +- ActivityFavourite{ +- result.linkedActivities(), +- result.linkedActivities().contains(globalActivity), +- }); ++ m_favorites.insert(result.url().path().remove(QLatin1String(".desktop")), ++ ActivityFavorite{ ++ result.linkedActivities(), ++ result.linkedActivities().contains(globalActivity), ++ }); + } + } + } +diff --git a/runners/services/servicerunner.h b/runners/services/servicerunner.h +index e0507ea459..571d22d90c 100644 +--- a/runners/services/servicerunner.h ++++ b/runners/services/servicerunner.h +@@ -33,11 +33,11 @@ public: + void run(const KRunner::RunnerContext &context, const KRunner::QueryMatch &match) override; + void init() override; + +- struct ActivityFavourite { ++ struct ActivityFavorite { + QStringList linkedActivities; + bool isGlobal; + }; +- QMap m_favourites; ++ QMap m_favorites; + + protected: + void setupMatch(const KService::Ptr &service, KRunner::QueryMatch &action); +-- +2.51.0 + + +From 537d0cf67d600cb40636f9aaef7db6957f002eb2 Mon Sep 17 00:00:00 2001 +From: Harald Sitter +Date: Sun, 13 Jul 2025 16:06:50 +0200 +Subject: [PATCH 2/7] servicerunner: use designated initializers + +makes code easier to read +--- + runners/services/servicerunner.cpp | 4 ++-- + 1 file changed, 2 insertions(+), 2 deletions(-) + +diff --git a/runners/services/servicerunner.cpp b/runners/services/servicerunner.cpp +index 357558a77d..2cade45b26 100644 +--- a/runners/services/servicerunner.cpp ++++ b/runners/services/servicerunner.cpp +@@ -468,8 +468,8 @@ void ServiceRunner::processActivitiesResults(const ResultSet &results) + if (result.url().scheme() == applicationScheme) { + m_favorites.insert(result.url().path().remove(QLatin1String(".desktop")), + ActivityFavorite{ +- result.linkedActivities(), +- result.linkedActivities().contains(globalActivity), ++ .linkedActivities = result.linkedActivities(), ++ .isGlobal = result.linkedActivities().contains(globalActivity), + }); + } + } +-- +2.51.0 + + +From 2c5eb156410c022a50a5b6e08a6abc454dd49b83 Mon Sep 17 00:00:00 2001 +From: Harald Sitter +Date: Sun, 13 Jul 2025 16:09:37 +0200 +Subject: [PATCH 3/7] servicerunner: use ranges algorithms + +makes for nicer to read code +--- + runners/services/servicerunner.cpp | 10 +++++----- + 1 file changed, 5 insertions(+), 5 deletions(-) + +diff --git a/runners/services/servicerunner.cpp b/runners/services/servicerunner.cpp +index 2cade45b26..87b38a2da3 100644 +--- a/runners/services/servicerunner.cpp ++++ b/runners/services/servicerunner.cpp +@@ -46,15 +46,15 @@ int weightedLength(const QString &query) + + inline bool contains(const QString &result, const QList &queryList) + { +- return std::all_of(queryList.cbegin(), queryList.cend(), [&result](QStringView query) { ++ return std::ranges::all_of(queryList, [&result](QStringView query) { + return result.contains(query, Qt::CaseInsensitive); + }); + } + + inline bool contains(const QStringList &results, const QList &queryList) + { +- return std::all_of(queryList.cbegin(), queryList.cend(), [&results](QStringView query) { +- return std::any_of(results.cbegin(), results.cend(), [&query](QStringView result) { ++ return std::ranges::all_of(queryList, [&results](QStringView query) { ++ return std::ranges::any_of(results, [&query](QStringView result) { + return result.contains(query, Qt::CaseInsensitive); + }); + }); +@@ -327,7 +327,7 @@ private: + setupMatch(service, match); + + qreal relevance = 0.4; +- if (std::any_of(categories.begin(), categories.end(), [this](const QString &category) { ++ if (std::ranges::any_of(categories, [this](const QString &category) { + return category.compare(query, Qt::CaseInsensitive) == 0; + })) { + relevance = 0.6; +@@ -499,7 +499,7 @@ void ServiceRunner::run(const KRunner::RunnerContext & /*context*/, const KRunne + job = new KIO::ApplicationLauncherJob(service); + } else { + const auto actions = service->actions(); +- auto it = std::find_if(actions.begin(), actions.end(), [&actionName](const KServiceAction &action) { ++ auto it = std::ranges::find_if(actions, [&actionName](const KServiceAction &action) { + return action.name() == actionName; + }); + Q_ASSERT(it != actions.end()); +-- +2.51.0 + + +From 0599abb0af9d1da43d8067dd59b8afad7c7be9c6 Mon Sep 17 00:00:00 2001 +From: Harald Sitter +Date: Sun, 13 Jul 2025 16:11:05 +0200 +Subject: [PATCH 4/7] servicerunner: put helper functions into anon namespace + +they are translation unit local after all +--- + runners/services/servicerunner.cpp | 4 ++++ + 1 file changed, 4 insertions(+) + +diff --git a/runners/services/servicerunner.cpp b/runners/services/servicerunner.cpp +index 87b38a2da3..baef7ae50f 100644 +--- a/runners/services/servicerunner.cpp ++++ b/runners/services/servicerunner.cpp +@@ -38,6 +38,8 @@ + #include "debug.h" + + using namespace Qt::StringLiterals; ++namespace ++{ + + int weightedLength(const QString &query) + { +@@ -60,6 +62,8 @@ inline bool contains(const QStringList &results, const QList &query + }); + } + ++} // namespace ++ + /** + * @brief Finds all KServices for a given runner query + */ +-- +2.51.0 + + +From 2f81c3ab0520729ed4f97d666b5c74258eed149b Mon Sep 17 00:00:00 2001 +From: Harald Sitter +Date: Sun, 13 Jul 2025 16:12:37 +0200 +Subject: [PATCH 5/7] servicerunner: typos-- + +--- + runners/services/autotests/servicerunnertest.cpp | 4 ++-- + runners/services/servicerunner.cpp | 10 +++++----- + runners/services/servicerunner.h | 2 +- + 3 files changed, 8 insertions(+), 8 deletions(-) + +diff --git a/runners/services/autotests/servicerunnertest.cpp b/runners/services/autotests/servicerunnertest.cpp +index ecb8a4816c..fcfd3275ac 100644 +--- a/runners/services/autotests/servicerunnertest.cpp ++++ b/runners/services/autotests/servicerunnertest.cpp +@@ -27,7 +27,7 @@ private Q_SLOTS: + void initTestCase(); + void cleanupTestCase(); + +- void testExcutableExactMatch(); ++ void testExecutableExactMatch(); + void testKonsoleVsYakuakeComment(); + void testSystemSettings(); + void testSystemSettings2(); +@@ -76,7 +76,7 @@ void ServiceRunnerTest::cleanupTestCase() + { + } + +-void ServiceRunnerTest::testExcutableExactMatch() ++void ServiceRunnerTest::testExecutableExactMatch() + { + const auto matches = launchQuery(QStringLiteral("Virtual Machine Manager ServiceRunnerTest")); // virt-manager.desktop + QVERIFY(std::any_of(matches.cbegin(), matches.cend(), [](const KRunner::QueryMatch &match) { +diff --git a/runners/services/servicerunner.cpp b/runners/services/servicerunner.cpp +index baef7ae50f..ced1b526ce 100644 +--- a/runners/services/servicerunner.cpp ++++ b/runners/services/servicerunner.cpp +@@ -125,7 +125,7 @@ private: + GenericName, + Comment, + }; +- qreal increaseMatchRelavance(const QString &serviceProperty, const QList &strList, Category category) ++ qreal increaseMatchRelevance(const QString &serviceProperty, const QList &strList, Category category) + { + // Increment the relevance based on all the words (other than the first) of the query list + qreal relevanceIncrement = 0; +@@ -273,20 +273,20 @@ private: + categoryRelevance = KRunner::QueryMatch::CategoryRelevance::Highest; + } else if (const int idx = name.indexOf(queryList[0], 0, Qt::CaseInsensitive); idx != -1) { + relevance = 0.8; +- relevance += increaseMatchRelavance(name, queryList, Category::Name); ++ relevance += increaseMatchRelevance(name, queryList, Category::Name); + if (idx == 0) { + relevance += 0.1; + categoryRelevance = KRunner::QueryMatch::CategoryRelevance::High; + } + } else if (const int idx = service->genericName().indexOf(queryList[0], 0, Qt::CaseInsensitive); idx != -1) { + relevance = 0.65; +- relevance += increaseMatchRelavance(service->genericName(), queryList, Category::GenericName); ++ relevance += increaseMatchRelevance(service->genericName(), queryList, Category::GenericName); + if (idx == 0) { + relevance += 0.05; + } + } else if (const int idx = service->comment().indexOf(queryList[0], 0, Qt::CaseInsensitive); idx != -1) { + relevance = 0.5; +- relevance += increaseMatchRelavance(service->comment(), queryList, Category::Comment); ++ relevance += increaseMatchRelevance(service->comment(), queryList, Category::Comment); + if (idx == 0) { + relevance += 0.05; + } +@@ -481,7 +481,7 @@ void ServiceRunner::processActivitiesResults(const ResultSet &results) + + void ServiceRunner::match(KRunner::RunnerContext &context) + { +- ServiceFinder finder(this, m_services, m_activitiesConsuer.currentActivity()); ++ ServiceFinder finder(this, m_services, m_activitiesConsumer.currentActivity()); + finder.match(context); + } + +diff --git a/runners/services/servicerunner.h b/runners/services/servicerunner.h +index 571d22d90c..96a110789b 100644 +--- a/runners/services/servicerunner.h ++++ b/runners/services/servicerunner.h +@@ -46,7 +46,7 @@ private: + void processActivitiesResults(const ResultSet &results); + const Query m_kactivitiesQuery; + const ResultWatcher m_kactivitiesWatcher; +- const KActivities::Consumer m_activitiesConsuer; ++ const KActivities::Consumer m_activitiesConsumer; + QList m_services; + bool m_matching = false; + }; +-- +2.51.0 + + +From d25a269e9dbf6209ae51f94c298cb1ef640b045c Mon Sep 17 00:00:00 2001 +From: Harald Sitter +Date: Sun, 13 Jul 2025 16:14:53 +0200 +Subject: [PATCH 6/7] servicerunner: don't narrow qsizetype to int + +use auto instead since we don't actually care about their size anyway +since we only perform trivial >=0 checks +--- + runners/services/servicerunner.cpp | 10 +++++----- + 1 file changed, 5 insertions(+), 5 deletions(-) + +diff --git a/runners/services/servicerunner.cpp b/runners/services/servicerunner.cpp +index ced1b526ce..551717947f 100644 +--- a/runners/services/servicerunner.cpp ++++ b/runners/services/servicerunner.cpp +@@ -206,7 +206,7 @@ private: + static const auto specialArgs = {QStringLiteral("-qwindowtitle"), QStringLiteral("-qwindowicon"), QStringLiteral("--started-from-file")}; + + for (const auto &specialArg : specialArgs) { +- int index = resultingArgs.indexOf(specialArg); ++ auto index = resultingArgs.indexOf(specialArg); + if (index > -1) { + if (resultingArgs.count() > index) { + resultingArgs.removeAt(index); +@@ -271,20 +271,20 @@ private: + } else if (name.compare(query, Qt::CaseInsensitive) == 0) { + relevance = 1; + categoryRelevance = KRunner::QueryMatch::CategoryRelevance::Highest; +- } else if (const int idx = name.indexOf(queryList[0], 0, Qt::CaseInsensitive); idx != -1) { ++ } else if (const auto idx = name.indexOf(queryList[0], 0, Qt::CaseInsensitive); idx != -1) { + relevance = 0.8; + relevance += increaseMatchRelevance(name, queryList, Category::Name); + if (idx == 0) { + relevance += 0.1; + categoryRelevance = KRunner::QueryMatch::CategoryRelevance::High; + } +- } else if (const int idx = service->genericName().indexOf(queryList[0], 0, Qt::CaseInsensitive); idx != -1) { ++ } else if (const auto idx = service->genericName().indexOf(queryList[0], 0, Qt::CaseInsensitive); idx != -1) { + relevance = 0.65; + relevance += increaseMatchRelevance(service->genericName(), queryList, Category::GenericName); + if (idx == 0) { + relevance += 0.05; + } +- } else if (const int idx = service->comment().indexOf(queryList[0], 0, Qt::CaseInsensitive); idx != -1) { ++ } else if (const auto idx = service->comment().indexOf(queryList[0], 0, Qt::CaseInsensitive); idx != -1) { + relevance = 0.5; + relevance += increaseMatchRelevance(service->comment(), queryList, Category::Comment); + if (idx == 0) { +@@ -364,7 +364,7 @@ private: + } + seen(action); + +- const int matchIndex = action.text().indexOf(query, 0, Qt::CaseInsensitive); ++ const auto matchIndex = action.text().indexOf(query, 0, Qt::CaseInsensitive); + if (matchIndex < 0) { + continue; + } +-- +2.51.0 + + +From 1a14af41b78a192d10fb5dcef93bba430872eab4 Mon Sep 17 00:00:00 2001 +From: Harald Sitter +Date: Sun, 13 Jul 2025 16:15:55 +0200 +Subject: [PATCH 7/7] servicerunner: remove inline noise + +functions defined inside a definition are always inline +--- + runners/services/servicerunner.cpp | 8 ++++---- + 1 file changed, 4 insertions(+), 4 deletions(-) + +diff --git a/runners/services/servicerunner.cpp b/runners/services/servicerunner.cpp +index 551717947f..eb9f02e74b 100644 +--- a/runners/services/servicerunner.cpp ++++ b/runners/services/servicerunner.cpp +@@ -92,22 +92,22 @@ public: + } + + private: +- inline void seen(const KService::Ptr &service) ++ void seen(const KService::Ptr &service) + { + m_seen.insert(service->exec()); + } + +- inline void seen(const KServiceAction &action) ++ void seen(const KServiceAction &action) + { + m_seen.insert(action.exec()); + } + +- inline bool hasSeen(const KService::Ptr &service) ++ bool hasSeen(const KService::Ptr &service) + { + return m_seen.contains(service->exec()); + } + +- inline bool hasSeen(const KServiceAction &action) ++ bool hasSeen(const KServiceAction &action) + { + return m_seen.contains(action.exec()); + } +-- +2.51.0 + diff --git a/roles/kde/patches/plasma-workspace/pr5678.9.patch b/roles/kde/patches/plasma-workspace/pr5678.9.patch new file mode 100644 index 0000000..1c9421a --- /dev/null +++ b/roles/kde/patches/plasma-workspace/pr5678.9.patch @@ -0,0 +1,913 @@ +From 312c215e717654e55fa48ec968f412201d2a5544 Mon Sep 17 00:00:00 2001 +From: Harald Sitter +Date: Mon, 14 Jul 2025 17:28:14 +0200 +Subject: [PATCH] servicerunner: fuzzy match + +use a bitap implementation instead of doing awkward contains dances. +this should lead to somewhat more reliable results, which are now more +comprehensively asserted in the unit test + +at the heart of this is a new fuzzyScore function that assigns a score +to a service vis a vis a query. this score is adjusted depending on +which field it is regarding (name > genericname > keywords). +this should hopefully ensure that a match against name outweighs most +other matches. all scores are eventually assembled into a final score +that gets used as match relevance +--- + runners/services/autotests/CMakeLists.txt | 3 + + runners/services/autotests/bitaptest.cpp | 70 +++++ + .../autotests/fixtures/audacity.desktop | 2 +- + .../fixtures/org.kde.discover.desktop | 17 ++ + .../autotests/fixtures/org.kde.kpat.desktop | 2 +- + .../services/autotests/servicerunnertest.cpp | 94 ++++-- + runners/services/bitap.h | 178 +++++++++++ + runners/services/levenshtein.h | 58 ++++ + runners/services/servicerunner.cpp | 286 +++++++++++------- + 9 files changed, 576 insertions(+), 134 deletions(-) + create mode 100644 runners/services/autotests/bitaptest.cpp + create mode 100755 runners/services/autotests/fixtures/org.kde.discover.desktop + create mode 100644 runners/services/bitap.h + create mode 100644 runners/services/levenshtein.h + +diff --git a/runners/services/autotests/CMakeLists.txt b/runners/services/autotests/CMakeLists.txt +index 04849a2928..ff7ec66634 100644 +--- a/runners/services/autotests/CMakeLists.txt ++++ b/runners/services/autotests/CMakeLists.txt +@@ -6,3 +6,6 @@ remove_definitions(-DQT_NO_CAST_FROM_ASCII) + ecm_add_test(servicerunnertest.cpp TEST_NAME servicerunnertest + LINK_LIBRARIES Qt::Test KF6::Service KF6::Runner) + krunner_configure_test(servicerunnertest krunner_services) ++ ++ecm_add_test(bitaptest.cpp TEST_NAME bitaptest ++ LINK_LIBRARIES Qt::Test) +diff --git a/runners/services/autotests/bitaptest.cpp b/runners/services/autotests/bitaptest.cpp +new file mode 100644 +index 0000000000..1a1cb856ec +--- /dev/null ++++ b/runners/services/autotests/bitaptest.cpp +@@ -0,0 +1,70 @@ ++// SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL ++// SPDX-FileCopyrightText: 2025 Harald Sitter ++ ++#include ++#include ++#include ++#include ++#include ++#include ++#include ++ ++#include "../bitap.h" ++ ++class BitapTest : public QObject ++{ ++ Q_OBJECT ++private Q_SLOTS: ++ void initTestCase() ++ { ++ } ++ void cleanupTestCase() ++ { ++ } ++ ++ void testBitap() ++ { ++ using namespace Bitap; ++ // The macro has trouble with designated initializers, so we wrap them in (). ++ QCOMPARE(bitap(u"hello world", u"hello", 1), (Match{.end = 4, .distance = 0})); ++ QCOMPARE(bitap(u"wireshark", u"di", 1), (Match{.end = 1, .distance = 1})); ++ QCOMPARE(bitap(u"discover", u"disk", 1), (Match{.end = 2, .distance = 1})); ++ QCOMPARE(bitap(u"discover", u"disc", 1), (Match{.end = 3, .distance = 0})); ++ QCOMPARE(bitap(u"discover", u"scov", 1), (Match{.end = 5, .distance = 0})); ++ QCOMPARE(bitap(u"discover", u"diki", 1), std::nullopt); ++ QCOMPARE(bitap(u"discover", u"obo", 1), std::nullopt); ++ // With a hamming distance of 1 this may match because it is a single transposition. ++ QCOMPARE(bitap(u"discover", u"dicsover", 1), (Match{.end = 7, .distance = 1})); ++ // … but with three characters out of place things should not match. ++ QCOMPARE(bitap(u"discover", u"dicosver", 1), std::nullopt); ++ // pattern too long ++ QCOMPARE(bitap(u"discover", u" !\"#$%&'()*+,-./0123456789:;<=>?@ABCDEFGHIJKLMNOPQRSTUVWXYZ[\\]^_`abcdefghijklmnopqrstuvwxyz{|}~", 1), std::nullopt); ++ // This is not a transposition as per Damerau–Levenshtein distance because the characters are not adjacent. ++ QCOMPARE(bitap(u"steam", u"skeap", 1), std::nullopt); ++ // Deletion required ++ QCOMPARE(bitap(u"discover", u"discover", 1), (Match{.end = 7, .distance = 0})); ++ QCOMPARE(bitap(u"discover", u"discovery", 1), (Match{.end = 7, .distance = 1})); ++ // Insertion required ++ QCOMPARE(bitap(u"discover", u"dicover", 1), (Match{.end = 7, .distance = 1})); ++ } ++ ++ void testScore() ++ { ++ using namespace Bitap; ++ // aperfectten has 10 big beautiful indexes. The maximum end is therefore 10. ++ QCOMPARE(score(u"aperfectten", Match{.end = 10, .distance = 0}, 1), 1.0); ++ QCOMPARE(score(u"aperfectten", Match{.end = 4, .distance = 0}, 1), 0.4); ++ QCOMPARE(score(u"aperfectten", Match{.end = 4, .distance = 1}, 1), 0.35); ++ QCOMPARE(score(u"aperfectten", Match{.end = 0, .distance = 0}, 0), 0); ++ QCOMPARE(score(u"aperfectten", Match{.end = 0, .distance = 0}, 1), 0); ++ QCOMPARE(score(u"aperfectten", Match{.end = 1, .distance = 1}, 1), 0.05); ++ ++ QCOMPARE(score(u"abc", Match{.end = 2, .distance = 1}, 1), 0.95); ++ // Ask for distance 0 but it has a distance so this is a super bad match. ++ QCOMPARE(score(u"abc", Match{.end = 2, .distance = 1}, 0), 0); ++ } ++}; ++ ++QTEST_MAIN(BitapTest) ++ ++#include "bitaptest.moc" +diff --git a/runners/services/autotests/fixtures/audacity.desktop b/runners/services/autotests/fixtures/audacity.desktop +index 7613d9f32f..05e1b9d929 100644 +--- a/runners/services/autotests/fixtures/audacity.desktop ++++ b/runners/services/autotests/fixtures/audacity.desktop +@@ -1,5 +1,5 @@ + [Desktop Entry] +-Name=Audacity ++Name=Audacity ServiceRunnerTest + GenericName=Sound Editor + Comment=Record and edit audio files + Keywords=audio;sound;alsa;jack;editor; +diff --git a/runners/services/autotests/fixtures/org.kde.discover.desktop b/runners/services/autotests/fixtures/org.kde.discover.desktop +new file mode 100755 +index 0000000000..978b2b4152 +--- /dev/null ++++ b/runners/services/autotests/fixtures/org.kde.discover.desktop +@@ -0,0 +1,17 @@ ++# SPDX-FileCopyrightText: None ++# SPDX-License-Identifier: CC0-1.0 ++[Desktop Entry] ++Name=Discover ServiceRunnerTest ++Comment=Install and remove apps and add-ons ++MimeType=application/vnd.flatpak;application/vnd.flatpak.repo;application/vnd.flatpak.ref; ++Exec=plasma-discover %F ++Icon=plasmadiscover ++Type=Application ++X-DocPath=plasma-discover/index.html ++InitialPreference=5 ++NoDisplay=false ++Actions=Updates; ++SingleMainWindow=true ++GenericName=Software Center ++Categories=Qt;KDE;System; ++Keywords=program;software;store;repository;package;add;install;uninstall;remove;update;apps;applications;games;flatpak;snap;addons;add-ons;firmware; +diff --git a/runners/services/autotests/fixtures/org.kde.kpat.desktop b/runners/services/autotests/fixtures/org.kde.kpat.desktop +index 71d7fd2a89..3a91d89afe 100644 +--- a/runners/services/autotests/fixtures/org.kde.kpat.desktop ++++ b/runners/services/autotests/fixtures/org.kde.kpat.desktop +@@ -1,7 +1,7 @@ + # SPDX-FileCopyrightText: 2022 Alexander Lohnau + # SPDX-License-Identifier: CC0-1.0 + [Desktop Entry] +-Name=KPatience ++Name=KPatience ServiceRunnerTest + Exec=true -qwindowtitle %c %u + Type=Application + Icon=kpat +diff --git a/runners/services/autotests/servicerunnertest.cpp b/runners/services/autotests/servicerunnertest.cpp +index fcfd3275ac..b911667a3b 100644 +--- a/runners/services/autotests/servicerunnertest.cpp ++++ b/runners/services/autotests/servicerunnertest.cpp +@@ -36,6 +36,10 @@ private Q_SLOTS: + void testINotifyUsage(); + void testSpecialArgs(); + void testEnv(); ++ void testDisassociation(); ++ void testMultipleKeywords(); ++ void testMultipleNameWords(); ++ void testDiscover(); + }; + + void ServiceRunnerTest::initTestCase() +@@ -86,8 +90,8 @@ void ServiceRunnerTest::testExecutableExactMatch() + + void ServiceRunnerTest::testKonsoleVsYakuakeComment() + { +- // Yakuake has konsole mentioned in comment, should be rated lower. +- const auto matches = launchQuery(QStringLiteral("kons")); ++ // Yakuake has konsole mentioned in comment, should not be listed (if it was it should be lower) ++ auto matches = launchQueryAndSort(QStringLiteral("kons")); + + bool konsoleFound = false; + bool yakuakeFound = false; +@@ -97,17 +101,10 @@ void ServiceRunnerTest::testKonsoleVsYakuakeComment() + continue; + } + +- if (match.text() == QLatin1String("Konsole ServiceRunnerTest")) { +- QCOMPARE(match.relevance(), 0.99); +- konsoleFound = true; +- } else if (match.text() == QLatin1String("Yakuake ServiceRunnerTest")) { +- // Rates lower because it doesn't have it in the name. +- QCOMPARE(match.relevance(), 0.59); +- yakuakeFound = true; +- } +- } +- QVERIFY(konsoleFound); +- QVERIFY(yakuakeFound); ++ QCOMPARE(texts, ++ QStringList({ ++ u"Konsole ServiceRunnerTest"_s, ++ })); + } + + void ServiceRunnerTest::testSystemSettings() +@@ -150,8 +147,9 @@ void ServiceRunnerTest::testSystemSettings2() + foreignSystemSettingsFound = true; + } + } +- QVERIFY(systemSettingsFound); +- QVERIFY(!foreignSystemSettingsFound); ++ ++ // The matched texts will contain much more because of the generic search term. Make sure our settings win. ++ QCOMPARE(texts.at(0), u"System Settings ServiceRunnerTest"_s); + } + + void ServiceRunnerTest::testCategories() +@@ -172,10 +170,6 @@ void ServiceRunnerTest::testCategories() + QVERIFY(std::none_of(matches.cbegin(), matches.cend(), [](const KRunner::QueryMatch &match) { + return match.text() == QLatin1String("Konsole ServiceRunnerTest"); + })); +- +- // Query too short to match any category +- matches = launchQuery(QStringLiteral("Dumm")); +- QVERIFY(matches.isEmpty()); + } + + void ServiceRunnerTest::testJumpListActions() +@@ -234,6 +228,68 @@ void ServiceRunnerTest::testEnv() + })); + } + ++void ServiceRunnerTest::testDisassociation() ++{ ++ // This test makes sure that we do not associate a service with a query that is not relevant. ++ auto matches = launchQueryAndSort(u"new laptop com"_s); // particularly notorious because it has two three letter words; 'com' is an incomplete word ++ ++ QStringList texts; ++ for (const auto &match : matches) { ++ texts.push_back(match.text()); ++ } ++ ++ QCOMPARE(texts, QStringList()); ++} ++ ++void ServiceRunnerTest::testMultipleKeywords() ++{ ++ auto matches = launchQueryAndSort(u"text editor programming"_s); ++ ++ QStringList texts; ++ for (const auto &match : matches) { ++ texts.push_back(match.text()); ++ } ++ ++ QCOMPARE(texts, ++ QStringList({ ++ u"Kate ServiceRunnerTest"_s, ++ })); ++} ++ ++void ServiceRunnerTest::testMultipleNameWords() ++{ ++ auto matches = launchQueryAndSort(u"system settings"_s); ++ ++ QStringList texts; ++ for (const auto &match : matches) { ++ if (!match.text().contains("ServiceRunnerTest"_L1)) { ++ continue; ++ } ++ texts.push_back(match.text()); ++ } ++ ++ QCOMPARE(texts, ++ QStringList({ ++ u"System Settings ServiceRunnerTest"_s, ++ })); ++} ++ ++void ServiceRunnerTest::testDiscover() ++{ ++ auto matches = launchQueryAndSort(u"disco"_s); ++ ++ QStringList texts; ++ for (const auto &match : matches) { ++ texts.push_back(match.text()); ++ } ++ ++ qDebug() << texts; ++ QCOMPARE(texts, ++ QStringList({ ++ u"Discover ServiceRunnerTest"_s, ++ })); ++} ++ + QTEST_MAIN(ServiceRunnerTest) + + #include "servicerunnertest.moc" +diff --git a/runners/services/bitap.h b/runners/services/bitap.h +new file mode 100644 +index 0000000000..a6aedb7eaf +--- /dev/null ++++ b/runners/services/bitap.h +@@ -0,0 +1,178 @@ ++// SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL ++// SPDX-FileCopyrightText: 2025 Harald Sitter ++ ++#pragma once ++ ++#include ++#include ++ ++#include ++#include ++#include ++ ++namespace Bitap ++{ ++ ++Q_DECLARE_LOGGING_CATEGORY(BITAP) ++Q_LOGGING_CATEGORY(BITAP, "org.kde.plasma.runner.services.bitap", QtWarningMsg) ++ ++struct Match { ++ qsizetype end; ++ qsizetype distance; ++ ++ bool operator==(const Match &other) const = default; ++}; ++ ++inline QDebug operator<<(QDebug dbg, const Bitap::Match &match) ++{ ++ dbg.nospace() << "Bitap::Match(" << match.end << ", " << match.distance << ")"; ++ return dbg; ++} ++ ++// Bitap is a bit of a complicated algorithm thanks to bitwise operations. I've opted to replace them with bitsets for readability. ++// It creates a patternMask based on all characters in the pattern. Basically each character gets assigned a representative bit. ++// e.g. in the pattern 'abc' the character 'a' would be 110, 'b' 101, 'c' 011. ++// This is a bit expensive up front but allows it to carry out everything else using bitwise operations. ++// For each match we set a matching bit in the bits vector. ++// Matching happens within a hamming distance, meaning up to `hammingDistance` characters can be out of place. ++inline std::optional bitap(const QStringView &name, const QStringView &pattern, int hammingDistance) ++{ ++ qCDebug(BITAP) << "Bitap called with name:" << name << "and pattern:" << pattern << "with hamming distance:" << hammingDistance; ++ const auto patternEndIndex = pattern.size() - 1; ++ if (name == pattern) { ++ return Match{.end = patternEndIndex, .distance = 0}; // Perfect match ++ } ++ ++ if (pattern.isEmpty() || name.isEmpty()) { ++ return std::nullopt; ++ } ++ ++ // Being a bitset we could have any number of bits, but practically we probably don't need more than 64, most bitaps I've seen even use 32. ++ constexpr auto maxMaskBits = 64; ++ using Mask = std::bitset; ++ using PatternMask = std::array::max()>; ++ ++ // The way bitap works is that each bit of the Mask represents a character position. Because of this we cannot match ++ // more characters than we have bits for. ++ // -1 because one bit is used for the result (I think) ++ if (pattern.size() >= qsizetype(Mask().size()) - 1) { ++ qCWarning(BITAP) << "Pattern is too long for bitap algorithm, max length is" << Mask().size() - 1; ++ return std::nullopt; ++ } ++ ++ const PatternMask patternMask = [&pattern, &name] { ++ PatternMask patternMask; ++ // The following is an optimized version of patternMask.fill(Mask().set()); to set all **necessary** bits to 1. ++ for (const auto &qchar : pattern) { ++ patternMask.at(qchar.unicode()).set(); ++ } ++ for (const auto &qchar : name) { ++ patternMask.at(qchar.unicode()).set(); ++ } ++ ++ for (int i = 0; i < pattern.size(); ++i) { ++ const auto char_ = pattern.at(i).unicode(); ++ patternMask.at(char_).reset(i); // unset the relevant index bits ++ } ++ ++ if (BITAP().isDebugEnabled()) { ++ for (const auto &i : pattern) { ++ const auto char_ = i.unicode(); ++ qCDebug(BITAP) << "Pattern mask for" << char_ << "is" << patternMask.at(char_).to_string(); ++ } ++ } ++ ++ return patternMask; ++ }(); ++ ++ Match match{ ++ .end = -1, // -1 means no match found for convenience ++ .distance = name.size(), ++ }; ++ ++ std::vector bits((hammingDistance + 1), Mask().set().reset(0)); ++ std::vector transpositions(bits.cbegin(), bits.cend()); ++ for (int i = 0; i < name.size(); ++i) { ++ const auto &char_ = name.at(i); ++ auto previousBit = bits[0]; ++ const auto mask = patternMask.at(char_.unicode()); ++ bits[0] |= mask; ++ bits[0] <<= 1; ++ ++ for (int j = 1; j <= hammingDistance; ++j) { ++ auto bit = bits[j]; ++ auto current = (bit | mask) << 1; ++ // https://en.wikipedia.org/wiki/Damerau%E2%80%93Levenshtein_distance ++ auto substitute = previousBit << 1; ++ auto delete_ = bits[j - 1] << 1; ++ auto insert = previousBit; ++ auto transpose = (transpositions[j - 1] | (mask << 1)) << 1; ++ bits[j] = current & substitute & transpose & delete_ & insert; ++ transpositions[j - 1] = (previousBit << 1) | mask; ++ previousBit = bit; ++ } ++ ++ if (BITAP().isDebugEnabled()) { ++ qCDebug(BITAP) << "After processing character" << char_ << "at index" << i; ++ for (const auto &bit : bits) { ++ qCDebug(BITAP) << "bit" << bit.to_string(); ++ } ++ } ++ ++ for (int k = 0; k <= hammingDistance; ++k) { ++ // If the bit at the end of the mask is 0, it means we have a match. ++ if (0 == (bits[k] & Mask().set(pattern.size()))) { ++ if (k < match.distance && match.end < i) { ++ qCDebug(BITAP) << "Match found at index" << i << "with hamming distance" << k << "better than previous match with distance" ++ << match.distance << "at index" << match.end; ++ match = { ++ .end = i, ++ .distance = k, ++ }; ++ } ++ // We do not return early because we want to find the best match, not just any. ++ // e.g. with a maximum distance of 1 `disc` could match `disc` either at index two with distance one, or at index three with distance zero. ++ } ++ } ++ } ++ ++ // Because we use a complete Damerau–Levenshtein distance the return value is a bit complicated. The trick is that the distance incurs a negative penalty ++ // in relation to the max distance. While an end that is closer to the real end is generally favorably. Combining the two into a single value ++ // would complicate the meaning of the return value to mean "approximate end with random penalty". This is garbage to reason about so instead we return ++ // both values and then assign them meaning in the score function. ++ if (match.end != -1) { ++ return match; ++ } ++ ++ qCDebug(BITAP) << "No match found for pattern" << pattern << "in name" << name; ++ return std::nullopt; ++} ++ ++inline qreal score(const QStringView &name, const auto &match, auto hammingDistance) ++{ ++ // Normalize the score to a value between 0.0 and 1.0 ++ // No distance means the score is directly correlated to the end index. The more characters matched the higher the score. ++ // Any distance will lower the score by a sub 0.1 margin. ++ ++ if (name.size() == 0) { ++ return 0.0; // No name, no score. ++ } ++ ++ const auto maxEnd = name.size() - 1; ++ const auto penalty = [&] { ++ if (hammingDistance <= 0) { ++ return 1.0; // No penalty for no distance ++ } ++ constexpr auto tenth = 10.0; ++ constexpr auto half = 2.0; ++ return qreal(match.distance) / qreal(hammingDistance) / tenth / half; ++ }(); ++ auto score = qreal(match.end) / qreal(maxEnd); ++ // Prevent underflows when the penalty is larger than the score. ++ score = std::max(0.0, score - penalty); ++ ++ Q_ASSERT(score >= 0.0 && score <= 1.0); ++ return score; ++} ++ ++} // namespace Bitap +diff --git a/runners/services/levenshtein.h b/runners/services/levenshtein.h +new file mode 100644 +index 0000000000..0efb960be3 +--- /dev/null ++++ b/runners/services/levenshtein.h +@@ -0,0 +1,58 @@ ++// SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL ++// SPDX-FileCopyrightText: 2025 Harald Sitter ++ ++#pragma ++ ++#include ++#include ++ ++namespace Levenshtein ++{ ++ ++inline int distance(const QStringView &name, const QStringView &query) ++{ ++ if (name == query) { ++ return 0; ++ } ++ ++ std::vector distance0(query.size() + 1, 0); ++ std::vector distance1(query.size() + 1, 0); ++ ++ for (int i = 0; i <= query.size(); ++i) { ++ distance0[i] = i; ++ } ++ ++ for (int i = 0; i < name.size(); ++i) { ++ distance1[0] = i + 1; ++ for (int j = 0; j < query.size(); ++j) { ++ const auto deletionCost = distance0[j + 1] + 1; ++ const auto insertionCost = distance1[j] + 1; ++ const auto substitutionCost = [&] { ++ if (name[i] == query[j]) { ++ return distance0[j]; ++ } ++ return distance0[j] + 1; ++ }(); ++ distance1[j + 1] = std::min({deletionCost, insertionCost, substitutionCost}); ++ } ++ std::swap(distance0, distance1); ++ } ++ return distance0[query.size()]; ++} ++ ++inline qreal score(const QStringView &name, int distance) ++{ ++ // Normalize the distance to a value between 0.0 and 1.0 ++ // The maximum distance is the length of the pattern. ++ // If the distance is 0, it means a perfect match, so we return 1.0. ++ // If the distance is equal to the length of the pattern, we return 0.0. ++ if (distance == 0) { ++ return 1.0; ++ } ++ if (distance >= name.size()) { ++ return 0.0; ++ } ++ return 1.0 - (qreal(distance) / qreal(name.size())); ++} ++ ++} // namespace Levenshtein +diff --git a/runners/services/servicerunner.cpp b/runners/services/servicerunner.cpp +index eb9f02e74b..3d5de8feb2 100644 +--- a/runners/services/servicerunner.cpp ++++ b/runners/services/servicerunner.cpp +@@ -1,7 +1,7 @@ + /* + SPDX-FileCopyrightText: 2006 Aaron Seigo + SPDX-FileCopyrightText: 2014 Vishesh Handa +- SPDX-FileCopyrightText: 2016-2020 Harald Sitter ++ SPDX-FileCopyrightText: 2016-2025 Harald Sitter + SPDX-FileCopyrightText: 2022-2023 Alexander Lohnau + + SPDX-License-Identifier: LGPL-2.0-only +@@ -21,6 +21,7 @@ + #include + + #include ++#include + #include + #include + #include +@@ -35,22 +36,130 @@ + #include + #include + ++#include "bitap.h" + #include "debug.h" ++#include "levenshtein.h" + + using namespace Qt::StringLiterals; + namespace + { + +-int weightedLength(const QString &query) ++struct Score { ++ qreal value = 0.0; // The final score, it is the sum of all scores. ++ KRunner::QueryMatch::CategoryRelevance categoryRelevance = KRunner::QueryMatch::CategoryRelevance::Lowest; // The category relevance of the match. ++}; ++ ++struct ScoreCard { ++ Bitap::Match bitap; ++ qreal bitapScore; ++ int levenshtein; ++ qreal levenshteinScore; ++}; ++ ++QDebug operator<<(QDebug dbg, const ScoreCard &card) + { +- return KStringHandler::logicalLength(query); ++ dbg.nospace() << "Scorecard(" << "bitap: " << card.bitap << ", bitapScore: " << card.bitapScore << ", levenshtein: " << card.levenshtein ++ << ", levenshteinScore: " << card.levenshteinScore << ")"; ++ return dbg; + } + +-inline bool contains(const QString &result, const QList &queryList) ++using ScoreCards = std::vector; ++ ++struct WeightedScoreCard { ++ ScoreCards cards; ++ qreal weight; ++}; ++ ++QDebug operator<<(QDebug dbg, const WeightedScoreCard &card) + { +- return std::ranges::all_of(queryList, [&result](QStringView query) { +- return result.contains(query, Qt::CaseInsensitive); +- }); ++ ++ dbg.nospace() << "WeightedCard["; ++ for (const auto &scoreCard : card.cards) { ++ dbg.nospace() << scoreCard; ++ if (&scoreCard != &card.cards.back()) { ++ dbg.nospace() << ", "; ++ } ++ } ++ dbg.nospace() << "]"; ++ return dbg; ++} ++ ++auto makeScores(const auto ¬NormalizedString, const auto &queryList) { ++ if (notNormalizedString.isEmpty()) { ++ return ScoreCards{}; // No string, no score. ++ } ++ ++ const auto string = notNormalizedString.toLower(); ++ ++ ScoreCards cards; ++ for (const auto &queryItem : queryList) { ++ constexpr auto maxDistance = 1; ++ const auto bitap = Bitap::bitap(string, queryItem, maxDistance); ++ if (!bitap) { ++ // One of the query items didn't match. This means the entire query is not a match ++ return ScoreCards{}; ++ } ++ ++ const auto bitapScore = Bitap::score(string, bitap.value(), maxDistance); ++ ++ // Mind that we give different levels of bonus. This is important to imply ordering within competing matches of the same "type". ++ // If we perfectly match that gives a bonus for not requiring any changes. ++ const auto noSubstitionBonus = Bitap::score(string, bitap.value(), 0) == 1.0 ? 4.0 : 1.0; ++ // If we match the entire length of the string that gets a bonus (disregarding distance, that was considered above). ++ const auto completeMatchBonus = bitap->end >= (queryItem.size() - 1) ? 3.0 : 1.0; ++ // If the string starts with the query item that gets a bonus. ++ const auto startsWithBonus = (string.startsWith(queryItem, Qt::CaseInsensitive)) ? 2.0 : 1.0; ++ ++ // Also consider the distance between the input and the query item. ++ // If one is "yolotrollingservice" and the other is "yolo" then we must consider them worse matches than say "yolotroll". ++ const auto levenshtein = Levenshtein::distance(string, queryItem); ++ ++ cards.emplace_back(ScoreCard{ ++ .bitap = *bitap, ++ .bitapScore = bitapScore + completeMatchBonus + noSubstitionBonus + startsWithBonus, ++ .levenshtein = levenshtein, ++ .levenshteinScore = Levenshtein::score(string, levenshtein), ++ }); ++ } ++ ++ return cards; ++}; ++ ++ ++auto makeScoreFromList(const auto &queryList, const QStringList &strings) { ++ // This turns the loop inside out. For every query item we must find a match in our keywords or we discard ++ ScoreCards cards; ++ // e.g. text,editor,programming ++ for (const auto &queryItem : queryList) { ++ // e.g. text;txt;editor;programming;programmer;development;developer;code; ++ auto found = false; ++ ScoreCards queryCards; ++ for (const auto &string : strings) { ++ auto stringCards = makeScores(string, QList{queryItem}); ++ if (stringCards.empty()) { ++ continue; // The combination didn't match. ++ } ++ for (auto &scoreCard : stringCards) { ++ if (scoreCard.levenshteinScore < 0.8) { ++ continue; // Not a good match, skip it. We are very strict with keywords ++ } ++ found = true; ++ queryCards.append_range(stringCards); ++ } ++ // We do not break because other string might also match, improving the score. ++ } ++ if (!found) { ++ // No item in strings matched the query item. This means the entire query is not a match. ++ return ScoreCards{}; ++ } ++ cards.append_range(queryCards); ++ } ++ return cards; ++}; ++ ++int weightedLength(const QString &query) ++{ ++ return KStringHandler::logicalLength(query); + } + + inline bool contains(const QStringList &results, const QList &queryList) +@@ -79,7 +188,7 @@ public: + + void match(KRunner::RunnerContext &context) + { +- query = context.query(); ++ query = context.query().toLower(); + // Splitting the query term to match using subsequences + queryList = QStringView(query).split(QLatin1Char(' ')); + weightedTermLength = weightedLength(query); +@@ -120,36 +229,6 @@ private: + return ret; + } + +- enum class Category { +- Name, +- GenericName, +- Comment, +- }; +- qreal increaseMatchRelevance(const QString &serviceProperty, const QList &strList, Category category) +- { +- // Increment the relevance based on all the words (other than the first) of the query list +- qreal relevanceIncrement = 0; +- +- for (int i = 1; i < strList.size(); ++i) { +- const auto &str = strList.at(i); +- if (category == Category::Name) { +- if (serviceProperty.contains(str, Qt::CaseInsensitive)) { +- relevanceIncrement += 0.01; +- } +- } else if (category == Category::GenericName) { +- if (serviceProperty.contains(str, Qt::CaseInsensitive)) { +- relevanceIncrement += 0.01; +- } +- } else if (category == Category::Comment) { +- if (serviceProperty.contains(str, Qt::CaseInsensitive)) { +- relevanceIncrement += 0.01; +- } +- } +- } +- +- return relevanceIncrement; +- } +- + void setupMatch(const KService::Ptr &service, KRunner::QueryMatch &match) + { + const QString name = service->name(); +@@ -219,96 +298,77 @@ private: + return resultingArgs.join(QLatin1Char(' ')); + } + +- void matchNameKeywordAndGenericName() ++ [[nodiscard]] std::optional fuzzyScore(KService::Ptr service) + { +- const auto nameKeywordAndGenericNameFilter = [this](const KService::Ptr &service) { +- // Name +- if (contains(service->name(), queryList)) { +- return true; +- } +- // If the term length is < 3, no real point searching the untranslated Name, Keywords and GenericName +- if (weightedTermLength < 3) { +- return false; +- } +- if (contains(service->untranslatedName(), queryList)) { +- return true; +- } ++ if (queryList.isEmpty()) { ++ return std::nullopt; // No query, no score. ++ } ++ ++ const auto name = service->name(); ++ if (name.compare(query, Qt::CaseInsensitive) == 0) { ++ // Absolute match. Can't get any better than this. ++ return Score{.value = std::numeric_limits::max(), .categoryRelevance = KRunner::QueryMatch::CategoryRelevance::Highest}; ++ } + +- // Keywords +- if (contains(service->keywords(), queryList)) { +- return true; ++ std::array weightedCards = { ++ WeightedScoreCard{.cards = makeScores(name, queryList), .weight = 1.0}, ++ WeightedScoreCard{.cards = makeScores(service->untranslatedName(), queryList), .weight = 0.8}, ++ WeightedScoreCard{.cards = makeScores(service->genericName(), queryList), .weight = 0.6}, ++ WeightedScoreCard{.cards = makeScoreFromList(queryList, service->keywords()), .weight = 0.1}, ++ }; ++ ++ if (RUNNER_SERVICES().isDebugEnabled()) { ++ qCDebug(RUNNER_SERVICES) << "+++++++ Weighted Cards for" << name; ++ for (const auto &weightedCard : weightedCards) { ++ qCDebug(RUNNER_SERVICES) << weightedCard; + } +- // GenericName +- if (contains(service->genericName(), queryList) || contains(service->untranslatedGenericName(), queryList)) { +- return true; ++ qCDebug(RUNNER_SERVICES) << "-------"; ++ } ++ ++ int scores = 1; // starts at 1 to avoid division by zero ++ qreal finalScore = 0.0; ++ for (const auto &weightedCard : weightedCards) { ++ if (weightedCard.cards.empty()) { ++ continue; // No scores, no match. + } +- // Comment +- if (contains(service->comment(), queryList)) { +- return true; ++ ++ qreal weightedScore = 0.0; ++ for (const auto &scoreCard : weightedCard.cards) { ++ weightedScore += (scoreCard.bitapScore + scoreCard.levenshteinScore) * weightedCard.weight; ++ scores++; + } + +- return false; +- }; ++ finalScore += weightedScore; ++ } ++ finalScore = finalScore / scores; // Average the score for this card + +- for (const KService::Ptr &service : m_services) { +- if (!nameKeywordAndGenericNameFilter(service) || disqualify(service)) { +- continue; +- } ++ qCDebug(RUNNER_SERVICES) << "Final score for" << name << "is" << finalScore; ++ if (finalScore > 0.0) { ++ return Score{.value = finalScore, .categoryRelevance = KRunner::QueryMatch::CategoryRelevance::Moderate}; ++ } + +- const QString id = service->storageId(); +- const QString name = service->name(); ++ return std::nullopt; ++ } + +- KRunner::QueryMatch::CategoryRelevance categoryRelevance = KRunner::QueryMatch::CategoryRelevance::Moderate; +- qreal relevance(0.6); ++ void matchNameKeywordAndGenericName() ++ { ++ static auto isTest = QStandardPaths::isTestModeEnabled(); + +- // If the term was < 3 chars and NOT at the beginning of the App's name, then chances are the user doesn't want that app +- if (weightedTermLength < 3) { +- if (name.startsWith(query, Qt::CaseInsensitive)) { +- relevance = 0.9; +- } else { +- continue; +- } +- } else if (name.compare(query, Qt::CaseInsensitive) == 0) { +- relevance = 1; +- categoryRelevance = KRunner::QueryMatch::CategoryRelevance::Highest; +- } else if (const auto idx = name.indexOf(queryList[0], 0, Qt::CaseInsensitive); idx != -1) { +- relevance = 0.8; +- relevance += increaseMatchRelevance(name, queryList, Category::Name); +- if (idx == 0) { +- relevance += 0.1; +- categoryRelevance = KRunner::QueryMatch::CategoryRelevance::High; +- } +- } else if (const auto idx = service->genericName().indexOf(queryList[0], 0, Qt::CaseInsensitive); idx != -1) { +- relevance = 0.65; +- relevance += increaseMatchRelevance(service->genericName(), queryList, Category::GenericName); +- if (idx == 0) { +- relevance += 0.05; +- } +- } else if (const auto idx = service->comment().indexOf(queryList[0], 0, Qt::CaseInsensitive); idx != -1) { +- relevance = 0.5; +- relevance += increaseMatchRelevance(service->comment(), queryList, Category::Comment); +- if (idx == 0) { +- relevance += 0.05; +- } ++ for (const KService::Ptr &service : m_services) { ++ if (isTest && !service->name().contains("ServiceRunnerTest"_L1)) { ++ continue; // Skip services that are not part of the test. + } + + KRunner::QueryMatch match(m_runner); +- match.setCategoryRelevance(categoryRelevance); +- setupMatch(service, match); +- if (service->categories().contains(QLatin1String("KDE"))) { +- qCDebug(RUNNER_SERVICES) << "found a kde thing" << id << match.subtext() << relevance; +- relevance += .09; +- } +- +- if (const auto foundIt = m_runner->m_favorites.constFind(service->desktopEntryName()); foundIt != m_runner->m_favorites.cend()) { +- if (foundIt->isGlobal || foundIt->linkedActivities.contains(m_currentActivity)) { +- qCDebug(RUNNER_SERVICES) << "entry is a favorite" << id << match.subtext() << relevance; +- relevance *= 1.25; // Give favorites a relative boost, +- } ++ auto score = fuzzyScore(service); ++ if (!score || disqualify(service)) { ++ continue; + } + +- qCDebug(RUNNER_SERVICES) << name << "is this relevant:" << relevance; +- match.setRelevance(relevance); ++ setupMatch(service, match); ++ match.setCategoryRelevance(score->categoryRelevance); ++ match.setRelevance(score->value); ++ qCDebug(RUNNER_SERVICES) << match.text() << "is this relevant:" << match.relevance() << "category relevance" << match.categoryRelevance(); + + matches << match; + } +-- +2.51.0 + diff --git a/roles/kde/patches/plasma-workspace/pr5734.patch b/roles/kde/patches/plasma-workspace/pr5734.patch new file mode 100644 index 0000000..ffe6605 --- /dev/null +++ b/roles/kde/patches/plasma-workspace/pr5734.patch @@ -0,0 +1,133 @@ +From 0168ee68b484995ed9398d31004dd80678ac7e37 Mon Sep 17 00:00:00 2001 +From: Kai Uwe Broulik +Date: Tue, 5 Aug 2025 12:57:00 +0200 +Subject: [PATCH] Close USB device added notification when devicenotifier pops + up + +The device notification is supposed to be super quick feedback that +"something" got detected. Once it has been identified as storage +device (disks spun up and what not), devicenotifier will show up +and then you have two popups. +--- + applets/devicenotifier/CMakeLists.txt | 1 + + applets/devicenotifier/devicefiltercontrol.cpp | 14 ++++++++++++++ + applets/devicenotifier/devicefiltercontrol.h | 1 + + applets/devicenotifier/qml/main.qml | 1 + + devicenotifications/devicenotifications.cpp | 8 ++++++++ + devicenotifications/devicenotifications.h | 3 +++ + 6 files changed, 28 insertions(+) + +diff --git a/applets/devicenotifier/CMakeLists.txt b/applets/devicenotifier/CMakeLists.txt +index d87964dc46d..fa415837e68 100644 +--- a/applets/devicenotifier/CMakeLists.txt ++++ b/applets/devicenotifier/CMakeLists.txt +@@ -31,6 +31,7 @@ plasma_add_applet(org.kde.plasma.devicenotifier + + target_link_libraries(org.kde.plasma.devicenotifier + PRIVATE ++ Qt::DBus + Qt::Qml + Plasma::Plasma + KF6::Solid +diff --git a/applets/devicenotifier/devicefiltercontrol.cpp b/applets/devicenotifier/devicefiltercontrol.cpp +index dfdb51a4304..f585eb2f063 100644 +--- a/applets/devicenotifier/devicefiltercontrol.cpp ++++ b/applets/devicenotifier/devicefiltercontrol.cpp +@@ -11,9 +11,14 @@ + #include "devicecontrol.h" + #include "devicestatemonitor_p.h" + ++#include ++#include ++ + #include + #include + ++using namespace Qt::Literals::StringLiterals; ++ + DeviceFilterControl::DeviceFilterControl(QObject *parent) + : QSortFilterProxyModel(parent) + , m_filterType(Removable) +@@ -54,6 +59,15 @@ void DeviceFilterControl::unmountAllRemovables() + qCDebug(APPLETS::DEVICENOTIFIER) << "Device Filter Control: unmount all removables function finished"; + } + ++void DeviceFilterControl::dismissUsbDeviceAddedNotification() ++{ ++ QDBusMessage msg = QDBusMessage::createMethodCall(u"org.kde.kded6"_s, ++ u"/modules/devicenotifications"_s, ++ u"org.kde.plasma.devicenotifications"_s, ++ u"dismissUsbDeviceAdded"_s); ++ QDBusConnection::sessionBus().call(msg, QDBus::NoBlock); ++} ++ + QBindable DeviceFilterControl::bindableLastUdi() + { + return &m_lastUdi; +diff --git a/applets/devicenotifier/devicefiltercontrol.h b/applets/devicenotifier/devicefiltercontrol.h +index e4c0a321657..fa6266fb197 100644 +--- a/applets/devicenotifier/devicefiltercontrol.h ++++ b/applets/devicenotifier/devicefiltercontrol.h +@@ -41,6 +41,7 @@ public: + Q_ENUM(DevicesType) + + Q_INVOKABLE void unmountAllRemovables(); ++ Q_INVOKABLE void dismissUsbDeviceAddedNotification(); + + explicit DeviceFilterControl(QObject *parent = nullptr); + ~DeviceFilterControl() override; +diff --git a/applets/devicenotifier/qml/main.qml b/applets/devicenotifier/qml/main.qml +index 7fcd76a6d16..c7fe6e6197d 100644 +--- a/applets/devicenotifier/qml/main.qml ++++ b/applets/devicenotifier/qml/main.qml +@@ -35,6 +35,7 @@ PlasmoidItem { + onLastUdiChanged: { + if (lastDeviceAdded) { + if (Plasmoid.configuration.popupOnNewDevice) { ++ filterModel.dismissUsbDeviceAddedNotification(); + devicenotifier.expanded = true; + fullRepresentationItem.spontaneousOpen = true; + } +diff --git a/devicenotifications/devicenotifications.cpp b/devicenotifications/devicenotifications.cpp +index 71ae0ff340e..196e28ca948 100644 +--- a/devicenotifications/devicenotifications.cpp ++++ b/devicenotifications/devicenotifications.cpp +@@ -323,6 +323,14 @@ void KdedDeviceNotifications::setupWaylandOutputListener() + wl_callback_add_listener(syncCallback, &syncCallbackListener, this); + } + ++void KdedDeviceNotifications::dismissUsbDeviceAdded() ++{ ++ if (m_usbDeviceAddedNotification) { ++ m_usbDeviceAddedNotification->close(); ++ m_usbDeviceAddedNotification = nullptr; ++ } ++} ++ + void KdedDeviceNotifications::notifyOutputAdded() + { + if (m_deviceAddedTimer.isActive()) { +diff --git a/devicenotifications/devicenotifications.h b/devicenotifications/devicenotifications.h +index ab7e6b3ff9b..75005193287 100644 +--- a/devicenotifications/devicenotifications.h ++++ b/devicenotifications/devicenotifications.h +@@ -77,6 +77,7 @@ private: + class KdedDeviceNotifications : public KDEDModule + { + Q_OBJECT ++ Q_CLASSINFO("D-Bus Interface", "org.kde.plasma.devicenotifications") + + public: + KdedDeviceNotifications(QObject *parent, const QVariantList &args); +@@ -84,6 +85,8 @@ public: + + void setupWaylandOutputListener(); + ++ Q_SCRIPTABLE void dismissUsbDeviceAdded(); ++ + private: + void notifyOutputAdded(); + void notifyOutputRemoved(); +-- +GitLab + diff --git a/roles/kde/patches/plasma-workspace/pr5746.patch b/roles/kde/patches/plasma-workspace/pr5746.patch new file mode 100644 index 0000000..d4eace1 --- /dev/null +++ b/roles/kde/patches/plasma-workspace/pr5746.patch @@ -0,0 +1,33 @@ +From f6ec2847358178a5b6ff0497e52d1e2be43d2a48 Mon Sep 17 00:00:00 2001 +From: =?UTF-8?q?Luan=20Vitor=20Simi=C3=A3o=20oliveira?= + +Date: Fri, 8 Aug 2025 14:41:49 -0300 +Subject: [PATCH] kcms/style: add special case for Adwaita gtk theme + +Allows the user to set the GTK theme to the default "Adwaita" +needs to be special cased because the theme is implemented in code. +--- + kcms/style/gtkthemesmodel.cpp | 7 ++++++- + 1 file changed, 6 insertions(+), 1 deletion(-) + +diff --git a/kcms/style/gtkthemesmodel.cpp b/kcms/style/gtkthemesmodel.cpp +index 002e87dbd0d..6dcdf4f1a1d 100644 +--- a/kcms/style/gtkthemesmodel.cpp ++++ b/kcms/style/gtkthemesmodel.cpp +@@ -35,7 +35,12 @@ void GtkThemesModel::load() + if (possibleThemeDirectory.dirName() == u"Breeze-Dark") { + continue; + } +- ++ if (possibleThemeDirectory.dirName() == u"Default") { ++ // Adwaita is a special case, since it is implemented inside GTK itself ++ // also setting gtk-theme-name to "Default" breaks dark theme ++ gtk3ThemesNames.insert(QStringLiteral("Adwaita"), possibleThemeDirectory.path()); ++ continue; ++ } + gtk3ThemesNames.insert(possibleThemeDirectory.dirName(), possibleThemeDirectory.path()); + } + } +-- +GitLab + diff --git a/roles/kde/patches/plasma-workspace/pr5782.patch b/roles/kde/patches/plasma-workspace/pr5782.patch new file mode 100644 index 0000000..a20c5e1 --- /dev/null +++ b/roles/kde/patches/plasma-workspace/pr5782.patch @@ -0,0 +1,32 @@ +From 4ab3894d75e1f9c6c7738a893a9b707ff0575953 Mon Sep 17 00:00:00 2001 +From: Nate Graham +Date: Thu, 21 Aug 2025 19:37:33 -0600 +Subject: [PATCH] notifications: make "you missed some notifications" + notification transient + +Its purpose is to direct you to the notifications history. If you're +seeing it *in* the notification history, its purpose has been bypassed +because you're already where it wanted to take you. + +Don't show it in the notification history. +--- + libnotificationmanager/notifications.cpp | 3 +++ + 1 file changed, 3 insertions(+) + +diff --git a/libnotificationmanager/notifications.cpp b/libnotificationmanager/notifications.cpp +index f68c342e7e9..128665f4de9 100644 +--- a/libnotificationmanager/notifications.cpp ++++ b/libnotificationmanager/notifications.cpp +@@ -917,6 +917,9 @@ void Notifications::showInhibitionSummary(Urgency urgency, const QStringList &bl + notification->setIconName(u"preferences-desktop-notification-bell"_s); + notification->setFlags(KNotification::CloseOnTimeout); + notification->setComponentName(u"libnotificationmanager"_s); ++ // Don't put it in the history because this doesn't make sense; if you're seeing it ++ // in the history, you're seeing the notifications it was telling you about! ++ notification->setHint(u"transient"_s, true); + + const QString showNotificationsText = i18nc("@action:button Show the notifications popup", "Show Notifications"); + +-- +GitLab + diff --git a/roles/kde/patches/plasma-workspace/pr5788.patch b/roles/kde/patches/plasma-workspace/pr5788.patch new file mode 100644 index 0000000..96f3980 --- /dev/null +++ b/roles/kde/patches/plasma-workspace/pr5788.patch @@ -0,0 +1,42 @@ +From f0d2dd20803f2eee364d26656715b89e7c74366c Mon Sep 17 00:00:00 2001 +From: David Redondo +Date: Wed, 27 Aug 2025 09:40:43 +0200 +Subject: [PATCH] servicerunner: use vector::insert on compilers that don't + support append_range yet + +g++ only gained support for it with g++ 15 which was released this month. +--- + runners/services/servicerunner.cpp | 8 ++++++++ + 1 file changed, 8 insertions(+) + +diff --git a/runners/services/servicerunner.cpp b/runners/services/servicerunner.cpp +index 2ccc9a0af37..9e9d4e70a72 100644 +--- a/runners/services/servicerunner.cpp ++++ b/runners/services/servicerunner.cpp +@@ -142,7 +142,11 @@ auto makeScoreFromList(const auto &queryList, const QStringList &strings) { + continue; // Not a good match, skip it. We are very strict with keywords + } + found = true; ++#ifdef __cpp_lib_containers_ranges + queryCards.append_range(stringCards); ++#else ++ queryCards.insert(queryCards.end(), stringCards.cbegin(), stringCards.cend()); ++#endif + } + // We do not break because other string might also match, improving the score. + } +@@ -150,7 +154,11 @@ auto makeScoreFromList(const auto &queryList, const QStringList &strings) { + // No item in strings matched the query item. This means the entire query is not a match. + return ScoreCards{}; + } ++#ifdef __cpp_lib_containers_ranges + cards.append_range(queryCards); ++#else ++ cards.insert(cards.end(), queryCards.cbegin(), queryCards.cend()); ++#endif + } + return cards; + }; +-- +GitLab +