Desktop/kde: add patches

This commit is contained in:
Toast 2025-09-19 18:00:15 +02:00
parent b6119bd2cc
commit 09d0847ea4
14 changed files with 2981 additions and 0 deletions

View file

@ -0,0 +1,2 @@
Frameworks 6.18:
Pr 54 https://invent.kde.org/frameworks/frameworkintegration/-/merge_requests/54

View file

@ -0,0 +1,50 @@
From 516a3642796563bcc7a13cd02795de3077a861b7 Mon Sep 17 00:00:00 2001
From: Nate Graham <nate@kde.org>
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

View file

@ -1,3 +1,5 @@
Plasma 6.5.0: Plasma 6.5.0:
Pr 3612 https://invent.kde.org/plasma/kwin/-/merge_requests/3612 Pr 3612 https://invent.kde.org/plasma/kwin/-/merge_requests/3612
Pr 7823 https://invent.kde.org/plasma/kwin/-/merge_requests/7823 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

View file

@ -0,0 +1,669 @@
From 9f6c92806490d662117575a766f9fcb01e253344 Mon Sep 17 00:00:00 2001
From: Xaver Hugl <xaver.hugl@kde.org>
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<QList<Output *>> &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<DpmsInputEventFilter> 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<PlasmaWindowActivationInterface> activation;
if (showNotify) {
- activation = waylandServer()->plasmaActivationFeedback()->createActivation(appId);
+ m_lastToken = newToken;
+ m_activation = waylandServer()->plasmaActivationFeedback()->createActivation(appId);
}
- m_currentActivationToken = std::make_unique<ActivationToken>(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<const SurfaceInterface> surface;
- uint serial;
- SeatInterface *seat;
- QString applicationId;
- bool showNotify;
- std::unique_ptr<PlasmaWindowActivationInterface> activation;
- };
- std::unique_ptr<ActivationToken> m_currentActivationToken;
+ QString m_lastToken;
+ std::unique_ptr<PlasmaWindowActivationInterface> m_activation;
};
}
--
GitLab
From 6c673a479412902a14c06046199f976e2192dc65 Mon Sep 17 00:00:00 2001
From: Xaver Hugl <xaver.hugl@kde.org>
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 <xaver.hugl@kde.org>
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 <xaver.hugl@kde.org>
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 <KWayland/Client/seat.h>
#include <KWayland/Client/surface.h>
#include <netwm.h>
#include <xcb/xcb_icccm.h>
@@ -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<std::unique_ptr<KWayland::Client::Surface>> surfaces;
+ std::vector<std::unique_ptr<Test::XdgToplevel>> shellSurfaces;
+ std::vector<Window *> 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<XdgActivationToken> createToken();
+};
+
struct Connection
{
static std::unique_ptr<Connection> setup(AdditionalWaylandInterfaces interfaces = AdditionalWaylandInterfaces());
@@ -757,6 +786,7 @@ struct Connection
std::unique_ptr<ColorManagerV1> colorManager;
std::unique_ptr<FifoManagerV1> fifoManager;
std::unique_ptr<PresentationTime> presentationTime;
+ std::unique_ptr<XdgActivation> 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> Connection::setup(AdditionalWaylandInterfaces flags)
c->presentationTime = std::make_unique<PresentationTime>(*c->registry, name, version);
}
}
+ if (flags & AdditionalWaylandInterface::XdgActivation) {
+ if (interface == xdg_activation_v1_interface.name) {
+ c->xdgActivation = std::make_unique<XdgActivation>(*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<XdgActivationToken> XdgActivation::createToken()
+{
+ return std::make_unique<XdgActivationToken>(get_activation_token());
+}
+
void keyboardKeyPressed(quint32 key, quint32 time)
{
auto virtualKeyboard = static_cast<WaylandTestApplication *>(kwinApp())->virtualKeyboard();
--
GitLab

View file

@ -0,0 +1,71 @@
From dc692e89f101a47b9049b1f6ae4cc3cebef46edb Mon Sep 17 00:00:00 2001
From: Xaver Hugl <xaver.hugl@kde.org>
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<PlasmaWindowActivationInterface> m_activation;
};
--
GitLab

View file

@ -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 Pr 5627 https://invent.kde.org/plasma/plasma-workspace/-/merge_requests/5627
Commit 8202ba92 https://invent.kde.org/plasma/plasma-workspace/-/commit/8202ba92b610c691b8bc6bab8ad5a1c3b9ac73da 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 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

View file

@ -0,0 +1,456 @@
From 439b251bcb3ea24f52c052e7244fb7ced04503aa Mon Sep 17 00:00:00 2001
From: Bohdan Onofriichuk <bogdan.onofriuchuk@gmail.com>
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 <qydwhotmail@gmail.com>
+# 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 <qydwhotmail@gmail.com>
-# 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

View file

@ -0,0 +1,177 @@
From aa1e466e5f7684a8b624c34d466dda9d10a331d2 Mon Sep 17 00:00:00 2001
From: Nate Graham <nate@kde.org>
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 <chrono>
+#include <knotification.h>
#include <wayland-client.h>
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 <QHash>
#include <QList>
+#include <QPointer>
#include <QSocketNotifier>
#include <QString>
#include <QTimer>
#include <KDEDModule>
+#include <KNotification>
#include <libudev.h>
@@ -98,4 +100,7 @@ private:
QTimer m_deviceAddedTimer;
QTimer m_deviceRemovedTimer;
+
+ QPointer<KNotification> m_usbDeviceAddedNotification;
+ QPointer<KNotification> m_usbDeviceRemovedNotification;
};
--
2.51.0
From 05f72383fd0b29105f3b5494759500d26b38ffc2 Mon Sep 17 00:00:00 2001
From: Nate Graham <nate@kde.org>
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

View file

@ -0,0 +1,392 @@
From 97c77a8e3259d77cb615dadd1c92185545513ebb Mon Sep 17 00:00:00 2001
From: Harald Sitter <sitter@kde.org>
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<QString, ActivityFavourite> m_favourites;
+ QMap<QString, ActivityFavorite> 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 <sitter@kde.org>
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 <sitter@kde.org>
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<QStringView> &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<QStringView> &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 <sitter@kde.org>
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<QStringView> &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 <sitter@kde.org>
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<QStringView> &strList, Category category)
+ qreal increaseMatchRelevance(const QString &serviceProperty, const QList<QStringView> &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<KService::Ptr> m_services;
bool m_matching = false;
};
--
2.51.0
From d25a269e9dbf6209ae51f94c298cb1ef640b045c Mon Sep 17 00:00:00 2001
From: Harald Sitter <sitter@kde.org>
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 <sitter@kde.org>
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

View file

@ -0,0 +1,913 @@
From 312c215e717654e55fa48ec968f412201d2a5544 Mon Sep 17 00:00:00 2001
From: Harald Sitter <sitter@kde.org>
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 <sitter@kde.org>
+
+#include <QDebug>
+#include <QDir>
+#include <QFile>
+#include <QObject>
+#include <QStandardPaths>
+#include <QTest>
+#include <QThread>
+
+#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 DamerauLevenshtein 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 <alexander.lohnau@gmx.de>
# 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 <sitter@kde.org>
+
+#pragma once
+
+#include <bitset>
+#include <optional>
+
+#include <QDebug>
+#include <QLoggingCategory>
+#include <QString>
+
+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<Match> 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<maxMaskBits>;
+ using PatternMask = std::array<Mask, std::numeric_limits<char16_t>::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<Mask> bits((hammingDistance + 1), Mask().set().reset(0));
+ std::vector<Mask> 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 DamerauLevenshtein 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 <sitter@kde.org>
+
+#pragma
+
+#include <QLoggingCategory>
+#include <QString>
+
+namespace Levenshtein
+{
+
+inline int distance(const QStringView &name, const QStringView &query)
+{
+ if (name == query) {
+ return 0;
+ }
+
+ std::vector<int> distance0(query.size() + 1, 0);
+ std::vector<int> 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 <aseigo@kde.org>
SPDX-FileCopyrightText: 2014 Vishesh Handa <vhanda@kde.org>
- SPDX-FileCopyrightText: 2016-2020 Harald Sitter <sitter@kde.org>
+ SPDX-FileCopyrightText: 2016-2025 Harald Sitter <sitter@kde.org>
SPDX-FileCopyrightText: 2022-2023 Alexander Lohnau <alexander.lohnau@gmx.de>
SPDX-License-Identifier: LGPL-2.0-only
@@ -21,6 +21,7 @@
#include <QUrlQuery>
#include <KApplicationTrader>
+#include <KFuzzyMatcher>
#include <KLocalizedString>
#include <KNotificationJobUiDelegate>
#include <KServiceAction>
@@ -35,22 +36,130 @@
#include <KIO/ApplicationLauncherJob>
#include <KIO/DesktopExecParser>
+#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<QStringView> &queryList)
+using ScoreCards = std::vector<ScoreCard>;
+
+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 &notNormalizedString, 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<QStringView> &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<QStringView> &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<Score> 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<decltype(Score::value)>::max(), .categoryRelevance = KRunner::QueryMatch::CategoryRelevance::Highest};
+ }
- // Keywords
- if (contains(service->keywords(), queryList)) {
- return true;
+ std::array<WeightedScoreCard, 4> 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

View file

@ -0,0 +1,133 @@
From 0168ee68b484995ed9398d31004dd80678ac7e37 Mon Sep 17 00:00:00 2001
From: Kai Uwe Broulik <kde@privat.broulik.de>
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 <QDBusConnection>
+#include <QDBusMessage>
+
#include <Solid/Device>
#include <Solid/OpticalDrive>
+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<QString> 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

View file

@ -0,0 +1,33 @@
From f6ec2847358178a5b6ff0497e52d1e2be43d2a48 Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Luan=20Vitor=20Simi=C3=A3o=20oliveira?=
<luanv.oliveira@outlook.com>
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

View file

@ -0,0 +1,32 @@
From 4ab3894d75e1f9c6c7738a893a9b707ff0575953 Mon Sep 17 00:00:00 2001
From: Nate Graham <nate@kde.org>
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

View file

@ -0,0 +1,42 @@
From f0d2dd20803f2eee364d26656715b89e7c74366c Mon Sep 17 00:00:00 2001
From: David Redondo <kde@david-redondo.de>
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