mirror of
https://invent.kde.org/plasma/plasma-desktop.git
synced 2026-11-03 22:59:45 -09:00
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:
parent
20c98065bc
commit
81f846f92d
9 changed files with 232 additions and 21 deletions
|
|
@ -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})
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
72
kconf_update/50-krunner-activate-typing.sh
Executable file
72
kconf_update/50-krunner-activate-typing.sh
Executable 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
|
||||
7
kconf_update/50-krunner-activate-typing.upd
Normal file
7
kconf_update/50-krunner-activate-typing.upd
Normal 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
|
||||
Loading…
Reference in a new issue