FolderView: Add configurable type-ahead search

This MR adds the ability to use Dolphin-style type-ahead search/icon
selection directly on the desktop, and unifies configuration with KRunner,
allowing selection of who handles the keypresses on Desktop. This config
is available in ConfigIcons view.

* Feature: Users can type to jump to icons.
* UX Safety: It is disabled by default to ensure that pressing a key still
  opens KRunner by default.
* Configurability: Added radio buttons to select between type-ahead behaviour
  and krunner. (option dissapears if krunner is not running and feature becomes
  enabled automatically)
* Old config migration: Added an update script to auto-enable KRunner to react
  to keypresses if it was disabled earlier, since now FolderView config takes
  precedence.
* Detects if KRunner is not available and hides configuration, defaulting to
  capture keypresses on desktop and reacting to them. Also active for popups.

Since ActivateWhenTypingOnDesktop in KRunner's config is no longer used to
control KRunner's launch, the config option was removed and behavior defaulted
to true in https://invent.kde.org/plasma/plasma-workspace/-/merge_requests/6399.

FEATURE: 427961
FIXED-IN: 6.7.0
This commit is contained in:
Guillermo Steren 2026-03-18 20:59:43 +00:00 committed by Nate Graham
parent 20c98065bc
commit 81f846f92d
9 changed files with 232 additions and 21 deletions

View file

@ -322,6 +322,9 @@ endif()
install(FILES org.kde.plasmashell.metainfo.xml DESTINATION ${KDE_INSTALL_METAINFODIR})
install(FILES kde-mimeapps.list DESTINATION ${KDE_INSTALL_APPDIR})
install(FILES kconf_update/50-krunner-activate-typing.upd DESTINATION ${KDE_INSTALL_KCONFUPDATEDIR})
install(PROGRAMS kconf_update/50-krunner-activate-typing.sh DESTINATION ${KDE_INSTALL_KCONFUPDATEDIR})
# add clang-format target for all our real source files
file(GLOB_RECURSE ALL_CLANG_FORMAT_SOURCE_FILES *.cpp *.h)
kde_clang_format(${ALL_CLANG_FORMAT_SOURCE_FILES})

View file

@ -135,5 +135,9 @@
<label>Show hidden files.</label>
<default>false</default>
</entry>
<entry name="useTypeAhead" type="Bool">
<label>Use type-ahead to select desktop icons in Folder View.</label>
<default>false</default>
</entry>
</group>
</kcfg>

View file

@ -16,12 +16,15 @@ import org.kde.iconthemes as KIconThemes
import org.kde.config // for KAuthorized
import org.kde.kirigami as Kirigami
import org.kde.kcmutils as KCM
import org.kde.private.desktopcontainment.folder as Folder
KCM.SimpleKCM {
id: configIcons
property bool isPopup: (Plasmoid.location !== PlasmaCore.Types.Floating)
readonly property bool krunnerAvailable: Folder.KRunnerChecker.krunnerAvailable
property string cfg_icon: Plasmoid.configuration.icon
property alias cfg_useCustomIcon: useCustomIcon.checked
property alias cfg_arrangement: arrangement.currentIndex
@ -40,6 +43,7 @@ KCM.SimpleKCM {
property alias cfg_iconSize: iconSize.value
property alias cfg_labelWidth: labelWidth.currentIndex
property alias cfg_textLines: textLines.value
property bool cfg_useTypeAhead: Plasmoid.configuration.useTypeAhead
readonly property bool lockedByKiosk: !KAuthorized.authorize("editable_desktop_icons")
@ -316,6 +320,36 @@ KCM.SimpleKCM {
Kirigami.FormData.isSection: true
}
ColumnLayout {
// Only show if it's the desktop (not a popup) AND KRunner is present/running
visible: !configIcons.isPopup && configIcons.krunnerAvailable
Kirigami.FormData.label: i18nc("@title:group", "Typing on the desktop:")
Kirigami.FormData.buddyFor: typeAheadRadioButton
spacing: Kirigami.Units.smallSpacing
RadioButton {
id: typeAheadRadioButton
text: i18nc("@option:radio Typing on the desktop selects items", "Selects items")
checked: configIcons.cfg_useTypeAhead
onToggled: if (checked) configIcons.cfg_useTypeAhead = true
}
RadioButton {
text: i18nc("@option:radio Typing on the desktop activates KRunner", "Activates KRunner")
checked: !configIcons.cfg_useTypeAhead
onToggled: if (checked) configIcons.cfg_useTypeAhead = false
}
}
Item {
Kirigami.FormData.isSection: true
visible: !configIcons.isPopup && configIcons.krunnerAvailable
implicitHeight: Kirigami.Units.smallSpacing
}
CheckBox {
id: renameInline

View file

@ -54,9 +54,11 @@ FocusScope {
property var history: []
property var lastPosition: null
property bool goingBack: false
readonly property bool krunnerAvailable: Folder.KRunnerChecker.krunnerAvailable
property BackButtonItem backButton: null
property var dialog: null
property Item editor: null
property string searchString: ""
property int previouslySelectedItemIndex: -1
@ -1057,7 +1059,14 @@ FocusScope {
}
}
Timer {
id: typeAheadTimer
interval: 500 // Resets search string after 0.5 seconds of inactivity
onTriggered: searchString = ""
}
Keys.onPressed: event => {
// Start by assuming we might handle it
event.accepted = true;
if (event.key === Qt.Key_Control) {
@ -1086,7 +1095,44 @@ FocusScope {
dir.refresh();
} else if (event.matches(StandardKey.SelectAll)) {
positioner.setRangeSelected(0, count - 1);
} else {
} else if ( (!Plasmoid.isContainment || !main.krunnerAvailable || Plasmoid.configuration.useTypeAhead) && event.text.length === 1 && event.modifiers === Qt.NoModifier) {
// If View is not the Desktop there is no need to check if useTypeAhead is enabled.
typeAheadTimer.restart();
const charPressed = event.text.toLowerCase();
if (searchString.length >= 1 && searchString.indexOf(charPressed) !== 0) {
searchString += charPressed;
} else if (searchString !== charPressed) {
searchString = charPressed;
}
let matches = [];
for (let i = 0; i < gridView.count; i++) {
const itemData = positioner.data(positioner.index(i, 0), Qt.DisplayRole);
if (itemData && itemData.toLowerCase().indexOf(searchString) === 0) {
matches.push(i);
}
}
if (matches.length > 0) {
let nextIdx = matches[0];
if (searchString.length === 1) {
for (let j = 0; j < matches.length; j++) {
if (matches[j] > gridView.currentIndex) {
nextIdx = matches[j];
break;
}
}
}
currentIndex = nextIdx;
dir.clearSelection();
dir.setSelected(positioner.map(nextIdx));
} else {
searchString = charPressed;
}
}
// Fallback for everything else (Escape, Enter, or if Type-Ahead is disabled)
else {
event.accepted = false;
}
}

View file

@ -36,6 +36,11 @@
#include <qplatformdefs.h>
#include <KAuthorized>
#include <QDBusConnection>
#include <QDBusConnectionInterface>
#include <QStandardPaths>
#include <QQmlEngine>
#include <QJSEngine>
#include <KConfigGroup>
#include <KCoreDirLister>
#include <KDesktopFile>
@ -83,6 +88,65 @@ using namespace std::chrono_literals;
Q_LOGGING_CATEGORY(FOLDERMODEL, "plasma.containments.desktop.folder.foldermodel")
// Small helper exposed to QML to check whether KRunner is installed/running.
class KRunnerChecker : public QObject
{
Q_OBJECT
Q_PROPERTY(bool krunnerAvailable READ krunnerAvailable NOTIFY krunnerAvailableChanged)
public:
explicit KRunnerChecker(QObject *parent = nullptr)
: QObject(parent)
, m_krunnerAvailable(false)
{
const QString serviceName = QStringLiteral("org.kde.krunner");
const QString serviceFile = QStringLiteral("dbus-1/services/org.kde.krunner.service");
bool isRunning = QDBusConnection::sessionBus().interface()->isServiceRegistered(serviceName);
bool isInstalled = !QStandardPaths::locate(QStandardPaths::GenericDataLocation, serviceFile).isEmpty();
m_krunnerAvailable = isRunning || isInstalled;
auto iface = QDBusConnection::sessionBus().interface();
connect(iface, &QDBusConnectionInterface::serviceOwnerChanged, this, [this, serviceName, serviceFile](const QString &name, const QString &/*oldOwner*/, const QString &newOwner) {
if (name == serviceName) {
bool currentlyActive = !newOwner.isEmpty();
bool stillAvailable = currentlyActive;
// Fallback: If it stopped running, check if the service file still exists
if (!stillAvailable) {
stillAvailable = !QStandardPaths::locate(QStandardPaths::GenericDataLocation, serviceFile).isEmpty();
}
if (stillAvailable != m_krunnerAvailable) {
m_krunnerAvailable = stillAvailable;
Q_EMIT krunnerAvailableChanged();
}
}
});
}
bool krunnerAvailable() const { return m_krunnerAvailable; }
Q_SIGNALS:
void krunnerAvailableChanged();
private:
bool m_krunnerAvailable;
};
static QObject *krunner_checker_provider(QQmlEngine *, QJSEngine *)
{
return new KRunnerChecker();
}
static void registerKRunnerChecker()
{
qmlRegisterSingletonType<KRunnerChecker>("org.kde.private.desktopcontainment.folder", 1, 0, "KRunnerChecker", krunner_checker_provider);
}
Q_COREAPP_STARTUP_FUNCTION(registerKRunnerChecker)
class DragTrackerSingleton
{
public:
@ -2372,4 +2436,4 @@ bool FolderModel::isDeleteCommandShown()
return cg.readEntry("ShowDeleteCommand", false);
}
#include "moc_foldermodel.cpp"
#include "foldermodel.moc"

View file

@ -8,9 +8,6 @@
<entry name="freeFloating" key="FreeFloating" type="Bool">
<default>false</default>
</entry>
<entry name="activateWhenTypingOnDesktop" key="ActivateWhenTypingOnDesktop" type="Bool">
<default>true</default>
</entry>
<entry name="historyBehavior" type="Enum">
<label>Option to determine the behavior of KRunner's history and history completion</label>
<choices>

View file

@ -51,22 +51,6 @@ KCM.SimpleKCM {
Kirigami.FormData.isSection: false
}
QQC2.CheckBox {
Kirigami.FormData.label: i18nc("@label prefix for checkbutton", "Activation:")
checked: kcm.krunnerSettings.activateWhenTypingOnDesktop
onToggled: kcm.krunnerSettings.activateWhenTypingOnDesktop = checked
text: i18nc("@option:check", "Activate when pressing any key on the desktop")
KCM.SettingStateBinding {
configObject: kcm.krunnerSettings
settingName: "activateWhenTypingOnDesktop"
}
}
Item {
Kirigami.FormData.isSection: false
}
QQC2.RadioButton {
Kirigami.FormData.label: i18nc("@title:group prefix for radiobutton group", "History:")
checked: kcm.krunnerSettings.historyBehavior === KRunnerSettings.Disabled

View file

@ -0,0 +1,72 @@
#!/usr/bin/env sh
# SPDX-FileCopyrightText: 2026 Guillermo Steren <gstkein@gmail.com>
#
# SPDX-License-Identifier: GPL-2.0-or-later
# kconf update script to migrate users who previously disabled
# activateWhenTypingOnDesktop for KRunner. This script only flips
# users who explicitly set the key to "false" to "true" and
# leaves unset keys untouched.
# Minimal behavior:
# - Locate the user config file (prefer $HOME/.config/krunnerrc)
# - If the file exists and contains the key ActivateWhenTypingOnDesktop,
# set that key to "true" (make a backup first). Do not create the key.
# Locate config file
if [ -f "$HOME/.config/krunnerrc" ]; then
CONFIG_FILE="$HOME/.config/krunnerrc"
elif [ -n "${XDG_CONFIG_HOME:-}" ] && [ -f "${XDG_CONFIG_HOME}/krunnerrc" ]; then
CONFIG_FILE="${XDG_CONFIG_HOME}/krunnerrc"
else
# No existing user config found; nothing to do.
exit 0
fi
# Only modify if the key exists in the file
if grep -q '^[[:space:]]*ActivateWhenTypingOnDesktop[[:space:]]*=' "$CONFIG_FILE"; then
# Read the current value so we can react if it was explicitly "false"
OLDVAL=$(awk -F= '/^[[:space:]]*ActivateWhenTypingOnDesktop[[:space:]]*=/{gsub(/^[[:space:]]+|[[:space:]]+$/,"",$2); print $2; exit}' "$CONFIG_FILE")
cp -a "$CONFIG_FILE" "$CONFIG_FILE".bak 2>/dev/null || true
# Remove the key entirely from the user's config (migrate to default behavior).
sed -i '/^[[:space:]]*ActivateWhenTypingOnDesktop[[:space:]]*=.*/d' "$CONFIG_FILE"
# If the user explicitly disabled activateWhenTypingOnDesktop, also enable
# the containment/plasmoid option `useTypeAhead` where present.
if [ "${OLDVAL:-}" = "false" ]; then
SEARCH_DIR="${XDG_CONFIG_HOME:-$HOME/.config}"
# Prefer the standard applets file in $HOME/.config if present
candidates=()
if [ -f "$HOME/.config/plasma-org.kde.plasma.desktop-appletsrc" ]; then
candidates+=("$HOME/.config/plasma-org.kde.plasma.desktop-appletsrc")
fi
if [ -n "${XDG_CONFIG_HOME:-}" ] && [ -f "${XDG_CONFIG_HOME}/plasma-org.kde.plasma.desktop-appletsrc" ]; then
candidates+=("${XDG_CONFIG_HOME}/plasma-org.kde.plasma.desktop-appletsrc")
fi
# If none of the standard locations exist, search the config dir for matching files
if [ ${#candidates[@]} -eq 0 ]; then
while IFS= read -r -d '' f; do
candidates+=("$f")
done < <(find "$SEARCH_DIR" -type f -name 'plasma-org.kde.plasma.desktop-appletsrc*' -print0 2>/dev/null)
fi
for f in "${candidates[@]:-}"
do
if [ -z "$f" ] || [ ! -f "$f" ]; then
continue
fi
if grep -q '^[[:space:]]*useTypeAhead[[:space:]]*=' "$f" 2>/dev/null; then
cp -a "$f" "$f".bak 2>/dev/null || true
# Set any existing useTypeAhead assignments to true
sed -i 's/^[[:space:]]*useTypeAhead[[:space:]]*=.*/useTypeAhead=true/g' "$f"
fi
done
fi
fi
exit 0

View file

@ -0,0 +1,7 @@
# SPDX-FileCopyrightText: 2026 Guillermo Steren <gstkein@gmail.com>
# SPDX-License-Identifier: GPL-2.0-only OR GPL-3.0-only OR LicenseRef-KDE-Accepted-GPL
Version=6
Id=50-krunner-activate-typing
Script=50-krunner-activate-typing.sh,sh