Add per-tool settings and gesture customization

This commit is contained in:
ajp_anton
2026-09-05 05:02:34 +00:00
parent d6f30c5735
commit 38142b9499
26 changed files with 1383 additions and 83 deletions
+1 -1
View File
@@ -16,7 +16,7 @@ include(KDECMakeSettings)
include(KDECompilerSettings NO_POLICY_SCOPE)
find_package(Qt6 6.8 REQUIRED COMPONENTS Core Quick Test)
find_package(KF6 6.0 REQUIRED COMPONENTS Auth CoreAddons I18n KCMUtils)
find_package(KF6 6.0 REQUIRED COMPONENTS Auth Config CoreAddons I18n KCMUtils)
add_subdirectory(src)
if(BUILD_TESTING)
+5
View File
@@ -14,6 +14,11 @@ installation to a narrowly scoped KAuth helper. The helper accepts only valid
package names that independently resolve to that capability in the
`fedora-tools` repository.
Installed tools can also be configured or removed from the module. Package
operations run one at a time; other actions remain disabled until DNF finishes.
The module currently configures task-group shortcut behavior, touchpad hold-tap
timing and output, and the experimental fingerprint workaround.
Installing this module does not install or activate any other tool. Tools
remain usable without the module.
@@ -1,6 +1,6 @@
Name: fedora-tools-settings
Version: 0.1.0
Release: 1%{?dist}
Release: 5%{?dist}
Summary: Plasma System Settings module for Fedora Tools
License: MIT
@@ -14,6 +14,7 @@ BuildRequires: extra-cmake-modules
BuildRequires: gcc-c++
BuildRequires: kf6-kauth-devel
BuildRequires: kf6-kcmutils-devel
BuildRequires: kf6-kconfig-devel
BuildRequires: kf6-ki18n-devel
BuildRequires: kf6-kirigami-devel
BuildRequires: qt6-qtdeclarative-devel
@@ -58,5 +59,20 @@ install -Dpm 0644 %{SOURCE2} \
%{_datadir}/polkit-1/actions/se.ajpanton.fedoratools.policy
%changelog
* Sat Sep 05 2026 fedora-tools contributors - 0.1.0-5
- Fix button contrast and timing-value loading
- Separate touchpad anchor age from its stationary-time setting
* Sat Sep 05 2026 fedora-tools contributors - 0.1.0-4
- Improve tool descriptions and configuration help
- Add the minimum hold-tap duration setting
* Sat Sep 05 2026 fedora-tools contributors - 0.1.0-3
- Add compact deterministic tool rows and per-tool configuration
* Sat Sep 05 2026 fedora-tools contributors - 0.1.0-2
- Show installation progress beside the selected tool
- Add safe per-tool uninstall actions
* Sat Sep 05 2026 fedora-tools contributors - 0.1.0-1
- Initial package
+1
View File
@@ -13,6 +13,7 @@ target_link_libraries(kcm_fedora_tools PRIVATE
Qt6::Core
Qt6::Quick
KF6::AuthCore
KF6::ConfigCore
KF6::CoreAddons
KF6::I18n
KF6::KCMUtilsQuick
@@ -4,10 +4,14 @@
#include <KAuth/Action>
#include <KAuth/ExecuteJob>
#include <KConfigGroup>
#include <KJob>
#include <KLocalizedString>
#include <KPluginFactory>
#include <KSharedConfig>
#include <QFile>
#include <QRegularExpression>
#include <QTimer>
#include <algorithm>
@@ -33,8 +37,10 @@ FedoraToolsKcm::FedoraToolsKcm(QObject *parent, const KPluginMetaData &data)
}
setMessage(i18n("Could not start %1.", m_query.program()), true);
m_stage = QueryStage::Idle;
setPackageOperation({}, {});
setBusy(false);
});
loadToolSettings();
QTimer::singleShot(0, this, [this] {
refresh();
});
@@ -65,6 +71,56 @@ bool FedoraToolsKcm::fingerprintEnabled() const
return m_fingerprintEnabled;
}
QString FedoraToolsKcm::activePackage() const
{
return m_activePackage;
}
QString FedoraToolsKcm::packageOperation() const
{
return m_packageOperation;
}
bool FedoraToolsKcm::shortcutStartWithFirst() const
{
return m_shortcutStartWithFirst;
}
bool FedoraToolsKcm::shortcutInitialShiftOpensNew() const
{
return m_shortcutInitialShiftOpensNew;
}
bool FedoraToolsKcm::shortcutShiftCyclesBackward() const
{
return m_shortcutShiftCyclesBackward;
}
int FedoraToolsKcm::touchpadMinimumAnchorAge() const
{
return m_touchpadMinimumAnchorAge;
}
int FedoraToolsKcm::touchpadMinimumPause() const
{
return m_touchpadMinimumPause;
}
int FedoraToolsKcm::touchpadMinimumTap() const
{
return m_touchpadMinimumTap;
}
int FedoraToolsKcm::touchpadMaximumTap() const
{
return m_touchpadMaximumTap;
}
QString FedoraToolsKcm::touchpadOutputEvent() const
{
return m_touchpadOutputEvent;
}
void FedoraToolsKcm::refresh(bool refreshMetadata)
{
if (m_busy) {
@@ -98,12 +154,107 @@ void FedoraToolsKcm::installTool(const QString &packageName)
return;
}
setPackageOperation(packageName, QStringLiteral("install"));
runAuthorizedAction(QStringLiteral("installpackage"),
{{QStringLiteral("packageName"), packageName}},
i18n("%1 is now installed.", packageName),
true);
}
void FedoraToolsKcm::removeTool(const QString &packageName)
{
if (m_busy || !m_tools.mayRemove(packageName)) {
return;
}
if (packageName == QLatin1String("plasma-fingerprint-workaround") && m_fingerprintEnabled) {
setMessage(i18n("Disable the fingerprint workaround before removing its controller."), true);
return;
}
if (packageName == QLatin1String("plasma-task-group-shortcuts")) {
QProcess restore;
restore.start(QStringLiteral("/usr/bin/plasma-task-group-shortcuts"), {QStringLiteral("--restore-shortcuts")});
if (!restore.waitForFinished(10 * 1000) || restore.exitStatus() != QProcess::NormalExit || restore.exitCode() != 0) {
restore.kill();
restore.waitForFinished();
setMessage(i18n("Could not restore the original Meta+number shortcuts."), true);
return;
}
}
setPackageOperation(packageName, QStringLiteral("remove"));
runAuthorizedAction(QStringLiteral("removepackage"),
{{QStringLiteral("packageName"), packageName}},
i18n("%1 was removed.", packageName),
true);
}
void FedoraToolsKcm::saveShortcutSettings(bool startWithFirst,
bool initialShiftOpensNew,
bool shiftCyclesBackward)
{
const auto config = KSharedConfig::openConfig(QStringLiteral("plasma-task-group-shortcutsrc"));
KConfigGroup group(config, QStringLiteral("Settings"));
group.writeEntry("StartWithFirstWindow", startWithFirst);
group.writeEntry("InitialShiftOpensNewInstance", initialShiftOpensNew);
group.writeEntry("ShiftCyclesBackward", shiftCyclesBackward);
group.sync();
loadToolSettings();
setMessage(i18n("Shortcut settings were saved."));
}
void FedoraToolsKcm::resetShortcutSettings()
{
const auto config = KSharedConfig::openConfig(QStringLiteral("plasma-task-group-shortcutsrc"));
KConfigGroup group(config, QStringLiteral("Settings"));
group.deleteGroup();
group.sync();
loadToolSettings();
setMessage(i18n("Shortcut settings were reset to their defaults."));
}
void FedoraToolsKcm::saveTouchpadSettings(int minimumAnchorAge,
int minimumPause,
int minimumTap,
int maximumTap,
const QString &outputEvent)
{
static const QRegularExpression eventName(QStringLiteral("^(BTN|KEY)_[A-Z0-9_]+$"));
if (m_busy) {
return;
}
if (minimumAnchorAge < 0 || minimumAnchorAge > 2000
|| minimumPause < 0 || minimumPause > 2000
|| minimumTap < 0 || minimumTap > maximumTap
|| maximumTap < 1 || maximumTap > 2000
|| !eventName.match(outputEvent).hasMatch()) {
setMessage(i18n("Enter valid timing values and an evdev BTN_* or KEY_* output event."), true);
return;
}
runAuthorizedAction(QStringLiteral("configuretouchpad"),
{{QStringLiteral("operation"), QStringLiteral("save")},
{QStringLiteral("minimumAnchorAge"), minimumAnchorAge},
{QStringLiteral("minimumPause"), minimumPause},
{QStringLiteral("minimumTap"), minimumTap},
{QStringLiteral("maximumTap"), maximumTap},
{QStringLiteral("outputEvent"), outputEvent}},
i18n("Touchpad settings were saved. Log out and back in to apply them."),
false);
}
void FedoraToolsKcm::resetTouchpadSettings()
{
if (m_busy) {
return;
}
runAuthorizedAction(QStringLiteral("configuretouchpad"),
{{QStringLiteral("operation"), QStringLiteral("reset")}},
i18n("Touchpad settings were reset. Log out and back in to apply the defaults."),
false);
}
void FedoraToolsKcm::enableFingerprintWorkaround(const QUrl &rpmFile, bool force)
{
if (m_busy) {
@@ -155,6 +306,7 @@ void FedoraToolsKcm::queryFinished(int exitCode, QProcess::ExitStatus exitStatus
if (exitStatus != QProcess::NormalExit) {
setMessage(i18n("Package discovery terminated unexpectedly."), true);
m_stage = QueryStage::Idle;
setPackageOperation({}, {});
setBusy(false);
return;
}
@@ -183,6 +335,7 @@ void FedoraToolsKcm::queryFinished(int exitCode, QProcess::ExitStatus exitStatus
setMessage(details.isEmpty() ? i18n("The Fedora Tools repository is unavailable.") : details, true);
updateFingerprintStatus();
m_stage = QueryStage::Idle;
setPackageOperation({}, {});
setBusy(false);
return;
}
@@ -244,6 +397,7 @@ void FedoraToolsKcm::finishRefresh()
m_tools.setPackages(m_installedPackages, m_availablePackages, m_updates);
updateFingerprintStatus();
m_stage = QueryStage::Idle;
setPackageOperation({}, {});
setBusy(false);
}
@@ -274,6 +428,7 @@ void FedoraToolsKcm::actionFinished(KJob *job, const QString &successMessage, bo
const QString helperMessage = executeJob ? executeJob->data().value(QStringLiteral("message")).toString() : QString();
const QString errorMessage = helperMessage.isEmpty() ? job->errorString() : helperMessage;
setMessage(errorMessage, true);
setPackageOperation({}, {});
setBusy(false);
return;
}
@@ -283,6 +438,7 @@ void FedoraToolsKcm::actionFinished(KJob *job, const QString &successMessage, bo
refresh();
} else {
updateFingerprintStatus();
loadToolSettings();
setBusy(false);
}
setMessage(successMessage);
@@ -297,6 +453,16 @@ void FedoraToolsKcm::setBusy(bool busy)
Q_EMIT busyChanged();
}
void FedoraToolsKcm::setPackageOperation(const QString &packageName, const QString &operation)
{
if (m_activePackage == packageName && m_packageOperation == operation) {
return;
}
m_activePackage = packageName;
m_packageOperation = operation;
Q_EMIT packageOperationChanged();
}
void FedoraToolsKcm::setMessage(const QString &message, bool error)
{
if (m_message == message && m_error == error) {
@@ -320,4 +486,41 @@ void FedoraToolsKcm::updateFingerprintStatus()
}
}
void FedoraToolsKcm::loadToolSettings()
{
const auto shortcutConfig = KSharedConfig::openConfig(QStringLiteral("plasma-task-group-shortcutsrc"));
shortcutConfig->reparseConfiguration();
const KConfigGroup shortcuts(shortcutConfig, QStringLiteral("Settings"));
m_shortcutStartWithFirst = shortcuts.readEntry("StartWithFirstWindow", true);
m_shortcutInitialShiftOpensNew = shortcuts.readEntry("InitialShiftOpensNewInstance", true);
m_shortcutShiftCyclesBackward = shortcuts.readEntry("ShiftCyclesBackward", true);
m_touchpadMinimumAnchorAge = 100;
m_touchpadMinimumPause = 100;
m_touchpadMinimumTap = 10;
m_touchpadMaximumTap = 150;
m_touchpadOutputEvent = QStringLiteral("BTN_MIDDLE");
QFile touchpadConfig(QStringLiteral("/etc/touchpad-hold-tap.conf"));
if (touchpadConfig.open(QIODevice::ReadOnly | QIODevice::Text)) {
while (!touchpadConfig.atEnd()) {
const QList<QByteArray> setting = touchpadConfig.readLine().trimmed().split('=');
if (setting.size() != 2) {
continue;
}
if (setting.at(0) == "MINIMUM_ANCHOR_AGE_MS") {
m_touchpadMinimumAnchorAge = setting.at(1).toInt();
} else if (setting.at(0) == "MINIMUM_PAUSE_MS") {
m_touchpadMinimumPause = setting.at(1).toInt();
} else if (setting.at(0) == "MINIMUM_TAP_MS") {
m_touchpadMinimumTap = setting.at(1).toInt();
} else if (setting.at(0) == "MAXIMUM_TAP_MS") {
m_touchpadMaximumTap = setting.at(1).toInt();
} else if (setting.at(0) == "OUTPUT_EVENT") {
m_touchpadOutputEvent = QString::fromLatin1(setting.at(1));
}
}
}
Q_EMIT toolSettingsChanged();
}
#include "fedoratoolskcm.moc"
@@ -21,6 +21,16 @@ class FedoraToolsKcm : public KQuickConfigModule
Q_PROPERTY(QString message READ message NOTIFY messageChanged)
Q_PROPERTY(bool error READ error NOTIFY messageChanged)
Q_PROPERTY(bool fingerprintEnabled READ fingerprintEnabled NOTIFY fingerprintEnabledChanged)
Q_PROPERTY(QString activePackage READ activePackage NOTIFY packageOperationChanged)
Q_PROPERTY(QString packageOperation READ packageOperation NOTIFY packageOperationChanged)
Q_PROPERTY(bool shortcutStartWithFirst READ shortcutStartWithFirst NOTIFY toolSettingsChanged)
Q_PROPERTY(bool shortcutInitialShiftOpensNew READ shortcutInitialShiftOpensNew NOTIFY toolSettingsChanged)
Q_PROPERTY(bool shortcutShiftCyclesBackward READ shortcutShiftCyclesBackward NOTIFY toolSettingsChanged)
Q_PROPERTY(int touchpadMinimumAnchorAge READ touchpadMinimumAnchorAge NOTIFY toolSettingsChanged)
Q_PROPERTY(int touchpadMinimumPause READ touchpadMinimumPause NOTIFY toolSettingsChanged)
Q_PROPERTY(int touchpadMinimumTap READ touchpadMinimumTap NOTIFY toolSettingsChanged)
Q_PROPERTY(int touchpadMaximumTap READ touchpadMaximumTap NOTIFY toolSettingsChanged)
Q_PROPERTY(QString touchpadOutputEvent READ touchpadOutputEvent NOTIFY toolSettingsChanged)
public:
FedoraToolsKcm(QObject *parent, const KPluginMetaData &data);
@@ -30,10 +40,25 @@ public:
QString message() const;
bool error() const;
bool fingerprintEnabled() const;
QString activePackage() const;
QString packageOperation() const;
bool shortcutStartWithFirst() const;
bool shortcutInitialShiftOpensNew() const;
bool shortcutShiftCyclesBackward() const;
int touchpadMinimumAnchorAge() const;
int touchpadMinimumPause() const;
int touchpadMinimumTap() const;
int touchpadMaximumTap() const;
QString touchpadOutputEvent() const;
Q_INVOKABLE void refresh(bool refreshMetadata = false);
Q_INVOKABLE void clearMessage();
Q_INVOKABLE void installTool(const QString &packageName);
Q_INVOKABLE void removeTool(const QString &packageName);
Q_INVOKABLE void saveShortcutSettings(bool startWithFirst, bool initialShiftOpensNew, bool shiftCyclesBackward);
Q_INVOKABLE void resetShortcutSettings();
Q_INVOKABLE void saveTouchpadSettings(int minimumAnchorAge, int minimumPause, int minimumTap, int maximumTap, const QString &outputEvent);
Q_INVOKABLE void resetTouchpadSettings();
Q_INVOKABLE void enableFingerprintWorkaround(const QUrl &rpmFile, bool force);
Q_INVOKABLE void disableFingerprintWorkaround();
@@ -41,6 +66,8 @@ Q_SIGNALS:
void busyChanged();
void messageChanged();
void fingerprintEnabledChanged();
void packageOperationChanged();
void toolSettingsChanged();
private:
enum class QueryStage {
@@ -57,8 +84,10 @@ private:
void runAuthorizedAction(const QString &name, const QVariantMap &arguments, const QString &successMessage, bool refreshTools);
void actionFinished(KJob *job, const QString &successMessage, bool refreshTools);
void setBusy(bool busy);
void setPackageOperation(const QString &packageName, const QString &operation);
void setMessage(const QString &message, bool error = false);
void updateFingerprintStatus();
void loadToolSettings();
ToolModel m_tools;
QProcess m_query;
@@ -70,5 +99,15 @@ private:
bool m_busy = false;
bool m_error = false;
bool m_fingerprintEnabled = false;
QString m_activePackage;
QString m_packageOperation;
QString m_message;
bool m_shortcutStartWithFirst = true;
bool m_shortcutInitialShiftOpensNew = true;
bool m_shortcutShiftCyclesBackward = true;
int m_touchpadMinimumAnchorAge = 100;
int m_touchpadMinimumPause = 100;
int m_touchpadMinimumTap = 10;
int m_touchpadMaximumTap = 150;
QString m_touchpadOutputEvent = QStringLiteral("BTN_MIDDLE");
};
+102
View File
@@ -8,6 +8,7 @@
#include <QFileInfo>
#include <QObject>
#include <QProcess>
#include <QRegularExpression>
namespace
{
@@ -71,6 +72,37 @@ bool isPublishedTool(const QString &packageName)
}
return false;
}
bool isInstalledTool(const QString &packageName)
{
QProcess process;
process.start(QStringLiteral("/usr/bin/rpm"),
{QStringLiteral("-q"),
QStringLiteral("--whatprovides"),
QStringLiteral("fedora-tools-tool"),
QStringLiteral("--qf"),
QStringLiteral("%{name}\n")});
if (!process.waitForFinished(30 * 1000) || process.exitCode() != 0) {
return false;
}
for (const QByteArray &line : process.readAllStandardOutput().split('\n')) {
if (QString::fromUtf8(line) == packageName) {
return true;
}
}
return false;
}
bool fingerprintWorkaroundEnabled()
{
QProcess process;
process.start(QStringLiteral("/usr/bin/plasma-fingerprint-workaround"), {QStringLiteral("status")});
return process.waitForFinished(10 * 1000)
&& process.exitStatus() == QProcess::NormalExit
&& process.exitCode() == 0
&& process.readAllStandardOutput().startsWith("Fingerprint workaround: enabled");
}
}
class FedoraToolsHelper : public QObject
@@ -94,6 +126,76 @@ public Q_SLOTS:
packageName});
}
KAuth::ActionReply removepackage(const QVariantMap &arguments)
{
const QString packageName = arguments.value(QStringLiteral("packageName")).toString();
if (!isValidPackageName(packageName) || !isInstalledTool(packageName)) {
KAuth::ActionReply reply = KAuth::ActionReply::HelperErrorReply();
reply.addData(QStringLiteral("message"), QStringLiteral("The requested package is not an installed Fedora tool."));
return reply;
}
if (packageName == QLatin1String("plasma-fingerprint-workaround") && fingerprintWorkaroundEnabled()) {
KAuth::ActionReply reply = KAuth::ActionReply::HelperErrorReply();
reply.addData(QStringLiteral("message"), QStringLiteral("Disable the fingerprint workaround before removing its controller."));
return reply;
}
return commandReply(QStringLiteral("/usr/bin/dnf5"),
{QStringLiteral("remove"),
QStringLiteral("--assumeyes"),
QStringLiteral("--no-autoremove"),
packageName});
}
KAuth::ActionReply configuretouchpad(const QVariantMap &arguments)
{
if (!isInstalledTool(QStringLiteral("touchpad-hold-tap"))) {
KAuth::ActionReply reply = KAuth::ActionReply::HelperErrorReply();
reply.addData(QStringLiteral("message"), QStringLiteral("The touchpad hold-tap tool is not installed."));
return reply;
}
const QString operation = arguments.value(QStringLiteral("operation")).toString();
const QString controller = QStringLiteral("/usr/bin/touchpad-hold-tap-config");
if (!QFileInfo(controller).isExecutable()) {
KAuth::ActionReply reply = KAuth::ActionReply::HelperErrorReply();
reply.addData(QStringLiteral("message"), QStringLiteral("The touchpad configurator is not installed."));
return reply;
}
if (operation == QLatin1String("reset")) {
return commandReply(controller, {QStringLiteral("reset")});
}
if (operation != QLatin1String("save")) {
return KAuth::ActionReply::InvalidActionReply();
}
const int minimumAnchorAge = arguments.value(QStringLiteral("minimumAnchorAge")).toInt();
const int minimumPause = arguments.value(QStringLiteral("minimumPause")).toInt();
const int minimumTap = arguments.value(QStringLiteral("minimumTap")).toInt();
const int maximumTap = arguments.value(QStringLiteral("maximumTap")).toInt();
const QString outputEvent = arguments.value(QStringLiteral("outputEvent")).toString();
static const QRegularExpression eventName(QStringLiteral("^(BTN|KEY)_[A-Z0-9_]+$"));
if (minimumAnchorAge < 0 || minimumAnchorAge > 2000
|| minimumPause < 0 || minimumPause > 2000
|| minimumTap < 0 || minimumTap > maximumTap
|| maximumTap < 1 || maximumTap > 2000
|| !eventName.match(outputEvent).hasMatch()) {
KAuth::ActionReply reply = KAuth::ActionReply::HelperErrorReply();
reply.addData(QStringLiteral("message"), QStringLiteral("The requested touchpad settings are invalid."));
return reply;
}
return commandReply(controller,
{QStringLiteral("set"),
QStringLiteral("--minimum-anchor-age-ms"), QString::number(minimumAnchorAge),
QStringLiteral("--minimum-pause-ms"), QString::number(minimumPause),
QStringLiteral("--minimum-tap-ms"), QString::number(minimumTap),
QStringLiteral("--maximum-tap-ms"), QString::number(maximumTap),
QStringLiteral("--output-event"), outputEvent});
}
KAuth::ActionReply fingerprintworkaround(const QVariantMap &arguments)
{
const QString operation = arguments.value(QStringLiteral("operation")).toString();
@@ -7,6 +7,16 @@ Name=Install a Fedora tool
Description=Install a package from the Fedora Tools repository
Policy=auth_admin
[se.ajpanton.fedoratools.removepackage]
Name=Remove a Fedora tool
Description=Remove an installed Fedora Tools package
Policy=auth_admin
[se.ajpanton.fedoratools.configuretouchpad]
Name=Configure the touchpad hold-tap gesture
Description=Change system-wide settings for the touchpad hold-tap plugin
Policy=auth_admin
[se.ajpanton.fedoratools.fingerprintworkaround]
Name=Change the Plasma fingerprint workaround
Description=Install or remove the experimental patched KScreenLocker package
+14 -5
View File
@@ -42,7 +42,10 @@ QVariant ToolModel::data(const QModelIndex &index, int role) const
case UpdateAvailableRole:
return tool.updateAvailable;
case ConfigurableRole:
return tool.installed && tool.packageName == QStringLiteral("plasma-fingerprint-workaround");
return tool.installed
&& (tool.packageName == QStringLiteral("plasma-fingerprint-workaround")
|| tool.packageName == QStringLiteral("plasma-task-group-shortcuts")
|| tool.packageName == QStringLiteral("touchpad-hold-tap"));
default:
return {};
}
@@ -83,16 +86,15 @@ void ToolModel::setPackages(const QList<PackageRecord> &installed,
tool.packageName = package.name;
tool.availableVersion = package.version;
tool.architecture = package.architecture;
tool.summary = package.summary;
if (!tool.installed) {
tool.summary = package.summary;
}
tool.available = true;
tool.updateAvailable = tool.installed && updates.contains(package.name);
}
QList<Tool> merged = tools.values();
std::ranges::sort(merged, [](const Tool &left, const Tool &right) {
if (left.installed != right.installed) {
return left.installed > right.installed;
}
return left.packageName < right.packageName;
});
@@ -107,3 +109,10 @@ bool ToolModel::mayInstall(const QString &packageName) const
return tool.packageName == packageName && tool.available;
});
}
bool ToolModel::mayRemove(const QString &packageName) const
{
return std::ranges::any_of(m_tools, [&packageName](const Tool &tool) {
return tool.packageName == packageName && tool.installed;
});
}
+1
View File
@@ -46,6 +46,7 @@ public:
const QList<PackageRecord> &available,
const QSet<QString> &updates = {});
bool mayInstall(const QString &packageName) const;
bool mayRemove(const QString &packageName) const;
private:
QList<Tool> m_tools;
+420 -9
View File
@@ -12,14 +12,25 @@ import org.kde.kirigami as Kirigami
KCM.SimpleKCM {
id: root
property bool showingFingerprintSettings: false
property string settingsPage: ""
implicitWidth: Kirigami.Units.gridUnit * 36
implicitHeight: Kirigami.Units.gridUnit * 30
StackLayout {
anchors.fill: parent
currentIndex: root.showingFingerprintSettings ? 1 : 0
currentIndex: {
if (root.settingsPage === "plasma-fingerprint-workaround") {
return 1
}
if (root.settingsPage === "plasma-task-group-shortcuts") {
return 2
}
if (root.settingsPage === "touchpad-hold-tap") {
return 3
}
return 0
}
ColumnLayout {
spacing: Kirigami.Units.largeSpacing
@@ -50,7 +61,7 @@ KCM.SimpleKCM {
Controls.BusyIndicator {
Layout.alignment: Qt.AlignHCenter
visible: kcm.busy
visible: kcm.busy && kcm.activePackage.length === 0
running: visible
}
@@ -83,6 +94,7 @@ KCM.SimpleKCM {
Kirigami.Icon {
source: "applications-system"
opacity: toolCard.installed ? 1 : 0
implicitWidth: Kirigami.Units.iconSizes.medium
implicitHeight: width
}
@@ -124,14 +136,96 @@ KCM.SimpleKCM {
visible: toolCard.configurable
text: i18n("Configure…")
enabled: !kcm.busy
onClicked: root.showingFingerprintSettings = true
onClicked: {
kcm.clearMessage()
root.settingsPage = toolCard.packageName
if (root.settingsPage === "plasma-task-group-shortcuts") {
shortcutPage.loadSettings()
} else if (root.settingsPage === "touchpad-hold-tap") {
touchpadPage.loadSettings()
}
}
}
Controls.Button {
StackLayout {
id: installControl
visible: toolCard.available && (!toolCard.installed || toolCard.updateAvailable)
text: toolCard.installed ? i18n("Update") : i18n("Install")
enabled: !kcm.busy
onClicked: kcm.installTool(toolCard.packageName)
Layout.minimumWidth: Kirigami.Units.gridUnit * 5
Layout.preferredWidth: Layout.minimumWidth
Layout.maximumWidth: Layout.minimumWidth
Layout.preferredHeight: installButton.implicitHeight
currentIndex: kcm.activePackage === toolCard.packageName
&& kcm.packageOperation === "install" ? 1 : 0
Controls.Button {
id: installButton
text: toolCard.installed ? i18n("Update") : i18n("Install")
enabled: !kcm.busy
onClicked: kcm.installTool(toolCard.packageName)
}
Item {
implicitWidth: installButton.implicitWidth
implicitHeight: installButton.implicitHeight
Controls.BusyIndicator {
anchors.centerIn: parent
width: Kirigami.Units.iconSizes.smallMedium
height: width
running: installControl.currentIndex === 1
}
}
}
StackLayout {
id: removeControl
visible: toolCard.installed
Layout.minimumWidth: Kirigami.Units.gridUnit * 5
Layout.preferredWidth: Layout.minimumWidth
Layout.maximumWidth: Layout.minimumWidth
Layout.preferredHeight: removeButton.implicitHeight
currentIndex: kcm.activePackage === toolCard.packageName
&& kcm.packageOperation === "remove" ? 1 : 0
Controls.Button {
id: removeButton
text: i18n("Uninstall")
enabled: !kcm.busy
onClicked: kcm.removeTool(toolCard.packageName)
contentItem: Controls.Label {
text: removeButton.text
color: Kirigami.Theme.textColor
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
}
background: Rectangle {
radius: Kirigami.Units.cornerRadius
color: Qt.tint(
Kirigami.Theme.backgroundColor,
Qt.alpha(Kirigami.Theme.negativeTextColor,
removeButton.down ? 0.45 : removeButton.hovered ? 0.35 : 0.25))
border.color: Kirigami.Theme.negativeTextColor
opacity: removeButton.enabled ? 1 : 0.5
}
}
Item {
implicitWidth: removeButton.implicitWidth
implicitHeight: removeButton.implicitHeight
Controls.BusyIndicator {
anchors.centerIn: parent
width: Kirigami.Units.iconSizes.smallMedium
height: width
running: removeControl.currentIndex === 1
}
}
}
}
}
@@ -166,7 +260,7 @@ KCM.SimpleKCM {
enabled: !kcm.busy
onClicked: {
kcm.clearMessage()
root.showingFingerprintSettings = false
root.settingsPage = ""
}
}
@@ -256,6 +350,323 @@ KCM.SimpleKCM {
Layout.fillHeight: true
}
}
ColumnLayout {
id: shortcutPage
spacing: Kirigami.Units.largeSpacing
function loadSettings() {
cycleStart.currentIndex = kcm.shortcutStartWithFirst ? 0 : 1
initialShift.checked = kcm.shortcutInitialShiftOpensNew
reverseShift.checked = kcm.shortcutShiftCyclesBackward
}
RowLayout {
Layout.fillWidth: true
Controls.ToolButton {
text: i18n("Back to tools")
icon.name: "go-previous"
enabled: !kcm.busy
onClicked: {
kcm.clearMessage()
root.settingsPage = ""
}
}
Kirigami.Heading {
Layout.fillWidth: true
text: i18n("Task-group shortcuts")
level: 2
}
}
Kirigami.InlineMessage {
Layout.fillWidth: true
visible: kcm.message.length > 0
text: kcm.message
type: kcm.error ? Kirigami.MessageType.Error : Kirigami.MessageType.Positive
}
Controls.Label {
Layout.fillWidth: true
text: i18n("Plasma normally assigns Meta+number shortcuts to individual Task Manager entries, so every ungrouped window consumes a number. This tool instead assigns each number to an application group in Task Manager order.\n\nPress Meta+number to select a group, then keep Meta held and press the same number again to cycle through that application's windows. Releasing Meta ends the cycle.")
wrapMode: Text.WordWrap
}
Kirigami.FormLayout {
Layout.fillWidth: true
Controls.ComboBox {
id: cycleStart
Kirigami.FormData.label: i18n("New cycle starts with:")
model: [i18n("First window"), i18n("Window after the active one")]
}
Controls.CheckBox {
id: initialShift
Kirigami.FormData.label: i18n("Shift on first press:")
text: i18n("Open a new window")
}
Controls.CheckBox {
id: reverseShift
Kirigami.FormData.label: i18n("Shift while cycling:")
text: i18n("Cycle backward")
}
}
Kirigami.InlineMessage {
Layout.fillWidth: true
visible: initialShift.checked && reverseShift.checked
type: Kirigami.MessageType.Information
text: i18n("With both “Open a new window” and “Cycle backward” enabled, start a backward cycle with Meta+number without Shift. Keep Meta held and use Meta+Shift+number for the following backward steps.")
}
RowLayout {
Layout.fillWidth: true
Item {
Layout.fillWidth: true
}
Controls.Button {
text: i18n("Restore Defaults")
icon.name: "edit-undo"
enabled: !kcm.busy
onClicked: kcm.resetShortcutSettings()
}
Controls.Button {
text: i18n("Save")
icon.name: "document-save"
enabled: !kcm.busy
onClicked: kcm.saveShortcutSettings(
cycleStart.currentIndex === 0,
initialShift.checked,
reverseShift.checked)
}
}
Controls.Label {
Layout.fillWidth: true
text: i18n("Changes apply the next time a Meta+number shortcut is pressed.")
wrapMode: Text.WordWrap
color: Kirigami.Theme.disabledTextColor
}
Item {
Layout.fillHeight: true
}
Connections {
target: kcm
function onToolSettingsChanged() {
shortcutPage.loadSettings()
}
}
}
ColumnLayout {
id: touchpadPage
spacing: Kirigami.Units.largeSpacing
function loadSettings() {
minimumAnchorAge.value = kcm.touchpadMinimumAnchorAge
minimumPause.value = kcm.touchpadMinimumPause
maximumTap.value = kcm.touchpadMaximumTap
minimumTap.value = kcm.touchpadMinimumTap
outputEvent.editText = kcm.touchpadOutputEvent
}
RowLayout {
Layout.fillWidth: true
Controls.ToolButton {
text: i18n("Back to tools")
icon.name: "go-previous"
enabled: !kcm.busy
onClicked: {
kcm.clearMessage()
root.settingsPage = ""
}
}
Kirigami.Heading {
Layout.fillWidth: true
text: i18n("Touchpad hold-tap")
level: 2
}
}
Kirigami.InlineMessage {
Layout.fillWidth: true
visible: kcm.message.length > 0
text: kcm.message
type: kcm.error ? Kirigami.MessageType.Error : Kirigami.MessageType.Positive
}
Controls.Label {
Layout.fillWidth: true
text: i18n("Hold one anchor finger on the touchpad, then briefly tap with a second finger.\nMinimum anchor age is how long the first finger must have been touching the touchpad, separating this gesture from an ordinary two-finger tap.\nRequired stationary time is how long the anchor must remain within the movement tolerance before the tap; set it to 0 ms to allow hold-taps while the anchor is moving.\nMinimum tap duration rejects implausibly brief contacts, such as hardware noise that could otherwise produce unintended or duplicate actions.\nMaximum tap duration rejects a second finger that was held rather than tapped.\nOutput event selects the mouse button or single keyboard key emitted for a successful gesture.")
wrapMode: Text.WordWrap
}
Kirigami.FormLayout {
Layout.fillWidth: true
Controls.SpinBox {
id: minimumAnchorAge
Kirigami.FormData.label: i18n("Minimum anchor age:")
from: 0
to: 2000
editable: true
textFromValue: function(value, locale) {
return i18n("%1 ms", value)
}
valueFromText: function(text, locale) {
return parseInt(text)
}
}
Controls.SpinBox {
id: minimumPause
Kirigami.FormData.label: i18n("Required stationary time:")
from: 0
to: 2000
editable: true
textFromValue: function(value, locale) {
return i18n("%1 ms", value)
}
valueFromText: function(text, locale) {
return parseInt(text)
}
}
Controls.SpinBox {
id: minimumTap
Kirigami.FormData.label: i18n("Minimum tap duration:")
from: 0
to: maximumTap.value
editable: true
textFromValue: function(value, locale) {
return i18n("%1 ms", value)
}
valueFromText: function(text, locale) {
return parseInt(text)
}
}
Controls.SpinBox {
id: maximumTap
Kirigami.FormData.label: i18n("Maximum tap duration:")
from: 1
to: 2000
editable: true
textFromValue: function(value, locale) {
return i18n("%1 ms", value)
}
valueFromText: function(text, locale) {
return parseInt(text)
}
}
Controls.ComboBox {
id: outputEvent
Kirigami.FormData.label: i18n("Output event:")
editable: true
model: [
"BTN_MIDDLE",
"BTN_LEFT",
"BTN_RIGHT",
"BTN_SIDE",
"BTN_EXTRA",
"KEY_ENTER",
"KEY_ESC",
"KEY_SPACE",
"KEY_F13",
"KEY_F14",
"KEY_F15",
"KEY_F16"
]
}
}
Controls.Label {
Layout.fillWidth: true
text: i18n("The output is one Linux evdev BTN_* or KEY_* event. The default BTN_MIDDLE produces a middle click.")
wrapMode: Text.WordWrap
color: Kirigami.Theme.disabledTextColor
}
RowLayout {
Layout.alignment: Qt.AlignHCenter
visible: kcm.busy
Controls.BusyIndicator {
running: parent.visible
}
Controls.Label {
text: i18n("Applying change…")
}
}
RowLayout {
Layout.fillWidth: true
Item {
Layout.fillWidth: true
}
Controls.Button {
text: i18n("Restore Defaults")
icon.name: "edit-undo"
enabled: !kcm.busy
onClicked: kcm.resetTouchpadSettings()
}
Controls.Button {
text: i18n("Save")
icon.name: "document-save"
enabled: !kcm.busy
onClicked: kcm.saveTouchpadSettings(
minimumAnchorAge.value,
minimumPause.value,
minimumTap.value,
maximumTap.value,
outputEvent.editText.trim().toUpperCase())
}
}
Controls.Label {
Layout.fillWidth: true
text: i18n("Log out and back in after saving or restoring defaults so KWin reloads the libinput plugin.")
wrapMode: Text.WordWrap
color: Kirigami.Theme.disabledTextColor
}
Item {
Layout.fillHeight: true
}
Connections {
target: kcm
function onToolSettingsChanged() {
touchpadPage.loadSettings()
}
}
}
}
Dialogs.FileDialog {
@@ -23,6 +23,8 @@ private Q_SLOTS:
QCOMPARE(model.data(index, ToolModel::AvailableRole).toBool(), true);
QCOMPARE(model.data(index, ToolModel::UpdateAvailableRole).toBool(), true);
QCOMPARE(model.mayInstall(QStringLiteral("touchpad-hold-tap")), true);
QCOMPARE(model.mayRemove(QStringLiteral("touchpad-hold-tap")), true);
QCOMPARE(model.mayRemove(QStringLiteral("missing-tool")), false);
}
void doesNotGuessUpdatesFromDifferentVersionStrings()
@@ -44,6 +46,34 @@ private Q_SLOTS:
QCOMPARE(model.data(model.index(0), ToolModel::ConfigurableRole).toBool(), true);
}
void sortsAlphabeticallyRegardlessOfInstallState()
{
ToolModel model;
const PackageRecord installed{QStringLiteral("touchpad-hold-tap"), QStringLiteral("1"), QStringLiteral("noarch"), QStringLiteral("Touchpad")};
const PackageRecord availableFirst{QStringLiteral("plasma-always-show-unlock"), QStringLiteral("1"), QStringLiteral("noarch"), QStringLiteral("Lock screen")};
const PackageRecord availableLast{QStringLiteral("touchpad-hold-tap"), QStringLiteral("1"), QStringLiteral("noarch"), QStringLiteral("Old touchpad summary")};
model.setPackages({installed}, {availableFirst, availableLast});
QCOMPARE(model.data(model.index(0), ToolModel::PackageNameRole).toString(),
QStringLiteral("plasma-always-show-unlock"));
QCOMPARE(model.data(model.index(1), ToolModel::PackageNameRole).toString(),
QStringLiteral("touchpad-hold-tap"));
QCOMPARE(model.data(model.index(1), ToolModel::SummaryRole).toString(),
QStringLiteral("Touchpad"));
}
void makesInstalledToolsWithSettingsConfigurable()
{
ToolModel model;
const PackageRecord shortcuts{QStringLiteral("plasma-task-group-shortcuts"), QStringLiteral("1"), QStringLiteral("x86_64"), QStringLiteral("Shortcuts")};
const PackageRecord touchpad{QStringLiteral("touchpad-hold-tap"), QStringLiteral("1"), QStringLiteral("noarch"), QStringLiteral("Touchpad")};
model.setPackages({shortcuts, touchpad}, {shortcuts, touchpad});
QCOMPARE(model.data(model.index(0), ToolModel::ConfigurableRole).toBool(), true);
QCOMPARE(model.data(model.index(1), ToolModel::ConfigurableRole).toBool(), true);
}
};
QTEST_MAIN(ToolModelTest)