├── templates ├── runner6 │ ├── runner.png │ ├── Messages.sh │ ├── uninstall.sh │ ├── src │ │ ├── CMakeLists.txt │ │ ├── %{APPNAMELC}.json │ │ ├── %{APPNAMELC}.h │ │ └── %{APPNAMELC}.cpp │ ├── install.sh │ ├── README.md │ ├── CMakeLists.txt │ └── runner.kdevtemplate ├── runner6python │ ├── runner.png │ ├── krunner-plugininstallerrc │ ├── uninstall.sh │ ├── %{APPNAMELC}.desktop │ ├── install.sh │ ├── README.md │ ├── main.py │ └── runnerpy.kdevtemplate ├── .clang-format └── CMakeLists.txt ├── autotests ├── plugins │ ├── metadatafile1.json.license │ ├── plasma-runner-testconversionfile.desktop │ ├── metadatafile2.desktop │ ├── metadatafile1.desktop │ ├── fakerunnerplugin.cpp │ ├── metadatafile1.json │ ├── dbusrunnertestmulti.desktop │ ├── dbusrunnertestruntimeconfig.desktop │ ├── dbusrunnertest.desktop │ ├── plasma-runner-dbusrunnertest.desktop │ ├── testremoterunner.h │ ├── suspendedrunner.cpp │ ├── fakerunner.h │ └── testremoterunner.cpp ├── testmetadataconversion.cpp ├── modelwidgettest.cpp ├── pluginbenchmarker.cpp ├── CMakeLists.txt ├── threadingtest.cpp ├── runnermatchmethodstest.cpp ├── runnermanagerhistorytest.cpp ├── runnermanagersinglerunnermodetest.cpp ├── runnermanagertest.cpp └── dbusrunnertest.cpp ├── README.md.license ├── .git-blame-ignore-revs ├── ExtraDesktop.sh ├── src ├── krunner.qdoc ├── krunner-index.qdoc ├── krunner.qdocconf ├── action.cpp ├── abstractrunner_p.h ├── dbusrunner_p.h ├── runnersyntax.cpp ├── model │ ├── runnerresultsmodel_p.h │ ├── resultsmodel.h │ ├── runnerresultsmodel.cpp │ └── resultsmodel.cpp ├── action.h ├── kpluginmetadata_utils_p.h ├── runnersyntax.h ├── CMakeLists.txt ├── dbusutils_p.h ├── data │ └── org.kde.krunner1.xml ├── runnercontext.h ├── abstractrunner.cpp ├── abstractrunnertest.h ├── querymatch.cpp ├── runnercontext.cpp ├── runnermanager.h ├── querymatch.h └── abstractrunner.h ├── metainfo.yaml ├── .gitlab-ci.yml ├── .gitignore ├── KF6RunnerConfig.cmake.in ├── .kde-ci.yml ├── README.md ├── LICENSES ├── LicenseRef-KDE-Accepted-LGPL.txt ├── BSD-2-Clause.txt └── CC0-1.0.txt ├── REUSE.toml ├── KF6KRunnerMacros.cmake └── CMakeLists.txt /templates/runner6/runner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KDE/krunner/HEAD/templates/runner6/runner.png -------------------------------------------------------------------------------- /autotests/plugins/metadatafile1.json.license: -------------------------------------------------------------------------------- 1 | SPDX-FileCopyrightText: none 2 | SPDX-License-Identifier: CC0-1.0 3 | -------------------------------------------------------------------------------- /templates/runner6python/runner.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/KDE/krunner/HEAD/templates/runner6python/runner.png -------------------------------------------------------------------------------- /templates/runner6/Messages.sh: -------------------------------------------------------------------------------- 1 | #! /usr/bin/env bash 2 | $XGETTEXT src/*.cpp -o $podir/plasma_runner_org.kde.%{APPNAMELC}.pot 3 | -------------------------------------------------------------------------------- /README.md.license: -------------------------------------------------------------------------------- 1 | SPDX-FileCopyrightText: Alexander Lohnau 2 | SPDX-License-Identifier: CC0-1.0 3 | 4 | -------------------------------------------------------------------------------- /.git-blame-ignore-revs: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: none 2 | # SPDX-License-Identifier: CC0-1.0 3 | 4 | # clang-format 5 | 4c453cecbed22d9947c3ec58fb2d5eaff8bc6a8d 6 | -------------------------------------------------------------------------------- /templates/.clang-format: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2021 Alexander Lohnau 2 | # SPDX-License-Identifier: CC0-1.0 3 | DisableFormat: true 4 | SortIncludes: false 5 | -------------------------------------------------------------------------------- /autotests/plugins/plasma-runner-testconversionfile.desktop: -------------------------------------------------------------------------------- 1 | [Desktop Entry] 2 | Type=Service 3 | Name=DBus runner test 4 | Comment=Some Comment 5 | X-KDE-PluginInfo-Name=testconversionfile 6 | -------------------------------------------------------------------------------- /autotests/plugins/metadatafile2.desktop: -------------------------------------------------------------------------------- 1 | [Desktop Entry] 2 | Name=DBus runner test 3 | X-KDE-ServiceTypes=Plasma/Runner 4 | Type=Service 5 | X-Plasma-Runner-Unique-Results=true 6 | X-Plasma-Runner-Weak-Results=false 7 | -------------------------------------------------------------------------------- /templates/runner6python/krunner-plugininstallerrc: -------------------------------------------------------------------------------- 1 | # File for krunner-plugininstaller executable. This allows for easier installation in the "Get New Plugins" button of the config module 2 | MetaDataFile=%{APPNAMELC}.desktop 3 | Exec=%{PROJECTDIR}/main.py 4 | -------------------------------------------------------------------------------- /autotests/plugins/metadatafile1.desktop: -------------------------------------------------------------------------------- 1 | [Desktop Entry] 2 | Name=DBus runner test 3 | X-KDE-ServiceTypes=Plasma/Runner 4 | Type=Service 5 | X-KDE-PluginInfo-EnabledByDefault=true 6 | X-Plasma-Runner-Unique-Results=true 7 | X-Plasma-Runner-Weak-Results=true 8 | -------------------------------------------------------------------------------- /ExtraDesktop.sh: -------------------------------------------------------------------------------- 1 | #! /bin/sh 2 | 3 | # SPDX-FileCopyrightText: none 4 | # SPDX-License-Identifier: CC0-1.0 5 | 6 | #This file outputs in a separate line each file with a .desktop syntax 7 | #that needs to be translated but has a non .desktop extension 8 | find -name \*.kdevtemplate -print 9 | -------------------------------------------------------------------------------- /templates/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: KDE Contributors 2 | # SPDX-License-Identifier: BSD-2-Clause 3 | 4 | set(apptemplate_DIRS 5 | runner6 6 | runner6python 7 | ) 8 | 9 | kde_package_app_templates(TEMPLATES ${apptemplate_DIRS} INSTALL_DIR ${KDE_INSTALL_KAPPTEMPLATESDIR}) 10 | -------------------------------------------------------------------------------- /templates/runner6/uninstall.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Exit if something fails 4 | set -e 5 | 6 | cd build 7 | sudo make uninstall 8 | 9 | # KRunner needs to be restarted for the changes to be applied 10 | # we can just kill it and it will be started when the shortcut is invoked 11 | kquitapp6 krunner 12 | -------------------------------------------------------------------------------- /src/krunner.qdoc: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: CC-BY-SA-4.0 2 | // SPDX-FileCopyrightText: None 3 | 4 | /*! 5 | \module KRunner 6 | \title KRunner C++ Classes 7 | \ingroup modules 8 | \cmakepackage KF6 9 | \cmakecomponent Runner 10 | 11 | \brief Framework for Plasma runners. 12 | */ 13 | -------------------------------------------------------------------------------- /autotests/plugins/fakerunnerplugin.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | SPDX-FileCopyrightText: 2021 Alexander Lohnau 3 | SPDX-License-Identifier: LGPL-2.0-or-later 4 | */ 5 | 6 | #include "fakerunner.h" 7 | 8 | K_PLUGIN_CLASS_WITH_JSON(FakeRunner, "metadatafile1.json") 9 | 10 | #include "fakerunnerplugin.moc" 11 | -------------------------------------------------------------------------------- /autotests/plugins/metadatafile1.json: -------------------------------------------------------------------------------- 1 | { 2 | "KPlugin": { 3 | "EnabledByDefault": true, 4 | "Name": "DBus runner test", 5 | "ServiceTypes": [ 6 | "Plasma/Runner" 7 | ] 8 | }, 9 | "X-Plasma-Runner-Unique-Results": true, 10 | "X-Plasma-Runner-Weak-Results": true 11 | } 12 | -------------------------------------------------------------------------------- /templates/runner6python/uninstall.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Exit if something fails 4 | set -e 5 | 6 | prefix="${XDG_DATA_HOME:-$HOME/.local/share}" 7 | krunner_dbusdir="$prefix/krunner/dbusplugins" 8 | 9 | rm $prefix/dbus-1/services/org.kde.%{APPNAMELC}.service 10 | rm $krunner_dbusdir/%{APPNAMELC}.desktop 11 | kquitapp6 krunner 12 | 13 | -------------------------------------------------------------------------------- /templates/runner6/src/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | add_definitions(-DTRANSLATION_DOMAIN=\"plasma_runner_org.kde.%{APPNAMELC}\") 2 | 3 | kcoreaddons_add_plugin(%{APPNAMELC} INSTALL_NAMESPACE "kf6/krunner") 4 | 5 | target_sources(%{APPNAMELC} PRIVATE 6 | %{APPNAMELC}.cpp 7 | ) 8 | 9 | target_link_libraries(%{APPNAMELC} 10 | Qt6::Gui # QAction 11 | KF6::Runner 12 | KF6::I18n 13 | ) 14 | -------------------------------------------------------------------------------- /templates/runner6/install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Exit if something fails 4 | set -e 5 | 6 | mkdir -p build 7 | cd build 8 | 9 | cmake -DKDE_INSTALL_USE_QT_SYS_PATHS=ON -DCMAKE_BUILD_TYPE=RelWithDebInfo .. 10 | make -j$(nproc) 11 | sudo make install 12 | 13 | # KRunner needs to be restarted for the changes to be applied 14 | # we can just kill it and it will be started when the shortcut is invoked 15 | kquitapp6 krunner 16 | -------------------------------------------------------------------------------- /metainfo.yaml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: none 2 | # SPDX-License-Identifier: CC0-1.0 3 | 4 | description: Parallelized query system 5 | tier: 3 6 | type: solution 7 | platforms: 8 | - name: Linux 9 | - name: FreeBSD 10 | portingAid: false 11 | deprecated: false 12 | release: true 13 | libraries: 14 | - cmake: "KF6::Runner" 15 | cmakename: KF6Runner 16 | 17 | public_lib: true 18 | group: Frameworks 19 | subgroup: Tier 3 20 | -------------------------------------------------------------------------------- /.gitlab-ci.yml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2020 Volker Krause 2 | # SPDX-License-Identifier: CC0-1.0 3 | 4 | include: 5 | - project: sysadmin/ci-utilities 6 | file: 7 | - /gitlab-templates/linux-qt6.yml 8 | - /gitlab-templates/linux-qt6-next.yml 9 | - /gitlab-templates/reuse-lint.yml 10 | - /gitlab-templates/freebsd-qt6.yml 11 | - /gitlab-templates/xml-lint.yml 12 | - /gitlab-templates/yaml-lint.yml 13 | -------------------------------------------------------------------------------- /templates/runner6/src/%{APPNAMELC}.json: -------------------------------------------------------------------------------- 1 | { 2 | "KPlugin": { 3 | "Authors": [ 4 | { 5 | "Email": "%{EMAIL}", 6 | "Name": "%{AUTHOR}" 7 | } 8 | ], 9 | "Description": "%{APPNAME} runner", 10 | "EnabledByDefault": true, 11 | "Icon": "planetkde", 12 | "License": "LGPL 2.1+", 13 | "Name": "%{APPNAME}", 14 | "Version": "0.1" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /templates/runner6python/%{APPNAMELC}.desktop: -------------------------------------------------------------------------------- 1 | [Desktop Entry] 2 | Name=%{APPNAME} 3 | Comment=%{APPNAME} Runner written in Python 4 | Type=Service 5 | Icon=planetkde 6 | X-KDE-PluginInfo-Author=%{AUTHOR} 7 | X-KDE-PluginInfo-Email=%{EMAIL} 8 | X-KDE-PluginInfo-Name=%{APPNAMELC} 9 | X-KDE-PluginInfo-Version=1.0 10 | X-KDE-PluginInfo-License=LGPL 11 | X-KDE-PluginInfo-EnabledByDefault=true 12 | X-Plasma-API=DBus 13 | X-Plasma-DBusRunner-Service=org.kde.%{APPNAMELC} 14 | -------------------------------------------------------------------------------- /templates/runner6/README.md: -------------------------------------------------------------------------------- 1 | # %{APPNAME} 2 | 3 | ### Build instructions 4 | 5 | After installing the required headers and CMake config files on your distro, the `install.sh` script can be run. 6 | 7 | After this the runner shows up in systemsettings: 8 | `systemsettings kcm_plasmasearch` 9 | 10 | You can also launch KRunner via Alt-F2 or Alt-Space and you will find your runner. 11 | 12 | If you feel confident about your runner you can upload it to the KDE Store 13 | https://store.kde.org/browse?cat=628&ord=latest. 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: none 2 | # SPDX-License-Identifier: CC0-1.0 3 | 4 | # Ignore the following files 5 | *~ 6 | *.[oa] 7 | *.diff 8 | *.kate-swp 9 | *.kdev4 10 | .kdev_include_paths 11 | *.kdevelop.pcs 12 | *.moc 13 | *.moc.cpp 14 | *.orig 15 | *.user 16 | .*.swp 17 | .swp.* 18 | Doxyfile 19 | Makefile 20 | avail 21 | random_seed 22 | /build*/ 23 | CMakeLists.txt.user* 24 | *.unc-backup* 25 | .cmake/ 26 | .clang-format 27 | cmake-build-debug* 28 | .idea 29 | .vscode 30 | /compile_commands.json 31 | .clangd 32 | .cache 33 | -------------------------------------------------------------------------------- /templates/runner6python/install.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # Standalone install script for copying files 4 | 5 | set -e 6 | 7 | prefix="${XDG_DATA_HOME:-$HOME/.local/share}" 8 | krunner_dbusdir="$prefix/krunner/dbusplugins" 9 | services_dir="$prefix/dbus-1/services/" 10 | 11 | mkdir -p $krunner_dbusdir 12 | mkdir -p $services_dir 13 | 14 | cp %{APPNAMELC}.desktop $krunner_dbusdir 15 | printf "[D-BUS Service]\nName=org.kde.%{APPNAMELC}\nExec=\"$PWD/main.py\"" > $services_dir/org.kde.%{APPNAMELC}.service 16 | 17 | kquitapp6 krunner 18 | 19 | -------------------------------------------------------------------------------- /autotests/plugins/dbusrunnertestmulti.desktop: -------------------------------------------------------------------------------- 1 | [Desktop Entry] 2 | Name=DBus runner testmulti 3 | Comment=DBus runner testmulti 4 | X-KDE-ServiceTypes=Plasma/Runner 5 | Type=Service 6 | Icon=internet-web-browser 7 | X-KDE-PluginInfo-Author=Some Developer 8 | X-KDE-PluginInfo-Email=kde@example.com 9 | X-KDE-PluginInfo-Name=dbusrunnertestmulti 10 | X-KDE-PluginInfo-Version=1.0 11 | X-KDE-PluginInfo-License=LGPL 12 | X-KDE-PluginInfo-EnabledByDefault=true 13 | X-Plasma-API=DBus 14 | X-Plasma-DBusRunner-Service=net.krunnertests.multi.* 15 | X-Plasma-DBusRunner-Path=/dave 16 | -------------------------------------------------------------------------------- /KF6RunnerConfig.cmake.in: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: Friedrich W. H. Kossebau 2 | # SPDX-FileCopyrightText: Aleix Pol 3 | # SPDX-License-Identifier: BSD-2-Clause 4 | 5 | @PACKAGE_INIT@ 6 | 7 | include(CMakeFindDependencyMacro) 8 | find_dependency(Qt6 @REQUIRED_QT_VERSION@ CONFIG REQUIRED Core) 9 | find_dependency(KF6CoreAddons "@KF_DEP_VERSION@") # KPluginFactory 10 | 11 | @PACKAGE_SETUP_AUTOMOC_VARIABLES@ 12 | 13 | include("${CMAKE_CURRENT_LIST_DIR}/KF6RunnerTargets.cmake") 14 | include("${CMAKE_CURRENT_LIST_DIR}/KF6KRunnerMacros.cmake") 15 | -------------------------------------------------------------------------------- /src/krunner-index.qdoc: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: CC-BY-SA-4.0 2 | // SPDX-FileCopyrightText: None 3 | 4 | /*! 5 | \page krunner-index.html 6 | \title KRunner 7 | 8 | Framework for Plasma runners. 9 | 10 | \section1 Using the Module 11 | 12 | \include {module-use.qdocinc} {using the c++ api} 13 | 14 | \section2 Building with CMake 15 | 16 | \include {module-use.qdocinc} {building with cmake} {KF6} {Runner} {KF6::Runner} 17 | 18 | \section1 API Reference 19 | 20 | \list 21 | \li \l{KRunner C++ Classes} 22 | \endlist 23 | */ 24 | -------------------------------------------------------------------------------- /.kde-ci.yml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: none 2 | # SPDX-License-Identifier: CC0-1.0 3 | 4 | Dependencies: 5 | - 'on': ['Linux', 'FreeBSD', 'Windows', 'macOS'] 6 | 'require': 7 | 'frameworks/extra-cmake-modules': '@same' 8 | 'frameworks/kconfig': '@same' 9 | 'frameworks/kcoreaddons': '@same' 10 | 'frameworks/threadweaver': '@same' 11 | 'frameworks/ki18n': '@same' 12 | 'frameworks/kitemmodels': '@same' 13 | 'frameworks/kwindowsystem': '@same' 14 | 15 | Options: 16 | test-before-installing: True 17 | require-passing-tests-on: ['Linux', 'FreeBSD'] 18 | cppcheck-ignore-files: ['templates/'] 19 | -------------------------------------------------------------------------------- /templates/runner6/src/%{APPNAMELC}.h: -------------------------------------------------------------------------------- 1 | /* 2 | SPDX-FileCopyrightText: %{CURRENT_YEAR} %{AUTHOR} <%{EMAIL}> 3 | 4 | SPDX-License-Identifier: LGPL-2.1-or-later 5 | */ 6 | 7 | #pragma once 8 | 9 | #include 10 | 11 | class %{APPNAME} : public KRunner::AbstractRunner 12 | { 13 | Q_OBJECT 14 | 15 | public: 16 | %{APPNAME}(QObject *parent, const KPluginMetaData &data); 17 | 18 | // KRunner::AbstractRunner API 19 | void match(KRunner::RunnerContext &context) override; 20 | void run(const KRunner::RunnerContext &context, const KRunner::QueryMatch &match) override; 21 | }; 22 | -------------------------------------------------------------------------------- /autotests/plugins/dbusrunnertestruntimeconfig.desktop: -------------------------------------------------------------------------------- 1 | [Desktop Entry] 2 | Name=DBus runner test 3 | Comment=DBus runner test 4 | X-KDE-ServiceTypes=Plasma/Runner 5 | Type=Service 6 | Icon=internet-web-browser 7 | X-KDE-PluginInfo-Author=Some Developer 8 | X-KDE-PluginInfo-Email=kde@example.com 9 | X-KDE-PluginInfo-Name=dbusrunnertest 10 | X-KDE-PluginInfo-Version=1.0 11 | X-KDE-PluginInfo-License=LGPL 12 | X-KDE-PluginInfo-EnabledByDefault=true 13 | X-Plasma-API=DBus2 14 | X-Plasma-DBusRunner-Service=net.krunnertests.dave 15 | X-Plasma-DBusRunner-Path=/dave 16 | X-Plasma-Request-Actions-Once=true 17 | X-Plasma-Runner-Syntaxes=syntax1,syntax2 18 | X-Plasma-Runner-Syntax-Descriptions=description1,description2 19 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # KRunner 2 | 3 | Framework for Plasma runners 4 | 5 | ## Introduction 6 | 7 | The Plasma workspace provides an application called KRunner which, among other 8 | things, allows one to type into a text area which causes various actions and 9 | information that match the text appear as the text is being typed. 10 | 11 | One application for this is the universal runner you can launch with ALT-F2. 12 | 13 | This functionality is provided via plugins loaded at runtime called "Runners". 14 | These plugins can be used by any application using the Plasma library. The 15 | KRunner framework is used to write these plugins, as explained in 16 | [this tutorial](https://develop.kde.org/docs/plasma/krunner/) 17 | 18 | -------------------------------------------------------------------------------- /templates/runner6/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.16) 2 | 3 | project(%{APPNAMELC}) 4 | 5 | set(QT_MIN_VERSION "6.5.0") 6 | set(KF_MIN_VERSION "6.0.0") 7 | 8 | find_package(ECM ${KF_MIN_VERSION} REQUIRED NO_MODULE) 9 | set(CMAKE_MODULE_PATH ${ECM_MODULE_PATH}) 10 | find_package(Qt6 ${QT_MIN_VERSION} CONFIG REQUIRED COMPONENTS Core Gui) 11 | find_package(KF6 ${KF_MIN_VERSION} REQUIRED COMPONENTS Runner I18n) 12 | 13 | include(KDEInstallDirs) 14 | include(KDEClangFormat) 15 | include(KDECMakeSettings) 16 | include(KDECompilerSettings NO_POLICY_SCOPE) 17 | include(FeatureSummary) 18 | 19 | add_subdirectory(src) 20 | 21 | feature_summary(WHAT ALL INCLUDE_QUIET_PACKAGES FATAL_ON_MISSING_REQUIRED_PACKAGES) 22 | -------------------------------------------------------------------------------- /LICENSES/LicenseRef-KDE-Accepted-LGPL.txt: -------------------------------------------------------------------------------- 1 | This library is free software; you can redistribute it and/or 2 | modify it under the terms of the GNU Lesser General Public 3 | License as published by the Free Software Foundation; either 4 | version 3 of the license or (at your option) any later version 5 | that is accepted by the membership of KDE e.V. (or its successor 6 | approved by the membership of KDE e.V.), which shall act as a 7 | proxy as defined in Section 6 of version 3 of the license. 8 | 9 | This program is distributed in the hope that it will be useful, 10 | but WITHOUT ANY WARRANTY; without even the implied warranty of 11 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 12 | GNU General Public License for more details. 13 | -------------------------------------------------------------------------------- /autotests/plugins/dbusrunnertest.desktop: -------------------------------------------------------------------------------- 1 | [Desktop Entry] 2 | Name=DBus runner test 3 | Comment=DBus runner test 4 | X-KDE-ServiceTypes=Plasma/Runner 5 | Type=Service 6 | Icon=internet-web-browser 7 | X-KDE-PluginInfo-Author=Some Developer 8 | X-KDE-PluginInfo-Email=kde@example.com 9 | X-KDE-PluginInfo-Name=dbusrunnertest 10 | X-KDE-PluginInfo-Version=1.0 11 | X-KDE-PluginInfo-License=LGPL 12 | X-KDE-PluginInfo-EnabledByDefault=true 13 | X-Plasma-API=DBus 14 | X-Plasma-DBusRunner-Service=net.krunnertests.dave 15 | X-Plasma-DBusRunner-Path=/dave 16 | X-Plasma-Request-Actions-Once=true 17 | X-Plasma-Runner-Min-Letter-Count=3 18 | X-Plasma-Runner-Match-Regex=^fo 19 | X-Plasma-Runner-Syntaxes=syntax1,syntax2 20 | X-Plasma-Runner-Syntax-Descriptions=description1,description2 21 | -------------------------------------------------------------------------------- /autotests/plugins/plasma-runner-dbusrunnertest.desktop: -------------------------------------------------------------------------------- 1 | [Desktop Entry] 2 | Name=DBus runner test 3 | Comment=DBus runner test 4 | X-KDE-ServiceTypes=Plasma/Runner 5 | Type=Service 6 | Icon=internet-web-browser 7 | X-KDE-PluginInfo-Author=Some Developer 8 | X-KDE-PluginInfo-Email=kde@example.com 9 | X-KDE-PluginInfo-Name=dbusrunnertest 10 | X-KDE-PluginInfo-Version=1.0 11 | X-KDE-PluginInfo-License=LGPL 12 | X-KDE-PluginInfo-EnabledByDefault=true 13 | X-Plasma-API=DBus 14 | X-Plasma-DBusRunner-Service=net.krunnertests.dave 15 | X-Plasma-DBusRunner-Path=/dave 16 | X-Plasma-Request-Actions-Once=true 17 | X-Plasma-Runner-Min-Letter-Count=3 18 | X-Plasma-Runner-Match-Regex=^fo 19 | X-Plasma-Runner-Syntaxes=syntax1,syntax2 20 | X-Plasma-Runner-Syntax-Descriptions=description1,description2 21 | -------------------------------------------------------------------------------- /autotests/plugins/testremoterunner.h: -------------------------------------------------------------------------------- 1 | /* 2 | SPDX-FileCopyrightText: 2017 David Edmundson 3 | 4 | SPDX-License-Identifier: LGPL-2.0-or-later 5 | */ 6 | 7 | #pragma once 8 | 9 | #include "../src/dbusutils_p.h" 10 | #include 11 | #include 12 | 13 | class TestRemoteRunner : public QObject 14 | { 15 | Q_OBJECT 16 | public: 17 | explicit TestRemoteRunner(const QString &serviceName, bool showLifecycleMethodCalls); 18 | 19 | public Q_SLOTS: 20 | KRunner::Actions Actions(); 21 | RemoteMatches Match(const QString &searchTerm); 22 | void SetActivationToken(const QString &token); 23 | void Run(const QString &id, const QString &actionId); 24 | void Teardown(); 25 | QVariantMap Config(); 26 | 27 | private: 28 | bool m_showLifecycleMethodCalls = false; 29 | }; 30 | -------------------------------------------------------------------------------- /REUSE.toml: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: Alexander Lohnau 2 | # SPDX-License-Identifier: CC0-1.0 3 | 4 | version = 1 5 | SPDX-PackageName = "krunner" 6 | SPDX-PackageSupplier = "Alexander Lohnau " 7 | SPDX-PackageDownloadLocation = "https://invent.kde.org/frameworks/krunner" 8 | 9 | [[annotations]] 10 | path = "autotests/**.desktop" 11 | precedence = "aggregate" 12 | SPDX-FileCopyrightText = "KRunner contributors" 13 | SPDX-License-Identifier = "CC0-1.0" 14 | 15 | [[annotations]] 16 | path = "templates/runner6python/**" 17 | precedence = "aggregate" 18 | SPDX-FileCopyrightText = "Alexander Lohnau" 19 | SPDX-License-Identifier = "LGPL-2.1-or-later" 20 | 21 | [[annotations]] 22 | path = "templates/runner6/**" 23 | precedence = "aggregate" 24 | SPDX-FileCopyrightText = "KDE Contributors" 25 | SPDX-License-Identifier = "LGPL-2.1-or-later" 26 | -------------------------------------------------------------------------------- /autotests/testmetadataconversion.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | SPDX-FileCopyrightText: 2021 Alexander Lohnau 3 | SPDX-License-Identifier: LGPL-2.1-or-later 4 | */ 5 | 6 | #include "kpluginmetadata_utils_p.h" 7 | #include 8 | 9 | class TestMetaDataConversion : public QObject 10 | { 11 | Q_OBJECT 12 | 13 | private Q_SLOTS: 14 | void testMetaDataConversion() 15 | { 16 | const KPluginMetaData data = parseMetaDataFromDesktopFile(QFINDTESTDATA("plugins/plasma-runner-testconversionfile.desktop")); 17 | QVERIFY(data.isValid()); 18 | QCOMPARE(data.pluginId(), "testconversionfile"); 19 | QCOMPARE(data.name(), "DBus runner test"); 20 | QCOMPARE(data.description(), "Some Comment"); 21 | } 22 | }; 23 | 24 | QTEST_MAIN(TestMetaDataConversion) 25 | 26 | #include "testmetadataconversion.moc" 27 | -------------------------------------------------------------------------------- /autotests/plugins/suspendedrunner.cpp: -------------------------------------------------------------------------------- 1 | 2 | /* 3 | SPDX-FileCopyrightText: 2023 Alexander Lohnau 4 | SPDX-License-Identifier: LGPL-2.0-or-later 5 | */ 6 | 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | 13 | using namespace KRunner; 14 | 15 | class SuspendedRunner : public AbstractRunner 16 | { 17 | public: 18 | explicit SuspendedRunner(QObject *parent, const KPluginMetaData &metadata) 19 | : AbstractRunner(parent, metadata) 20 | { 21 | } 22 | void reloadConfiguration() override 23 | { 24 | QThread::msleep(3000); 25 | } 26 | 27 | void match(RunnerContext &context) override 28 | { 29 | QueryMatch m(this); 30 | m.setText("bla"); 31 | context.addMatch(m); 32 | } 33 | }; 34 | 35 | K_PLUGIN_CLASS_WITH_JSON(SuspendedRunner, "metadatafile1.json") 36 | 37 | #include "suspendedrunner.moc" 38 | -------------------------------------------------------------------------------- /templates/runner6python/README.md: -------------------------------------------------------------------------------- 1 | ### %{APPNAME} 2 | 3 | This plugin provides a simple template for a KRunner plugin using dbus. 4 | 5 | The install script copies the KRunner config file and a D-Bus activation service file to their appropriate locations. 6 | This way the Python script gets executed when KRunner requests matches and it does not need to be autostarted. 7 | 8 | ```bash 9 | mkdir -p ~/.local/share/krunner/dbusplugins/ 10 | cp %{APPNAMELC}.desktop ~/.local/share/krunner/dbusplugins/ 11 | kquitapp6 krunner 12 | python3 %{APPNAMELC}.py 13 | ``` 14 | 15 | After that you should see your runner when typing `hello` in KRunner. 16 | 17 | More information regarding the D-Bus API can be found here: 18 | 19 | * https://invent.kde.org/frameworks/krunner/-/blob/master/src/data/org.kde.krunner1.xml 20 | * https://develop.kde.org/docs/features/d-bus/introduction_to_dbus/ 21 | 22 | 23 | If you feel confident about your runner you can upload it to the KDE Store https://store.kde.org/browse?cat=628&ord=latest. 24 | -------------------------------------------------------------------------------- /src/krunner.qdocconf: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: CC-BY-SA-4.0 2 | # SPDX-FileCopyrightText: None 3 | 4 | include($KDE_DOCS/global/qt-module-defaults.qdocconf) 5 | 6 | project = KRunner 7 | description = Framework for Plasma runners 8 | 9 | documentationinheaders = true 10 | 11 | headerdirs += . 12 | sourcedirs += . 13 | 14 | outputformats = HTML 15 | 16 | navigation.landingpage = "KRunner" 17 | 18 | depends += kde qtcore qtgui 19 | 20 | qhp.projects = KRunner 21 | 22 | qhp.KRunner.file = krunner.qhp 23 | qhp.KRunner.namespace = org.kde.krunner.$QT_VERSION_TAG 24 | qhp.KRunner.virtualFolder = krunner 25 | qhp.KRunner.indexTitle = KRunner 26 | qhp.KRunner.indexRoot = 27 | 28 | qhp.KRunner.subprojects = classes 29 | qhp.KRunner.subprojects.classes.title = C++ Classes 30 | qhp.KRunner.subprojects.classes.indexTitle = KRunner C++ Classes 31 | qhp.KRunner.subprojects.classes.selectors = class fake:headerfile 32 | qhp.KRunner.subprojects.classes.sortPages = true 33 | 34 | tagfile = krunner.tags 35 | -------------------------------------------------------------------------------- /templates/runner6/src/%{APPNAMELC}.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | SPDX-FileCopyrightText: %{CURRENT_YEAR} %{AUTHOR} <%{EMAIL}> 3 | 4 | SPDX-License-Identifier: LGPL-2.1-or-later 5 | */ 6 | 7 | #include "%{APPNAMELC}.h" 8 | 9 | #include 10 | 11 | %{APPNAME}::%{APPNAME}(QObject *parent, const KPluginMetaData &data) 12 | : KRunner::AbstractRunner(parent, data) 13 | { 14 | // Provide usage help for this plugin 15 | addSyntax(QStringLiteral("sometriggerword :q:"), i18n("Description for this syntax")); 16 | } 17 | 18 | void %{APPNAME}::match(KRunner::RunnerContext &context) 19 | { 20 | const QString term = context.query(); 21 | if (term.compare(QLatin1String("hello"), Qt::CaseInsensitive) == 0) { 22 | KRunner::QueryMatch match(this); 23 | match.setText(i18n("Hello from %{APPNAME}")); 24 | context.addMatch(match); 25 | } 26 | } 27 | 28 | void %{APPNAME}::run(const KRunner::RunnerContext &context, const KRunner::QueryMatch &match) 29 | { 30 | Q_UNUSED(context) 31 | Q_UNUSED(match) 32 | 33 | // TODO 34 | } 35 | 36 | K_PLUGIN_CLASS_WITH_JSON(%{APPNAME}, "%{APPNAMELC}.json") 37 | 38 | // needed for the QObject subclass declared as part of K_PLUGIN_CLASS_WITH_JSON 39 | #include "%{APPNAMELC}.moc" 40 | 41 | #include "moc_%{APPNAMELC}.cpp" 42 | -------------------------------------------------------------------------------- /LICENSES/BSD-2-Clause.txt: -------------------------------------------------------------------------------- 1 | Copyright (c) All rights reserved. 2 | 3 | Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: 4 | 5 | 1. Redistributions of source code must retain the above copyright notice, this list of conditions and the following disclaimer. 6 | 7 | 2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions and the following disclaimer in the documentation and/or other materials provided with the distribution. 8 | 9 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 10 | -------------------------------------------------------------------------------- /templates/runner6python/main.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/python3 2 | 3 | import dbus.service 4 | from dbus.mainloop.glib import DBusGMainLoop 5 | from gi.repository import GLib 6 | 7 | DBusGMainLoop(set_as_default=True) 8 | 9 | objpath = "/runner" # Default value for X-Plasma-DBusRunner-Path metadata property 10 | iface = "org.kde.krunner1" 11 | 12 | 13 | class Runner(dbus.service.Object): 14 | def __init__(self): 15 | dbus.service.Object.__init__(self, dbus.service.BusName("org.kde.%{APPNAMELC}", dbus.SessionBus()), objpath) 16 | 17 | @dbus.service.method(iface, in_signature='s', out_signature='a(sssida{sv})') 18 | def Match(self, query: str): 19 | """This method is used to get the matches and it returns a list of tupels""" 20 | if query == "hello": 21 | # data, text, icon, type (KRunner::QueryType), relevance (0-1), properties (subtext, category, multiline(bool) and urls) 22 | return [("Hello", "Hello from %{APPNAME}!", "document-edit", 100, 1.0, {'subtext': 'Demo Subtext'})] 23 | return [] 24 | 25 | @dbus.service.method(iface, out_signature='a(sss)') 26 | def Actions(self): 27 | # id, text, icon 28 | return [("id", "Tooltip", "planetkde")] 29 | 30 | @dbus.service.method(iface, in_signature='ss') 31 | def Run(self, data: str, action_id: str): 32 | print(data, action_id) 33 | 34 | 35 | runner = Runner() 36 | loop = GLib.MainLoop() 37 | loop.run() 38 | -------------------------------------------------------------------------------- /autotests/modelwidgettest.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | SPDX-FileCopyrightText: 2023 Alexander Lohnau 3 | SPDX-License-Identifier: LGPL-2.0-or-later 4 | */ 5 | 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | 12 | #include 13 | 14 | using namespace KRunner; 15 | 16 | class TestObject : public QWidget 17 | { 18 | Q_OBJECT 19 | public: 20 | explicit TestObject() 21 | : QWidget() 22 | { 23 | auto *model = new ResultsModel(this); 24 | // Comment this line out to load all the system runners 25 | model->runnerManager()->loadRunner(KPluginMetaData::findPluginById(QStringLiteral("krunnertest"), QStringLiteral("fakerunnerplugin"))); 26 | 27 | auto *view = new QListView(this); 28 | view->setModel(model); 29 | view->setAlternatingRowColors(true); 30 | 31 | auto *edit = new QLineEdit(this); 32 | connect(edit, &QLineEdit::textChanged, model, &ResultsModel::setQueryString); 33 | edit->setText("foo"); 34 | 35 | auto *l = new QVBoxLayout(this); 36 | l->addWidget(edit); 37 | l->addWidget(view); 38 | } 39 | }; 40 | 41 | int main(int argc, char **argv) 42 | { 43 | QApplication app(argc, argv); 44 | 45 | TestObject obj; 46 | obj.show(); 47 | 48 | return app.exec(); 49 | } 50 | 51 | #include "modelwidgettest.moc" 52 | -------------------------------------------------------------------------------- /src/action.cpp: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Alexander Lohnau 2 | // SPDX-License-Identifier: LGPL-2.0-or-later 3 | #include "action.h" 4 | 5 | #include 6 | 7 | namespace KRunner 8 | { 9 | class ActionPrivate 10 | { 11 | public: 12 | explicit ActionPrivate(const QString &id, const QString &text, const QString &iconName) 13 | : m_id(id) 14 | , m_text(text) 15 | , m_iconSource(iconName) 16 | { 17 | } 18 | explicit ActionPrivate() = default; 19 | explicit ActionPrivate(const ActionPrivate &action) = default; 20 | const QString m_id; 21 | const QString m_text; 22 | const QString m_iconSource; 23 | }; 24 | 25 | Action::Action(const QString &id, const QString &iconName, const QString &text) 26 | : d(new ActionPrivate(id, text, iconName)) 27 | { 28 | } 29 | Action::Action(const Action &action) 30 | : d(new ActionPrivate(*action.d)) 31 | { 32 | } 33 | Action::Action() 34 | : d(new ActionPrivate()) 35 | { 36 | } 37 | 38 | Action::~Action() = default; 39 | Action &Action::operator=(const Action &other) 40 | { 41 | d = std::make_unique(*other.d); 42 | return *this; 43 | } 44 | 45 | QString Action::id() const 46 | { 47 | return d->m_id; 48 | } 49 | QString Action::text() const 50 | { 51 | return d->m_text; 52 | } 53 | QString Action::iconSource() const 54 | { 55 | return d->m_iconSource; 56 | } 57 | } 58 | 59 | #include "moc_action.cpp" 60 | -------------------------------------------------------------------------------- /autotests/plugins/fakerunner.h: -------------------------------------------------------------------------------- 1 | /* 2 | SPDX-FileCopyrightText: 2021 Alexander Lohnau 3 | SPDX-License-Identifier: LGPL-2.0-or-later 4 | */ 5 | 6 | #include 7 | #include 8 | #include 9 | #include 10 | #include 11 | 12 | using namespace KRunner; 13 | 14 | class FakeRunner : public AbstractRunner 15 | { 16 | public: 17 | explicit FakeRunner(QObject *parent, const KPluginMetaData &metadata) 18 | : AbstractRunner(parent, metadata) 19 | , m_action("someid", "sometext", "dialog-ok") 20 | { 21 | } 22 | ~FakeRunner() 23 | { 24 | // qWarning() << Q_FUNC_INFO; 25 | } 26 | 27 | void match(RunnerContext &context) override 28 | { 29 | // Do not use nested event loop, because that would be quit when quitting the QThread's event loop 30 | QThread::msleep(50); 31 | if (context.query().startsWith(QLatin1String("foo"))) { 32 | context.addMatch(createDummyMatch(QStringLiteral("foo"), 0.1)); 33 | context.addMatch(createDummyMatch(QStringLiteral("bar"), 0.2)); 34 | } 35 | } 36 | 37 | private: 38 | QueryMatch createDummyMatch(const QString &text, qreal relevance) 39 | { 40 | QueryMatch queryMatch(this); 41 | queryMatch.setId(text); 42 | queryMatch.setText(text); 43 | queryMatch.setRelevance(relevance); 44 | queryMatch.setActions({m_action}); 45 | return queryMatch; 46 | } 47 | KRunner::Action m_action; 48 | }; 49 | -------------------------------------------------------------------------------- /src/abstractrunner_p.h: -------------------------------------------------------------------------------- 1 | /* 2 | SPDX-FileCopyrightText: 2020-2023 Alexander Lohnau 3 | SPDX-License-Identifier: LGPL-2.0-or-later 4 | */ 5 | #include "abstractrunner.h" 6 | #include "runnersyntax.h" 7 | #include 8 | #include 9 | #include 10 | 11 | namespace KRunner 12 | { 13 | class AbstractRunnerPrivate 14 | { 15 | public: 16 | explicit AbstractRunnerPrivate(AbstractRunner *r, const KPluginMetaData &data) 17 | : runnerDescription(data) 18 | , translatedName(data.name()) 19 | , runner(r) 20 | , minLetterCount(data.value(QStringLiteral("X-Plasma-Runner-Min-Letter-Count"), 0)) 21 | , hasUniqueResults(data.value(QStringLiteral("X-Plasma-Runner-Unique-Results"), false)) 22 | , hasWeakResults(data.value(QStringLiteral("X-Plasma-Runner-Weak-Results"), false)) 23 | { 24 | if (const QString regexStr = data.value(QStringLiteral("X-Plasma-Runner-Match-Regex")); !regexStr.isEmpty()) { 25 | matchRegex = QRegularExpression(regexStr); 26 | hasMatchRegex = matchRegex.isValid() && !matchRegex.pattern().isEmpty(); 27 | } 28 | } 29 | 30 | QReadWriteLock lock; 31 | const KPluginMetaData runnerDescription; 32 | // We can easily call this a few hundred times for a few queries. Thus just reuse the value and not do a lookup of the translated string every time 33 | const QString translatedName; 34 | const AbstractRunner *runner; 35 | QList syntaxes; 36 | std::optional suspendMatching; 37 | int minLetterCount = 0; 38 | QRegularExpression matchRegex; 39 | bool hasMatchRegex = false; 40 | const bool hasUniqueResults = false; 41 | const bool hasWeakResults = false; 42 | }; 43 | } 44 | -------------------------------------------------------------------------------- /src/dbusrunner_p.h: -------------------------------------------------------------------------------- 1 | /* 2 | SPDX-FileCopyrightText: 2017 David Edmundson 3 | 4 | SPDX-License-Identifier: LGPL-2.0-or-later 5 | */ 6 | 7 | #pragma once 8 | 9 | #include 10 | 11 | #include "dbusutils_p.h" 12 | #include 13 | #include 14 | #include 15 | #include 16 | 17 | namespace KRunner 18 | { 19 | class DBusRunner : public KRunner::AbstractRunner 20 | { 21 | Q_OBJECT 22 | 23 | public: 24 | explicit DBusRunner(QObject *parent, const KPluginMetaData &data); 25 | 26 | // matchInternal is overwritten. Meaning we do not need the original match 27 | void match(KRunner::RunnerContext &) override 28 | { 29 | } 30 | void reloadConfiguration() override; 31 | void run(const KRunner::RunnerContext &context, const KRunner::QueryMatch &match) override; 32 | 33 | Q_INVOKABLE void matchInternal(KRunner::RunnerContext context); 34 | 35 | private: 36 | // Returns RemoteActions with service name as key 37 | void requestActions(); 38 | void requestActionsForService(const QString &service, const std::function &finishedCallback); 39 | QList convertMatches(const QString &service, const RemoteMatches &remoteMatches); 40 | void requestConfig(); 41 | static QImage decodeImage(const RemoteImage &remoteImage); 42 | QSet m_matchingServices; 43 | QHash> m_actions; 44 | const QString m_path; 45 | const bool m_hasUniqueResults; 46 | const bool m_requestActionsOnce; 47 | bool m_actionsForSessionRequested = false; 48 | bool m_matchWasCalled = false; 49 | bool m_callLifecycleMethods = false; 50 | const QString m_ifaceName; 51 | QSet m_requestedActionServices; 52 | }; 53 | } 54 | -------------------------------------------------------------------------------- /KF6KRunnerMacros.cmake: -------------------------------------------------------------------------------- 1 | #.rst: 2 | # KF6KRunnerMacros 3 | # --------------------------- 4 | # 5 | # This module provides the ``krunner_configure_test`` function which takes the test- and runner target as a parameter. 6 | # This will add the compile definitions for the AbstractRunnerTest header. 7 | # In case of DBus runners the DESKTOP_FILE parameter must be set. This is required for loading the runner from the 8 | # metadata file. 9 | # SPDX-FileCopyrightText: 2020 Alexander Lohnau 10 | # SPDX-License-Identifier: BSD-2-Clause 11 | 12 | function(krunner_configure_test TEST_TARGET RUNNER_TARGET) 13 | set(options) 14 | set(oneValueArgs DESKTOP_FILE) 15 | set(multiValueArgs) 16 | cmake_parse_arguments(ARGS "${options}" "${oneValueArgs}" "${multiValueArgs}" ${ARGN}) 17 | get_target_property(target_type ${RUNNER_TARGET} TYPE) 18 | if(target_type STREQUAL "EXECUTABLE") 19 | if(NOT ARGS_DESKTOP_FILE) 20 | message(FATAL_ERROR "In case of a dbus runner the DESKTOP_FILE must be provided") 21 | endif() 22 | target_compile_definitions(${TEST_TARGET} 23 | PRIVATE 24 | KRUNNER_DBUS_RUNNER_TESTING=1 25 | KRUNNER_TEST_DBUS_EXECUTABLE="$" 26 | KRUNNER_TEST_DESKTOP_FILE="${ARGS_DESKTOP_FILE}" 27 | ) 28 | else() 29 | target_compile_definitions(${TEST_TARGET} 30 | PRIVATE 31 | KRUNNER_DBUS_RUNNER_TESTING=0 32 | KRUNNER_TEST_RUNNER_PLUGIN_DIR="$" 33 | KRUNNER_TEST_RUNNER_PLUGIN_NAME="$" 34 | ) 35 | endif() 36 | add_dependencies(${TEST_TARGET} ${RUNNER_TARGET}) 37 | endfunction() 38 | -------------------------------------------------------------------------------- /src/runnersyntax.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | SPDX-FileCopyrightText: 2009 Aaron Seigo 3 | SPDX-FileCopyrightText: 2023 Alexander Lohnau 4 | 5 | SPDX-License-Identifier: LGPL-2.0-or-later 6 | */ 7 | 8 | #include "runnersyntax.h" 9 | 10 | #include 11 | 12 | namespace KRunner 13 | { 14 | class RunnerSyntaxPrivate 15 | { 16 | public: 17 | RunnerSyntaxPrivate(const QStringList &_exampleQueries, const QString &_description) 18 | : exampleQueries(prepareExampleQueries(_exampleQueries)) 19 | , description(_description) 20 | { 21 | } 22 | 23 | static QStringList prepareExampleQueries(const QStringList &queries) 24 | { 25 | Q_ASSERT_X(!queries.isEmpty(), "KRunner::RunnerSyntax", "List of example queries must not be empty"); 26 | QStringList exampleQueries; 27 | for (const QString &query : queries) { 28 | Q_ASSERT_X(!query.isEmpty(), "KRunner::RunnerSyntax", "Example query must not be empty!"); 29 | const static QString termDescription = i18n("search term"); 30 | const QString termDesc(QLatin1Char('<') + termDescription + QLatin1Char('>')); 31 | exampleQueries.append(QString(query).replace(QLatin1String(":q:"), termDesc)); 32 | } 33 | return exampleQueries; 34 | } 35 | 36 | const QStringList exampleQueries; 37 | const QString description; 38 | }; 39 | 40 | RunnerSyntax::RunnerSyntax(const QStringList &exampleQueries, const QString &description) 41 | : d(new RunnerSyntaxPrivate(exampleQueries, description)) 42 | { 43 | Q_ASSERT_X(!exampleQueries.isEmpty(), "KRunner::RunnerSyntax", "Example queries must not be empty"); 44 | } 45 | 46 | RunnerSyntax::RunnerSyntax(const RunnerSyntax &other) 47 | : d(new RunnerSyntaxPrivate(*other.d)) 48 | { 49 | } 50 | 51 | RunnerSyntax::~RunnerSyntax() = default; 52 | 53 | RunnerSyntax &RunnerSyntax::operator=(const RunnerSyntax &rhs) 54 | { 55 | d = std::make_unique(*rhs.d); 56 | return *this; 57 | } 58 | 59 | QStringList RunnerSyntax::exampleQueries() const 60 | { 61 | return d->exampleQueries; 62 | } 63 | 64 | QString RunnerSyntax::description() const 65 | { 66 | return d->description; 67 | } 68 | 69 | } // KRunner namespace 70 | -------------------------------------------------------------------------------- /src/model/runnerresultsmodel_p.h: -------------------------------------------------------------------------------- 1 | /* 2 | * This file is part of the KDE Milou Project 3 | * SPDX-FileCopyrightText: 2019 Kai Uwe Broulik 4 | * 5 | * SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL 6 | * 7 | */ 8 | 9 | #pragma once 10 | 11 | #include 12 | #include 13 | #include 14 | #include 15 | 16 | #include 17 | 18 | namespace KRunner 19 | { 20 | class RunnerManager; 21 | } 22 | 23 | namespace KRunner 24 | { 25 | class RunnerResultsModel : public QAbstractItemModel 26 | { 27 | Q_OBJECT 28 | 29 | public: 30 | explicit RunnerResultsModel(const KConfigGroup &configGroup, const KConfigGroup &stateConfigGroup, QObject *parent = nullptr); 31 | 32 | QString queryString() const; 33 | void setQueryString(const QString &queryString, const QString &runner); 34 | Q_SIGNAL void queryStringChanged(const QString &queryString); 35 | 36 | void setRunnerManager(KRunner::RunnerManager *manager); 37 | Q_SIGNAL void runnerManagerChanged(); 38 | 39 | /* 40 | * Clears the model content and resets the runner context, i.e. no new items will appear. 41 | */ 42 | void clear(); 43 | 44 | bool run(const QModelIndex &idx); 45 | bool runAction(const QModelIndex &idx, int actionNumber); 46 | 47 | int columnCount(const QModelIndex &parent) const override; 48 | int rowCount(const QModelIndex &parent) const override; 49 | 50 | QVariant data(const QModelIndex &index, int role) const override; 51 | 52 | QModelIndex index(int row, int column, const QModelIndex &parent = QModelIndex()) const override; 53 | QModelIndex parent(const QModelIndex &child) const override; 54 | 55 | KRunner::RunnerManager *runnerManager() const; 56 | KRunner::QueryMatch fetchMatch(const QModelIndex &idx) const; 57 | 58 | QStringList m_favoriteIds; 59 | Q_SIGNALS: 60 | void queryStringChangeRequested(const QString &queryString, int pos); 61 | 62 | void matchesChanged(); 63 | 64 | private: 65 | void onMatchesChanged(const QList &matches); 66 | 67 | KRunner::RunnerManager *m_manager = nullptr; 68 | QString m_queryString; 69 | QString m_prevRunner; 70 | bool m_hasMatches = false; 71 | QStringList m_categories; 72 | QHash> m_matches; 73 | }; 74 | 75 | } // namespace Milou 76 | -------------------------------------------------------------------------------- /src/action.h: -------------------------------------------------------------------------------- 1 | // SPDX-FileCopyrightText: 2023 Alexander Lohnau 2 | // SPDX-License-Identifier: LGPL-2.0-or-later 3 | #ifndef KRUNNER_ACTION_H 4 | #define KRUNNER_ACTION_H 5 | 6 | #include "krunner_export.h" 7 | 8 | #include 9 | #include 10 | #include 11 | 12 | namespace KRunner 13 | { 14 | class ActionPrivate; 15 | /*! 16 | * \class KRunner::Action 17 | * \inheaderfile KRunner/Action 18 | * \inmodule KRunner 19 | * 20 | * \brief This class represents an action that will be shown next to a match. 21 | * 22 | * The goal is to make it more reliable, because QIcon::fromTheme which is often needed in a QAction constructor is not thread safe. 23 | * Also, it makes the API more consistent with the org.kde.krunner1 DBus interface and forces consumers to set an icon. 24 | * 25 | * \since 6.0 26 | */ 27 | class KRUNNER_EXPORT Action final 28 | { 29 | Q_GADGET 30 | /*! 31 | * \property KRunner::Action::text 32 | * User-visible text 33 | */ 34 | Q_PROPERTY(QString text READ text CONSTANT) 35 | /*! 36 | * \property KRunner::Action::iconSource 37 | * Source for the icon: Name of the icon from a theme, file path or file URL 38 | */ 39 | Q_PROPERTY(QString iconSource READ iconSource CONSTANT) 40 | public: 41 | /*! 42 | * Constructs a new action 43 | * 44 | * \a id ID which identifies the action uniquely within the context of the respective runner plugin 45 | * 46 | * \a iconSource name for the icon, that can be passed in to QIcon::fromTheme or file path/URL 47 | */ 48 | explicit Action(const QString &id, const QString &iconSource, const QString &text); 49 | 50 | /*! 51 | * Empty constructor creating invalid action 52 | */ 53 | Action(); 54 | 55 | ~Action(); 56 | 57 | Action(const KRunner::Action &other); 58 | 59 | Action &operator=(const Action &other); 60 | 61 | /*! 62 | * Check if the action is valid 63 | */ 64 | explicit operator bool() const 65 | { 66 | return !id().isEmpty(); 67 | } 68 | 69 | bool operator==(const KRunner::Action &other) const 70 | { 71 | return id() == other.id(); 72 | } 73 | 74 | QString id() const; 75 | QString text() const; 76 | QString iconSource() const; 77 | 78 | private: 79 | std::unique_ptr d; 80 | }; 81 | 82 | using Actions = QList; 83 | } 84 | 85 | Q_DECLARE_METATYPE(KRunner::Action) 86 | #endif 87 | -------------------------------------------------------------------------------- /autotests/pluginbenchmarker.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | SPDX-FileCopyrightText: 2023 Alexander Lohnau 3 | SPDX-License-Identifier: LGPL-2.0-or-later 4 | */ 5 | #include "runnermanager.h" 6 | #include 7 | #include 8 | #include 9 | 10 | #include 11 | 12 | static void runQuery(const QString &runnerId, const QString &query, int iterations) 13 | { 14 | QEventLoop loop; 15 | KRunner::RunnerManager manager; 16 | manager.setAllowedRunners({runnerId}); 17 | manager.launchQuery("test"); 18 | qWarning() << "Following runners are loaded" << manager.runners(); 19 | QObject::connect(&manager, &KRunner::RunnerManager::queryFinished, &loop, &QEventLoop::quit); 20 | for (int i = 0; i < iterations; ++i) { 21 | // Setup of match session is done for first query automatically 22 | for (int j = 1; j <= query.size(); ++j) { 23 | manager.launchQuery(query.mid(0, j)); 24 | loop.exec(); 25 | } 26 | manager.matchSessionComplete(); 27 | } 28 | } 29 | 30 | int main(int argv, char **argc) 31 | { 32 | QCoreApplication app(argv, argc); 33 | QCommandLineOption iterationsOption("iterations", "Number of iterations where the query will be run", "iterations"); 34 | 35 | QCommandLineParser parser; 36 | parser.addOption(iterationsOption); 37 | parser.addPositionalArgument("runner", "The runnerId you want to load"); 38 | parser.addPositionalArgument("query", "The query to run, each letter is launched as it's own query"); 39 | parser.process(app); 40 | 41 | const QStringList positionalArguments = parser.positionalArguments(); 42 | if (positionalArguments.size() != 2) { 43 | qWarning() << "The runnerId and query must be specified as the only positional arguments"; 44 | exit(1); 45 | } 46 | const QString runner = positionalArguments.at(0); 47 | const QString query = positionalArguments.at(1); 48 | int iterations = parser.isSet(iterationsOption) ? parser.value(iterationsOption).toInt() : 1; 49 | if (iterations < 1) { 50 | qWarning() << "invalid iterations value set, it must be more than 1" << iterations; 51 | } 52 | 53 | QTimer::singleShot(0, &app, [&app, runner, query, iterations]() { 54 | runQuery(runner, query, iterations); 55 | QTimer::singleShot(10, &app, &QCoreApplication::quit); 56 | std::cout << "Finished running queries" << std::endl; 57 | }); 58 | 59 | return app.exec(); 60 | } 61 | -------------------------------------------------------------------------------- /autotests/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: 2021 Alexander Lohnau 2 | # SPDX-License-Identifier: BSD-2-Clause 3 | find_package(Qt6 ${REQUIRED_QT_VERSION} CONFIG REQUIRED Test) 4 | 5 | include(ECMAddTests) 6 | 7 | remove_definitions(-DQT_NO_CAST_FROM_ASCII) 8 | 9 | ecm_add_tests( 10 | dbusrunnertest.cpp 11 | runnermatchmethodstest.cpp 12 | runnermanagerhistorytest.cpp 13 | runnermanagersinglerunnermodetest.cpp 14 | runnermanagertest.cpp 15 | testmetadataconversion.cpp 16 | threadingtest.cpp 17 | LINK_LIBRARIES Qt6::Gui Qt6::DBus Qt6::Test KF6::Runner KF6::ConfigCore 18 | ) 19 | 20 | kcoreaddons_add_plugin(fakerunnerplugin SOURCES plugins/fakerunnerplugin.cpp INSTALL_NAMESPACE "krunnertest" STATIC) 21 | target_link_libraries(fakerunnerplugin KF6Runner Qt6::Gui) 22 | 23 | kcoreaddons_add_plugin(suspendedrunnerplugin SOURCES plugins/suspendedrunner.cpp INSTALL_NAMESPACE "krunnertest2" STATIC) 24 | target_link_libraries(suspendedrunnerplugin KF6Runner) 25 | 26 | kcoreaddons_target_static_plugins(runnermanagerhistorytest NAMESPACE krunnertest) 27 | kcoreaddons_target_static_plugins(runnermanagertest NAMESPACE krunnertest) 28 | kcoreaddons_target_static_plugins(runnermanagertest NAMESPACE krunnertest2) 29 | kcoreaddons_target_static_plugins(threadingtest NAMESPACE krunnertest) 30 | 31 | add_executable(testremoterunner) 32 | qt_add_dbus_adaptor(demoapp_dbus_adaptor_SRCS "../src/data/org.kde.krunner1.xml" plugins/testremoterunner.h TestRemoteRunner) 33 | target_sources(testremoterunner PRIVATE plugins/testremoterunner.cpp ${demoapp_dbus_adaptor_SRCS}) 34 | target_link_libraries(testremoterunner 35 | Qt6::DBus 36 | Qt6::Gui 37 | KF6::Runner 38 | ) 39 | 40 | include(../KF6KRunnerMacros.cmake) 41 | krunner_configure_test(dbusrunnertest testremoterunner DESKTOP_FILE "${CMAKE_CURRENT_SOURCE_DIR}/plugins/dbusrunnertest.desktop") 42 | krunner_configure_test(runnermanagersinglerunnermodetest testremoterunner DESKTOP_FILE "${CMAKE_CURRENT_SOURCE_DIR}/plugins/dbusrunnertest.desktop") 43 | krunner_configure_test(runnermanagertest testremoterunner DESKTOP_FILE "${CMAKE_CURRENT_SOURCE_DIR}/plugins/dbusrunnertest.desktop") 44 | krunner_configure_test(threadingtest testremoterunner DESKTOP_FILE "${CMAKE_CURRENT_SOURCE_DIR}/plugins/dbusrunnertest.desktop") 45 | 46 | find_package(Qt6 ${QT_MIN_VERSION} OPTIONAL_COMPONENTS Widgets) 47 | if (TARGET Qt6::Widgets) 48 | add_executable(modelwidgettest modelwidgettest.cpp) 49 | target_link_libraries(modelwidgettest KF6::Runner Qt6::Widgets) 50 | kcoreaddons_target_static_plugins(modelwidgettest NAMESPACE krunnertest) 51 | 52 | endif() 53 | add_executable(pluginbenchmarker pluginbenchmarker.cpp) 54 | target_link_libraries(pluginbenchmarker KF6::Runner) 55 | -------------------------------------------------------------------------------- /src/kpluginmetadata_utils_p.h: -------------------------------------------------------------------------------- 1 | /* 2 | SPDX-FileCopyrightText: 2022 Alexander Lohnau 3 | 4 | SPDX-License-Identifier: LGPL-2.0-or-later 5 | */ 6 | 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | 14 | template 15 | inline void copyIfExists(const KConfigGroup &grp, QJsonObject &obj, const char *key, const T &t = QString()) 16 | { 17 | copyAndRenameIfExists(grp, obj, key, key, t); 18 | } 19 | 20 | template 21 | inline void copyAndRenameIfExists(const KConfigGroup &grp, QJsonObject &obj, const char *oldKey, const char *key, const T &t) 22 | { 23 | if (grp.hasKey(oldKey)) { 24 | obj.insert(QLatin1String(key), grp.readEntry(oldKey, t)); 25 | } 26 | } 27 | inline KPluginMetaData parseMetaDataFromDesktopFile(const QString &fileName) 28 | { 29 | KDesktopFile file(fileName); 30 | const KConfigGroup grp = file.desktopGroup(); 31 | 32 | QJsonObject kplugin; 33 | copyIfExists(grp, kplugin, "Name"); 34 | copyIfExists(grp, kplugin, "Icon"); 35 | copyAndRenameIfExists(grp, kplugin, "X-KDE-PluginInfo-Name", "Id", QString()); 36 | copyAndRenameIfExists(grp, kplugin, "Comment", "Description", QString()); 37 | copyAndRenameIfExists(grp, kplugin, "X-KDE-PluginInfo-EnabledByDefault", "EnabledByDefault", false); 38 | QJsonObject root; 39 | root.insert(QLatin1String("KPlugin"), kplugin); 40 | 41 | copyIfExists(grp, root, "X-Plasma-DBusRunner-Service"); 42 | copyIfExists(grp, root, "X-Plasma-DBusRunner-Path"); 43 | copyIfExists(grp, root, "X-Plasma-Runner-Unique-Results", false); 44 | copyIfExists(grp, root, "X-Plasma-Runner-Weak-Results", false); 45 | copyIfExists(grp, root, "X-Plasma-API"); 46 | copyIfExists(grp, root, "X-Plasma-Request-Actions-Once", false); 47 | copyIfExists(grp, root, "X-Plasma-Runner-Min-Letter-Count", 0); 48 | copyIfExists(grp, root, "X-Plasma-Runner-Match-Regex"); 49 | copyIfExists(grp, root, "X-KDE-ConfigModule"); // DBus-Runners may also specify KCMs 50 | root.insert(QLatin1String("X-Plasma-Runner-Syntaxes"), QJsonArray::fromStringList(grp.readEntry("X-Plasma-Runner-Syntaxes", QStringList()))); 51 | root.insert(QLatin1String("X-Plasma-Runner-Syntax-Descriptions"), 52 | QJsonArray::fromStringList(grp.readEntry("X-Plasma-Runner-Syntax-Descriptions", QStringList()))); 53 | QJsonObject author; 54 | author.insert(QLatin1String("Name"), grp.readEntry("X-KDE-PluginInfo-Author")); 55 | author.insert(QLatin1String("Email"), grp.readEntry("X-KDE-PluginInfo-Email")); 56 | author.insert(QLatin1String("Website"), grp.readEntry("X-KDE-PluginInfo-Website")); 57 | 58 | return KPluginMetaData(root, fileName); 59 | } 60 | -------------------------------------------------------------------------------- /autotests/threadingtest.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | SPDX-FileCopyrightText: 2023 Alexander Lohnau 3 | SPDX-License-Identifier: LGPL-2.1-or-later 4 | */ 5 | #include 6 | #include 7 | #include 8 | #include 9 | 10 | using namespace KRunner; 11 | 12 | class ThreadingTest : public AbstractRunnerTest 13 | { 14 | Q_OBJECT 15 | 16 | AbstractRunner *fakeRunner = nullptr; 17 | private Q_SLOTS: 18 | void init() 19 | { 20 | initProperties(); 21 | startDBusRunnerProcess({QStringLiteral("net.krunnertests.dave")}); 22 | fakeRunner = manager->loadRunner(KPluginMetaData::findPluginById("krunnertest", "fakerunnerplugin")); 23 | QCOMPARE(manager->runners().size(), 2); 24 | } 25 | void cleanup() 26 | { 27 | killRunningDBusProcesses(); 28 | } 29 | 30 | void testParallelQuerying() 31 | { 32 | constexpr int trottlingDelay = 250; 33 | manager->launchQuery("fooDelay300"); 34 | 35 | QSignalSpy changedSpy(manager.get(), &RunnerManager::matchesChanged); 36 | QSignalSpy finishedSpy(manager.get(), &RunnerManager::queryFinished); 37 | QVERIFY(changedSpy.wait(trottlingDelay + 10)); // Due to throttling, otherwise we'd have this signal after 50 ms 38 | QCOMPARE(finishedSpy.count(), 0); 39 | 40 | const auto matches = manager->matches(); 41 | QCOMPARE(matches.size(), 2); 42 | QVERIFY(std::ranges::all_of(matches, [this](QueryMatch m) { 43 | return m.runner() == fakeRunner; 44 | })); 45 | 46 | QVERIFY(finishedSpy.wait(trottlingDelay - 5)); 47 | QCOMPARE(changedSpy.count(), 2); 48 | 49 | QCOMPARE(manager->matches().size(), 3); 50 | } 51 | 52 | void testDeletionOfRunningJob() 53 | { 54 | QPointer ptr(fakeRunner); 55 | manager->setAllowedRunners({"fakerunnerplugin"}); 56 | manager->launchQuery("foo"); 57 | QThread::msleep(1); // Wait for runner to be invoked and query started 58 | 59 | QVERIFY(manager->querying()); 60 | manager.reset(nullptr); 61 | QVERIFY(ptr); // Runner should not be deleted or reset now 62 | 63 | // try it maximal 5 seconds with running the event loop in-between 64 | QTRY_VERIFY_WITH_TIMEOUT(!ptr, 5000); 65 | } 66 | 67 | void testTeardownWhileJobIsRunning() 68 | { 69 | manager->launchQuery("fooDelay500"); 70 | manager->matchSessionComplete(); 71 | QSignalSpy spy(fakeRunner, &AbstractRunner::teardown); 72 | QSignalSpy dbusSpy(manager->runners().constFirst(), &AbstractRunner::teardown); 73 | spy.wait(100); 74 | QCOMPARE(dbusSpy.count(), 0); // Should not be called due to the match fimeout 75 | } 76 | 77 | void benchmarkQuerying() 78 | { 79 | QSKIP("Skipped by default"); 80 | QStandardPaths::setTestModeEnabled(false); 81 | manager = std::make_unique(); 82 | QBENCHMARK_ONCE { 83 | launchQuery("test"); 84 | launchQuery("spell bla"); 85 | launchQuery("define test"); 86 | } 87 | } 88 | }; 89 | 90 | QTEST_MAIN(ThreadingTest) 91 | 92 | #include "threadingtest.moc" 93 | -------------------------------------------------------------------------------- /src/runnersyntax.h: -------------------------------------------------------------------------------- 1 | /* 2 | SPDX-FileCopyrightText: 2009 Aaron Seigo 3 | SPDX-FileCopyrightText: 2023 Alexander Lohnau 4 | 5 | SPDX-License-Identifier: LGPL-2.0-or-later 6 | */ 7 | 8 | #ifndef KRUNNER_RUNNERSYNTAX_H 9 | #define KRUNNER_RUNNERSYNTAX_H 10 | 11 | #include 12 | #include 13 | 14 | #include "krunner_export.h" 15 | 16 | namespace KRunner 17 | { 18 | class RunnerSyntaxPrivate; 19 | /*! 20 | * \class KRunner::RunnerSyntax 21 | * \inheaderfile KRunner/RunnerSyntax 22 | * \inmodule KRunner 23 | * 24 | * \brief Represents a query prototype that the runner accepts. 25 | * 26 | * These can be 27 | * created and registered with AbstractRunner::addSyntax to 28 | * allow applications to show to the user what the runner is currently capable of doing. 29 | * 30 | * Lets say the runner has a trigger word and then the user can type anything after that. In that case you could use 31 | * ":q:" as a placeholder, which will get expanded to i18n("search term") and be put in brackets. 32 | * \code 33 | * KRunner::RunnerSyntax syntax(QStringLiteral("sometriggerword :q:"), i18n("Description for this syntax")); 34 | * addSyntax(syntax); 35 | * \endcode 36 | * 37 | * But if the query the user has to enter is sth. specific like a program, 38 | * url or file you should use a custom placeholder to make it easier to understand. 39 | * \code 40 | * KRunner::RunnerSyntax syntax(QStringLiteral("sometriggereword <%1>").arg(i18n("program name"))), i18n("Description for this syntax")); 41 | * addSyntax(syntax); 42 | * \endcode 43 | */ 44 | class KRUNNER_EXPORT RunnerSyntax 45 | { 46 | public: 47 | /*! 48 | * Constructs a RunnerSyntax with one example query 49 | * 50 | * \a exampleQuery See the class description for examples and placeholder conventions. 51 | * 52 | * \a description A description of what the described syntax does from the user's point of view. 53 | */ 54 | explicit inline RunnerSyntax(const QString &exampleQuery, const QString &description) 55 | : RunnerSyntax(QStringList(exampleQuery), description) 56 | { 57 | } 58 | 59 | /*! 60 | * Constructs a RunnerSyntax with multiple example queries 61 | * 62 | * \a exampleQueries See the class description for examples and placeholder conventions. 63 | * 64 | * \a description A description of what the described syntax does from the user's point of view. 65 | * This description should be true for all example queries. In case they differ, consider using multiple syntaxes. 66 | * 67 | * \since 5.106 68 | */ 69 | explicit RunnerSyntax(const QStringList &exampleQueries, const QString &description); 70 | 71 | ~RunnerSyntax(); 72 | 73 | /*! 74 | * Returns the example queries associated with this Syntax object 75 | */ 76 | QStringList exampleQueries() const; 77 | 78 | /*! 79 | * Returns the user visible description of what the syntax does 80 | */ 81 | QString description() const; 82 | 83 | RunnerSyntax &operator=(const RunnerSyntax &rhs); 84 | explicit RunnerSyntax(const RunnerSyntax &other); 85 | 86 | private: 87 | std::unique_ptr d; 88 | }; 89 | 90 | } // namespace KRunner 91 | 92 | #endif // multiple inclusion guard 93 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: KDE Contributors 2 | # SPDX-License-Identifier: BSD-2-Clause 3 | 4 | cmake_minimum_required(VERSION 3.16) 5 | 6 | set(KF_VERSION "6.22.0") # handled by release scripts 7 | set(KF_DEP_VERSION "6.21.0") # handled by release scripts 8 | project(KRunner VERSION ${KF_VERSION}) 9 | 10 | # ECM setup 11 | include(FeatureSummary) 12 | find_package(ECM 6.21.0 NO_MODULE) 13 | set_package_properties(ECM PROPERTIES TYPE REQUIRED DESCRIPTION "Extra CMake Modules." URL "https://commits.kde.org/extra-cmake-modules") 14 | feature_summary(WHAT REQUIRED_PACKAGES_NOT_FOUND FATAL_ON_MISSING_REQUIRED_PACKAGES) 15 | 16 | set(CMAKE_MODULE_PATH ${ECM_MODULE_PATH}) 17 | 18 | include(KDEInstallDirs) 19 | include(KDECMakeSettings) 20 | include(KDEFrameworkCompilerSettings NO_POLICY_SCOPE) 21 | 22 | include(ECMGenerateExportHeader) 23 | include(ECMSetupVersion) 24 | include(ECMGenerateHeaders) 25 | include(CMakePackageConfigHelpers) 26 | include(KDEPackageAppTemplates) 27 | include(ECMQtDeclareLoggingCategory) 28 | include(ECMSetupQtPluginMacroNames) 29 | include(ECMDeprecationSettings) 30 | include(KDEGitCommitHooks) 31 | include(ECMGenerateQDoc) 32 | 33 | set(EXCLUDE_DEPRECATED_BEFORE_AND_AT 0 CACHE STRING "Control the range of deprecated API excluded from the build [default=0].") 34 | 35 | set(krunner_version_header "${CMAKE_CURRENT_BINARY_DIR}/src/krunner_version.h") 36 | ecm_setup_version(PROJECT 37 | VARIABLE_PREFIX KRUNNER 38 | VERSION_HEADER "${krunner_version_header}" 39 | PACKAGE_VERSION_FILE "${CMAKE_CURRENT_BINARY_DIR}/KF6RunnerConfigVersion.cmake" 40 | SOVERSION 6 41 | ) 42 | 43 | # Dependencies 44 | set(REQUIRED_QT_VERSION 6.8.0) 45 | 46 | find_package(Qt6 ${REQUIRED_QT_VERSION} NO_MODULE REQUIRED Gui) 47 | 48 | ecm_set_disabled_deprecation_versions( 49 | QT 6.10.0 50 | KF 6.18.0 51 | ) 52 | 53 | find_package(KF6Config ${KF_DEP_VERSION} REQUIRED) 54 | find_package(KF6CoreAddons ${KF_DEP_VERSION} REQUIRED) 55 | find_package(KF6I18n ${KF_DEP_VERSION} REQUIRED) 56 | find_package(KF6ItemModels ${KF_DEP_VERSION}) 57 | find_package(KF6WindowSystem ${KF_DEP_VERSION} REQUIRED) 58 | 59 | # Subdirectories 60 | add_subdirectory(src) 61 | if (BUILD_TESTING) 62 | add_subdirectory(autotests) 63 | endif() 64 | add_subdirectory(templates) 65 | 66 | # Create a Config.cmake and a ConfigVersion.cmake file and install them 67 | set(CMAKECONFIG_INSTALL_DIR "${KDE_INSTALL_CMAKEPACKAGEDIR}/KF6Runner") 68 | 69 | configure_package_config_file( 70 | "${CMAKE_CURRENT_SOURCE_DIR}/KF6RunnerConfig.cmake.in" 71 | "${CMAKE_CURRENT_BINARY_DIR}/KF6RunnerConfig.cmake" 72 | INSTALL_DESTINATION "${CMAKECONFIG_INSTALL_DIR}" 73 | ) 74 | 75 | install(FILES 76 | "${CMAKE_CURRENT_BINARY_DIR}/KF6RunnerConfig.cmake" 77 | "${CMAKE_CURRENT_BINARY_DIR}/KF6RunnerConfigVersion.cmake" 78 | "${CMAKE_CURRENT_SOURCE_DIR}/KF6KRunnerMacros.cmake" 79 | DESTINATION "${CMAKECONFIG_INSTALL_DIR}" 80 | COMPONENT Devel) 81 | 82 | install(EXPORT KF6RunnerTargets 83 | DESTINATION "${CMAKECONFIG_INSTALL_DIR}" 84 | FILE KF6RunnerTargets.cmake 85 | NAMESPACE KF6::) 86 | 87 | install(FILES "${krunner_version_header}" 88 | DESTINATION "${KDE_INSTALL_INCLUDEDIR_KF}/KRunner" 89 | COMPONENT Devel) 90 | 91 | include(ECMFeatureSummary) 92 | ecm_feature_summary(WHAT ALL FATAL_ON_MISSING_REQUIRED_PACKAGES) 93 | 94 | kde_configure_git_pre_commit_hook(CHECKS CLANG_FORMAT) 95 | -------------------------------------------------------------------------------- /src/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | # SPDX-FileCopyrightText: KDE Contributors 2 | # SPDX-License-Identifier: BSD-2-Clause 3 | 4 | add_library(KF6Runner SHARED) 5 | add_library(KF6::Runner ALIAS KF6Runner) 6 | 7 | set_target_properties(KF6Runner PROPERTIES 8 | VERSION ${KRUNNER_VERSION} 9 | SOVERSION ${KRUNNER_SOVERSION} 10 | EXPORT_NAME Runner 11 | ) 12 | 13 | target_sources(KF6Runner PRIVATE 14 | abstractrunner.cpp 15 | abstractrunner.h 16 | abstractrunnertest.h 17 | dbusrunner.cpp 18 | dbusrunner_p.h 19 | dbusutils_p.h 20 | querymatch.cpp 21 | querymatch.h 22 | runnercontext.cpp 23 | runnercontext.h 24 | runnermanager.cpp 25 | runnermanager.h 26 | runnersyntax.cpp 27 | runnersyntax.h 28 | action.h 29 | action.cpp 30 | 31 | model/runnerresultsmodel.cpp 32 | model/runnerresultsmodel_p.h 33 | model/resultsmodel.cpp 34 | model/resultsmodel.h 35 | ) 36 | ecm_qt_declare_logging_category(KF6Runner 37 | HEADER krunner_debug.h 38 | IDENTIFIER KRUNNER 39 | CATEGORY_NAME kf.runner 40 | OLD_CATEGORY_NAMES org.kde.krunner 41 | DESCRIPTION "KRunner" 42 | EXPORT KRUNNER 43 | ) 44 | set_property(SOURCE "data/org.kde.krunner1.xml" PROPERTY INCLUDE dbusutils_p.h) 45 | 46 | ecm_generate_export_header(KF6Runner 47 | BASE_NAME KRunner 48 | GROUP_BASE_NAME KF 49 | VERSION ${KF_VERSION} 50 | USE_VERSION_HEADER 51 | DEPRECATED_BASE_VERSION 0 52 | DEPRECATION_VERSIONS 53 | EXCLUDE_DEPRECATED_BEFORE_AND_AT ${EXCLUDE_DEPRECATED_BEFORE_AND_AT} 54 | ) 55 | 56 | target_include_directories(KF6Runner INTERFACE "$") 57 | 58 | target_link_libraries(KF6Runner 59 | PUBLIC 60 | Qt6::Core 61 | KF6::CoreAddons # KPluginFactory 62 | KF6::ConfigCore 63 | PRIVATE 64 | Qt6::DBus 65 | Qt6::Gui 66 | KF6::ConfigCore 67 | KF6::I18n 68 | KF6::ItemModels 69 | KF6::WindowSystem 70 | ) 71 | ecm_generate_headers(KRunner_CamelCase_HEADERS 72 | HEADER_NAMES 73 | AbstractRunner 74 | Action 75 | RunnerContext 76 | RunnerManager 77 | RunnerSyntax 78 | QueryMatch 79 | AbstractRunnerTest 80 | 81 | PREFIX KRunner 82 | REQUIRED_HEADERS KRunner_HEADERS 83 | ) 84 | ecm_generate_headers(KRunner_CamelCase_HEADERS 85 | HEADER_NAMES ResultsModel 86 | PREFIX KRunner 87 | REQUIRED_HEADERS KRunner_HEADERS 88 | RELATIVE model 89 | ) 90 | 91 | # Install files 92 | 93 | install(TARGETS KF6Runner 94 | EXPORT KF6RunnerTargets 95 | ${KF_INSTALL_TARGETS_DEFAULT_ARGS}) 96 | 97 | install(FILES ${KRunner_CamelCase_HEADERS} 98 | DESTINATION ${KDE_INSTALL_INCLUDEDIR_KF}/KRunner/KRunner 99 | COMPONENT Devel) 100 | 101 | install(FILES 102 | ${CMAKE_CURRENT_BINARY_DIR}/krunner_export.h 103 | ${KRunner_HEADERS} 104 | DESTINATION ${KDE_INSTALL_INCLUDEDIR_KF}/KRunner/krunner 105 | COMPONENT Devel) 106 | 107 | ecm_qt_install_logging_categories( 108 | EXPORT KRUNNER 109 | FILE krunner.categories 110 | DESTINATION ${KDE_INSTALL_LOGGINGCATEGORIESDIR} 111 | ) 112 | 113 | ecm_generate_qdoc(KF6Runner krunner.qdocconf) 114 | 115 | install(FILES 116 | "data/org.kde.krunner1.xml" 117 | DESTINATION ${KDE_INSTALL_DBUSINTERFACEDIR} 118 | RENAME kf6_org.kde.krunner1.xml) 119 | -------------------------------------------------------------------------------- /autotests/runnermatchmethodstest.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | SPDX-FileCopyrightText: 2021 Alexander Lohnau 3 | 4 | SPDX-License-Identifier: LGPL-2.0-or-later 5 | */ 6 | 7 | #include "abstractrunner.h" 8 | #include "plugins/fakerunner.h" 9 | #include "runnermanager.h" 10 | 11 | #include 12 | #include 13 | #include 14 | #include 15 | 16 | #include "kpluginmetadata_utils_p.h" 17 | 18 | using namespace KRunner; 19 | 20 | inline QueryMatch createMatch(const QString &id, AbstractRunner *r = nullptr) 21 | { 22 | QueryMatch m(r); 23 | m.setId(id); 24 | return m; 25 | } 26 | 27 | class RunnerContextMatchMethodsTest : public QObject 28 | { 29 | Q_OBJECT 30 | public: 31 | RunnerContextMatchMethodsTest(); 32 | 33 | std::unique_ptr ctx; 34 | FakeRunner *runner1 = nullptr; 35 | FakeRunner *runner2 = nullptr; 36 | private Q_SLOTS: 37 | void init() 38 | { 39 | ctx = std::make_unique(); 40 | } 41 | void testAdd(); 42 | void testAddMulti(); 43 | void testDuplicateIds(); 44 | }; 45 | 46 | RunnerContextMatchMethodsTest::RunnerContextMatchMethodsTest() 47 | { 48 | QStandardPaths::setTestModeEnabled(true); 49 | const QByteArray defaultDataDirs = qEnvironmentVariableIsSet("XDG_DATA_DIRS") ? qgetenv("XDG_DATA_DIRS") : QByteArray("/usr/local:/usr"); 50 | const QByteArray modifiedDataDirs = QFile::encodeName(QCoreApplication::applicationDirPath()) + QByteArrayLiteral("/data:") + defaultDataDirs; 51 | qputenv("XDG_DATA_DIRS", modifiedDataDirs); 52 | KPluginMetaData data1 = parseMetaDataFromDesktopFile(QFINDTESTDATA("plugins/metadatafile1.desktop")); 53 | KPluginMetaData data2 = parseMetaDataFromDesktopFile(QFINDTESTDATA("plugins/metadatafile2.desktop")); 54 | QVERIFY(data1.isValid()); 55 | QVERIFY(data2.isValid()); 56 | runner1 = new FakeRunner(this, data1); 57 | runner2 = new FakeRunner(this, data2); 58 | } 59 | 60 | void RunnerContextMatchMethodsTest::testAdd() 61 | { 62 | QVERIFY(ctx->matches().isEmpty()); 63 | QVERIFY(ctx->addMatch(createMatch(QStringLiteral("m1")))); 64 | QVERIFY(ctx->addMatch(createMatch(QStringLiteral("m2")))); 65 | QCOMPARE(ctx->matches().count(), 2); 66 | QVERIFY(ctx->addMatch(createMatch(QStringLiteral("m3")))); 67 | QCOMPARE(ctx->matches().count(), 3); 68 | } 69 | 70 | void RunnerContextMatchMethodsTest::testAddMulti() 71 | { 72 | QVERIFY(ctx->matches().isEmpty()); 73 | QVERIFY(ctx->addMatches({createMatch(QStringLiteral("m1")), createMatch(QStringLiteral("m2"))})); 74 | QCOMPARE(ctx->matches().count(), 2); 75 | } 76 | 77 | void RunnerContextMatchMethodsTest::testDuplicateIds() 78 | { 79 | const QueryMatch match1 = createMatch(QStringLiteral("id1"), runner1); 80 | QVERIFY(ctx->addMatch(match1)); 81 | const QueryMatch match2 = createMatch(QStringLiteral("id1"), runner2); 82 | QVERIFY(ctx->addMatch(match2)); 83 | const QueryMatch match3 = createMatch(QStringLiteral("id2"), runner1); 84 | QVERIFY(ctx->addMatch(match3)); 85 | const QueryMatch match4 = createMatch(QStringLiteral("id3"), runner2); 86 | QVERIFY(ctx->addMatch(match4)); 87 | const QueryMatch match5 = createMatch(QStringLiteral("id3"), runner2); 88 | QVERIFY(ctx->addMatch(match5)); 89 | 90 | const QList matches = ctx->matches(); 91 | QCOMPARE(matches.size(), 3); 92 | // match2 should have replaced match1 93 | QCOMPARE(matches.at(0), match2); 94 | QCOMPARE(matches.at(1), match3); 95 | // match4 should not have been replaced, the runner does not have the weak property set 96 | QCOMPARE(matches.at(2), match4); 97 | } 98 | 99 | QTEST_MAIN(RunnerContextMatchMethodsTest) 100 | 101 | #include "runnermatchmethodstest.moc" 102 | -------------------------------------------------------------------------------- /src/dbusutils_p.h: -------------------------------------------------------------------------------- 1 | /* 2 | SPDX-FileCopyrightText: 2017, 2018 David Edmundson 3 | SPDX-FileCopyrightText: 2020 Alexander Lohnau 4 | SPDX-FileCopyrightText: 2020 Kai Uwe Broulik 5 | 6 | SPDX-License-Identifier: LGPL-2.0-or-later 7 | */ 8 | 9 | #pragma once 10 | 11 | #include "action.h" 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | 18 | struct RemoteMatch { 19 | // sssida{sv} 20 | QString id; 21 | QString text; 22 | QString iconName; 23 | int categoryRelevance = qToUnderlying(KRunner::QueryMatch::CategoryRelevance::Lowest); 24 | qreal relevance = 0; 25 | QVariantMap properties; 26 | }; 27 | 28 | typedef QList RemoteMatches; 29 | 30 | struct RemoteImage { 31 | // iiibiiay (matching notification spec image-data attribute) 32 | int width = 0; 33 | int height = 0; 34 | int rowStride = 0; 35 | bool hasAlpha = false; 36 | int bitsPerSample = 0; 37 | int channels = 0; 38 | QByteArray data; 39 | }; 40 | 41 | inline QDBusArgument &operator<<(QDBusArgument &argument, const RemoteMatch &match) 42 | { 43 | argument.beginStructure(); 44 | argument << match.id; 45 | argument << match.text; 46 | argument << match.iconName; 47 | argument << match.categoryRelevance; 48 | argument << match.relevance; 49 | argument << match.properties; 50 | argument.endStructure(); 51 | return argument; 52 | } 53 | 54 | inline const QDBusArgument &operator>>(const QDBusArgument &argument, RemoteMatch &match) 55 | { 56 | argument.beginStructure(); 57 | argument >> match.id; 58 | argument >> match.text; 59 | argument >> match.iconName; 60 | argument >> match.categoryRelevance; 61 | argument >> match.relevance; 62 | argument >> match.properties; 63 | argument.endStructure(); 64 | 65 | return argument; 66 | } 67 | 68 | inline QDBusArgument &operator<<(QDBusArgument &argument, const KRunner::Action &action) 69 | { 70 | argument.beginStructure(); 71 | argument << action.id(); 72 | argument << action.text(); 73 | argument << action.iconSource(); 74 | argument.endStructure(); 75 | return argument; 76 | } 77 | 78 | inline const QDBusArgument &operator>>(const QDBusArgument &argument, KRunner::Action &action) 79 | { 80 | QString id; 81 | QString text; 82 | QString iconName; 83 | argument.beginStructure(); 84 | argument >> id; 85 | argument >> text; 86 | argument >> iconName; 87 | argument.endStructure(); 88 | action = KRunner::Action(id, iconName, text); 89 | return argument; 90 | } 91 | 92 | inline QDBusArgument &operator<<(QDBusArgument &argument, const RemoteImage &image) 93 | { 94 | argument.beginStructure(); 95 | argument << image.width; 96 | argument << image.height; 97 | argument << image.rowStride; 98 | argument << image.hasAlpha; 99 | argument << image.bitsPerSample; 100 | argument << image.channels; 101 | argument << image.data; 102 | argument.endStructure(); 103 | return argument; 104 | } 105 | 106 | inline const QDBusArgument &operator>>(const QDBusArgument &argument, RemoteImage &image) 107 | { 108 | argument.beginStructure(); 109 | argument >> image.width; 110 | argument >> image.height; 111 | argument >> image.rowStride; 112 | argument >> image.hasAlpha; 113 | argument >> image.bitsPerSample; 114 | argument >> image.channels; 115 | argument >> image.data; 116 | argument.endStructure(); 117 | return argument; 118 | } 119 | 120 | Q_DECLARE_METATYPE(QList) 121 | Q_DECLARE_METATYPE(RemoteMatch) 122 | Q_DECLARE_METATYPE(RemoteMatches) 123 | Q_DECLARE_METATYPE(RemoteImage) 124 | -------------------------------------------------------------------------------- /templates/runner6/runner.kdevtemplate: -------------------------------------------------------------------------------- 1 | # KDE Config File 2 | [General] 3 | Name=C++ (Qt6) 4 | Name[ar]=سي ++ (Qt6) 5 | Name[ast]=C++ (Qt6) 6 | Name[bg]=C++ (Qt6) 7 | Name[ca]=C++ (Qt6) 8 | Name[ca@valencia]=C++ (Qt6) 9 | Name[cs]=C++ (Qt6) 10 | Name[de]=C++ (Qt6) 11 | Name[el]=C++ (Qt6) 12 | Name[en_GB]=C++ (Qt6) 13 | Name[eo]=C++ (Qt6) 14 | Name[es]=C++ (Qt6) 15 | Name[eu]=C++ (Qt6) 16 | Name[fi]=C++ (Qt6) 17 | Name[fr]=C++ (Qt6) 18 | Name[gl]=C++ (Qt6) 19 | Name[he]=C++ (Qt6) 20 | Name[hu]=C++ (Qt6) 21 | Name[ia]=C++ (Qt6) 22 | Name[it]=C++ (Qt6) 23 | Name[ka]=C++ (Qt6) 24 | Name[ko]=C++(Qt6) 25 | Name[lt]=C++ (Qt6) 26 | Name[lv]=C++ (Qt6) 27 | Name[nl]=C++ (Qt6) 28 | Name[nn]=C++ (Qt6) 29 | Name[pa]=C++ (Qt6) 30 | Name[pl]=C++ (Qt6) 31 | Name[pt_BR]=C++ (Qt6) 32 | Name[ro]=C++ (Qt6) 33 | Name[ru]=C++ (Qt6) 34 | Name[sa]=C++ (Qt6) 35 | Name[sl]=C++ (Qt6) 36 | Name[sv]=C++ (Qt6) 37 | Name[tr]=C++ (Qt6) 38 | Name[uk]=C++ (Qt6) 39 | Name[zh_CN]=C++ (Qt6) 40 | Name[zh_TW]=C++ (Qt6) 41 | Comment=Plasma Runner Template. A plasma runner template 42 | Comment[ar]=قالب «مشغّل بلازما». قالب لِ‍«مشغّل بلازما» 43 | Comment[az]=Plasma başlatma sətri üçün uzantıların şablonu 44 | Comment[bg]=Plasma Runner шаблон 45 | Comment[ca]=Plantilla de Runner del Plasma. Una plantilla de «Runner» per al Plasma 46 | Comment[ca@valencia]=Plantilla de Runner de Plasma. Una plantilla de «Runner» per a Plasma 47 | Comment[cs]=Šablona spouštěče Plasma 48 | Comment[da]=Skabelon til Plasma-runner. En skabelon til en Plasma-runner 49 | Comment[de]=Eine Vorlage für einen Plasma-Runner 50 | Comment[el]=Πρότυπο Plasma Runner. ένα πρότυπο εκτελεστή plasma 51 | Comment[en_GB]=Plasma Runner Template. A plasma runner template 52 | Comment[eo]=Plasma-rulilŝablono 53 | Comment[es]=Plantilla para Plasma Runner. Una plantilla para Plasma Runner. 54 | Comment[et]=Plasma Runneri mall. 55 | Comment[eu]=Plasma «Runner» txantiloia. Plasma «runner» txantiloi bat 56 | Comment[fi]=Plasma-suoritusohjelmamalli. 57 | Comment[fr]=Un modèle de lanceur pour Plasma. 58 | Comment[gl]=Modelo de executor de Plasma. Un modelo de executor de Plasma. 59 | Comment[he]=תבנית ריצה של פלזמה. תבנית ריצה של פלזמה 60 | Comment[hi]=प्लाज्मा रनर नमूना। एक प्लाज्मा रनर नमूना 61 | Comment[hu]=Plasma futtatósablon. 62 | Comment[ia]=Patrono de executor (runner) de Plasma. Un patrono de executor de Plasma 63 | Comment[id]=Plasma Runner Template. Sebuah template pejalan plasma 64 | Comment[it]=Modello di esecutore di Plasma. Un modello di esecutore di Plasma 65 | Comment[ka]=Plasma-ის დამხმარე პროცესის შაბლონი. Plasma-ის დამხმარე პროცესის შაბლონი 66 | Comment[ko]=Plasma 실행기 템플릿. Plasma 실행기 템플릿 67 | Comment[lt]=Plasma paleidiklio šablonas. Plasma paleidiklio šablonas 68 | Comment[lv]=„Plasma“ palaidēja veidne. „Plasma“ palaidēja veidne 69 | Comment[nb]=Plasma kjørermal. En Plasma-kjørermal 70 | Comment[nl]=Sjabloon voor Plasma-runner. Een plasma-starter sjabloon 71 | Comment[nn]=Malfil for Plasma-køyrar. 72 | Comment[pa]=ਪਲਾਜ਼ਮਾ ਰਨਰ ਟੈਪਲੇਟ। ਇੱਕ ਪਲਾਜ਼ਮਾ ਰਨਰ ਟੈਪਲੇਟ 73 | Comment[pl]=Szablon uruchamiania Plazmy. Szablon uruchamiania plazmy 74 | Comment[pt]=Modelo de Execução do Plasma. Um modelo de módulo de execução do Plasma 75 | Comment[pt_BR]=Modelo de módulo de execução do Plasma. Um modelo de módulos de execução do Plasma 76 | Comment[ro]=Șablon de executor Plasma. Un șablon de executor Plasma 77 | Comment[ru]=Шаблон расширения для строки запуска Plasma 78 | Comment[sa]=प्लाज्मा धावक टेम्पलेट। एकः प्लाज्मा धावकः टेम्पलेट् 79 | Comment[sk]=Šablóna Plasma Runner. Šablóna plasma runner 80 | Comment[sl]=Plasma Runner Template. Predloga zaganjalnika Plasma 81 | Comment[sr]=Шаблон плазма извођача. 82 | Comment[sr@ijekavian]=Шаблон плазма извођача. 83 | Comment[sr@ijekavianlatin]=Šablon plasma izvođača. 84 | Comment[sr@latin]=Šablon plasma izvođača. 85 | Comment[sv]=Plasma-mall för körning av program. Mall för Plasma körning av program 86 | Comment[tg]=Қолиби иҷрокунандаи Plasma. Қолиби иҷрокунандаи Plasma 87 | Comment[tr]=Plasma Çalıştırıcı Şablonu. Bir Plasma çalıştırıcı şablonu 88 | Comment[uk]=Шаблон засобу запуску Плазми 89 | Comment[vi]=Bản mẫu trình chạy Plasma. Một bản mẫu trình chạy Plasma 90 | Comment[zh_CN]=Plasma Runner 模板。一个 Plasma Runner 模板 91 | Comment[zh_TW]=Plasma Runner 範本。一個 plasma runner 的範本 92 | Category=Plasma/KRunner 93 | Icon=runner.png 94 | -------------------------------------------------------------------------------- /autotests/runnermanagerhistorytest.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | SPDX-FileCopyrightText: 2021 Alexander Lohnau 3 | SPDX-License-Identifier: LGPL-2.1-or-later 4 | */ 5 | 6 | #include "runnermanager.h" 7 | 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | using namespace KRunner; 18 | class RunnerManagerHistoryTest : public QObject 19 | { 20 | Q_OBJECT 21 | public: 22 | RunnerManagerHistoryTest() 23 | { 24 | QStandardPaths::setTestModeEnabled(true); 25 | stateConfigFile = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + QDir::separator() + "krunnerstaterc"; 26 | } 27 | 28 | private: 29 | QString stateConfigFile; 30 | void addToHistory(const QStringList &queries, RunnerManager &manager) 31 | { 32 | QCOMPARE(manager.runners().count(), 1); 33 | for (const QString &query : queries) { 34 | QueryMatch match(manager.runners().constFirst()); 35 | // Make sure internally the term and untrimmedTerm are set 36 | manager.launchQuery(query, "thisrunnerdoesnotexist"); 37 | manager.searchContext()->setQuery(query); 38 | manager.run(match); 39 | } 40 | } 41 | void launchQuery(const QString &query, RunnerManager *manager) 42 | { 43 | QSignalSpy spy(manager, &KRunner::RunnerManager::queryFinished); 44 | manager->launchQuery(query); 45 | QVERIFY2(spy.wait(), "RunnerManager did not emit the queryFinished signal"); 46 | } 47 | 48 | private Q_SLOTS: 49 | void init() 50 | { 51 | if (QFileInfo::exists(stateConfigFile)) { 52 | QFile::remove(stateConfigFile); 53 | } 54 | } 55 | void testRunnerHistory(); 56 | void testRunnerHistory_data(); 57 | void testHistorySuggestionsAndRemoving(); 58 | }; 59 | 60 | void RunnerManagerHistoryTest::testRunnerHistory() 61 | { 62 | QFETCH(const QStringList, queries); 63 | QFETCH(const QStringList, expectedEntries); 64 | 65 | RunnerManager manager; 66 | manager.setAllowedRunners({QStringLiteral("fakerunnerplugin")}); 67 | manager.loadRunner(KPluginMetaData::findPluginById(QStringLiteral("krunnertest"), QStringLiteral("fakerunnerplugin"))); 68 | addToHistory(queries, manager); 69 | QCOMPARE(manager.history(), expectedEntries); 70 | } 71 | 72 | void RunnerManagerHistoryTest::testRunnerHistory_data() 73 | { 74 | QTest::addColumn("queries"); 75 | QTest::addColumn("expectedEntries"); 76 | 77 | QTest::newRow("should add simple entry to history") << QStringList{"test"} << QStringList{"test"}; 78 | QTest::newRow("should not add entry that starts with space") << QStringList{" test"} << QStringList{}; 79 | QTest::newRow("should not add duplicate entries") << QStringList{"test", "test"} << QStringList{"test"}; 80 | QTest::newRow("should not add duplicate entries but put last run at beginning") << QStringList{"test", "test2", "test"} << QStringList{"test", "test2"}; 81 | } 82 | 83 | void RunnerManagerHistoryTest::testHistorySuggestionsAndRemoving() 84 | { 85 | RunnerManager manager; 86 | manager.setAllowedRunners({QStringLiteral("fakerunnerplugin")}); 87 | manager.loadRunner(KPluginMetaData::findPluginById(QStringLiteral("krunnertest"), QStringLiteral("fakerunnerplugin"))); 88 | const QStringList queries = {"test1", "test2", "test3"}; 89 | addToHistory(queries, manager); 90 | QStringList expectedBeforeRemoval = QStringList{"test3", "test2", "test1"}; 91 | QCOMPARE(manager.history(), expectedBeforeRemoval); 92 | QCOMPARE(manager.getHistorySuggestion("t"), "test3"); 93 | QCOMPARE(manager.getHistorySuggestion("doesnotexist"), QString()); 94 | 95 | manager.removeFromHistory(42); 96 | QCOMPARE(manager.history(), expectedBeforeRemoval); 97 | manager.removeFromHistory(0); 98 | QStringList expectedAfterRemoval = QStringList{"test2", "test1"}; 99 | QCOMPARE(manager.history(), expectedAfterRemoval); 100 | QCOMPARE(manager.getHistorySuggestion("t"), "test2"); 101 | } 102 | 103 | QTEST_MAIN(RunnerManagerHistoryTest) 104 | 105 | #include "runnermanagerhistorytest.moc" 106 | -------------------------------------------------------------------------------- /src/data/org.kde.krunner1.xml: -------------------------------------------------------------------------------- 1 | 2 | 8 | 9 | 10 | 11 | 15 | 16 | 17 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 56 | 57 | 58 | 59 | 60 | 63 | 64 | 67 | 68 | 71 | 72 | 73 | 79 | 80 | 81 | 102 | 103 | 104 | 105 | 106 | 107 | -------------------------------------------------------------------------------- /src/runnercontext.h: -------------------------------------------------------------------------------- 1 | /* 2 | SPDX-FileCopyrightText: 2006-2007 Aaron Seigo 3 | 4 | SPDX-License-Identifier: LGPL-2.0-or-later 5 | */ 6 | 7 | #ifndef KRUNNER_RUNNERCONTEXT_H 8 | #define KRUNNER_RUNNERCONTEXT_H 9 | 10 | #include 11 | #include 12 | #include 13 | 14 | #include "krunner_export.h" 15 | 16 | class KConfigGroup; 17 | 18 | namespace KRunner 19 | { 20 | class RunnerManager; 21 | class QueryMatch; 22 | class AbstractRunner; 23 | class RunnerContextPrivate; 24 | 25 | /*! 26 | * \class KRunner::RunnerContext 27 | * \inheaderfile KRunner/RunnerContext 28 | * \inmodule KRunner 29 | * 30 | * \brief The RunnerContext class provides information related to a search, 31 | * including the search term and collected matches. 32 | */ 33 | class KRUNNER_EXPORT RunnerContext final 34 | { 35 | public: 36 | /*! 37 | * 38 | */ 39 | explicit RunnerContext(RunnerManager *manager = nullptr); 40 | 41 | RunnerContext(const RunnerContext &other); 42 | 43 | RunnerContext &operator=(const RunnerContext &other); 44 | 45 | ~RunnerContext(); 46 | 47 | /*! 48 | * Sets the query term for this object and attempts to determine 49 | * the type of the search. 50 | */ 51 | void setQuery(const QString &term); 52 | 53 | /*! 54 | * Returns the current search query term. 55 | */ 56 | QString query() const; 57 | 58 | /*! 59 | * Returnss true if this context is no longer valid and therefore 60 | * matching using it should abort. 61 | * While not required to be used within runners, it provides a nice way 62 | * to avoid unnecessary processing in runners that may run for an extended 63 | * period (as measured in 10s of ms) and therefore improve the user experience. 64 | */ 65 | bool isValid() const; 66 | 67 | /*! 68 | * Appends lists of matches to the list of matches. 69 | * 70 | * \a matches the matches to add 71 | * 72 | * Returns true if matches were added, false if matches were e.g. outdated 73 | */ 74 | bool addMatches(const QList &matches); 75 | 76 | /*! 77 | * Appends a match to the existing list of matches. 78 | * 79 | * If you are going to be adding multiple matches, it is 80 | * more performant to use addMatches instead. 81 | * 82 | * \a match the match to add 83 | * 84 | * Returns true if the match was added, false otherwise. 85 | */ 86 | bool addMatch(const QueryMatch &match); 87 | 88 | /*! 89 | * Retrieves all available matches for the current search term. 90 | * 91 | * Returns a list of matches 92 | */ 93 | QList matches() const; 94 | 95 | /*! 96 | * Request that KRunner updates the query string and stasy open, even after running a match. 97 | * This method is const so it can be called in a const context. 98 | * 99 | * \a text Text that will be displayed in the search field 100 | * 101 | * \a cursorPosition Position of the cursor, if this is different than the length of the text, 102 | * the characters between the position and text will be selected 103 | * 104 | * \since 5.90 105 | */ 106 | void requestQueryStringUpdate(const QString &text, int cursorPosition) const; 107 | 108 | /*! 109 | * Returns true if the current query is a single runner query 110 | */ 111 | bool singleRunnerQueryMode() const; 112 | 113 | /*! 114 | * Set this to true in the AbstractRunner::run method to prevent the entry 115 | * from being saved to the history. 116 | * \since 5.90 117 | */ 118 | void ignoreCurrentMatchForHistory() const; 119 | 120 | private: 121 | KRUNNER_NO_EXPORT void increaseLaunchCount(const QueryMatch &match); 122 | KRUNNER_NO_EXPORT QString requestedQueryString() const; 123 | KRUNNER_NO_EXPORT int requestedCursorPosition() const; 124 | KRUNNER_NO_EXPORT bool shouldIgnoreCurrentMatchForHistory() const; 125 | // Sets single runner query mode. Note that a call to reset() will turn off single runner query mode. 126 | KRUNNER_NO_EXPORT void setSingleRunnerQueryMode(bool enabled); 127 | 128 | friend class RunnerManager; 129 | friend class AbstractRunner; 130 | friend class DBusRunner; 131 | friend class RunnerManagerPrivate; 132 | 133 | KRUNNER_NO_EXPORT void restore(const KConfigGroup &config); 134 | KRUNNER_NO_EXPORT void save(KConfigGroup &config); 135 | KRUNNER_NO_EXPORT void reset(); 136 | KRUNNER_NO_EXPORT void setJobStartTs(qint64 queryStartTs); 137 | KRUNNER_NO_EXPORT QString runnerJobId(AbstractRunner *runner) const; 138 | 139 | QExplicitlySharedDataPointer d; 140 | }; 141 | } 142 | 143 | Q_DECLARE_METATYPE(KRunner::RunnerContext) 144 | #endif 145 | -------------------------------------------------------------------------------- /templates/runner6python/runnerpy.kdevtemplate: -------------------------------------------------------------------------------- 1 | [General] 2 | Name=Python KRunner Plugin (Qt6) 3 | Name[ar]=ملحقة مشغّلك لبايثون (كيوت6) 4 | Name[bg]=Приставка на Python KRunner (Qt6) 5 | Name[ca]=Connector Python del KRunner (Qt6) 6 | Name[ca@valencia]=Connector en Python de KRunner (Qt6) 7 | Name[cs]=Modul KRunneru v Pythonu (Qt6) 8 | Name[de]=KRunner-Modul für Python (Qt6) 9 | Name[el]=Πρόσθετο Python KRunner (Qt6) 10 | Name[en_GB]=Python KRunner Plugin (Qt6) 11 | Name[eo]=Python KRunner-kromprogramo (Qt6) 12 | Name[es]=Complemento en Python para KRunner (Qt6) 13 | Name[eu]=Python «KRunner» plugina 14 | Name[fi]=Python-KRunner-liitännäinen (Qt6) 15 | Name[fr]=Module externe « KRunner » pour Python (Qt6) 16 | Name[gl]=Complemento de Python para KRunner (Qt6) 17 | Name[he]=תוסף KRunner ב־Python‏ (Qt6) 18 | Name[hu]=KRunner Python-bővítmény (Qt6) 19 | Name[ia]=Plugin de KRunner de Python (Qt6) 20 | Name[it]=Estensione Python di KRunner (Qt6) 21 | Name[ka]=KRunner-ის Python-ის დამატება (Qt6) 22 | Name[ko]=Python KRunner 플러그인(Qt6) 23 | Name[lt]=Python KRunner įskiepis (Qt6) 24 | Name[lv]=„Python“ „KRunner“ spraudnis (Qt6) 25 | Name[nl]=Python KRunner-plug-in (Qt6) 26 | Name[nn]=Python KRunner-tillegg (Qt6) 27 | Name[pa]=ਪਾਈਥਨ ਕੇਰਨਰ ਪਲੱਗਇਨ (Qt6) 28 | Name[pl]=Wtyczka Pythona dla KRunnera (Qt6) 29 | Name[pt_BR]=Plugin Python do KRunner (Qt6) 30 | Name[ro]=Extensie Python pentru KRunner (Qt6) 31 | Name[ru]=Расширение KRunner на Python (Qt6) 32 | Name[sa]=Python KRunner Plugin (Qt6) 33 | Name[sl]=Vtičnik za Python KRunner (Qt6) 34 | Name[sv]=Python-insticksprogram för Kör program (Qt6) 35 | Name[tr]=Python K Çalıştır Eklentisi (Qt6) 36 | Name[uk]=Додаток до KRunner мовою Python (Qt6) 37 | Name[zh_CN]=Python KRunner 插件 (Qt6) 38 | Name[zh_TW]=Python KRunner 外掛程式 (Qt6) 39 | Comment=Template for a KRunner Python Plugin using D-Bus 40 | Comment[ar]=قالب لملحق بايثون لمشغلك باستخدام D-Bus 41 | Comment[az]=D-Bus ilə istifadə olunan KRunner Python Qoşması üçün nümunə 42 | Comment[bg]=Шаблон за KRunner Python Plugin използващ D-Bus 43 | Comment[ca]=Plantilla per a un connector Python del KRunner usant D-Bus 44 | Comment[ca@valencia]=Plantilla per a un connector en Python de KRunner utilitzant D-Bus 45 | Comment[cs]=Šablona pro pythonový zásuvný modul KRunneru používající D-Bus 46 | Comment[da]=Skabelon til et KRunner Python-plugin med brug af D-Bus 47 | Comment[de]=Vorlage für ein KRunner-Modul für Python, das D-Bus verwendet 48 | Comment[el]=Πρότυπο για ένα πρόσθετο του KRunner σε Python με χρήση D-Bus 49 | Comment[en_GB]=Template for a KRunner Python Plugin using D-Bus 50 | Comment[eo]=Ŝablono por KRunner Python-kromprogramo uzanta D-Bus 51 | Comment[es]=Plantilla para un complemento en Python para KRunner que usa D-Bus 52 | Comment[et]=D-Busi kasutav KRunneri Pythoni plugina mall 53 | Comment[eu]=D-Bus erabiltzen duen «KRunner» Python plugin batentzako txantiloia 54 | Comment[fi]=Malli KRunnerin Python-liitännäiseksi D-Busia käyttäen 55 | Comment[fr]=Modèle pour le module externe « KRunner » pour Python, utilisant « D-Bus » 56 | Comment[gl]=Modelo de complemento de Python para KRunner usando D-Bus 57 | Comment[he]=תבנית לתוסף Python של KRunner באמצעות D-Bus 58 | Comment[hi]=डी-बस का प्रयोग करते हुए एक केरनर पायथन प्लगइन का नमूना 59 | Comment[hu]=Sablon a D-Bust használó KRunner Python-bővítményekhez 60 | Comment[ia]=Patrono per un Plugin de Python de KRunner usante D-Bus 61 | Comment[id]=Templat untuk sebuah Plugin Python KRunner menggunakan D-Bus 62 | Comment[it]=Modello per un'estensione Python di KRunner che usa D-Bus 63 | Comment[ka]=KRunner-ის Python-ის დამატების შაბლონი D-Bus-ის გამოყენებით 64 | Comment[ko]=D-Bus를 사용하는 KRunner Python 플러그인 템플릿 65 | Comment[lt]=Šablonas, skirtas KRunner Python įskiepiui, naudojantis D-Bus 66 | Comment[lv]=Veidne „KRunner“ „Python“ spraudnim, izmantojot „D-Bus“ 67 | Comment[nl]=Sjabloon voor een Python-plug-in van KRunner met gebruik van D-Bus 68 | Comment[nn]=Mal for eit Python-basert KRunner-tillegg som brukar D-Bus 69 | Comment[pl]=Szablon wtyczki Pythona dla KRunnera, używająca D-Bus 70 | Comment[pt]=Modelo para um 'Plugin' em Python do KRunner que usa o D-Bus 71 | Comment[pt_BR]=Modelo para um plugin Python do KRunner usando o D-Bus 72 | Comment[ro]=Șablon pentru o extensie KRunner în Python folosind DBus 73 | Comment[ru]=Шаблон расширения KRunner на Python, использующий D-Bus 74 | Comment[sa]=D-Bus इत्यस्य उपयोगेन KRunner Python Plugin कृते Template 75 | Comment[sk]=Šablóna pre KRunner Python plugin využívajúci D-Bus 76 | Comment[sl]=Predloga za vtičnik Python KRunner z uporabo D-Bus 77 | Comment[sv]=Mall för ett Python-insticksprogram för Kör program med användning av D-Bus 78 | Comment[tr]=D-Bus kullanan bir K Çalıştır Python Eklentisi için şablon 79 | Comment[uk]=Шаблон для додатка Python KRunner з використанням D-Bus 80 | Comment[vi]=Bản mẫu cho một phần cài cắm KRunner bằng Python sử dụng D-Bus 81 | Comment[zh_CN]=使用 D-Bus 的 KRunner Python 插件模板 82 | Comment[zh_TW]=使用 D-Bus 的 KRunner Python 外掛程式的範本 83 | Category=Plasma/KRunner 84 | Icon=runner.png 85 | -------------------------------------------------------------------------------- /autotests/plugins/testremoterunner.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | SPDX-FileCopyrightText: 2017 David Edmundson 3 | 4 | SPDX-License-Identifier: LGPL-2.0-or-later 5 | */ 6 | 7 | #include "testremoterunner.h" 8 | 9 | #include 10 | #include 11 | #include 12 | 13 | #include 14 | 15 | #include "krunner1adaptor.h" 16 | 17 | // Test DBus runner, if the search term contains "foo" it returns a match, otherwise nothing 18 | // Run prints a line to stdout 19 | 20 | TestRemoteRunner::TestRemoteRunner(const QString &serviceName, bool showLifecycleMethodCalls) 21 | { 22 | new Krunner1Adaptor(this); 23 | qDBusRegisterMetaType(); 24 | qDBusRegisterMetaType(); 25 | qDBusRegisterMetaType(); 26 | qDBusRegisterMetaType(); 27 | qDBusRegisterMetaType(); 28 | const bool connected = QDBusConnection::sessionBus().registerService(serviceName); 29 | Q_ASSERT(connected); 30 | const bool registered = QDBusConnection::sessionBus().registerObject(QStringLiteral("/dave"), this); 31 | Q_ASSERT(registered); 32 | m_showLifecycleMethodCalls = showLifecycleMethodCalls; 33 | } 34 | 35 | static RemoteImage serializeImage(const QImage &image) 36 | { 37 | QImage convertedImage = image.convertToFormat(QImage::Format_RGBA8888); 38 | RemoteImage remoteImage; 39 | remoteImage.width = convertedImage.width(); 40 | remoteImage.height = convertedImage.height(); 41 | remoteImage.rowStride = convertedImage.bytesPerLine(); 42 | remoteImage.hasAlpha = true, remoteImage.bitsPerSample = 8; 43 | remoteImage.channels = 4, remoteImage.data = QByteArray(reinterpret_cast(convertedImage.constBits()), convertedImage.sizeInBytes()); 44 | return remoteImage; 45 | } 46 | 47 | RemoteMatches TestRemoteRunner::Match(const QString &searchTerm) 48 | { 49 | RemoteMatches ms; 50 | std::cout << "Matching:" << qPrintable(searchTerm) << std::endl; 51 | if (searchTerm == QLatin1String("fooCostomIcon")) { 52 | RemoteMatch m; 53 | m.id = QStringLiteral("id2"); 54 | m.text = QStringLiteral("Match 1"); 55 | m.categoryRelevance = qToUnderlying(KRunner::QueryMatch::CategoryRelevance::Highest); 56 | m.relevance = 0.8; 57 | QImage icon(10, 10, QImage::Format_RGBA8888); 58 | icon.fill(Qt::blue); 59 | m.properties[QStringLiteral("icon-data")] = QVariant::fromValue(serializeImage(icon)); 60 | 61 | ms << m; 62 | } else if (searchTerm.startsWith(QLatin1String("fooDelay"))) { 63 | // This special query string "fooDelayNNNN" allows us to introduce a desired delay 64 | // to simulate a slow query 65 | const int requestedDelayMs = searchTerm.mid(8).toInt(); 66 | RemoteMatch m; 67 | m.id = QStringLiteral("id3"); 68 | m.text = QStringLiteral("Match 1"); 69 | m.iconName = QStringLiteral("icon1"); 70 | m.categoryRelevance = qToUnderlying(KRunner::QueryMatch::CategoryRelevance::Highest); 71 | m.relevance = 0.8; 72 | m.properties[QStringLiteral("actions")] = QStringList(QStringLiteral("action1")); 73 | QThread::msleep(requestedDelayMs); 74 | ms << m; 75 | } else if (searchTerm.contains(QLatin1String("foo"))) { 76 | RemoteMatch m; 77 | m.id = QStringLiteral("id1"); 78 | m.text = QStringLiteral("Match 1"); 79 | m.iconName = QStringLiteral("icon1"); 80 | m.categoryRelevance = qToUnderlying(KRunner::QueryMatch::CategoryRelevance::Highest); 81 | m.relevance = 0.8; 82 | m.properties[QStringLiteral("actions")] = QStringList(QStringLiteral("action1")); 83 | m.properties[QStringLiteral("multiline")] = true; 84 | ms << m; 85 | } 86 | return ms; 87 | } 88 | 89 | KRunner::Actions TestRemoteRunner::Actions() 90 | { 91 | std::cout << "Actions" << std::endl; 92 | KRunner::Action action("action1", "document-browser", "Action 1"); 93 | 94 | KRunner::Action action2("action2", "document-browser", "Action 2"); 95 | return QList{action, action2}; 96 | } 97 | 98 | void TestRemoteRunner::SetActivationToken(const QString &token) 99 | { 100 | std::cout << "Activation Token:" << qPrintable(token) << std::endl; 101 | } 102 | 103 | void TestRemoteRunner::Run(const QString &id, const QString &actionId) 104 | { 105 | std::cout << "Running:" << qPrintable(id) << ":" << qPrintable(actionId) << std::endl; 106 | std::cout.flush(); 107 | } 108 | 109 | void TestRemoteRunner::Teardown() 110 | { 111 | if (m_showLifecycleMethodCalls) { 112 | std::cout << "Teardown" << std::endl; 113 | std::cout.flush(); 114 | } 115 | } 116 | 117 | QVariantMap TestRemoteRunner::Config() 118 | { 119 | if (m_showLifecycleMethodCalls) { 120 | std::cout << "Config" << std::endl; 121 | std::cout.flush(); 122 | } 123 | 124 | return { 125 | {"MatchRegex", "^fo"}, 126 | {"MinLetterCount", 4}, 127 | }; 128 | } 129 | 130 | int main(int argc, char **argv) 131 | { 132 | QCoreApplication app(argc, argv); 133 | const auto arguments = app.arguments(); 134 | Q_ASSERT(arguments.count() >= 2); 135 | TestRemoteRunner r(arguments[1], arguments.count() == 3); 136 | app.exec(); 137 | } 138 | 139 | #include "moc_testremoterunner.cpp" 140 | -------------------------------------------------------------------------------- /autotests/runnermanagersinglerunnermodetest.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | SPDX-FileCopyrightText: 2021 Alexander Lohnau 3 | SPDX-License-Identifier: LGPL-2.1-or-later 4 | */ 5 | 6 | #include "runnermanager.h" 7 | 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | 17 | #include "abstractrunnertest.h" 18 | #include "kpluginmetadata_utils_p.h" 19 | 20 | using namespace KRunner; 21 | 22 | class RunnerManagerSingleRunnerModeTest : public AbstractRunnerTest 23 | { 24 | Q_OBJECT 25 | private Q_SLOTS: 26 | void loadTwoRunners() 27 | { 28 | startDBusRunnerProcess({QStringLiteral("net.krunnertests.dave")}); 29 | startDBusRunnerProcess({QStringLiteral("net.krunnertests.multi.a1")}, QStringLiteral("net.krunnertests.multi.a1")); 30 | qputenv("XDG_DATA_DIRS", QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation).toLocal8Bit()); 31 | QCoreApplication::setLibraryPaths(QStringList()); 32 | initProperties(); 33 | auto md = parseMetaDataFromDesktopFile(QFINDTESTDATA("plugins/dbusrunnertestmulti.desktop")); 34 | QVERIFY(md.isValid()); 35 | manager->loadRunner(md); 36 | } 37 | 38 | void cleanup() 39 | { 40 | killRunningDBusProcesses(); 41 | const QString configFile = QStandardPaths::locate(QStandardPaths::ConfigLocation, "runnermanagersinglerunnermodetestrcm"); 42 | if (!configFile.isEmpty()) { 43 | QFile::remove(configFile); 44 | } 45 | } 46 | 47 | void testAllRunnerResults(); 48 | void testSingleRunnerResults(); 49 | void testNonExistentRunnerId(); 50 | void testLoadingDisabledRunner(); 51 | }; 52 | 53 | void RunnerManagerSingleRunnerModeTest::testAllRunnerResults() 54 | { 55 | loadTwoRunners(); 56 | launchQuery("foo"); 57 | const auto matches = manager->matches(); 58 | QCOMPARE(matches.count(), 2); 59 | const QStringList ids = {matches.at(0).runner()->id(), matches.at(1).runner()->id()}; 60 | // Both runners should have produced one result 61 | QVERIFY(ids.contains("dbusrunnertest")); 62 | QVERIFY(ids.contains("dbusrunnertestmulti")); 63 | } 64 | 65 | void RunnerManagerSingleRunnerModeTest::testSingleRunnerResults() 66 | { 67 | loadTwoRunners(); 68 | launchQuery("foo", "dbusrunnertest"); 69 | QCOMPARE(manager->matches().count(), 1); 70 | QCOMPARE(manager->matches().constFirst().runner()->id(), "dbusrunnertest"); 71 | launchQuery("foo", "dbusrunnertestmulti"); 72 | QCOMPARE(manager->matches().count(), 1); 73 | QCOMPARE(manager->matches().constFirst().runner()->id(), "dbusrunnertestmulti"); 74 | } 75 | 76 | void RunnerManagerSingleRunnerModeTest::testNonExistentRunnerId() 77 | { 78 | loadTwoRunners(); 79 | manager->launchQuery("foo", "bla"); // This internally calls reset, we just wait a bit and make sure there are no matches 80 | QTest::qSleep(250); 81 | QVERIFY(manager->matches().isEmpty()); 82 | } 83 | 84 | void RunnerManagerSingleRunnerModeTest::testLoadingDisabledRunner() 85 | { 86 | loadTwoRunners(); 87 | // Make sure the runner is disabled in the config 88 | auto config = KSharedConfig::openConfig(); 89 | config->deleteGroup("Plugins"); 90 | config->group("Plugins").writeEntry("dbusrunnertestEnabled", false); 91 | // reset our manager to start clean 92 | manager = std::make_unique(config->group("Plugins"), config->group("State"), this); 93 | // Ensure no system runners are picked up 94 | manager->setAllowedRunners(QStringList{"dbusrunnertest", "dbusrunnertestmulti"}); 95 | // Copy the service files to the appropriate location and only load runners from there 96 | QString location = QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation) + "/krunner/dbusplugins/"; 97 | QDir().mkpath(location); 98 | QFile::copy(QFINDTESTDATA("plugins/dbusrunnertest.desktop"), location + "dbusrunnertest.desktop"); 99 | QFile::copy(QFINDTESTDATA("plugins/dbusrunnertestmulti.desktop"), location + "dbusrunnertestmulti.desktop"); 100 | 101 | // Only enabled runner should be loaded and have results 102 | auto matches = launchQuery("foo"); 103 | QCOMPARE(manager->runners().count(), 1); 104 | QCOMPARE(manager->runners().constFirst()->id(), "dbusrunnertestmulti"); 105 | QCOMPARE(matches.count(), 1); 106 | QCOMPARE(matches.constFirst().runner()->id(), "dbusrunnertestmulti"); 107 | 108 | // Only enabled runner should be loaded and have results 109 | matches = launchQuery("foo", "dbusrunnertest"); 110 | QCOMPARE(manager->runners().count(), 2); 111 | const QStringList ids{manager->runners().at(0)->id(), manager->runners().at(1)->id()}; 112 | QVERIFY(ids.contains("dbusrunnertestmulti")); 113 | QVERIFY(ids.contains("dbusrunnertest")); 114 | QCOMPARE(matches.count(), 1); 115 | QCOMPARE(matches.constFirst().runner()->id(), "dbusrunnertest"); 116 | 117 | // Only enabled runner should used for querying 118 | matches = launchQuery("foo"); 119 | QCOMPARE(manager->runners().count(), 2); 120 | QCOMPARE(matches.count(), 1); 121 | QCOMPARE(matches.constFirst().runner()->id(), "dbusrunnertestmulti"); 122 | } 123 | 124 | QTEST_MAIN(RunnerManagerSingleRunnerModeTest) 125 | 126 | #include "runnermanagersinglerunnermodetest.moc" 127 | -------------------------------------------------------------------------------- /src/abstractrunner.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | SPDX-FileCopyrightText: 2006-2007 Aaron Seigo 3 | SPDX-FileCopyrightText: 2020-2023 Alexander Lohnau 4 | 5 | SPDX-License-Identifier: LGPL-2.0-or-later 6 | */ 7 | 8 | #include "abstractrunner.h" 9 | #include "abstractrunner_p.h" 10 | 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | 17 | #include 18 | #include 19 | #include 20 | 21 | namespace KRunner 22 | { 23 | AbstractRunner::AbstractRunner(QObject *parent, const KPluginMetaData &pluginMetaData) 24 | : QObject(nullptr) 25 | , d(new AbstractRunnerPrivate(this, pluginMetaData)) 26 | { 27 | // By now, runners who do "qobject_cast(parent)" should have saved the value 28 | // By setting the parent to a nullptr, we are allowed to move the object to another thread 29 | Q_ASSERT(parent); 30 | setObjectName(pluginMetaData.pluginId()); // Only for debugging purposes 31 | 32 | // Suspend matching while we initialize the runner. Once it is ready, the last query will be run 33 | QTimer::singleShot(0, this, [this]() { 34 | init(); 35 | // In case the runner didn't specify anything explicitly, we resume matching after the initialization 36 | bool doesNotHaveExplicitSuspend = true; 37 | { 38 | QReadLocker l(&d->lock); 39 | doesNotHaveExplicitSuspend = !d->suspendMatching.has_value(); 40 | } 41 | if (doesNotHaveExplicitSuspend) { 42 | suspendMatching(false); 43 | } 44 | }); 45 | } 46 | 47 | AbstractRunner::~AbstractRunner() = default; 48 | 49 | KConfigGroup AbstractRunner::config() const 50 | { 51 | KConfigGroup runners(KSharedConfig::openConfig(QStringLiteral("krunnerrc")), QStringLiteral("Runners")); 52 | return runners.group(id()); 53 | } 54 | 55 | void AbstractRunner::reloadConfiguration() 56 | { 57 | } 58 | 59 | void AbstractRunner::addSyntax(const RunnerSyntax &syntax) 60 | { 61 | d->syntaxes.append(syntax); 62 | } 63 | 64 | void AbstractRunner::setSyntaxes(const QList &syntaxes) 65 | { 66 | d->syntaxes = syntaxes; 67 | } 68 | 69 | QList AbstractRunner::syntaxes() const 70 | { 71 | return d->syntaxes; 72 | } 73 | 74 | QMimeData *AbstractRunner::mimeDataForMatch(const QueryMatch &match) 75 | { 76 | if (match.urls().isEmpty()) { 77 | return nullptr; 78 | } 79 | auto *result = new QMimeData(); 80 | result->setUrls(match.urls()); 81 | return result; 82 | } 83 | 84 | void AbstractRunner::run(const KRunner::RunnerContext & /*search*/, const KRunner::QueryMatch & /*action*/) 85 | { 86 | } 87 | 88 | QString AbstractRunner::name() const 89 | { 90 | return d->translatedName; 91 | } 92 | 93 | QString AbstractRunner::id() const 94 | { 95 | return d->runnerDescription.pluginId(); 96 | } 97 | 98 | KPluginMetaData AbstractRunner::metadata() const 99 | { 100 | return d->runnerDescription; 101 | } 102 | 103 | void AbstractRunner::init() 104 | { 105 | reloadConfiguration(); 106 | } 107 | 108 | bool AbstractRunner::isMatchingSuspended() const 109 | { 110 | QReadLocker lock(&d->lock); 111 | return d->suspendMatching.value_or(true); 112 | } 113 | 114 | void AbstractRunner::suspendMatching(bool suspend) 115 | { 116 | QWriteLocker lock(&d->lock); 117 | if (d->suspendMatching.has_value() && d->suspendMatching.value() == suspend) { 118 | return; 119 | } 120 | 121 | d->suspendMatching = suspend; 122 | if (!suspend) { 123 | Q_EMIT matchingResumed(); 124 | } 125 | } 126 | 127 | int AbstractRunner::minLetterCount() const 128 | { 129 | return d->minLetterCount; 130 | } 131 | 132 | void AbstractRunner::setMinLetterCount(int count) 133 | { 134 | d->minLetterCount = count; 135 | } 136 | 137 | QRegularExpression AbstractRunner::matchRegex() const 138 | { 139 | return d->matchRegex; 140 | } 141 | 142 | void AbstractRunner::setMatchRegex(const QRegularExpression ®ex) 143 | { 144 | d->matchRegex = regex; 145 | d->hasMatchRegex = regex.isValid() && !regex.pattern().isEmpty(); 146 | } 147 | 148 | void AbstractRunner::setTriggerWords(const QStringList &triggerWords) 149 | { 150 | int minTriggerWordLetters = 0; 151 | QString constructedRegex = QStringLiteral("^"); 152 | for (const QString &triggerWord : triggerWords) { 153 | // We want to link them with an or 154 | if (constructedRegex.length() > 1) { 155 | constructedRegex += QLatin1Char('|'); 156 | } 157 | constructedRegex += QRegularExpression::escape(triggerWord); 158 | if (minTriggerWordLetters == 0 || triggerWord.length() < minTriggerWordLetters) { 159 | minTriggerWordLetters = triggerWord.length(); 160 | } 161 | } 162 | // If we can reject the query because of the length we don't need the regex 163 | setMinLetterCount(minTriggerWordLetters); 164 | setMatchRegex(QRegularExpression(constructedRegex)); 165 | } 166 | 167 | bool AbstractRunner::hasMatchRegex() const 168 | { 169 | return d->hasMatchRegex; 170 | } 171 | 172 | void AbstractRunner::matchInternal(KRunner::RunnerContext context) 173 | { 174 | if (context.isValid()) { // Otherwise, we would just waste resources 175 | match(context); 176 | } 177 | Q_EMIT matchInternalFinished(context.runnerJobId(this)); 178 | } 179 | // Suspend the runner while reloading the config 180 | void AbstractRunner::reloadConfigurationInternal() 181 | { 182 | bool isSuspended = isMatchingSuspended(); 183 | suspendMatching(true); 184 | reloadConfiguration(); 185 | suspendMatching(isSuspended); 186 | } 187 | 188 | } // KRunner namespace 189 | 190 | #include "moc_abstractrunner.cpp" 191 | -------------------------------------------------------------------------------- /src/abstractrunnertest.h: -------------------------------------------------------------------------------- 1 | /* 2 | SPDX-FileCopyrightText: 2020 Alexander Lohnau 3 | SPDX-License-Identifier: LGPL-2.0-or-later 4 | */ 5 | 6 | #ifndef KRUNNER_ABSTRACTRUNNERTEST_H 7 | #define KRUNNER_ABSTRACTRUNNERTEST_H 8 | 9 | #include 10 | #include 11 | #include 12 | #include 13 | 14 | #include 15 | #include 16 | #if KRUNNER_DBUS_RUNNER_TESTING 17 | #include 18 | #include 19 | #include 20 | #include 21 | #include 22 | #endif 23 | 24 | namespace KRunner 25 | { 26 | /* 27 | * This class provides a basic structure for a runner test. 28 | * The compile definitions should be configured using the `krunner_configure_test` cmake macro 29 | */ 30 | class AbstractRunnerTest : public QObject 31 | { 32 | public: 33 | using QObject::QObject; 34 | std::unique_ptr manager = nullptr; 35 | KRunner::AbstractRunner *runner = nullptr; 36 | 37 | /* 38 | * Load the runner and set the manager and runner properties. 39 | */ 40 | void initProperties() 41 | { 42 | qputenv("LC_ALL", "C.utf-8"); 43 | manager.reset(new KRunner::RunnerManager()); 44 | 45 | #if KRUNNER_DBUS_RUNNER_TESTING 46 | auto md = manager->convertDBusRunnerToJson(QStringLiteral(KRUNNER_TEST_DESKTOP_FILE)); 47 | QVERIFY(md.isValid()); 48 | manager->loadRunner(md); 49 | #else 50 | const QString pluginId = QFileInfo(QStringLiteral(KRUNNER_TEST_RUNNER_PLUGIN_NAME)).completeBaseName(); 51 | auto metaData = KPluginMetaData::findPluginById(QStringLiteral(KRUNNER_TEST_RUNNER_PLUGIN_DIR), pluginId); 52 | QVERIFY2(metaData.isValid(), 53 | qPrintable(QStringLiteral("Could not find plugin %1 in folder %2").arg(pluginId, QStringLiteral(KRUNNER_TEST_RUNNER_PLUGIN_DIR)))); 54 | 55 | // Set internal variables 56 | manager->loadRunner(metaData); 57 | #endif 58 | QCOMPARE(manager->runners().count(), 1); 59 | runner = manager->runners().constFirst(); 60 | 61 | // Just make sure all went well 62 | QVERIFY(runner); 63 | } 64 | 65 | /* 66 | * Launch a query and wait for the RunnerManager to finish 67 | * @param query 68 | * @param runnerName 69 | * @return matches of the current query 70 | */ 71 | QList launchQuery(const QString &query, const QString &runnerName = QString()) 72 | { 73 | QSignalSpy spy(manager.get(), &KRunner::RunnerManager::queryFinished); 74 | manager->launchQuery(query, runnerName); 75 | if (!QTest::qVerify(spy.wait(), "spy.wait()", "RunnerManager did not emit the queryFinished signal", __FILE__, __LINE__)) { 76 | return {}; 77 | } 78 | return manager->matches(); 79 | } 80 | #if KRUNNER_DBUS_RUNNER_TESTING 81 | /* 82 | * Launch the configured DBus executable with the given arguments and wait for the process to be started. 83 | * @param args 84 | * @param waitForService Wait for this service to be registered, this will default to the service from the metadata 85 | * @return Process that was successfully started 86 | */ 87 | QProcess *startDBusRunnerProcess(const QStringList &args = {}, const QString waitForService = QString()) 88 | { 89 | qputenv("LC_ALL", "C.utf-8"); 90 | QProcess *process = new QProcess(); 91 | auto md = manager->convertDBusRunnerToJson(QStringLiteral(KRUNNER_TEST_DESKTOP_FILE)); 92 | QString serviceToWatch = waitForService; 93 | if (serviceToWatch.isEmpty()) { 94 | serviceToWatch = md.value(QStringLiteral("X-Plasma-DBusRunner-Service")); 95 | } 96 | QEventLoop loop; 97 | // Wait for the service to show up. Same logic as the dbusrunner 98 | connect(QDBusConnection::sessionBus().interface(), 99 | &QDBusConnectionInterface::serviceOwnerChanged, 100 | &loop, 101 | [&loop, serviceToWatch](const QString &serviceName, const QString &, const QString &newOwner) { 102 | if (serviceName == serviceToWatch && !newOwner.isEmpty()) { 103 | loop.quit(); 104 | } 105 | }); 106 | 107 | // Otherwise, we just wait forever without any indication what we are waiting for 108 | QTimer::singleShot(10000, &loop, [&loop, process]() { 109 | loop.quit(); 110 | 111 | if (process->state() == QProcess::ProcessState::NotRunning) { 112 | qWarning() << "stderr of" << KRUNNER_TEST_DBUS_EXECUTABLE << "is:"; 113 | qWarning().noquote() << process->readAllStandardError(); 114 | } 115 | Q_ASSERT_X(false, "AbstractRunnerTest::startDBusRunnerProcess", "DBus service was not registered within 10 seconds"); 116 | }); 117 | process->start(QStringLiteral(KRUNNER_TEST_DBUS_EXECUTABLE), args); 118 | loop.exec(); 119 | process->waitForStarted(5); 120 | 121 | Q_ASSERT(process->state() == QProcess::ProcessState::Running); 122 | m_runningProcesses << process; 123 | return process; 124 | } 125 | 126 | /* 127 | * Kill all processes that got started with the startDBusRunnerProcess 128 | */ 129 | void killRunningDBusProcesses() 130 | { 131 | for (auto &process : std::as_const(m_runningProcesses)) { 132 | process->kill(); 133 | QVERIFY(process->waitForFinished()); 134 | if (QTest::currentTestFailed()) { 135 | qWarning().noquote() << "Output from " << process->program() << ": " << process->readAll(); 136 | } 137 | } 138 | qDeleteAll(m_runningProcesses); 139 | m_runningProcesses.clear(); 140 | } 141 | 142 | private: 143 | QList m_runningProcesses; 144 | #endif 145 | }; 146 | } 147 | 148 | #endif 149 | -------------------------------------------------------------------------------- /src/model/resultsmodel.h: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: 2019 Kai Uwe Broulik 3 | * SPDX-FileCopyrightText: 2023 Alexander Lohnau 4 | * 5 | * SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL 6 | * 7 | */ 8 | 9 | #ifndef KRUNNER_RESULTSMODEL 10 | #define KRUNNER_RESULTSMODEL 11 | 12 | #include "krunner_export.h" 13 | 14 | #include 15 | #include 16 | #include 17 | #include 18 | #include 19 | 20 | namespace KRunner 21 | { 22 | class ResultsModelPrivate; 23 | 24 | /*! 25 | * \class KRunner::ResultsModel 26 | * \inheaderfile KRunner/ResultsModel 27 | * \inmodule KRunner 28 | * 29 | * \brief A model that exposes and sorts results for a given query. 30 | * 31 | * \since 6.0 32 | */ 33 | class KRUNNER_EXPORT ResultsModel : public QSortFilterProxyModel 34 | { 35 | Q_OBJECT 36 | 37 | /*! 38 | * \property KRunner::ResultsModel::queryString 39 | * 40 | * The query string to run 41 | */ 42 | Q_PROPERTY(QString queryString READ queryString WRITE setQueryString NOTIFY queryStringChanged) 43 | /*! 44 | * \property KRunner::ResultsModel::limit 45 | * 46 | * The preferred maximum number of matches in the model 47 | * 48 | * If there are lots of results from different categories, 49 | * the limit can be slightly exceeded. 50 | * 51 | * Default is 0, which means no limit. 52 | */ 53 | Q_PROPERTY(int limit READ limit WRITE setLimit RESET resetLimit NOTIFY limitChanged) 54 | /*! 55 | * \property KRunner::ResultsModel::querying 56 | * 57 | * Whether the query is currently being run 58 | * 59 | * This can be used to show a busy indicator 60 | */ 61 | Q_PROPERTY(bool querying READ querying NOTIFY queryingChanged) 62 | 63 | /*! 64 | * \property KRunner::ResultsModel::singleRunner 65 | * 66 | * The single runner to use for querying in single runner mode 67 | * 68 | * Defaults to empty string which means all runners 69 | */ 70 | Q_PROPERTY(QString singleRunner READ singleRunner WRITE setSingleRunner NOTIFY singleRunnerChanged) 71 | 72 | /*! 73 | * \property KRunner::ResultsModel::singleRunnerMetaData 74 | */ 75 | Q_PROPERTY(KPluginMetaData singleRunnerMetaData READ singleRunnerMetaData NOTIFY singleRunnerChanged) 76 | 77 | /*! 78 | * \property KRunner::ResultsModel::runnerManager 79 | */ 80 | Q_PROPERTY(KRunner::RunnerManager *runnerManager READ runnerManager WRITE setRunnerManager NOTIFY runnerManagerChanged) 81 | 82 | /*! 83 | * \property KRunner::ResultsModel::favoriteIds 84 | */ 85 | Q_PROPERTY(QStringList favoriteIds READ favoriteIds WRITE setFavoriteIds NOTIFY favoriteIdsChanged) 86 | 87 | public: 88 | /*! 89 | * 90 | */ 91 | explicit ResultsModel(const KConfigGroup &configGroup, const KConfigGroup &stateConfigGroup, QObject *parent = nullptr); 92 | 93 | /*! 94 | * 95 | */ 96 | explicit ResultsModel(QObject *parent = nullptr); 97 | ~ResultsModel() override; 98 | 99 | /*! 100 | * \value IdRole 101 | * \value CategoryRelevanceRole 102 | * \value RelevanceRole 103 | * \value EnabledRole 104 | * \value CategoryRole 105 | * \value SubtextRole 106 | * \value ActionsRole 107 | * \value MultiLineRole 108 | * \value UrlsRole 109 | * \omitvalue QueryMatchRole 110 | * \omitvalue FavoriteIndexRole 111 | * \omitvalue FavoriteCountRole 112 | */ 113 | enum Roles { 114 | IdRole = Qt::UserRole + 1, 115 | CategoryRelevanceRole, 116 | RelevanceRole, 117 | EnabledRole, 118 | CategoryRole, 119 | SubtextRole, 120 | ActionsRole, 121 | MultiLineRole, 122 | UrlsRole, 123 | QueryMatchRole, 124 | FavoriteIndexRole, 125 | FavoriteCountRole, 126 | }; 127 | Q_ENUM(Roles) 128 | 129 | QString queryString() const; 130 | void setQueryString(const QString &queryString); 131 | Q_SIGNAL void queryStringChanged(const QString &queryString); 132 | 133 | /*! 134 | * IDs of favorite plugins. Those plugins are always in a fixed order before the other ones. 135 | * 136 | * \a ids KPluginMetaData::pluginId values of plugins 137 | */ 138 | void setFavoriteIds(const QStringList &ids); 139 | QStringList favoriteIds() const; 140 | Q_SIGNAL void favoriteIdsChanged(); 141 | 142 | int limit() const; 143 | void setLimit(int limit); 144 | void resetLimit(); 145 | Q_SIGNAL void limitChanged(); 146 | 147 | bool querying() const; 148 | Q_SIGNAL void queryingChanged(); 149 | 150 | QString singleRunner() const; 151 | void setSingleRunner(const QString &runner); 152 | Q_SIGNAL void singleRunnerChanged(); 153 | 154 | KPluginMetaData singleRunnerMetaData() const; 155 | 156 | QHash roleNames() const override; 157 | 158 | /*! 159 | * Clears the model content and resets the runner context, i.e. no new items will appear. 160 | */ 161 | Q_INVOKABLE void clear(); 162 | 163 | /*! 164 | * Run the result at the given model index \a idx 165 | */ 166 | Q_INVOKABLE bool run(const QModelIndex &idx); 167 | /*! 168 | * Run the action \a actionNumber at given model index \a idx 169 | */ 170 | Q_INVOKABLE bool runAction(const QModelIndex &idx, int actionNumber); 171 | 172 | /*! 173 | * Get mime data for the result at given model index \a idx 174 | */ 175 | Q_INVOKABLE QMimeData *getMimeData(const QModelIndex &idx) const; 176 | 177 | /*! 178 | * Get match for the result at given model index \a idx 179 | */ 180 | KRunner::QueryMatch getQueryMatch(const QModelIndex &idx) const; 181 | 182 | KRunner::RunnerManager *runnerManager() const; 183 | /*! 184 | * \since 6.9 185 | */ 186 | void setRunnerManager(KRunner::RunnerManager *manager); 187 | /*! 188 | * \since 6.9 189 | */ 190 | Q_SIGNAL void runnerManagerChanged(); 191 | 192 | Q_SIGNALS: 193 | /*! 194 | * This signal is emitted when a an InformationalMatch is run, and it is advised 195 | * to update the search term, e.g. used for calculator runner results 196 | */ 197 | void queryStringChangeRequested(const QString &queryString, int pos); 198 | 199 | private: 200 | const std::unique_ptr d; 201 | }; 202 | 203 | } // namespace KRunner 204 | #endif 205 | -------------------------------------------------------------------------------- /autotests/runnermanagertest.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | SPDX-FileCopyrightText: 2022 Eduardo Cruz 3 | SPDX-License-Identifier: LGPL-2.1-or-later 4 | */ 5 | 6 | #include "runnermanager.h" 7 | 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | 16 | #include "abstractrunnertest.h" 17 | #include "kpluginmetadata_utils_p.h" 18 | 19 | Q_DECLARE_METATYPE(KRunner::QueryMatch) 20 | Q_DECLARE_METATYPE(QList) 21 | 22 | using namespace KRunner; 23 | 24 | class RunnerManagerTest : public AbstractRunnerTest 25 | { 26 | Q_OBJECT 27 | private Q_SLOTS: 28 | void initTestCase() 29 | { 30 | startDBusRunnerProcess({QStringLiteral("net.krunnertests.dave")}); 31 | qputenv("XDG_DATA_DIRS", QStandardPaths::writableLocation(QStandardPaths::GenericDataLocation).toLocal8Bit()); 32 | QCoreApplication::setLibraryPaths(QStringList()); 33 | initProperties(); 34 | qRegisterMetaType>(); 35 | } 36 | 37 | void cleanupTestCase() 38 | { 39 | killRunningDBusProcesses(); 40 | } 41 | 42 | /* 43 | * This will test the mechanismm that stalls for 250ms before emiting any result in RunnerManager::scheduleMatchesChanged() 44 | * and the mechanism that anticipates the last results emission in RunnerManager::jobDone(). 45 | */ 46 | void testScheduleMatchesChanged() 47 | { 48 | QSignalSpy spyQueryFinished(manager.get(), &KRunner::RunnerManager::queryFinished); 49 | QSignalSpy spyMatchesChanged(manager.get(), &KRunner::RunnerManager::matchesChanged); 50 | 51 | QVERIFY(spyQueryFinished.isValid()); 52 | QVERIFY(spyMatchesChanged.isValid()); 53 | 54 | QCOMPARE(spyQueryFinished.count(), 0); 55 | 56 | // This will track the total execution time 57 | QElapsedTimer timer; 58 | timer.start(); 59 | 60 | // This special string will simulate a 300ms delay 61 | manager->launchQuery("fooDelay300"); 62 | 63 | // However not yet a matcheschanged, it should be stalled for 250ms 64 | QCOMPARE(spyMatchesChanged.count(), 0); 65 | 66 | // After 250ms it will emit with empty matches, we wait for that. 67 | // We can't put a low upper limit on these wait() calls because the CI environment can be slow. 68 | QVERIFY(spyMatchesChanged.wait()); // This should take just a tad longer than 250ms. 69 | 70 | // This should have taken no less than 250ms. It waits for 250s before "giving up" and emitting an empty matches list. 71 | QVERIFY(timer.elapsed() >= 250); 72 | QCOMPARE(spyMatchesChanged.count(), 1); 73 | QCOMPARE(manager->matches().count(), 0); // This is the empty matches "reset" emission, result is not ready yet 74 | QCOMPARE(spyQueryFinished.count(), 0); // Still the same, query is not done 75 | 76 | // We programmed it to emit the result after 300ms, so we need to wait 50ms more for the next emission 77 | QVERIFY(spyQueryFinished.wait()); 78 | 79 | // This should have taken at least 300ms total, as we requested via the special query string 80 | QVERIFY(timer.elapsed() >= 300); 81 | 82 | // At this point RunnerManager::jobDone() should have anticipated the final emission. 83 | QCOMPARE(manager->matches().count(), 1); // The result is here 84 | QCOMPARE(spyQueryFinished.count(), 1); // Will have emited queryFinished, job is done 85 | QCOMPARE(spyMatchesChanged.count(), 2); // We had the second matchesChanged emission, now with the query result 86 | 87 | // Now we will make sure that RunnerManager::scheduleMatchesChanged() emits matchesChanged instantly 88 | // if we start a query with an empty string. It will never produce results, stalling is meaningless 89 | manager->launchQuery(""); 90 | QCOMPARE(spyMatchesChanged.count(), 3); // One more, instantly, without stall 91 | QCOMPARE(manager->matches().count(), 0); // Empty results for empty query string 92 | QVERIFY(spyQueryFinished.wait()); 93 | } 94 | 95 | /* 96 | * This will test queryFinished signal from reset() is emitted when the previous runners are 97 | * still running. 98 | */ 99 | void testQueryFinishedFromReset() 100 | { 101 | QSignalSpy spyQueryFinished(manager.get(), &KRunner::RunnerManager::queryFinished); 102 | 103 | manager->launchQuery("fooDelay1000"); 104 | QTest::qSleep(500); 105 | QCOMPARE(spyQueryFinished.size(), 0); 106 | 107 | manager->launchQuery("fooDelay300"); 108 | QCOMPARE(spyQueryFinished.size(), 1); // From reset() 109 | 110 | QVERIFY(spyQueryFinished.wait()); 111 | QCOMPARE(spyQueryFinished.size(), 2); 112 | } 113 | 114 | /* 115 | * When we delete the RunnerManager while a job is still running, we should not crash 116 | */ 117 | void testNotCrashWhenDeletingRunnerManager() 118 | { 119 | RunnerManager manager; 120 | manager.setAllowedRunners({QStringLiteral("fakerunnerplugin")}); 121 | manager.loadRunner(KPluginMetaData::findPluginById(QStringLiteral("krunnertest"), QStringLiteral("fakerunnerplugin"))); 122 | 123 | QCOMPARE(manager.runners().size(), 1); 124 | 125 | manager.launchQuery("somequery"); 126 | } 127 | 128 | void testRunnerManagerStateGroups() 129 | { 130 | auto stateGrp = KSharedConfig::openConfig(QString(), KConfig::NoGlobals)->group("Testme"); 131 | auto configGrp = KSharedConfig::openConfig(QString(), KConfig::NoGlobals)->group("Plugins"); 132 | stateGrp.deleteGroup(); 133 | RunnerManager manager(configGrp, stateGrp, this); 134 | manager.setAllowedRunners({QStringLiteral("fakerunnerplugin")}); 135 | manager.loadRunner(KPluginMetaData::findPluginById(QStringLiteral("krunnertest"), QStringLiteral("fakerunnerplugin"))); 136 | QSignalSpy spyQueryFinished(&manager, &KRunner::RunnerManager::queryFinished); 137 | 138 | manager.launchQuery("foo"); 139 | spyQueryFinished.wait(); 140 | manager.run(manager.matches().constFirst()); 141 | manager.matchSessionComplete(); 142 | } 143 | 144 | void testRunnerSuspendWhileReloadingConfig() 145 | { 146 | RunnerManager manager; 147 | manager.loadRunner(KPluginMetaData::findPluginById(QStringLiteral("krunnertest2"), QStringLiteral("suspendedrunnerplugin"))); 148 | QCOMPARE(manager.runners().size(), 1); 149 | 150 | AbstractRunner *runner = manager.runners().constFirst(); 151 | QVERIFY(runner->isMatchingSuspended()); 152 | 153 | QSignalSpy spy(&manager, &KRunner::RunnerManager::queryFinished); 154 | manager.launchQuery("foo"); 155 | QVERIFY2(spy.wait(), "RunnerManager did not emit the queryFinished signal"); 156 | 157 | QCOMPARE(manager.matches().size(), 1); 158 | 159 | QVERIFY(!runner->isMatchingSuspended()); 160 | } 161 | 162 | void testAbstractRunnerTestTimeout() 163 | { 164 | QEXPECT_FAIL("", "This test is expected to fail", Continue); 165 | const auto matches = launchQuery("fooDelay6000"); 166 | QVERIFY(matches.isEmpty()); 167 | } 168 | }; 169 | 170 | QTEST_MAIN(RunnerManagerTest) 171 | 172 | #include "runnermanagertest.moc" 173 | -------------------------------------------------------------------------------- /LICENSES/CC0-1.0.txt: -------------------------------------------------------------------------------- 1 | Creative Commons Legal Code 2 | 3 | CC0 1.0 Universal 4 | 5 | CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE 6 | LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN 7 | ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS 8 | INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES 9 | REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS 10 | PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM 11 | THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED 12 | HEREUNDER. 13 | 14 | Statement of Purpose 15 | 16 | The laws of most jurisdictions throughout the world automatically confer 17 | exclusive Copyright and Related Rights (defined below) upon the creator 18 | and subsequent owner(s) (each and all, an "owner") of an original work of 19 | authorship and/or a database (each, a "Work"). 20 | 21 | Certain owners wish to permanently relinquish those rights to a Work for 22 | the purpose of contributing to a commons of creative, cultural and 23 | scientific works ("Commons") that the public can reliably and without fear 24 | of later claims of infringement build upon, modify, incorporate in other 25 | works, reuse and redistribute as freely as possible in any form whatsoever 26 | and for any purposes, including without limitation commercial purposes. 27 | These owners may contribute to the Commons to promote the ideal of a free 28 | culture and the further production of creative, cultural and scientific 29 | works, or to gain reputation or greater distribution for their Work in 30 | part through the use and efforts of others. 31 | 32 | For these and/or other purposes and motivations, and without any 33 | expectation of additional consideration or compensation, the person 34 | associating CC0 with a Work (the "Affirmer"), to the extent that he or she 35 | is an owner of Copyright and Related Rights in the Work, voluntarily 36 | elects to apply CC0 to the Work and publicly distribute the Work under its 37 | terms, with knowledge of his or her Copyright and Related Rights in the 38 | Work and the meaning and intended legal effect of CC0 on those rights. 39 | 40 | 1. Copyright and Related Rights. A Work made available under CC0 may be 41 | protected by copyright and related or neighboring rights ("Copyright and 42 | Related Rights"). Copyright and Related Rights include, but are not 43 | limited to, the following: 44 | 45 | i. the right to reproduce, adapt, distribute, perform, display, 46 | communicate, and translate a Work; 47 | ii. moral rights retained by the original author(s) and/or performer(s); 48 | iii. publicity and privacy rights pertaining to a person's image or 49 | likeness depicted in a Work; 50 | iv. rights protecting against unfair competition in regards to a Work, 51 | subject to the limitations in paragraph 4(a), below; 52 | v. rights protecting the extraction, dissemination, use and reuse of data 53 | in a Work; 54 | vi. database rights (such as those arising under Directive 96/9/EC of the 55 | European Parliament and of the Council of 11 March 1996 on the legal 56 | protection of databases, and under any national implementation 57 | thereof, including any amended or successor version of such 58 | directive); and 59 | vii. other similar, equivalent or corresponding rights throughout the 60 | world based on applicable law or treaty, and any national 61 | implementations thereof. 62 | 63 | 2. Waiver. To the greatest extent permitted by, but not in contravention 64 | of, applicable law, Affirmer hereby overtly, fully, permanently, 65 | irrevocably and unconditionally waives, abandons, and surrenders all of 66 | Affirmer's Copyright and Related Rights and associated claims and causes 67 | of action, whether now known or unknown (including existing as well as 68 | future claims and causes of action), in the Work (i) in all territories 69 | worldwide, (ii) for the maximum duration provided by applicable law or 70 | treaty (including future time extensions), (iii) in any current or future 71 | medium and for any number of copies, and (iv) for any purpose whatsoever, 72 | including without limitation commercial, advertising or promotional 73 | purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each 74 | member of the public at large and to the detriment of Affirmer's heirs and 75 | successors, fully intending that such Waiver shall not be subject to 76 | revocation, rescission, cancellation, termination, or any other legal or 77 | equitable action to disrupt the quiet enjoyment of the Work by the public 78 | as contemplated by Affirmer's express Statement of Purpose. 79 | 80 | 3. Public License Fallback. Should any part of the Waiver for any reason 81 | be judged legally invalid or ineffective under applicable law, then the 82 | Waiver shall be preserved to the maximum extent permitted taking into 83 | account Affirmer's express Statement of Purpose. In addition, to the 84 | extent the Waiver is so judged Affirmer hereby grants to each affected 85 | person a royalty-free, non transferable, non sublicensable, non exclusive, 86 | irrevocable and unconditional license to exercise Affirmer's Copyright and 87 | Related Rights in the Work (i) in all territories worldwide, (ii) for the 88 | maximum duration provided by applicable law or treaty (including future 89 | time extensions), (iii) in any current or future medium and for any number 90 | of copies, and (iv) for any purpose whatsoever, including without 91 | limitation commercial, advertising or promotional purposes (the 92 | "License"). The License shall be deemed effective as of the date CC0 was 93 | applied by Affirmer to the Work. Should any part of the License for any 94 | reason be judged legally invalid or ineffective under applicable law, such 95 | partial invalidity or ineffectiveness shall not invalidate the remainder 96 | of the License, and in such case Affirmer hereby affirms that he or she 97 | will not (i) exercise any of his or her remaining Copyright and Related 98 | Rights in the Work or (ii) assert any associated claims and causes of 99 | action with respect to the Work, in either case contrary to Affirmer's 100 | express Statement of Purpose. 101 | 102 | 4. Limitations and Disclaimers. 103 | 104 | a. No trademark or patent rights held by Affirmer are waived, abandoned, 105 | surrendered, licensed or otherwise affected by this document. 106 | b. Affirmer offers the Work as-is and makes no representations or 107 | warranties of any kind concerning the Work, express, implied, 108 | statutory or otherwise, including without limitation warranties of 109 | title, merchantability, fitness for a particular purpose, non 110 | infringement, or the absence of latent or other defects, accuracy, or 111 | the present or absence of errors, whether or not discoverable, all to 112 | the greatest extent permissible under applicable law. 113 | c. Affirmer disclaims responsibility for clearing rights of other persons 114 | that may apply to the Work or any use thereof, including without 115 | limitation any person's Copyright and Related Rights in the Work. 116 | Further, Affirmer disclaims responsibility for obtaining any necessary 117 | consents, permissions or other rights required for any use of the 118 | Work. 119 | d. Affirmer understands and acknowledges that Creative Commons is not a 120 | party to this document and has no duty or obligation with respect to 121 | this CC0 or use of the Work. 122 | -------------------------------------------------------------------------------- /src/querymatch.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | SPDX-FileCopyrightText: 2006-2007 Aaron Seigo 3 | 4 | SPDX-License-Identifier: LGPL-2.0-or-later 5 | */ 6 | 7 | #include "querymatch.h" 8 | #include "action.h" 9 | 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | 16 | #include "abstractrunner.h" 17 | #include "abstractrunner_p.h" 18 | 19 | namespace KRunner 20 | { 21 | class QueryMatchPrivate : public QSharedData 22 | { 23 | public: 24 | explicit QueryMatchPrivate(AbstractRunner *r) 25 | : QSharedData() 26 | , runner(r) 27 | { 28 | } 29 | 30 | QueryMatchPrivate(const QueryMatchPrivate &other) 31 | : QSharedData(other) 32 | { 33 | QReadLocker l(&other.lock); 34 | runner = other.runner; 35 | categoryRelevance = other.categoryRelevance; 36 | relevance = other.relevance; 37 | selAction = other.selAction; 38 | enabled = other.enabled; 39 | idSetByData = other.idSetByData; 40 | matchCategory = other.matchCategory; 41 | id = other.id; 42 | text = other.text; 43 | subtext = other.subtext; 44 | icon = other.icon; 45 | iconName = other.iconName; 46 | data = other.data; 47 | urls = other.urls; 48 | actions = other.actions; 49 | multiLine = other.multiLine; 50 | } 51 | 52 | void setId(const QString &newId) 53 | { 54 | if (runner && runner->d->hasUniqueResults) { 55 | id = newId; 56 | } else { 57 | if (runner) { 58 | id = runner->id(); 59 | } 60 | if (!id.isEmpty()) { 61 | id.append(QLatin1Char('_')).append(newId); 62 | } 63 | } 64 | idSetByData = false; 65 | } 66 | 67 | mutable QReadWriteLock lock; 68 | QPointer runner; 69 | QString matchCategory; 70 | QString id; 71 | QString text; 72 | QString subtext; 73 | QString mimeType; 74 | QList urls; 75 | QIcon icon; 76 | QString iconName; 77 | QVariant data; 78 | qreal categoryRelevance = 50; 79 | qreal relevance = .7; 80 | KRunner::Action selAction; 81 | KRunner::Actions actions; 82 | bool enabled = true; 83 | bool idSetByData = false; 84 | bool multiLine = false; 85 | }; 86 | 87 | QueryMatch::QueryMatch(AbstractRunner *runner) 88 | : d(new QueryMatchPrivate(runner)) 89 | { 90 | } 91 | 92 | QueryMatch::QueryMatch(const QueryMatch &other) 93 | 94 | = default; 95 | 96 | QueryMatch::~QueryMatch() = default; 97 | 98 | bool QueryMatch::isValid() const 99 | { 100 | return d->runner != nullptr; 101 | } 102 | 103 | QString QueryMatch::id() const 104 | { 105 | if (d->id.isEmpty() && d->runner) { 106 | return d->runner->id(); 107 | } 108 | 109 | return d->id; 110 | } 111 | 112 | void QueryMatch::setCategoryRelevance(qreal relevance) 113 | { 114 | d->categoryRelevance = qBound(0.0, relevance, 100.0); 115 | } 116 | 117 | qreal QueryMatch::categoryRelevance() const 118 | { 119 | return d->categoryRelevance; 120 | } 121 | 122 | void QueryMatch::setMatchCategory(const QString &category) 123 | { 124 | d->matchCategory = category; 125 | } 126 | 127 | QString QueryMatch::matchCategory() const 128 | { 129 | if (d->matchCategory.isEmpty() && d->runner) { 130 | return d->runner->name(); 131 | } 132 | return d->matchCategory; 133 | } 134 | 135 | void QueryMatch::setRelevance(qreal relevance) 136 | { 137 | d->relevance = qMax(qreal(0.0), relevance); 138 | } 139 | 140 | qreal QueryMatch::relevance() const 141 | { 142 | return d->relevance; 143 | } 144 | 145 | AbstractRunner *QueryMatch::runner() const 146 | { 147 | return d->runner.data(); 148 | } 149 | 150 | void QueryMatch::setText(const QString &text) 151 | { 152 | QWriteLocker locker(&d->lock); 153 | d->text = text; 154 | } 155 | 156 | void QueryMatch::setSubtext(const QString &subtext) 157 | { 158 | QWriteLocker locker(&d->lock); 159 | d->subtext = subtext; 160 | } 161 | 162 | void QueryMatch::setData(const QVariant &data) 163 | { 164 | QWriteLocker locker(&d->lock); 165 | d->data = data; 166 | 167 | if (d->id.isEmpty() || d->idSetByData) { 168 | const QString matchId = data.toString(); 169 | if (!matchId.isEmpty()) { 170 | d->setId(matchId); 171 | d->idSetByData = true; 172 | } 173 | } 174 | } 175 | 176 | void QueryMatch::setId(const QString &id) 177 | { 178 | QWriteLocker locker(&d->lock); 179 | d->setId(id); 180 | } 181 | 182 | void QueryMatch::setIcon(const QIcon &icon) 183 | { 184 | QWriteLocker locker(&d->lock); 185 | d->icon = icon; 186 | } 187 | 188 | void QueryMatch::setIconName(const QString &iconName) 189 | { 190 | QWriteLocker locker(&d->lock); 191 | d->iconName = iconName; 192 | } 193 | 194 | QVariant QueryMatch::data() const 195 | { 196 | QReadLocker locker(&d->lock); 197 | return d->data; 198 | } 199 | 200 | QString QueryMatch::text() const 201 | { 202 | QReadLocker locker(&d->lock); 203 | return d->text; 204 | } 205 | 206 | QString QueryMatch::subtext() const 207 | { 208 | QReadLocker locker(&d->lock); 209 | return d->subtext; 210 | } 211 | 212 | QIcon QueryMatch::icon() const 213 | { 214 | QReadLocker locker(&d->lock); 215 | return d->icon; 216 | } 217 | 218 | QString QueryMatch::iconName() const 219 | { 220 | QReadLocker locker(&d->lock); 221 | return d->iconName; 222 | } 223 | 224 | void QueryMatch::setUrls(const QList &urls) 225 | { 226 | QWriteLocker locker(&d->lock); 227 | d->urls = urls; 228 | } 229 | 230 | QList QueryMatch::urls() const 231 | { 232 | QReadLocker locker(&d->lock); 233 | return d->urls; 234 | } 235 | 236 | void QueryMatch::setEnabled(bool enabled) 237 | { 238 | d->enabled = enabled; 239 | } 240 | 241 | bool QueryMatch::isEnabled() const 242 | { 243 | return d->enabled && d->runner; 244 | } 245 | 246 | KRunner::Action QueryMatch::selectedAction() const 247 | { 248 | return d->selAction; 249 | } 250 | 251 | void QueryMatch::setSelectedAction(const KRunner::Action &action) 252 | { 253 | d->selAction = action; 254 | } 255 | 256 | void QueryMatch::setMultiLine(bool multiLine) 257 | { 258 | d->multiLine = multiLine; 259 | } 260 | 261 | bool QueryMatch::isMultiLine() const 262 | { 263 | return d->multiLine; 264 | } 265 | 266 | QueryMatch &QueryMatch::operator=(const QueryMatch &other) 267 | { 268 | if (d != other.d) { 269 | d = other.d; 270 | } 271 | 272 | return *this; 273 | } 274 | 275 | bool QueryMatch::operator==(const QueryMatch &other) const 276 | { 277 | return (d == other.d); 278 | } 279 | 280 | bool QueryMatch::operator!=(const QueryMatch &other) const 281 | { 282 | return (d != other.d); 283 | } 284 | 285 | void QueryMatch::setActions(const QList &actions) 286 | { 287 | QWriteLocker locker(&d->lock); 288 | d->actions = actions; 289 | } 290 | 291 | void QueryMatch::addAction(const KRunner::Action &action) 292 | { 293 | QWriteLocker locker(&d->lock); 294 | d->actions << action; 295 | } 296 | 297 | KRunner::Actions QueryMatch::actions() const 298 | { 299 | QReadLocker locker(&d->lock); 300 | return d->actions; 301 | } 302 | 303 | QDebug operator<<(QDebug debug, const KRunner::QueryMatch &match) 304 | { 305 | QDebugStateSaver saver(debug); 306 | debug.nospace() << "QueryMatch(category: " << match.matchCategory() << " text:" << match.text() << ")"; 307 | return debug; 308 | } 309 | 310 | } // KRunner namespace 311 | -------------------------------------------------------------------------------- /src/runnercontext.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | SPDX-FileCopyrightText: 2006-2007 Aaron Seigo 3 | SPDX-FileCopyrightText: 2023 Alexander Lohnau 4 | 5 | SPDX-License-Identifier: LGPL-2.0-or-later 6 | */ 7 | 8 | #include "runnercontext.h" 9 | 10 | #include 11 | 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | 18 | #include 19 | #include 20 | 21 | #include "abstractrunner.h" 22 | #include "abstractrunner_p.h" 23 | #include "querymatch.h" 24 | #include "runnermanager.h" 25 | 26 | namespace KRunner 27 | { 28 | class RunnerContextPrivate : public QSharedData 29 | { 30 | public: 31 | explicit RunnerContextPrivate(RunnerManager *manager) 32 | : QSharedData() 33 | , m_manager(manager) 34 | { 35 | } 36 | 37 | RunnerContextPrivate(const RunnerContextPrivate &p) 38 | : QSharedData(p) 39 | , m_manager(p.m_manager) 40 | { 41 | } 42 | 43 | ~RunnerContextPrivate() = default; 44 | 45 | void invalidate() 46 | { 47 | m_isValid = false; 48 | } 49 | 50 | void addMatch(const QueryMatch &match) 51 | { 52 | if (match.runner() && match.runner()->d->hasUniqueResults) { 53 | if (uniqueIds.contains(match.id())) { 54 | const QueryMatch &existentMatch = uniqueIds.value(match.id()); 55 | if (existentMatch.runner() && existentMatch.runner()->d->hasWeakResults) { 56 | // There is an existing match with the same ID and we are allowed to replace it 57 | matches.removeOne(existentMatch); 58 | matches.append(match); 59 | } 60 | } else { 61 | // There is no existing match with the same id 62 | uniqueIds.insert(match.id(), match); 63 | matches.append(match); 64 | } 65 | } else { 66 | // Runner has the unique results property not set 67 | matches.append(match); 68 | } 69 | } 70 | 71 | void matchesChanged() 72 | { 73 | if (m_manager) { 74 | QMetaObject::invokeMethod(m_manager, "onMatchesChanged"); 75 | } 76 | } 77 | 78 | QReadWriteLock lock; 79 | QPointer m_manager; 80 | bool m_isValid = true; 81 | QList matches; 82 | QString term; 83 | bool singleRunnerQueryMode = false; 84 | bool shouldIgnoreCurrentMatchForHistory = false; 85 | QMap uniqueIds; 86 | QString requestedText; 87 | int requestedCursorPosition = 0; 88 | qint64 queryStartTs = 0; 89 | }; 90 | 91 | RunnerContext::RunnerContext(RunnerManager *manager) 92 | : d(new RunnerContextPrivate(manager)) 93 | { 94 | } 95 | 96 | // copy ctor 97 | RunnerContext::RunnerContext(const RunnerContext &other) 98 | { 99 | QReadLocker locker(&other.d->lock); 100 | d = other.d; 101 | } 102 | 103 | RunnerContext::~RunnerContext() = default; 104 | 105 | RunnerContext &RunnerContext::operator=(const RunnerContext &other) 106 | { 107 | if (this->d == other.d) { 108 | return *this; 109 | } 110 | 111 | auto oldD = d; // To avoid the old ptr getting destroyed while the mutex is locked 112 | QWriteLocker locker(&d->lock); 113 | QReadLocker otherLocker(&other.d->lock); 114 | d = other.d; 115 | return *this; 116 | } 117 | 118 | /*! 119 | * Resets the search term for this object. 120 | * This removes all current matches in the process and 121 | * turns off single runner query mode. 122 | * Copies of this object that are used by runner are invalidated 123 | * and adding matches will be a noop. 124 | */ 125 | void RunnerContext::reset() 126 | { 127 | { 128 | QWriteLocker locker(&d->lock); 129 | // We will detach if we are a copy of someone. But we will reset 130 | // if we are the 'main' context others copied from. Resetting 131 | // one RunnerContext makes all the copies obsolete. 132 | 133 | // We need to mark the q pointer of the detached RunnerContextPrivate 134 | // as dirty on detach to avoid receiving results for old queries 135 | d->invalidate(); 136 | } 137 | 138 | d.detach(); 139 | // But out detached version is valid! 140 | d->m_isValid = true; 141 | 142 | // we still have to remove all the matches, since if the 143 | // ref count was 1 (e.g. only the RunnerContext is using 144 | // the dptr) then we won't get a copy made 145 | d->matches.clear(); 146 | d->term.clear(); 147 | d->matchesChanged(); 148 | 149 | d->uniqueIds.clear(); 150 | d->singleRunnerQueryMode = false; 151 | d->shouldIgnoreCurrentMatchForHistory = false; 152 | } 153 | 154 | void RunnerContext::setQuery(const QString &term) 155 | { 156 | if (!this->query().isEmpty()) { 157 | reset(); 158 | } 159 | 160 | if (term.isEmpty()) { 161 | return; 162 | } 163 | 164 | d->requestedText.clear(); // Invalidate this field whenever the query changes 165 | d->term = term; 166 | } 167 | 168 | QString RunnerContext::query() const 169 | { 170 | // the query term should never be set after 171 | // a search starts. in fact, reset() ensures this 172 | // and setQuery(QString) calls reset() 173 | return d->term; 174 | } 175 | 176 | bool RunnerContext::isValid() const 177 | { 178 | QReadLocker locker(&d->lock); 179 | return d->m_isValid; 180 | } 181 | 182 | bool RunnerContext::addMatches(const QList &matches) 183 | { 184 | if (matches.isEmpty() || !isValid()) { 185 | // Bail out if the query is empty or the qptr is dirty 186 | return false; 187 | } 188 | 189 | { 190 | QWriteLocker locker(&d->lock); 191 | for (const QueryMatch &match : matches) { 192 | d->addMatch(match); 193 | } 194 | } 195 | d->matchesChanged(); 196 | 197 | return true; 198 | } 199 | 200 | bool RunnerContext::addMatch(const QueryMatch &match) 201 | { 202 | return addMatches({match}); 203 | } 204 | 205 | QList RunnerContext::matches() const 206 | { 207 | QReadLocker locker(&d->lock); 208 | QList matches = d->matches; 209 | return matches; 210 | } 211 | 212 | void RunnerContext::requestQueryStringUpdate(const QString &text, int cursorPosition) const 213 | { 214 | d->requestedText = text; 215 | d->requestedCursorPosition = cursorPosition; 216 | } 217 | 218 | void RunnerContext::setSingleRunnerQueryMode(bool enabled) 219 | { 220 | d->singleRunnerQueryMode = enabled; 221 | } 222 | 223 | bool RunnerContext::singleRunnerQueryMode() const 224 | { 225 | return d->singleRunnerQueryMode; 226 | } 227 | 228 | void RunnerContext::ignoreCurrentMatchForHistory() const 229 | { 230 | d->shouldIgnoreCurrentMatchForHistory = true; 231 | } 232 | 233 | bool RunnerContext::shouldIgnoreCurrentMatchForHistory() const 234 | { 235 | return d->shouldIgnoreCurrentMatchForHistory; 236 | } 237 | 238 | void RunnerContext::restore([[maybe_unused]] const KConfigGroup &config) 239 | { 240 | // TODO KF7: Drop 241 | } 242 | 243 | void RunnerContext::save([[maybe_unused]] KConfigGroup &config) 244 | { 245 | // TODO KF7: Drop 246 | } 247 | 248 | void RunnerContext::increaseLaunchCount([[maybe_unused]] const QueryMatch &match) 249 | { 250 | // TODO KF7: Drop 251 | } 252 | 253 | QString RunnerContext::requestedQueryString() const 254 | { 255 | return d->requestedText; 256 | } 257 | int RunnerContext::requestedCursorPosition() const 258 | { 259 | return d->requestedCursorPosition; 260 | } 261 | 262 | void RunnerContext::setJobStartTs(qint64 queryStartTs) 263 | { 264 | d->queryStartTs = queryStartTs; 265 | } 266 | QString RunnerContext::runnerJobId(AbstractRunner *runner) const 267 | { 268 | return QLatin1String("%1-%2-%3").arg(runner->id(), query(), QString::number(d->queryStartTs)); 269 | } 270 | 271 | } // KRunner namespace 272 | -------------------------------------------------------------------------------- /src/runnermanager.h: -------------------------------------------------------------------------------- 1 | /* 2 | SPDX-FileCopyrightText: 2006 Aaron Seigo 3 | SPDX-FileCopyrightText: 2007 Ryan P. Bitanga 4 | SPDX-FileCopyrightText: 2008 Jordi Polo 5 | SPDX-FileCopyrightText: 2023 Alexander Lohnau 6 | 7 | SPDX-License-Identifier: LGPL-2.0-or-later 8 | */ 9 | 10 | #ifndef KRUNNER_RUNNERMANAGER_H 11 | #define KRUNNER_RUNNERMANAGER_H 12 | 13 | #include 14 | #include 15 | 16 | #include 17 | #include 18 | 19 | #include "abstractrunner.h" 20 | #include "action.h" 21 | #include "krunner_export.h" 22 | #include 23 | 24 | class KConfigGroup; 25 | namespace KRunner 26 | { 27 | class AbstractRunnerTest; 28 | } 29 | 30 | namespace KRunner 31 | { 32 | class QueryMatch; 33 | class AbstractRunner; 34 | class RunnerContext; 35 | class RunnerManagerPrivate; 36 | 37 | /*! 38 | * \class KRunner::RunnerManager 39 | * \inheaderfile KRunner/RunnerManager 40 | * \inmodule KRunner 41 | * 42 | * \brief The RunnerManager class decides what installed runners are runnable, 43 | * and their ratings. It is the main proxy to the runners. 44 | */ 45 | class KRUNNER_EXPORT RunnerManager : public QObject 46 | { 47 | Q_OBJECT 48 | 49 | /*! 50 | * \property KRunner::RunnerManager::history 51 | */ 52 | Q_PROPERTY(QStringList history READ history NOTIFY historyChanged) 53 | 54 | /*! 55 | * \property KRunner::RunnerManager::querying 56 | */ 57 | Q_PROPERTY(bool querying READ querying NOTIFY queryingChanged) 58 | 59 | /*! 60 | * \property KRunner::RunnerManager::historyEnabled 61 | */ 62 | Q_PROPERTY(bool historyEnabled READ historyEnabled WRITE setHistoryEnabled NOTIFY historyEnabledChanged) 63 | 64 | public: 65 | /*! 66 | * Constructs a RunnerManager with the given parameters 67 | * 68 | * \a configurationGroup Config group used for reading enabled plugins 69 | * 70 | * \a stateGroup Config group used for storing history 71 | * 72 | * \since 6.0 73 | */ 74 | explicit RunnerManager(const KConfigGroup &pluginConfigGroup, const KConfigGroup &stateGroup, QObject *parent); 75 | 76 | /*! 77 | * Constructs a RunnerManager using the default locations for state/plugin config 78 | */ 79 | explicit RunnerManager(QObject *parent = nullptr); 80 | ~RunnerManager() override; 81 | 82 | /*! 83 | * Finds and returns a loaded runner or a nullptr 84 | * 85 | * \a pluginId the name of the runner plugin 86 | * 87 | * Returns Pointer to the runner 88 | */ 89 | AbstractRunner *runner(const QString &pluginId) const; 90 | 91 | /*! 92 | * Returns the list of all currently loaded runners 93 | */ 94 | QList runners() const; 95 | 96 | /*! 97 | * Retrieves the current context 98 | * 99 | * Returns pointer to the current context 100 | */ 101 | RunnerContext *searchContext() const; 102 | 103 | /*! 104 | * Retrieves all available matches found so far for the previously launched query 105 | */ 106 | QList matches() const; 107 | 108 | /*! 109 | * Runs a given match. This also respects the extra handling for the InformationalMatch. 110 | * 111 | * This also handles the history automatically 112 | * 113 | * \a match the match to be executed 114 | * 115 | * \a selectedAction the action returned by QueryMatch::actions that has been selected by the user, nullptr if none 116 | * 117 | * Returns if the RunnerWindow should close 118 | * 119 | * \since 6.0 120 | */ 121 | bool run(const QueryMatch &match, const KRunner::Action &action = {}); 122 | 123 | /*! 124 | * Returns the current query term set in launchQuery 125 | */ 126 | QString query() const; 127 | 128 | /*! 129 | * Returns History of this runner for the current activity. If the RunnerManager is not history 130 | * aware the global entries will be returned. 131 | * \since 5.78 132 | */ 133 | QStringList history() const; 134 | 135 | /*! 136 | * Delete the given index from the history. 137 | * 138 | * \a historyEntry 139 | * \since 5.78 140 | */ 141 | Q_INVOKABLE void removeFromHistory(int index); 142 | 143 | /*! 144 | * Get the suggested history entry for the typed query. If no entry is found an empty string is returned. 145 | * 146 | * \a typedQuery 147 | * 148 | * Returns completion for typedQuery 149 | * \since 5.78 150 | */ 151 | Q_INVOKABLE QString getHistorySuggestion(const QString &typedQuery) const; 152 | 153 | /*! 154 | * If history completion is enabled, the default value is true. 155 | * \since 5.78 156 | */ 157 | bool historyEnabled(); 158 | 159 | /*! 160 | * If the RunnerManager is currently querying 161 | * \since 6.7 162 | */ 163 | bool querying() const; 164 | 165 | /*! 166 | * Enables/disabled the history feature for the RunnerManager instance. 167 | * The value will not be persisted and is only kept during the object's lifetime. 168 | * 169 | * \since 6.0 170 | */ 171 | void setHistoryEnabled(bool enabled); 172 | 173 | /*! 174 | * Causes a reload of the current configuration 175 | * 176 | * This gets called automatically when the config in the KCM is saved 177 | */ 178 | void reloadConfiguration(); 179 | 180 | /*! 181 | * Sets a whitelist for the plugins that can be loaded by this manager. 182 | * 183 | * Runners that are disabled through the config will not be loaded. 184 | * 185 | * \a plugins the plugin names of allowed runners 186 | */ 187 | void setAllowedRunners(const QStringList &runners); 188 | 189 | /*! 190 | * Attempts to add the AbstractRunner plugin represented 191 | * by the plugin info passed in. Usually one can simply let 192 | * the configuration of plugins handle loading Runner plugins, 193 | * but in cases where specific runners should be loaded this 194 | * allows for that to take place 195 | * 196 | * \note Consider using setAllowedRunners in case you want to only allow specific runners 197 | * 198 | * \a pluginMetaData the metaData to use to load the plugin 199 | * 200 | * Returns the loaded runner or nullptr 201 | */ 202 | AbstractRunner *loadRunner(const KPluginMetaData &pluginMetaData); 203 | 204 | /*! 205 | * Returns mime data of the specified match 206 | */ 207 | QMimeData *mimeDataForMatch(const QueryMatch &match) const; 208 | 209 | /*! 210 | * Returns metadata list of all known Runner plugins 211 | * \since 5.72 212 | */ 213 | static QList runnerMetaDataList(); 214 | 215 | public Q_SLOTS: 216 | /*! 217 | * Call this method when the runners should be prepared for a query session. 218 | * Call matchSessionComplete when the query session is finished for the time 219 | * being. 220 | * \sa matchSessionComplete 221 | */ 222 | void setupMatchSession(); 223 | 224 | /*! 225 | * Call this method when the query session is finished for the time 226 | * being. 227 | * \sa prepareForMatchSession 228 | */ 229 | void matchSessionComplete(); 230 | 231 | /*! 232 | * Launch a query, this will create threads and return immediately. 233 | * When the information will be available can be known using the 234 | * matchesChanged signal. 235 | * 236 | * \a term the term we want to find matches for 237 | * 238 | * \a runnerId optional, if only one specific runner is to be used; 239 | * providing an id will put the manager into single runner mode 240 | */ 241 | void launchQuery(const QString &term, const QString &runnerId = QString()); 242 | 243 | /*! 244 | * Reset the current data and stops the query 245 | */ 246 | void reset(); 247 | 248 | /*! 249 | * Set the environment identifier for recording history and launch counts 250 | * \internal 251 | * \since 6.0 252 | */ 253 | Q_INVOKABLE void setHistoryEnvironmentIdentifier(const QString &identifier); 254 | 255 | Q_SIGNALS: 256 | /*! 257 | * Emitted each time a new match is added to the list 258 | */ 259 | void matchesChanged(const QList &matches); 260 | 261 | /*! 262 | * Emitted when the launchQuery finish 263 | */ 264 | void queryFinished(); 265 | 266 | /*! 267 | * Emitted when the querying status has changed 268 | * \since 6.7 269 | */ 270 | 271 | void queryingChanged(); 272 | 273 | /*! 274 | * Put the given search term in the KRunner search field 275 | * 276 | * \a term The term that should be displayed 277 | * 278 | * \a cursorPosition Where the cursor should be positioned 279 | * \since 6.0 280 | */ 281 | void requestUpdateQueryString(const QString &term, int cursorPosition); 282 | 283 | /*! 284 | * \sa historyEnabled 285 | * \since 5.78 286 | */ 287 | void historyEnabledChanged(); 288 | 289 | /*! 290 | * Emitted when the history has changed 291 | * \since 6.21 292 | */ 293 | 294 | void historyChanged(); 295 | 296 | private: 297 | // exported for dbusrunnertest 298 | KPluginMetaData convertDBusRunnerToJson(const QString &filename) const; 299 | KRUNNER_NO_EXPORT Q_INVOKABLE void onMatchesChanged(); 300 | 301 | std::unique_ptr d; 302 | KConfigWatcher::Ptr m_stateWatcher; 303 | 304 | friend class RunnerManagerPrivate; 305 | friend AbstractRunnerTest; 306 | friend AbstractRunner; 307 | }; 308 | 309 | } 310 | #endif 311 | -------------------------------------------------------------------------------- /src/querymatch.h: -------------------------------------------------------------------------------- 1 | /* 2 | SPDX-FileCopyrightText: 2006-2007 Aaron Seigo 3 | SPDX-FileCopyrightText: 2023 Alexander Lohnau 4 | 5 | SPDX-License-Identifier: LGPL-2.0-or-later 6 | */ 7 | 8 | #ifndef KRUNNER_QUERYMATCH_H 9 | #define KRUNNER_QUERYMATCH_H 10 | 11 | #include 12 | #include 13 | #include 14 | 15 | #include "krunner_export.h" 16 | 17 | class QIcon; 18 | class QVariant; 19 | 20 | namespace KRunner 21 | { 22 | class Action; 23 | class AbstractRunner; 24 | class QueryMatchPrivate; 25 | 26 | /*! 27 | * \class KRunner::QueryMatch 28 | * \inheaderfile KRunner/QueryMatch 29 | * \inmodule KRunner 30 | * 31 | * \brief A match returned by an AbstractRunner in response to a given RunnerContext. 32 | */ 33 | class KRUNNER_EXPORT QueryMatch 34 | { 35 | public: 36 | /*! 37 | * Constructs a PossibleMatch associated with a given RunnerContext 38 | * and runner. 39 | * 40 | * \a runner the runner this match belongs to 41 | */ 42 | explicit QueryMatch(AbstractRunner *runner = nullptr); 43 | 44 | QueryMatch(const QueryMatch &other); 45 | 46 | ~QueryMatch(); 47 | QueryMatch &operator=(const QueryMatch &other); 48 | bool operator==(const QueryMatch &other) const; 49 | bool operator!=(const QueryMatch &other) const; 50 | 51 | /*! 52 | * Returns the runner associated with this action 53 | */ 54 | AbstractRunner *runner() const; 55 | 56 | /*! 57 | * Returns true if the match is valid and can therefore be run, 58 | * an invalid match does not have an associated AbstractRunner 59 | */ 60 | bool isValid() const; 61 | 62 | /*! 63 | * Helper for reading standardized category relevance values 64 | * 65 | * \value Lowest 66 | * \value Low 67 | * \value Moderate 68 | * \value High 69 | * \value Highest 70 | */ 71 | enum class CategoryRelevance { 72 | Lowest = 0, 73 | Low = 30, 74 | Moderate = 50, 75 | High = 70, 76 | Highest = 100, 77 | }; 78 | 79 | /*! 80 | * Relevance for matches in the category. The match with the highest relevance is respected for the entire category. 81 | * This value only affects the sorting of categories and not the sorting within the category. Use setRelevance for this. 82 | * The value should be from 0 to 100. 83 | * 84 | * \since 6.0 85 | */ 86 | void setCategoryRelevance(CategoryRelevance relevance) 87 | { 88 | setCategoryRelevance(qToUnderlying(relevance)); 89 | } 90 | 91 | /*! 92 | * \internal Internal for now, consumers should utilize CategoryRelevance enum 93 | * 94 | * \since 6.0 95 | */ 96 | void setCategoryRelevance(qreal relevance); 97 | 98 | /*! 99 | * Category relevance for this match 100 | * 101 | * \since 6.0 102 | */ 103 | qreal categoryRelevance() const; 104 | 105 | /*! 106 | * Sets information about the type of the match which is 107 | * used to group the matches. 108 | * 109 | * This string should be translated as it is displayed in an UI. 110 | * The default is AbstractRunner::name 111 | */ 112 | void setMatchCategory(const QString &category); 113 | 114 | /*! 115 | * Extra information about the match which can be used 116 | * to categorize the type. 117 | * 118 | * The default is AbstractRunner::name 119 | */ 120 | QString matchCategory() const; 121 | 122 | /*! 123 | * Sets the relevance of this action for the search 124 | * it was created for. 125 | * 126 | * \a relevance a number between 0 and 1. 127 | */ 128 | void setRelevance(qreal relevance); 129 | 130 | /*! 131 | * The relevance of this action to the search. By default, 132 | * the relevance is 1. 133 | * 134 | * Returns a number between 0 and 1 135 | */ 136 | qreal relevance() const; 137 | 138 | /*! 139 | * Sets data to be used internally by the runner's AbstractRunner::run implementation. 140 | * 141 | * When set, it is also used to form part of the id for this match. 142 | * If that is inappropriate as an id, the runner may generate its own 143 | * id and set that with setId 144 | */ 145 | void setData(const QVariant &data); 146 | 147 | /*! 148 | * Returns the data associated with this match; usually runner-specific 149 | */ 150 | QVariant data() const; 151 | 152 | /*! 153 | * Sets the id for this match; useful if the id does not 154 | * match data().toString(). The id must be unique to all 155 | * matches from this runner, and should remain constant 156 | * for the same query for best results. 157 | * 158 | * If the "X-Plasma-Runner-Unique-Results" property from the metadata 159 | * is set to true, the runnerId will not be prepended to the ID. 160 | * This allows KRunner to de-duplicate results from different runners. 161 | * In case the runner's matches are less specific than ones from other runners, the 162 | * "X-Plasma-Runner-Weak-Results" property can be set so that duplicates from this 163 | * runner are removed. 164 | * 165 | * \a id the new identifying string to use to refer 166 | * to this entry 167 | */ 168 | void setId(const QString &id); 169 | 170 | /*! 171 | * Returns a string that can be used as an ID for this match, 172 | * even between different queries. It is based in part 173 | * on the source of the match (the AbstractRunner) and 174 | * distinguishing information provided by the runner, 175 | * ensuring global uniqueness as well as consistency 176 | * between query matches. 177 | */ 178 | QString id() const; 179 | 180 | /*! 181 | * Sets the main title text for this match; should be short 182 | * enough to fit nicely on one line in a user interface 183 | * For styled and multiline text, setMultiLine should be set to true 184 | * 185 | * \a text the text to use as the title 186 | */ 187 | void setText(const QString &text); 188 | 189 | /*! 190 | * Returns the title text for this match 191 | */ 192 | QString text() const; 193 | 194 | /*! 195 | * Sets the descriptive text for this match; can be longer 196 | * than the main title text 197 | * 198 | * \a text the text to use as the description 199 | */ 200 | void setSubtext(const QString &text); 201 | 202 | /*! 203 | * Returns the descriptive text for this match 204 | */ 205 | QString subtext() const; 206 | 207 | /*! 208 | * Sets the icon associated with this match 209 | * 210 | * Prefer using setIconName. 211 | * 212 | * \a icon the icon to show along with the match 213 | */ 214 | void setIcon(const QIcon &icon); 215 | 216 | /*! 217 | * Returns the icon for this match 218 | */ 219 | QIcon icon() const; 220 | 221 | /*! 222 | * Sets the icon name associated with this match 223 | * 224 | * \a icon the name of the icon to show along with the match 225 | * \since 5.24 226 | */ 227 | void setIconName(const QString &iconName); 228 | 229 | /*! 230 | * Returns the name of the icon for this match 231 | * \since 5.24 232 | */ 233 | QString iconName() const; 234 | 235 | /*! 236 | * Sets the urls, if any, associated with this match 237 | */ 238 | void setUrls(const QList &urls); 239 | 240 | /*! 241 | * Returns the urls for this match, empty list if none 242 | * These will be used in the default implementation of AbstractRunner::mimeDataForMatch 243 | */ 244 | QList urls() const; 245 | 246 | /*! 247 | * Sets whether or not this match can be activited 248 | * 249 | * \a enable true if the match is enabled and therefore runnable 250 | */ 251 | void setEnabled(bool enable); 252 | 253 | /*! 254 | * Returns true if the match is enabled and therefore runnable, otherwise false 255 | */ 256 | bool isEnabled() const; 257 | 258 | /*! 259 | * Set the actions for this match. 260 | * This method allows you to set the actions inside of the AbstractRunner::match method 261 | * \sa RunnerManager::actionsForMatch 262 | * \since 5.75 263 | */ 264 | void setActions(const QList &actions); 265 | 266 | /*! 267 | * Adds an action to this match 268 | * \since 5.75 269 | * \sa setActions 270 | */ 271 | void addAction(const KRunner::Action &action); 272 | 273 | /*! 274 | * List of actions set for this match 275 | * Returns actions 276 | * \since 5.75 277 | */ 278 | QList actions() const; 279 | 280 | /*! 281 | * The action that the user has selected when running the match. 282 | * This returns a nullptr if no action was selected. 283 | */ 284 | KRunner::Action selectedAction() const; 285 | 286 | /*! 287 | * Set if the text should be displayed as a multiLine string 288 | * 289 | * \a multiLine 290 | * \since 5.82 291 | */ 292 | void setMultiLine(bool multiLine); 293 | 294 | /*! 295 | * If the text should be displayed as a multiLine string 296 | * If no explicit value is set set using setMultiline it will default to false 297 | * Returns bool 298 | * \since 5.82 299 | */ 300 | bool isMultiLine() const; 301 | 302 | private: 303 | KRUNNER_NO_EXPORT void setSelectedAction(const KRunner::Action &action); 304 | friend class RunnerManager; 305 | QSharedDataPointer d; 306 | }; 307 | 308 | /// \since 6.0 309 | KRUNNER_EXPORT QDebug operator<<(QDebug debug, const KRunner::QueryMatch &match); 310 | } 311 | #endif 312 | -------------------------------------------------------------------------------- /autotests/dbusrunnertest.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | SPDX-FileCopyrightText: 2017 David Edmundson 3 | 4 | SPDX-License-Identifier: LGPL-2.0-or-later 5 | */ 6 | 7 | #include 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | 18 | #include "abstractrunnertest.h" 19 | #include "kpluginmetadata_utils_p.h" 20 | 21 | using namespace KRunner; 22 | 23 | Q_DECLARE_METATYPE(KRunner::QueryMatch) 24 | Q_DECLARE_METATYPE(QList) 25 | 26 | class DBusRunnerTest : public AbstractRunnerTest 27 | { 28 | Q_OBJECT 29 | public: 30 | DBusRunnerTest(); 31 | ~DBusRunnerTest() override; 32 | 33 | private Q_SLOTS: 34 | void cleanup(); 35 | void testMatch(); 36 | void testMulti(); 37 | void testFilterProperties(); 38 | void testFilterProperties_data(); 39 | void testRequestActionsOnce(); 40 | void testDBusRunnerSyntaxIntegration(); 41 | void testIconData(); 42 | void testLifecycleMethods(); 43 | void testRequestActionsWildcards(); 44 | }; 45 | 46 | DBusRunnerTest::DBusRunnerTest() 47 | : AbstractRunnerTest() 48 | { 49 | qRegisterMetaType>(); 50 | QStandardPaths::setTestModeEnabled(true); 51 | } 52 | 53 | DBusRunnerTest::~DBusRunnerTest() = default; 54 | 55 | void DBusRunnerTest::cleanup() 56 | { 57 | // Make sure kill the running processes after each test 58 | killRunningDBusProcesses(); 59 | } 60 | 61 | void DBusRunnerTest::testMatch() 62 | { 63 | QProcess *process = startDBusRunnerProcess({QStringLiteral("net.krunnertests.dave")}); 64 | initProperties(); 65 | const auto matches = launchQuery(QStringLiteral("foo")); 66 | 67 | // verify matches 68 | QCOMPARE(matches.count(), 1); 69 | auto result = matches.first(); 70 | 71 | // see testremoterunner.cpp 72 | QCOMPARE(result.id(), QStringLiteral("dbusrunnertest_id1")); // note the runner name is prepended 73 | QCOMPARE(result.text(), QStringLiteral("Match 1")); 74 | QCOMPARE(result.iconName(), QStringLiteral("icon1")); 75 | QCOMPARE(result.categoryRelevance(), qToUnderlying(KRunner::QueryMatch::CategoryRelevance::Highest)); 76 | QCOMPARE(result.isMultiLine(), true); 77 | // relevance can't be compared easily because RunnerContext meddles with it 78 | 79 | // verify actions 80 | QCOMPARE(result.actions().size(), 1); 81 | auto action = result.actions().constFirst(); 82 | 83 | QCOMPARE(action.text(), QStringLiteral("Action 1")); 84 | 85 | QSignalSpy processSpy(process, &QProcess::readyRead); 86 | manager->run(result); 87 | processSpy.wait(); 88 | QCOMPARE(process->readAllStandardOutput().trimmed().split('\n').constLast(), QByteArray("Running:id1:")); 89 | 90 | manager->run(result, action); 91 | processSpy.wait(); 92 | QCOMPARE(process->readAllStandardOutput().trimmed().split('\n').constLast(), QByteArray("Running:id1:action1")); 93 | } 94 | 95 | void DBusRunnerTest::testMulti() 96 | { 97 | startDBusRunnerProcess({QStringLiteral("net.krunnertests.multi.a1")}, QStringLiteral("net.krunnertests.multi.a1")); 98 | startDBusRunnerProcess({QStringLiteral("net.krunnertests.multi.a2")}, QStringLiteral("net.krunnertests.multi.a2")); 99 | manager = std::make_unique(); // This case is special, because we want to load the runners manually 100 | 101 | auto md = parseMetaDataFromDesktopFile(QFINDTESTDATA("plugins/dbusrunnertestmulti.desktop")); 102 | QVERIFY(md.isValid()); 103 | manager->loadRunner(md); 104 | const auto matches = launchQuery(QStringLiteral("foo")); 105 | 106 | // verify matches, must be one from each 107 | QCOMPARE(matches.count(), 2); 108 | 109 | const QString first = matches.at(0).data().toList().constFirst().toString(); 110 | const QString second = matches.at(1).data().toList().constFirst().toString(); 111 | QVERIFY(first != second); 112 | QVERIFY(first == QLatin1String("net.krunnertests.multi.a1") || first == QStringLiteral("net.krunnertests.multi.a2")); 113 | QVERIFY(second == QLatin1String("net.krunnertests.multi.a1") || second == QStringLiteral("net.krunnertests.multi.a2")); 114 | } 115 | 116 | void DBusRunnerTest::testRequestActionsOnce() 117 | { 118 | QProcess *process = startDBusRunnerProcess({QStringLiteral("net.krunnertests.dave")}); 119 | initProperties(); 120 | 121 | launchQuery(QStringLiteral("foo")); 122 | QVERIFY(!manager->matches().constFirst().actions().isEmpty()); 123 | manager->matchSessionComplete(); 124 | launchQuery(QStringLiteral("fooo")); 125 | const QString processOutput(process->readAllStandardOutput()); 126 | QCOMPARE(processOutput.count("Matching"), 2); 127 | QCOMPARE(processOutput.count("Actions"), 1); 128 | QVERIFY(!manager->matches().constFirst().actions().isEmpty()); 129 | } 130 | 131 | void DBusRunnerTest::testFilterProperties_data() 132 | { 133 | QTest::addColumn("rejectedQuery"); 134 | QTest::addColumn("acceptedQuery"); 135 | 136 | QTest::newRow("min-letter-count") << "fo" 137 | << "foo"; 138 | QTest::newRow("match-regex") << "barfoo" 139 | << "foobar"; 140 | } 141 | 142 | void DBusRunnerTest::testFilterProperties() 143 | { 144 | QFETCH(QString, rejectedQuery); 145 | QFETCH(QString, acceptedQuery); 146 | QProcess *process = startDBusRunnerProcess({QStringLiteral("net.krunnertests.dave")}); 147 | initProperties(); 148 | 149 | launchQuery(rejectedQuery); 150 | // Match method was not called, because of the min letter count or match regex property 151 | QVERIFY(process->readAllStandardOutput().isEmpty()); 152 | // accepted query fits those constraints 153 | launchQuery(acceptedQuery); 154 | QCOMPARE(QString(process->readAllStandardOutput()).remove("Actions").trimmed(), QStringLiteral("Matching:") + acceptedQuery); 155 | } 156 | 157 | void DBusRunnerTest::testDBusRunnerSyntaxIntegration() 158 | { 159 | startDBusRunnerProcess({QStringLiteral("net.krunnertests.dave")}); 160 | initProperties(); 161 | const QList syntaxes = runner->syntaxes(); 162 | QCOMPARE(syntaxes.size(), 2); 163 | 164 | QCOMPARE(syntaxes.at(0).exampleQueries().size(), 1); 165 | QCOMPARE(syntaxes.at(0).exampleQueries().constFirst(), QStringLiteral("syntax1")); 166 | QCOMPARE(syntaxes.at(0).description(), QStringLiteral("description1")); 167 | QCOMPARE(syntaxes.at(1).exampleQueries().size(), 1); 168 | QCOMPARE(syntaxes.at(1).exampleQueries().constFirst(), QStringLiteral("syntax2")); 169 | QCOMPARE(syntaxes.at(1).description(), QStringLiteral("description2")); 170 | } 171 | 172 | void DBusRunnerTest::testIconData() 173 | { 174 | startDBusRunnerProcess({QStringLiteral("net.krunnertests.dave")}); 175 | initProperties(); 176 | 177 | const auto matches = launchQuery(QStringLiteral("fooCostomIcon")); 178 | QCOMPARE(matches.count(), 1); 179 | auto result = matches.first(); 180 | 181 | QImage expectedIcon(10, 10, QImage::Format_RGBA8888); 182 | expectedIcon.fill(Qt::blue); 183 | 184 | QCOMPARE(result.icon().availableSizes().first(), QSize(10, 10)); 185 | QCOMPARE(result.icon().pixmap(QSize(10, 10)), QPixmap::fromImage(expectedIcon)); 186 | } 187 | 188 | void DBusRunnerTest::testLifecycleMethods() 189 | { 190 | QProcess *process = startDBusRunnerProcess({QStringLiteral("net.krunnertests.dave"), QString()}); 191 | manager = std::make_unique(); // This case is special, because we want to load the runners manually 192 | auto md = parseMetaDataFromDesktopFile(QFINDTESTDATA("plugins/dbusrunnertestruntimeconfig.desktop")); 193 | manager->loadRunner(md); 194 | QCOMPARE(manager->runners().count(), 1); 195 | // Match session should be set up automatically 196 | launchQuery(QStringLiteral("fooo")); 197 | 198 | // Make sure we got our match, end the match session and give the process a bit of time to get the DBus signal 199 | QTRY_COMPARE_WITH_TIMEOUT(manager->matches().count(), 1, 2000); 200 | manager->matchSessionComplete(); 201 | QTest::qWait(500); 202 | 203 | const QStringList lifeCycleSteps = QString::fromLocal8Bit(process->readAllStandardOutput()).split(QLatin1Char('\n'), Qt::SkipEmptyParts); 204 | const QStringList expectedLifeCycleSteps = { 205 | QStringLiteral("Config"), 206 | QStringLiteral("Actions"), 207 | QStringLiteral("Matching:fooo"), 208 | QStringLiteral("Teardown"), 209 | }; 210 | QCOMPARE(lifeCycleSteps, expectedLifeCycleSteps); 211 | 212 | // The query does not match our min letter count we set at runtime 213 | launchQuery(QStringLiteral("foo")); 214 | QVERIFY(manager->matches().isEmpty()); 215 | // The query does not match our match regex we set at runtime 216 | launchQuery(QStringLiteral("barfoo")); 217 | QVERIFY(manager->matches().isEmpty()); 218 | } 219 | 220 | void DBusRunnerTest::testRequestActionsWildcards() 221 | { 222 | initProperties(); 223 | manager = std::make_unique(); // This case is special, because we want to load the runners manually 224 | auto md = parseMetaDataFromDesktopFile(QFINDTESTDATA("plugins/dbusrunnertestmulti.desktop")); 225 | QVERIFY(md.isValid()); 226 | manager->loadRunner(md); 227 | QCOMPARE(manager->runners().count(), 1); 228 | 229 | startDBusRunnerProcess({QStringLiteral("net.krunnertests.multi.a1")}, QStringLiteral("net.krunnertests.multi.a1")); 230 | startDBusRunnerProcess({QStringLiteral("net.krunnertests.multi.a2")}, QStringLiteral("net.krunnertests.multi.a2")); 231 | const auto matches = launchQuery("foo"); 232 | QCOMPARE(matches.count(), 2); 233 | 234 | QCOMPARE(matches.at(0).actions().count(), 1); 235 | QCOMPARE(matches.at(0).actions(), matches.at(1).actions()); 236 | } 237 | 238 | QTEST_MAIN(DBusRunnerTest) 239 | 240 | #include "dbusrunnertest.moc" 241 | -------------------------------------------------------------------------------- /src/abstractrunner.h: -------------------------------------------------------------------------------- 1 | /* 2 | SPDX-FileCopyrightText: 2006-2007 Aaron Seigo 3 | SPDX-FileCopyrightText: 2020-2023 Alexander Lohnau 4 | 5 | SPDX-License-Identifier: LGPL-2.0-or-later 6 | */ 7 | 8 | #ifndef KRUNNER_ABSTRACTRUNNER_H 9 | #define KRUNNER_ABSTRACTRUNNER_H 10 | 11 | #include "krunner_export.h" 12 | 13 | #include 14 | #include 15 | 16 | #include 17 | #include 18 | 19 | #include 20 | 21 | #include "querymatch.h" 22 | #include "runnercontext.h" 23 | #include "runnersyntax.h" 24 | 25 | class KConfigGroup; 26 | class QMimeData; 27 | class QRegularExpression; 28 | class QIcon; 29 | 30 | /*! 31 | * \namespace KRunner 32 | * \inmodule KRunner 33 | */ 34 | namespace KRunner 35 | { 36 | class AbstractRunnerPrivate; 37 | 38 | /*! 39 | * \class KRunner::AbstractRunner 40 | * \inheaderfile KRunner/AbstractRunner 41 | * \inmodule KRunner 42 | * 43 | * \brief An abstract base class for Plasma Runner plugins. 44 | * 45 | * Be aware that runners will be moved to their own thread after being instantiated. 46 | * This means that except for AbstractRunner::run and the constructor, all methods will be non-blocking 47 | * for the UI. 48 | * Consider doing heavy resource initialization in the init method instead of the constructor. 49 | */ 50 | class KRUNNER_EXPORT AbstractRunner : public QObject 51 | { 52 | Q_OBJECT 53 | 54 | public: 55 | ~AbstractRunner() override; 56 | 57 | /*! 58 | * This is the main query method. It should trigger creation of 59 | * QueryMatch instances through RunnerContext::addMatch and 60 | * RunnerContext::addMatches. 61 | * 62 | * If the runner can run precisely the requested term (RunnerContext::query()), 63 | * it should create an exact match by setting the type to RunnerContext::ExactMatch. 64 | * The first runner that creates a QueryMatch will be the 65 | * default runner. Other runner's matches will be suggested in the 66 | * interface. Non-exact matches should be offered via RunnerContext::PossibleMatch. 67 | * 68 | * The match will be activated via run() if the user selects it. 69 | * 70 | * All matches need to be reported once this method returns. Asynchronous runners therefore need 71 | * to make use of a local event loop to wait for all matches. 72 | * 73 | * It is recommended to use local status data in async runners. The simplest way is 74 | * to have a separate class doing all the work like so: 75 | * 76 | * \code 77 | * void MyFancyAsyncRunner::match(RunnerContext &context) 78 | * { 79 | * QEventLoop loop; 80 | * MyAsyncWorker worker(context); 81 | * connect(&worker, &MyAsyncWorker::finished, &loop, &MyAsyncWorker::quit); 82 | * worker.work(); 83 | * loop.exec(); 84 | * } 85 | * \endcode 86 | * 87 | * Here MyAsyncWorker creates all the matches and calls RunnerContext::addMatch 88 | * in some internal slot. It emits the finished() signal once done which will 89 | * quit the loop and make the match() method return. 90 | * 91 | * Execution of the correct action should be handled in the run method. 92 | * 93 | * \warning Returning from this method means to end execution of the runner. 94 | * 95 | * \sa run(), RunnerContext::addMatch, RunnerContext::addMatches, QueryMatch 96 | */ 97 | virtual void match(KRunner::RunnerContext &context) = 0; 98 | 99 | /*! 100 | * Called whenever an exact or possible match associated with this 101 | * runner is triggered. 102 | * 103 | * \a context The context in which the match is triggered, i.e. for which 104 | * the match was created. 105 | * \a match The actual match to run/execute. 106 | */ 107 | virtual void run(const KRunner::RunnerContext &context, const KRunner::QueryMatch &match); 108 | 109 | /*! 110 | * Returns the plugin metadata for this runner that was passed in the constructor 111 | */ 112 | KPluginMetaData metadata() const; 113 | 114 | /*! 115 | * Returns the translated name from the runner's metadata 116 | */ 117 | QString name() const; 118 | 119 | /*! 120 | * Returns an id from the runner's metadata 121 | */ 122 | QString id() const; 123 | 124 | /*! 125 | * Reloads the runner's configuration. This is called when it's KCM in the PluginSelector is applied. 126 | * 127 | * This function may be used to set for example using setMatchRegex, setMinLetterCount or setTriggerWords. 128 | * 129 | * Also, syntaxes should be updated when this method is called. 130 | * 131 | * While reloading the config, matching is suspended. 132 | */ 133 | virtual void reloadConfiguration(); 134 | 135 | /*! 136 | * Returns the syntaxes the runner has registered that it accepts and understands 137 | */ 138 | QList syntaxes() const; 139 | 140 | /*! 141 | * Returns true if the runner is currently busy with non-interuptable work, signaling that 142 | * the RunnerManager may not query it or read it's config properties 143 | */ 144 | bool isMatchingSuspended() const; 145 | 146 | /*! 147 | * This is the minimum letter count for the query. If the query is shorter than this value 148 | * and KRunner is not in the singleRunnerMode, match method is not called. 149 | * This can be set using the X-Plasma-Runner-Min-Letter-Count property or the setMinLetterCount method. 150 | * The default value is 0. 151 | * 152 | * \sa setMinLetterCount 153 | * \sa match 154 | * \since 5.75 155 | */ 156 | int minLetterCount() const; 157 | 158 | /*! 159 | * Set the minLetterCount property 160 | * 161 | * \a count 162 | * \since 5.75 163 | */ 164 | void setMinLetterCount(int count); 165 | 166 | /*! 167 | * If this regex is set with a non empty pattern it must match the query in order for match being called. 168 | * 169 | * Just like the minLetterCount property this check is ignored when the runner is in the singleRunnerMode. 170 | * 171 | * In case both the regex and the letter count is set the letter count is checked first. 172 | * 173 | * Returns matchRegex property 174 | * \sa hasMatchRegex 175 | * \since 5.75 176 | */ 177 | QRegularExpression matchRegex() const; 178 | 179 | /*! 180 | * Set the matchRegex property 181 | * 182 | * \since 5.75 183 | */ 184 | void setMatchRegex(const QRegularExpression ®ex); 185 | 186 | /*! 187 | * Constructs internally a regex which requires the query to start with the trigger words. 188 | * Multiple words are concatenated with or, for instance: "^word1|word2|word3". 189 | * The trigger words are internally escaped. 190 | * Also the minLetterCount is set to the shortest word in the list. 191 | * \since 5.75 192 | * \sa matchRegex 193 | */ 194 | void setTriggerWords(const QStringList &triggerWords); 195 | 196 | /*! 197 | * If the runner has a valid regex and non empty regex 198 | * \internal 199 | * \since 5.75 200 | */ 201 | bool hasMatchRegex() const; 202 | 203 | Q_SIGNALS: 204 | /*! 205 | * This signal is emitted when matching is about to commence, giving runners 206 | * an opportunity to prepare themselves, e.g. loading data sets or preparing 207 | * IPC or network connections. Things that should be loaded once and remain 208 | * extant for the lifespan of the AbstractRunner should be done in init(). 209 | * \sa init() 210 | */ 211 | void prepare(); 212 | 213 | /*! 214 | * This signal is emitted when a session of matches is complete, giving runners 215 | * the opportunity to tear down anything set up as a result of the prepare() 216 | * method. 217 | */ 218 | void teardown(); 219 | 220 | protected: 221 | friend class RunnerManager; 222 | friend class RunnerManagerPrivate; 223 | 224 | /*! 225 | * Constructor for a KRunner plugin 226 | * 227 | * @note You should connect here to the prepare/teardown signals. However, avoid doing heavy initialization here 228 | * in favor of doing it in AbstractRunner::init 229 | * 230 | * \a parent parent object for this runner 231 | * 232 | * \a pluginMetaData metadata that was embedded in the runner 233 | * 234 | * \a args for compatibility with KPluginFactory, since 6.0 this can be omitted 235 | * \since 5.72 236 | */ 237 | explicit AbstractRunner(QObject *parent, const KPluginMetaData &pluginMetaData); 238 | 239 | /*! 240 | * Sets whether or not the runner is available for match requests. Useful to 241 | * prevent queries when the runner is in a busy state. 242 | * 243 | * \note Do not permanently suspend the runner. This is only intended as a temporary measure to 244 | * avoid useless queries being launched or async fetching of config/data being interfered with. 245 | */ 246 | void suspendMatching(bool suspend); 247 | 248 | /*! 249 | * Provides access to the runner's configuration object. 250 | * This config is saved in the "krunnerrc" file in the [Runners][] config group 251 | * Settings should be written in a KDE config module. See https://develop.kde.org/docs/plasma/krunner/#runner-configuration 252 | */ 253 | KConfigGroup config() const; 254 | 255 | /*! 256 | * Adds a registered syntax that this runner understands. This is used to 257 | * display to the user what this runner can understand and how it can be 258 | * used. 259 | * 260 | * \a syntax the syntax to register 261 | */ 262 | void addSyntax(const RunnerSyntax &syntax); 263 | 264 | /*! 265 | * Utility overload for creating a syntax based on the given parameters 266 | * \sa RunnerSyntax 267 | * \since 5.106 268 | */ 269 | inline void addSyntax(const QString &exampleQuery, const QString &description) 270 | { 271 | addSyntax(QStringList(exampleQuery), description); 272 | } 273 | 274 | inline void addSyntax(const QStringList &exampleQueries, const QString &description) 275 | { 276 | addSyntax(KRunner::RunnerSyntax(exampleQueries, description)); 277 | } 278 | 279 | /*! 280 | * Sets the list of syntaxes; passing in an empty list effectively clears 281 | * the syntaxes. 282 | * 283 | * \a the syntaxes to register for this runner 284 | */ 285 | void setSyntaxes(const QList &syntaxes); 286 | 287 | /*! 288 | * Reimplement this to run any initialization routines on first load. 289 | * Because it is executed in the runner's thread, it will not block the UI and is thus preferred. 290 | * By default, it calls reloadConfiguration(); 291 | * 292 | * Until the runner is initialized, it will not be queried by the RunnerManager. 293 | */ 294 | virtual void init(); 295 | 296 | /*! 297 | * Reimplement this if you want your runner to support serialization and drag and drop. 298 | * By default, this sets the QMimeData urls to the ones specified in QueryMatch::urls 299 | */ 300 | virtual QMimeData *mimeDataForMatch(const KRunner::QueryMatch &match); 301 | 302 | private: 303 | std::unique_ptr const d; 304 | KRUNNER_NO_EXPORT Q_INVOKABLE void matchInternal(KRunner::RunnerContext context); 305 | KRUNNER_NO_EXPORT Q_INVOKABLE void reloadConfigurationInternal(); 306 | KRUNNER_NO_EXPORT Q_SIGNAL void matchInternalFinished(const QString &jobId); 307 | KRUNNER_NO_EXPORT Q_SIGNAL void matchingResumed(); 308 | friend class RunnerManager; 309 | friend class RunnerContext; 310 | friend class RunnerContextPrivate; 311 | friend class QueryMatchPrivate; 312 | friend class DBusRunner; // Because it "overrides" matchInternal 313 | }; 314 | 315 | } // KRunner namespace 316 | #endif 317 | -------------------------------------------------------------------------------- /src/model/runnerresultsmodel.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | * SPDX-FileCopyrightText: 2019 Kai Uwe Broulik 3 | * 4 | * SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL 5 | * 6 | */ 7 | 8 | #include "runnerresultsmodel_p.h" 9 | 10 | #include 11 | 12 | #include 13 | 14 | #include "resultsmodel.h" 15 | 16 | namespace KRunner 17 | { 18 | RunnerResultsModel::RunnerResultsModel(const KConfigGroup &configGroup, const KConfigGroup &stateConfigGroup, QObject *parent) 19 | : QAbstractItemModel(parent) 20 | { 21 | // Invalid groups are passed in to avoid unneeded overloads and such 22 | setRunnerManager(configGroup.isValid() && stateConfigGroup.isValid() ? new RunnerManager(configGroup, stateConfigGroup, this) : new RunnerManager(this)); 23 | } 24 | 25 | KRunner::QueryMatch RunnerResultsModel::fetchMatch(const QModelIndex &idx) const 26 | { 27 | const QString category = m_categories.value(int(idx.internalId() - 1)); 28 | return m_matches.value(category).value(idx.row()); 29 | } 30 | 31 | void RunnerResultsModel::onMatchesChanged(const QList &matches) 32 | { 33 | // Build the list of new categories and matches 34 | QSet newCategories; 35 | // here we use QString as key since at this point we don't care about the order 36 | // of categories but just what matches we have for each one. 37 | // Below when we populate the actual m_matches we'll make sure to keep the order 38 | // of existing categories to avoid pointless model changes. 39 | QHash> newMatches; 40 | for (const auto &match : matches) { 41 | const QString category = match.matchCategory(); 42 | newCategories.insert(category); 43 | newMatches[category].append(match); 44 | } 45 | 46 | // Get rid of all categories that are no longer present 47 | auto it = m_categories.begin(); 48 | while (it != m_categories.end()) { 49 | const int categoryNumber = int(std::distance(m_categories.begin(), it)); 50 | 51 | if (!newCategories.contains(*it)) { 52 | beginRemoveRows(QModelIndex(), categoryNumber, categoryNumber); 53 | m_matches.remove(*it); 54 | it = m_categories.erase(it); 55 | endRemoveRows(); 56 | } else { 57 | ++it; 58 | } 59 | } 60 | 61 | // Update the existing categories by adding/removing new/removed rows and 62 | // updating changed ones 63 | for (auto it = m_categories.constBegin(); it != m_categories.constEnd(); ++it) { 64 | Q_ASSERT(newCategories.contains(*it)); 65 | 66 | const int categoryNumber = int(std::distance(m_categories.constBegin(), it)); 67 | const QModelIndex categoryIdx = index(categoryNumber, 0); 68 | 69 | // don't use operator[] as to not insert an empty list 70 | // TODO why? shouldn't m_categories and m_matches be in sync? 71 | auto oldCategoryIt = m_matches.find(*it); 72 | Q_ASSERT(oldCategoryIt != m_matches.end()); 73 | 74 | auto &oldMatchesInCategory = *oldCategoryIt; 75 | const auto newMatchesInCategory = newMatches.value(*it); 76 | 77 | Q_ASSERT(!oldMatchesInCategory.isEmpty()); 78 | Q_ASSERT(!newMatches.isEmpty()); 79 | 80 | // Emit a change for all existing matches if any of them changed 81 | // TODO only emit a change for the ones that changed 82 | bool emitDataChanged = false; 83 | 84 | const int oldCount = oldMatchesInCategory.count(); 85 | const int newCount = newMatchesInCategory.count(); 86 | 87 | const int countCeiling = qMin(oldCount, newCount); 88 | 89 | for (int i = 0; i < countCeiling; ++i) { 90 | auto &oldMatch = oldMatchesInCategory[i]; 91 | if (oldMatch != newMatchesInCategory.at(i)) { 92 | oldMatch = newMatchesInCategory.at(i); 93 | emitDataChanged = true; 94 | } 95 | } 96 | 97 | // Now that the source data has been updated, emit the data changes we noted down earlier 98 | if (emitDataChanged) { 99 | Q_EMIT dataChanged(index(0, 0, categoryIdx), index(countCeiling - 1, 0, categoryIdx)); 100 | } 101 | 102 | // Signal insertions for any new items 103 | if (newCount > oldCount) { 104 | beginInsertRows(categoryIdx, oldCount, newCount - 1); 105 | oldMatchesInCategory = newMatchesInCategory; 106 | endInsertRows(); 107 | } else if (newCount < oldCount) { 108 | beginRemoveRows(categoryIdx, newCount, oldCount - 1); 109 | oldMatchesInCategory = newMatchesInCategory; 110 | endRemoveRows(); 111 | } 112 | 113 | // Remove it from the "new" categories so in the next step we can add all genuinely new categories in one go 114 | newCategories.remove(*it); 115 | } 116 | 117 | // Finally add all the new categories 118 | if (!newCategories.isEmpty()) { 119 | beginInsertRows(QModelIndex(), m_categories.count(), m_categories.count() + newCategories.count() - 1); 120 | 121 | for (const QString &newCategory : newCategories) { 122 | const auto matchesInNewCategory = newMatches.value(newCategory); 123 | 124 | m_matches[newCategory] = matchesInNewCategory; 125 | m_categories.append(newCategory); 126 | } 127 | 128 | endInsertRows(); 129 | } 130 | 131 | Q_ASSERT(m_categories.count() == m_matches.count()); 132 | 133 | m_hasMatches = !m_matches.isEmpty(); 134 | 135 | Q_EMIT matchesChanged(); 136 | } 137 | 138 | QString RunnerResultsModel::queryString() const 139 | { 140 | return m_queryString; 141 | } 142 | 143 | void RunnerResultsModel::setQueryString(const QString &queryString, const QString &runner) 144 | { 145 | // If our query and runner are the same we don't need to query again 146 | if (m_queryString.trimmed() == queryString.trimmed() && m_prevRunner == runner) { 147 | return; 148 | } 149 | 150 | m_prevRunner = runner; 151 | m_queryString = queryString; 152 | m_hasMatches = false; 153 | if (queryString.isEmpty()) { 154 | clear(); 155 | } else if (!queryString.trimmed().isEmpty()) { 156 | m_manager->launchQuery(queryString, runner); 157 | } 158 | Q_EMIT queryStringChanged(queryString); // NOLINT(readability-misleading-indentation) 159 | } 160 | 161 | void RunnerResultsModel::clear() 162 | { 163 | m_manager->reset(); 164 | m_manager->matchSessionComplete(); 165 | 166 | // When our session is over, the term is also no longer relevant 167 | // If the same term is used again, the RunnerManager should be asked again 168 | if (!m_queryString.isEmpty()) { 169 | m_queryString.clear(); 170 | Q_EMIT queryStringChanged(m_queryString); 171 | } 172 | 173 | beginResetModel(); 174 | m_categories.clear(); 175 | m_matches.clear(); 176 | endResetModel(); 177 | 178 | m_hasMatches = false; 179 | } 180 | 181 | bool RunnerResultsModel::run(const QModelIndex &idx) 182 | { 183 | KRunner::QueryMatch match = fetchMatch(idx); 184 | if (match.isValid() && match.isEnabled()) { 185 | return m_manager->run(match); 186 | } 187 | return false; 188 | } 189 | 190 | bool RunnerResultsModel::runAction(const QModelIndex &idx, int actionNumber) 191 | { 192 | KRunner::QueryMatch match = fetchMatch(idx); 193 | if (!match.isValid() || !match.isEnabled()) { 194 | return false; 195 | } 196 | 197 | if (actionNumber < 0 || actionNumber >= match.actions().count()) { 198 | return false; 199 | } 200 | 201 | return m_manager->run(match, match.actions().at(actionNumber)); 202 | } 203 | 204 | int RunnerResultsModel::columnCount(const QModelIndex &parent) const 205 | { 206 | Q_UNUSED(parent); 207 | return 1; 208 | } 209 | 210 | int RunnerResultsModel::rowCount(const QModelIndex &parent) const 211 | { 212 | if (parent.column() > 0) { 213 | return 0; 214 | } 215 | 216 | if (!parent.isValid()) { // root level 217 | return m_categories.count(); 218 | } 219 | 220 | if (parent.internalId()) { 221 | return 0; 222 | } 223 | 224 | const QString category = m_categories.value(parent.row()); 225 | return m_matches.value(category).count(); 226 | } 227 | 228 | QVariant RunnerResultsModel::data(const QModelIndex &index, int role) const 229 | { 230 | if (!index.isValid()) { 231 | return {}; 232 | } 233 | 234 | if (index.internalId()) { // runner match 235 | if (int(index.internalId() - 1) >= m_categories.count()) { 236 | return {}; 237 | } 238 | 239 | KRunner::QueryMatch match = fetchMatch(index); 240 | if (!match.isValid()) { 241 | return {}; 242 | } 243 | 244 | switch (role) { 245 | case Qt::DisplayRole: 246 | return match.text(); 247 | case Qt::DecorationRole: 248 | if (!match.iconName().isEmpty()) { 249 | return match.iconName(); 250 | } 251 | return match.icon(); 252 | case ResultsModel::CategoryRelevanceRole: 253 | return match.categoryRelevance(); 254 | case ResultsModel::RelevanceRole: 255 | return match.relevance(); 256 | case ResultsModel::IdRole: 257 | return match.id(); 258 | case ResultsModel::EnabledRole: 259 | return match.isEnabled(); 260 | case ResultsModel::CategoryRole: 261 | return match.matchCategory(); 262 | case ResultsModel::SubtextRole: 263 | return match.subtext(); 264 | case ResultsModel::UrlsRole: 265 | return QVariant::fromValue(match.urls()); 266 | case ResultsModel::MultiLineRole: 267 | return match.isMultiLine(); 268 | case ResultsModel::ActionsRole: { 269 | const auto actions = match.actions(); 270 | QVariantList actionsList; 271 | actionsList.reserve(actions.size()); 272 | 273 | for (const KRunner::Action &action : actions) { 274 | actionsList.append(QVariant::fromValue(action)); 275 | } 276 | 277 | return actionsList; 278 | } 279 | case ResultsModel::QueryMatchRole: 280 | return QVariant::fromValue(match); 281 | } 282 | 283 | return {}; 284 | } 285 | 286 | // category 287 | if (index.row() >= m_categories.count()) { 288 | return {}; 289 | } 290 | 291 | switch (role) { 292 | case Qt::DisplayRole: 293 | return m_categories.at(index.row()); 294 | 295 | case ResultsModel::FavoriteIndexRole: { 296 | for (int i = 0; i < rowCount(index); ++i) { 297 | auto match = this->index(i, 0, index).data(ResultsModel::QueryMatchRole).value(); 298 | if (match.isValid()) { 299 | const QString id = match.runner()->id(); 300 | int idx = m_favoriteIds.indexOf(id); 301 | return idx == -1 ? m_favoriteIds.size() : idx; 302 | } 303 | } 304 | // Any match that is not a favorite will have a greater index than an actual favorite 305 | return m_favoriteIds.size(); 306 | } 307 | // Returns the highest type/role within the group 308 | case ResultsModel::CategoryRelevanceRole: { 309 | int highestType = 0; 310 | for (int i = 0; i < rowCount(index); ++i) { 311 | const int type = this->index(i, 0, index).data(ResultsModel::CategoryRelevanceRole).toInt(); 312 | if (type > highestType) { 313 | highestType = type; 314 | } 315 | } 316 | return highestType; 317 | } 318 | case ResultsModel::RelevanceRole: { 319 | qreal highestRelevance = 0.0; 320 | for (int i = 0; i < rowCount(index); ++i) { 321 | const qreal relevance = this->index(i, 0, index).data(ResultsModel::RelevanceRole).toReal(); 322 | if (relevance > highestRelevance) { 323 | highestRelevance = relevance; 324 | } 325 | } 326 | return highestRelevance; 327 | } 328 | } 329 | 330 | return {}; 331 | } 332 | 333 | QModelIndex RunnerResultsModel::index(int row, int column, const QModelIndex &parent) const 334 | { 335 | if (row < 0 || column != 0) { 336 | return {}; 337 | } 338 | 339 | if (parent.isValid()) { 340 | const QString category = m_categories.value(parent.row()); 341 | const auto matches = m_matches.value(category); 342 | if (row < matches.count()) { 343 | return createIndex(row, column, int(parent.row() + 1)); 344 | } 345 | 346 | return {}; 347 | } 348 | 349 | if (row < m_categories.count()) { 350 | return createIndex(row, column, nullptr); 351 | } 352 | 353 | return {}; 354 | } 355 | 356 | QModelIndex RunnerResultsModel::parent(const QModelIndex &child) const 357 | { 358 | if (child.internalId()) { 359 | return createIndex(int(child.internalId() - 1), 0, nullptr); 360 | } 361 | 362 | return {}; 363 | } 364 | 365 | KRunner::RunnerManager *RunnerResultsModel::runnerManager() const 366 | { 367 | return m_manager; 368 | } 369 | 370 | void RunnerResultsModel::setRunnerManager(KRunner::RunnerManager *manager) 371 | { 372 | disconnect(m_manager); 373 | m_manager = manager; 374 | 375 | connect(m_manager, &RunnerManager::matchesChanged, this, &RunnerResultsModel::onMatchesChanged); 376 | connect(m_manager, &RunnerManager::requestUpdateQueryString, this, &RunnerResultsModel::queryStringChangeRequested); 377 | Q_EMIT runnerManagerChanged(); 378 | } 379 | } 380 | 381 | #include "moc_runnerresultsmodel_p.cpp" 382 | -------------------------------------------------------------------------------- /src/model/resultsmodel.cpp: -------------------------------------------------------------------------------- 1 | /* 2 | * This file is part of the KDE Milou Project 3 | * SPDX-FileCopyrightText: 2019 Kai Uwe Broulik 4 | * SPDX-FileCopyrightText: 2023 Alexander Lohnau 5 | * 6 | * SPDX-License-Identifier: LGPL-2.1-only OR LGPL-3.0-only OR LicenseRef-KDE-Accepted-LGPL 7 | * 8 | */ 9 | 10 | #include "resultsmodel.h" 11 | 12 | #include "runnerresultsmodel_p.h" 13 | 14 | #include 15 | #include 16 | 17 | #include 18 | #include 19 | #include 20 | #include 21 | #include 22 | #include 23 | 24 | using namespace KRunner; 25 | 26 | /* 27 | * Sorts the matches and categories by their type and relevance 28 | * 29 | * A category gets type and relevance of the highest 30 | * scoring match within. 31 | */ 32 | class SortProxyModel : public QSortFilterProxyModel 33 | { 34 | Q_OBJECT 35 | 36 | public: 37 | explicit SortProxyModel(QObject *parent) 38 | : QSortFilterProxyModel(parent) 39 | { 40 | setDynamicSortFilter(true); 41 | sort(0, Qt::DescendingOrder); 42 | } 43 | 44 | void setQueryString(const QString &queryString) 45 | { 46 | const QStringList words = queryString.split(QLatin1Char(' '), Qt::SkipEmptyParts); 47 | if (m_words != words) { 48 | m_words = words; 49 | invalidate(); 50 | } 51 | } 52 | 53 | protected: 54 | [[nodiscard]] bool lessThan(const QModelIndex &sourceA, const QModelIndex &sourceB) const override 55 | { 56 | bool isCategoryComparison = !sourceA.internalId() && !sourceB.internalId(); 57 | Q_ASSERT((bool)sourceA.internalId() == (bool)sourceB.internalId()); 58 | // Only check the favorite index if we compare categories. For individual matches, they will always be the same 59 | if (isCategoryComparison) { 60 | const int favoriteA = sourceA.data(ResultsModel::FavoriteIndexRole).toInt(); 61 | const int favoriteB = sourceB.data(ResultsModel::FavoriteIndexRole).toInt(); 62 | if (favoriteA != favoriteB) { 63 | return favoriteA > favoriteB; 64 | } 65 | 66 | const int typeA = sourceA.data(ResultsModel::CategoryRelevanceRole).toReal(); 67 | const int typeB = sourceB.data(ResultsModel::CategoryRelevanceRole).toReal(); 68 | return typeA < typeB; 69 | } 70 | 71 | const qreal relevanceA = sourceA.data(ResultsModel::RelevanceRole).toReal(); 72 | const qreal relevanceB = sourceB.data(ResultsModel::RelevanceRole).toReal(); 73 | 74 | if (!qFuzzyCompare(relevanceA, relevanceB)) { 75 | return relevanceA < relevanceB; 76 | } 77 | 78 | return QSortFilterProxyModel::lessThan(sourceA, sourceB); 79 | } 80 | 81 | public: 82 | QStringList m_words; 83 | }; 84 | 85 | /* 86 | * Distributes the number of matches shown per category 87 | * 88 | * Each category may occupy a maximum of 1/(n+1) of the given @c limit, 89 | * this means the further down you get, the less matches there are. 90 | * There is at least one match shown per category. 91 | * 92 | * This model assumes the results to already be sorted 93 | * descending by their relevance/score. 94 | */ 95 | class CategoryDistributionProxyModel : public QSortFilterProxyModel 96 | { 97 | Q_OBJECT 98 | 99 | public: 100 | explicit CategoryDistributionProxyModel(QObject *parent) 101 | : QSortFilterProxyModel(parent) 102 | { 103 | } 104 | void setSourceModel(QAbstractItemModel *sourceModel) override 105 | { 106 | if (this->sourceModel()) { 107 | disconnect(this->sourceModel(), nullptr, this, nullptr); 108 | } 109 | 110 | QSortFilterProxyModel::setSourceModel(sourceModel); 111 | 112 | if (sourceModel) { 113 | connect(sourceModel, &QAbstractItemModel::rowsInserted, this, &CategoryDistributionProxyModel::invalidateFilter); 114 | connect(sourceModel, &QAbstractItemModel::rowsMoved, this, &CategoryDistributionProxyModel::invalidateFilter); 115 | connect(sourceModel, &QAbstractItemModel::rowsRemoved, this, &CategoryDistributionProxyModel::invalidateFilter); 116 | } 117 | } 118 | 119 | [[nodiscard]] int limit() const 120 | { 121 | return m_limit; 122 | } 123 | 124 | void setLimit(int limit) 125 | { 126 | if (m_limit == limit) { 127 | return; 128 | } 129 | m_limit = limit; 130 | invalidateFilter(); 131 | Q_EMIT limitChanged(); 132 | } 133 | 134 | Q_SIGNALS: 135 | void limitChanged(); 136 | 137 | protected: 138 | [[nodiscard]] bool filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const override 139 | { 140 | if (m_limit <= 0) { 141 | return true; 142 | } 143 | 144 | if (!sourceParent.isValid()) { 145 | return true; 146 | } 147 | 148 | const int categoryCount = sourceModel()->rowCount(); 149 | 150 | int maxItemsInCategory = m_limit; 151 | 152 | if (categoryCount > 1) { 153 | int itemsBefore = 0; 154 | for (int i = 0; i <= sourceParent.row(); ++i) { 155 | const int itemsInCategory = sourceModel()->rowCount(sourceModel()->index(i, 0)); 156 | 157 | // Take into account that every category gets at least one item shown 158 | const int availableSpace = m_limit - itemsBefore - std::ceil(m_limit / qreal(categoryCount)); 159 | 160 | // The further down the category is the less relevant it is and the less space it my occupy 161 | // First category gets max half the total limit, second category a third, etc 162 | maxItemsInCategory = std::min(availableSpace, int(std::ceil(m_limit / qreal(i + 2)))); 163 | 164 | // At least show one item per category 165 | maxItemsInCategory = std::max(1, maxItemsInCategory); 166 | 167 | itemsBefore += std::min(itemsInCategory, maxItemsInCategory); 168 | } 169 | } 170 | 171 | if (sourceRow >= maxItemsInCategory) { 172 | return false; 173 | } 174 | 175 | return true; 176 | } 177 | 178 | private: 179 | // if you change this, update the default in resetLimit() 180 | int m_limit = 0; 181 | }; 182 | 183 | /* 184 | * This model hides the root items of data originally in a tree structure 185 | * 186 | * KDescendantsProxyModel collapses the items but keeps all items in tact. 187 | * The root items of the RunnerMatchesModel represent the individual cateories 188 | * which we don't want in the resulting flat list. 189 | * This model maps the items back to the given @c treeModel and filters 190 | * out any item with an invalid parent, i.e. "on the root level" 191 | */ 192 | class HideRootLevelProxyModel : public QSortFilterProxyModel 193 | { 194 | Q_OBJECT 195 | 196 | public: 197 | explicit HideRootLevelProxyModel(QObject *parent) 198 | : QSortFilterProxyModel(parent) 199 | { 200 | } 201 | 202 | [[nodiscard]] QAbstractItemModel *treeModel() const 203 | { 204 | return m_treeModel; 205 | } 206 | void setTreeModel(QAbstractItemModel *treeModel) 207 | { 208 | m_treeModel = treeModel; 209 | invalidateFilter(); 210 | } 211 | 212 | protected: 213 | [[nodiscard]] bool filterAcceptsRow(int sourceRow, const QModelIndex &sourceParent) const override 214 | { 215 | KModelIndexProxyMapper mapper(sourceModel(), m_treeModel); 216 | const QModelIndex treeIdx = mapper.mapLeftToRight(sourceModel()->index(sourceRow, 0, sourceParent)); 217 | return treeIdx.parent().isValid(); 218 | } 219 | 220 | private: 221 | QAbstractItemModel *m_treeModel = nullptr; 222 | }; 223 | 224 | class KRunner::ResultsModelPrivate 225 | { 226 | public: 227 | explicit ResultsModelPrivate(const KConfigGroup &configGroup, const KConfigGroup &stateConfigGroup, ResultsModel *q) 228 | : q(q) 229 | , resultsModel(new RunnerResultsModel(configGroup, stateConfigGroup, q)) 230 | { 231 | } 232 | 233 | ResultsModel *q; 234 | 235 | QPointer runner = nullptr; 236 | 237 | RunnerResultsModel *const resultsModel; 238 | SortProxyModel *const sortModel = new SortProxyModel(q); 239 | CategoryDistributionProxyModel *const distributionModel = new CategoryDistributionProxyModel(q); 240 | KDescendantsProxyModel *const flattenModel = new KDescendantsProxyModel(q); 241 | HideRootLevelProxyModel *const hideRootModel = new HideRootLevelProxyModel(q); 242 | const KModelIndexProxyMapper mapper{q, resultsModel}; 243 | }; 244 | 245 | ResultsModel::ResultsModel(QObject *parent) 246 | : ResultsModel(KConfigGroup(), KConfigGroup(), parent) 247 | { 248 | } 249 | ResultsModel::ResultsModel(const KConfigGroup &configGroup, const KConfigGroup &stateConfigGroup, QObject *parent) 250 | : QSortFilterProxyModel(parent) 251 | , d(new ResultsModelPrivate(configGroup, stateConfigGroup, this)) 252 | { 253 | connect(d->resultsModel, &RunnerResultsModel::queryStringChanged, this, &ResultsModel::queryStringChanged); 254 | connect(runnerManager(), &RunnerManager::queryingChanged, this, &ResultsModel::queryingChanged); 255 | connect(d->resultsModel, &RunnerResultsModel::queryStringChangeRequested, this, &ResultsModel::queryStringChangeRequested); 256 | connect(d->resultsModel, &RunnerResultsModel::runnerManagerChanged, this, [this]() { 257 | connect(runnerManager(), &RunnerManager::queryingChanged, this, &ResultsModel::queryingChanged); 258 | }); 259 | 260 | // The matches for the old query string remain on display until the first set of matches arrive for the new query string. 261 | // Therefore we must not update the query string inside RunnerResultsModel exactly when the query string changes, otherwise it would 262 | // re-sort the old query string matches based on the new query string. 263 | // So we only make it aware of the query string change at the time when we receive the first set of matches for the new query string. 264 | connect(d->resultsModel, &RunnerResultsModel::matchesChanged, this, [this]() { 265 | d->sortModel->setQueryString(queryString()); 266 | }); 267 | 268 | connect(d->distributionModel, &CategoryDistributionProxyModel::limitChanged, this, &ResultsModel::limitChanged); 269 | 270 | // The data flows as follows: 271 | // - RunnerResultsModel 272 | // - SortProxyModel 273 | // - CategoryDistributionProxyModel 274 | // - KDescendantsProxyModel 275 | // - HideRootLevelProxyModel 276 | 277 | d->sortModel->setSourceModel(d->resultsModel); 278 | 279 | d->distributionModel->setSourceModel(d->sortModel); 280 | 281 | d->flattenModel->setSourceModel(d->distributionModel); 282 | 283 | d->hideRootModel->setSourceModel(d->flattenModel); 284 | d->hideRootModel->setTreeModel(d->resultsModel); 285 | 286 | setSourceModel(d->hideRootModel); 287 | 288 | // Initialize the runners, this will speed the first query up. 289 | // While there were lots of optimizations, instantiating plugins, creating threads and AbstractRunner::init is still heavy work 290 | QTimer::singleShot(0, this, [this]() { 291 | runnerManager()->runners(); 292 | }); 293 | } 294 | 295 | ResultsModel::~ResultsModel() = default; 296 | 297 | void ResultsModel::setFavoriteIds(const QStringList &ids) 298 | { 299 | d->resultsModel->m_favoriteIds = ids; 300 | Q_EMIT favoriteIdsChanged(); 301 | } 302 | 303 | QStringList ResultsModel::favoriteIds() const 304 | { 305 | return d->resultsModel->m_favoriteIds; 306 | } 307 | 308 | QString ResultsModel::queryString() const 309 | { 310 | return d->resultsModel->queryString(); 311 | } 312 | 313 | void ResultsModel::setQueryString(const QString &queryString) 314 | { 315 | d->resultsModel->setQueryString(queryString, singleRunner()); 316 | } 317 | 318 | int ResultsModel::limit() const 319 | { 320 | return d->distributionModel->limit(); 321 | } 322 | 323 | void ResultsModel::setLimit(int limit) 324 | { 325 | d->distributionModel->setLimit(limit); 326 | } 327 | 328 | void ResultsModel::resetLimit() 329 | { 330 | setLimit(0); 331 | } 332 | 333 | bool ResultsModel::querying() const 334 | { 335 | return runnerManager()->querying(); 336 | } 337 | 338 | QString ResultsModel::singleRunner() const 339 | { 340 | return d->runner ? d->runner->id() : QString(); 341 | } 342 | 343 | void ResultsModel::setSingleRunner(const QString &runnerId) 344 | { 345 | if (runnerId == singleRunner()) { 346 | return; 347 | } 348 | if (runnerId.isEmpty()) { 349 | d->runner = nullptr; 350 | } else { 351 | d->runner = runnerManager()->runner(runnerId); 352 | } 353 | Q_EMIT singleRunnerChanged(); 354 | } 355 | 356 | KPluginMetaData ResultsModel::singleRunnerMetaData() const 357 | { 358 | return d->runner ? d->runner->metadata() : KPluginMetaData(); 359 | } 360 | 361 | QHash ResultsModel::roleNames() const 362 | { 363 | auto names = QAbstractProxyModel::roleNames(); 364 | names[IdRole] = QByteArrayLiteral("matchId"); // "id" is QML-reserved 365 | names[EnabledRole] = QByteArrayLiteral("enabled"); 366 | names[CategoryRole] = QByteArrayLiteral("category"); 367 | names[SubtextRole] = QByteArrayLiteral("subtext"); 368 | names[UrlsRole] = QByteArrayLiteral("urls"); 369 | names[ActionsRole] = QByteArrayLiteral("actions"); 370 | names[MultiLineRole] = QByteArrayLiteral("multiLine"); 371 | return names; 372 | } 373 | 374 | void ResultsModel::clear() 375 | { 376 | d->resultsModel->clear(); 377 | } 378 | 379 | bool ResultsModel::run(const QModelIndex &idx) 380 | { 381 | KModelIndexProxyMapper mapper(this, d->resultsModel); 382 | const QModelIndex resultsIdx = mapper.mapLeftToRight(idx); 383 | if (!resultsIdx.isValid()) { 384 | return false; 385 | } 386 | return d->resultsModel->run(resultsIdx); 387 | } 388 | 389 | bool ResultsModel::runAction(const QModelIndex &idx, int actionNumber) 390 | { 391 | KModelIndexProxyMapper mapper(this, d->resultsModel); 392 | const QModelIndex resultsIdx = mapper.mapLeftToRight(idx); 393 | if (!resultsIdx.isValid()) { 394 | return false; 395 | } 396 | return d->resultsModel->runAction(resultsIdx, actionNumber); 397 | } 398 | 399 | QMimeData *ResultsModel::getMimeData(const QModelIndex &idx) const 400 | { 401 | if (auto resultIdx = d->mapper.mapLeftToRight(idx); resultIdx.isValid()) { 402 | return runnerManager()->mimeDataForMatch(d->resultsModel->fetchMatch(resultIdx)); 403 | } 404 | return nullptr; 405 | } 406 | 407 | KRunner::RunnerManager *ResultsModel::runnerManager() const 408 | { 409 | return d->resultsModel->runnerManager(); 410 | } 411 | 412 | KRunner::QueryMatch ResultsModel::getQueryMatch(const QModelIndex &idx) const 413 | { 414 | const QModelIndex resultIdx = d->mapper.mapLeftToRight(idx); 415 | return resultIdx.isValid() ? d->resultsModel->fetchMatch(resultIdx) : QueryMatch(); 416 | } 417 | 418 | void ResultsModel::setRunnerManager(KRunner::RunnerManager *manager) 419 | { 420 | d->resultsModel->setRunnerManager(manager); 421 | Q_EMIT runnerManagerChanged(); 422 | } 423 | 424 | #include "moc_resultsmodel.cpp" 425 | #include "resultsmodel.moc" 426 | --------------------------------------------------------------------------------