Add per-tool settings and gesture customization
This commit is contained in:
@@ -35,7 +35,8 @@ systems, architectures, or distributions may be limited.
|
||||
The optional `fedora-tools-settings` package adds a **Fedora Tools** page to
|
||||
Plasma System Settings. It discovers tools from this RPM repository, shows
|
||||
which are installed, and can install available tools after administrator
|
||||
authorization. Individual tools do not require the settings module.
|
||||
authorization. Installed tools can also be removed, and supported tools expose
|
||||
their settings there. Individual tools do not require the settings module.
|
||||
|
||||
### Touchpad hold-tap
|
||||
|
||||
@@ -44,6 +45,12 @@ second finger and emits a middle click. The held finger may move between taps.
|
||||
Repeating the tap while holding the first finger produces repeated clicks,
|
||||
including double middle clicks.
|
||||
|
||||
The minimum anchor age, required stationary time, tap durations, and emitted
|
||||
mouse button or keyboard event can be changed through Fedora Tools settings or
|
||||
with `touchpad-hold-tap-config`. A stationary time of zero allows recognition
|
||||
while the anchor is moving. Custom plugin settings are stored under `/etc` and
|
||||
survive package updates. Log out and back in after changing them.
|
||||
|
||||
The `touchpad-hold-tap` package requires libinput 1.30 or newer and a compositor
|
||||
that enables libinput plugins. Log out and back in after installation or an
|
||||
update so the compositor reloads the plugin.
|
||||
@@ -80,6 +87,9 @@ position and the application's windows form another. **Hide launcher when
|
||||
application starts** determines whether that separate launcher remains while
|
||||
the application is running. Other groups retain the Task Manager's sort order.
|
||||
|
||||
The first-window behavior and both Shift behaviors can be changed through
|
||||
Fedora Tools settings. Changes take effect with the next shortcut press.
|
||||
|
||||
If application grouping is disabled—or an application is excluded from
|
||||
grouping—each of its windows remains a separate shortcut position. **Only group
|
||||
when the Task Manager is full** does not alter shortcut groups; that would make
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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");
|
||||
};
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
});
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
Name: plasma-always-show-unlock
|
||||
Version: 0.1.0
|
||||
Release: 9%{?dist}
|
||||
Summary: Always show the Plasma lock-screen unlock prompt
|
||||
Release: 10%{?dist}
|
||||
Summary: Immediately show the Plasma unlock prompt without requiring input
|
||||
|
||||
License: MIT AND GPL-2.0-or-later
|
||||
URL: https://git.ajpanton.se/ajp_anton/fedora-tools
|
||||
@@ -57,6 +57,9 @@ fi
|
||||
%{_datadir}/plasma-always-show-unlock/LockScreenUi.patch
|
||||
|
||||
%changelog
|
||||
* Sat Sep 05 2026 fedora-tools contributors - 0.1.0-10
|
||||
- Clarify the package summary
|
||||
|
||||
* Sat Sep 05 2026 fedora-tools contributors - 0.1.0-9
|
||||
- Advertise the tool to Fedora Tools settings
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
Name: plasma-fingerprint-workaround
|
||||
Version: 0.1.0
|
||||
Release: 2%{?dist}
|
||||
Summary: Opt-in installer for a patched Fedora KScreenLocker
|
||||
Release: 3%{?dist}
|
||||
Summary: Opt-in patched KScreenLocker for fingerprint recovery after suspend
|
||||
|
||||
License: MIT
|
||||
URL: https://git.ajpanton.se/ajp_anton/fedora-tools
|
||||
@@ -51,6 +51,9 @@ install -D -m 0644 %{SOURCE3} \
|
||||
%{_datadir}/plasma-fingerprint-workaround/payload.conf
|
||||
|
||||
%changelog
|
||||
* Sat Sep 05 2026 fedora-tools contributors - 0.1.0-3
|
||||
- Clarify what the workaround patches
|
||||
|
||||
* Sat Sep 05 2026 fedora-tools contributors - 0.1.0-2
|
||||
- Advertise the tool to Fedora Tools settings
|
||||
|
||||
|
||||
@@ -36,6 +36,23 @@ constexpr auto configFile = "plasma-org.kde.plasma.desktop-appletsrc";
|
||||
constexpr auto componentId = "se.ajpanton.plasma-task-group-shortcuts";
|
||||
constexpr auto stateFile = "plasma-task-group-shortcutsrc";
|
||||
|
||||
struct ShortcutSettings {
|
||||
bool startWithFirst = true;
|
||||
bool initialShiftOpensNewInstance = true;
|
||||
bool shiftCyclesBackward = true;
|
||||
};
|
||||
|
||||
ShortcutSettings shortcutSettings()
|
||||
{
|
||||
const auto config = KSharedConfig::openConfig(QString::fromLatin1(stateFile));
|
||||
const KConfigGroup group(config, QStringLiteral("Settings"));
|
||||
return {
|
||||
group.readEntry("StartWithFirstWindow", true),
|
||||
group.readEntry("InitialShiftOpensNewInstance", true),
|
||||
group.readEntry("ShiftCyclesBackward", true),
|
||||
};
|
||||
}
|
||||
|
||||
KConfigGroup displacedShortcutGroup(int number)
|
||||
{
|
||||
const auto config = KSharedConfig::openConfig(QString::fromLatin1(stateFile));
|
||||
@@ -282,15 +299,20 @@ private:
|
||||
return;
|
||||
}
|
||||
|
||||
const ShortcutSettings settings = shortcutSettings();
|
||||
const bool continuing = row == m_lastShortcut;
|
||||
if (!continuing) {
|
||||
m_cyclePosition = 0;
|
||||
const bool backwards = shifted
|
||||
&& settings.shiftCyclesBackward
|
||||
&& !settings.initialShiftOpensNewInstance;
|
||||
m_cyclePosition = initialCyclePosition(
|
||||
m_model, tasks.at(row), settings.startWithFirst, backwards);
|
||||
} else {
|
||||
m_cyclePosition += shifted ? -1 : 1;
|
||||
m_cyclePosition += shifted && settings.shiftCyclesBackward ? -1 : 1;
|
||||
}
|
||||
m_lastShortcut = row;
|
||||
|
||||
if (shifted && !continuing) {
|
||||
if (shifted && !continuing && settings.initialShiftOpensNewInstance) {
|
||||
m_model.requestNewInstance(m_model.index(tasks.at(row), 0));
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
Name: plasma-task-group-shortcuts
|
||||
Version: 0.1.0
|
||||
Release: 13%{?dist}
|
||||
Summary: Application-group shortcuts for the Plasma Task Manager
|
||||
Release: 15%{?dist}
|
||||
Summary: Windows-like application-group shortcuts for the Plasma Task Manager
|
||||
|
||||
License: MIT
|
||||
URL: https://git.ajpanton.se/ajp_anton/fedora-tools
|
||||
@@ -54,6 +54,12 @@ install -Dpm 0644 %{SOURCE2} \
|
||||
%{_sysconfdir}/xdg/autostart/se.ajpanton.plasma-task-group-shortcuts-autostart.desktop
|
||||
|
||||
%changelog
|
||||
* Sat Sep 05 2026 fedora-tools contributors - 0.1.0-15
|
||||
- Clarify the package summary
|
||||
|
||||
* Sat Sep 05 2026 fedora-tools contributors - 0.1.0-14
|
||||
- Add configurable cycle start and Shift behavior
|
||||
|
||||
* Sat Sep 05 2026 fedora-tools contributors - 0.1.0-13
|
||||
- Advertise the tool to Fedora Tools settings
|
||||
|
||||
|
||||
@@ -72,3 +72,22 @@ QVector<int> logicalTaskRows(const QAbstractItemModel &model, bool separateLaunc
|
||||
}
|
||||
return rows;
|
||||
}
|
||||
|
||||
int initialCyclePosition(const QAbstractItemModel &model, int row, bool startWithFirst, bool backwards)
|
||||
{
|
||||
const QModelIndex group = model.index(row, 0);
|
||||
const int childCount = model.rowCount(group);
|
||||
if (childCount == 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (!startWithFirst) {
|
||||
for (int child = 0; child < childCount; ++child) {
|
||||
if (model.index(child, 0, group).data(AbstractTasksModel::IsActive).toBool()) {
|
||||
return child + (backwards ? -1 : 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return backwards ? childCount - 1 : 0;
|
||||
}
|
||||
|
||||
@@ -9,3 +9,4 @@ class QAbstractItemModel;
|
||||
|
||||
QModelIndex taskToActivate(const QAbstractItemModel &model, int row, int cyclePosition);
|
||||
QVector<int> logicalTaskRows(const QAbstractItemModel &model, bool separateLaunchers);
|
||||
int initialCyclePosition(const QAbstractItemModel &model, int row, bool startWithFirst, bool backwards);
|
||||
|
||||
@@ -17,6 +17,9 @@ private Q_SLOTS:
|
||||
void returnsTopLevelTask();
|
||||
void selectsChildByCyclePosition();
|
||||
void startsWithFirstChild();
|
||||
void startsWithLastChildWhenReversing();
|
||||
void startsAfterActiveChild();
|
||||
void startsBeforeActiveChildWhenReversing();
|
||||
void keepsSeparateLaunchersAsSlots();
|
||||
void mergesLauncherWithRunningApplication();
|
||||
};
|
||||
@@ -59,6 +62,45 @@ void TaskSelectionTest::startsWithFirstChild()
|
||||
QCOMPARE(taskToActivate(model, 0, 0), model.index(0, 0, model.index(0, 0)));
|
||||
}
|
||||
|
||||
void TaskSelectionTest::startsWithLastChildWhenReversing()
|
||||
{
|
||||
QStandardItemModel model;
|
||||
auto *group = new QStandardItem(QStringLiteral("Dolphin"));
|
||||
group->appendRow(new QStandardItem(QStringLiteral("one")));
|
||||
group->appendRow(new QStandardItem(QStringLiteral("two")));
|
||||
group->appendRow(new QStandardItem(QStringLiteral("three")));
|
||||
model.appendRow(group);
|
||||
|
||||
QCOMPARE(initialCyclePosition(model, 0, true, true), 2);
|
||||
}
|
||||
|
||||
void TaskSelectionTest::startsAfterActiveChild()
|
||||
{
|
||||
QStandardItemModel model;
|
||||
auto *group = new QStandardItem(QStringLiteral("Firefox"));
|
||||
group->appendRow(new QStandardItem(QStringLiteral("one")));
|
||||
group->appendRow(new QStandardItem(QStringLiteral("two")));
|
||||
group->appendRow(new QStandardItem(QStringLiteral("three")));
|
||||
model.appendRow(group);
|
||||
group->child(1)->setData(true, AbstractTasksModel::IsActive);
|
||||
|
||||
QCOMPARE(initialCyclePosition(model, 0, false, false), 2);
|
||||
}
|
||||
|
||||
void TaskSelectionTest::startsBeforeActiveChildWhenReversing()
|
||||
{
|
||||
QStandardItemModel model;
|
||||
auto *group = new QStandardItem(QStringLiteral("Firefox"));
|
||||
group->appendRow(new QStandardItem(QStringLiteral("one")));
|
||||
group->appendRow(new QStandardItem(QStringLiteral("two")));
|
||||
group->appendRow(new QStandardItem(QStringLiteral("three")));
|
||||
model.appendRow(group);
|
||||
group->child(0)->setData(true, AbstractTasksModel::IsActive);
|
||||
|
||||
QCOMPARE(initialCyclePosition(model, 0, false, true), -1);
|
||||
QCOMPARE(taskToActivate(model, 0, -1), model.index(2, 0, model.index(0, 0)));
|
||||
}
|
||||
|
||||
void TaskSelectionTest::keepsSeparateLaunchersAsSlots()
|
||||
{
|
||||
QStandardItemModel model;
|
||||
|
||||
@@ -14,6 +14,12 @@ install -m 0644 \
|
||||
"$repo_root/touchpad-hold-tap/tests/test-touchpad-hold-tap.lua" \
|
||||
"$topdir/SOURCES/test-touchpad-hold-tap.lua"
|
||||
install -m 0644 "$repo_root/README.md" "$topdir/SOURCES/README.md"
|
||||
install -m 0755 \
|
||||
"$repo_root/touchpad-hold-tap/touchpad-hold-tap-config" \
|
||||
"$topdir/SOURCES/touchpad-hold-tap-config"
|
||||
install -m 0755 \
|
||||
"$repo_root/touchpad-hold-tap/tests/test-touchpad-hold-tap-config" \
|
||||
"$topdir/SOURCES/test-touchpad-hold-tap-config"
|
||||
|
||||
rpmbuild \
|
||||
--define "_topdir $topdir" \
|
||||
|
||||
@@ -1,18 +1,23 @@
|
||||
-- SPDX-License-Identifier: MIT
|
||||
--
|
||||
-- Recognize one touchpad contact that has paused before a quick second-finger
|
||||
-- tap. Hide the tapping contact from libinput and emit a middle click instead.
|
||||
-- Recognize one established touchpad contact followed by a quick second-finger
|
||||
-- tap. Hide the tapping contact from libinput and emit a configured action.
|
||||
|
||||
libinput:register({1})
|
||||
|
||||
local MINIMUM_STATIONARY_US = 100 * 1000
|
||||
local MAXIMUM_TAP_US = 150 * 1000
|
||||
local settings = TOUCHPAD_HOLD_TAP_SETTINGS or {}
|
||||
local MINIMUM_ANCHOR_AGE_US = (settings.minimum_anchor_age_ms or 100) * 1000
|
||||
local MINIMUM_STATIONARY_US = (settings.minimum_pause_ms or 100) * 1000
|
||||
local MINIMUM_TAP_US = (settings.minimum_tap_ms or 10) * 1000
|
||||
local MAXIMUM_TAP_US = (settings.maximum_tap_ms or 150) * 1000
|
||||
local MAXIMUM_ANCHOR_MOVEMENT_MM = 1.5
|
||||
local MAXIMUM_TAPPER_MOVEMENT_MM = 1.0
|
||||
local OUTPUT_EVENT_NAME = settings.output_event or "BTN_MIDDLE"
|
||||
local OUTPUT_USAGE = evdev[OUTPUT_EVENT_NAME]
|
||||
|
||||
local states = {}
|
||||
local button_devices = {}
|
||||
local button_device = nil
|
||||
local output_devices = {}
|
||||
local output_device = nil
|
||||
|
||||
local tool_usages = {
|
||||
{ usage = evdev.BTN_TOOL_FINGER, count = 1 },
|
||||
@@ -131,18 +136,19 @@ local function restore_candidate(device, state, reason)
|
||||
"touchpad-hold-tap: restored candidate (" .. reason .. ")")
|
||||
end
|
||||
|
||||
local function emit_middle_click()
|
||||
if not button_device then
|
||||
local function emit_action()
|
||||
if not output_device then
|
||||
libinput:log_error(
|
||||
"touchpad-hold-tap: no pointer device available for button output")
|
||||
"touchpad-hold-tap: no device available for " ..
|
||||
OUTPUT_EVENT_NAME .. " output")
|
||||
return false
|
||||
end
|
||||
|
||||
button_device:append_frame({
|
||||
{ usage = evdev.BTN_MIDDLE, value = 1 },
|
||||
output_device:append_frame({
|
||||
{ usage = OUTPUT_USAGE, value = 1 },
|
||||
})
|
||||
button_device:append_frame({
|
||||
{ usage = evdev.BTN_MIDDLE, value = 0 },
|
||||
output_device:append_frame({
|
||||
{ usage = OUTPUT_USAGE, value = 0 },
|
||||
})
|
||||
return true
|
||||
end
|
||||
@@ -185,8 +191,14 @@ local function handle_tracking_begin(device, state, tracking_id, timestamp)
|
||||
state.anchor_slot = contact.slot
|
||||
elseif state.mode == "anchor" and previous_count == 1 then
|
||||
local anchor = state.contacts[state.anchor_slot]
|
||||
if anchor and anchor.rest_x and anchor.rest_y and
|
||||
timestamp - anchor.stationary_since >= MINIMUM_STATIONARY_US then
|
||||
local old_enough = anchor and
|
||||
timestamp - anchor.down_time >=
|
||||
MINIMUM_ANCHOR_AGE_US
|
||||
local stationary = MINIMUM_STATIONARY_US == 0 or
|
||||
(anchor and anchor.rest_x and anchor.rest_y and
|
||||
timestamp - anchor.stationary_since >=
|
||||
MINIMUM_STATIONARY_US)
|
||||
if old_enough and stationary then
|
||||
contact.hidden = true
|
||||
state.mode = "candidate"
|
||||
state.candidate_slot = contact.slot
|
||||
@@ -211,7 +223,9 @@ local function handle_tracking_end(device, state, timestamp)
|
||||
if contact.hidden and state.source_slot == state.candidate_slot then
|
||||
local anchor = state.contacts[state.anchor_slot]
|
||||
local duration = timestamp - contact.down_time
|
||||
local accepted = anchor and duration <= MAXIMUM_TAP_US
|
||||
local accepted = anchor and
|
||||
duration >= MINIMUM_TAP_US and
|
||||
duration <= MAXIMUM_TAP_US
|
||||
|
||||
state.contacts[state.source_slot] = nil
|
||||
state.candidate_slot = nil
|
||||
@@ -224,11 +238,12 @@ local function handle_tracking_end(device, state, timestamp)
|
||||
end
|
||||
|
||||
if accepted then
|
||||
if emit_middle_click() then
|
||||
libinput:log_debug("touchpad-hold-tap: middle click")
|
||||
if emit_action() then
|
||||
libinput:log_debug(
|
||||
"touchpad-hold-tap: emitted " .. OUTPUT_EVENT_NAME)
|
||||
end
|
||||
else
|
||||
libinput:log_debug("touchpad-hold-tap: ignored long tap")
|
||||
libinput:log_debug("touchpad-hold-tap: ignored tap duration")
|
||||
end
|
||||
return false, true
|
||||
end
|
||||
@@ -243,6 +258,12 @@ local function handle_tracking_end(device, state, timestamp)
|
||||
if contact_count(state, false) > 0 then
|
||||
state.mode = "passthrough"
|
||||
end
|
||||
elseif state.mode == "passthrough" and state.anchor_slot and
|
||||
state.contacts[state.anchor_slot] and
|
||||
contact_count(state, false) == 1 then
|
||||
-- A rejected second contact must not poison the original anchor.
|
||||
-- It can become eligible again without first being lifted.
|
||||
state.mode = "anchor"
|
||||
end
|
||||
reset_if_clear(state)
|
||||
return true, false
|
||||
@@ -272,7 +293,7 @@ local function handle_mt_axis(device, state, event, timestamp)
|
||||
remember_axis(contact, event.usage, event.value)
|
||||
|
||||
if contact.slot == state.anchor_slot then
|
||||
if state.mode == "candidate" and
|
||||
if state.mode == "candidate" and MINIMUM_STATIONARY_US > 0 and
|
||||
movement_exceeded_from(
|
||||
state, contact, state.candidate_anchor_x,
|
||||
state.candidate_anchor_y, MAXIMUM_ANCHOR_MOVEMENT_MM) then
|
||||
@@ -373,31 +394,45 @@ end
|
||||
|
||||
local function device_removed(device)
|
||||
states[device] = nil
|
||||
button_devices[device] = nil
|
||||
if button_device == device then
|
||||
button_device = nil
|
||||
for candidate in pairs(button_devices) do
|
||||
button_device = candidate
|
||||
output_devices[device] = nil
|
||||
if output_device == device then
|
||||
output_device = nil
|
||||
for candidate in pairs(output_devices) do
|
||||
output_device = candidate
|
||||
break
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
local function can_emit_output(properties, usages)
|
||||
if OUTPUT_EVENT_NAME:sub(1, 4) == "BTN_" then
|
||||
return (properties.ID_INPUT_MOUSE or
|
||||
properties.ID_INPUT_POINTINGSTICK) and
|
||||
not properties.ID_INPUT_TOUCHPAD and
|
||||
usages[evdev.REL_X] and usages[evdev.REL_Y]
|
||||
end
|
||||
return properties.ID_INPUT_KEYBOARD
|
||||
end
|
||||
|
||||
local function device_new(device)
|
||||
if not OUTPUT_USAGE then
|
||||
libinput:log_error(
|
||||
"touchpad-hold-tap: unknown output event " .. OUTPUT_EVENT_NAME)
|
||||
return
|
||||
end
|
||||
|
||||
local properties = device:udev_properties()
|
||||
local usages = device:usages()
|
||||
|
||||
-- Clickpads intentionally ignore BTN_MIDDLE from their own event node.
|
||||
-- Route synthesized buttons through an existing pointer device instead.
|
||||
if (properties.ID_INPUT_MOUSE or properties.ID_INPUT_POINTINGSTICK) and
|
||||
not properties.ID_INPUT_TOUCHPAD and
|
||||
usages[evdev.REL_X] and usages[evdev.REL_Y] then
|
||||
if not usages[evdev.BTN_MIDDLE] then
|
||||
device:enable_evdev_usage(evdev.BTN_MIDDLE)
|
||||
-- Clickpads ignore synthesized buttons on their own event node. Keyboard
|
||||
-- events likewise belong on a keyboard, so use a suitable existing device.
|
||||
if can_emit_output(properties, usages) then
|
||||
if not usages[OUTPUT_USAGE] then
|
||||
device:enable_evdev_usage(OUTPUT_USAGE)
|
||||
end
|
||||
button_devices[device] = true
|
||||
if not button_device then
|
||||
button_device = device
|
||||
output_devices[device] = true
|
||||
if not output_device then
|
||||
output_device = device
|
||||
end
|
||||
device:connect("device-removed", device_removed)
|
||||
end
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# SPDX-License-Identifier: MIT
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
plugin=$1
|
||||
configurator=$2
|
||||
test_root=$(mktemp -d)
|
||||
trap 'rm -rf -- "$test_root"' EXIT
|
||||
|
||||
install -Dpm 0644 "$plugin" \
|
||||
"$test_root/usr/lib64/libinput/plugins/90-touchpad-hold-tap.lua"
|
||||
|
||||
TOUCHPAD_HOLD_TAP_TEST_ROOT=$test_root bash "$configurator" set \
|
||||
--minimum-anchor-age-ms 125 \
|
||||
--minimum-pause-ms 175 \
|
||||
--minimum-tap-ms 15 \
|
||||
--maximum-tap-ms 225 \
|
||||
--output-event KEY_F13
|
||||
|
||||
config="$test_root/etc/touchpad-hold-tap.conf"
|
||||
override="$test_root/etc/libinput/plugins/90-touchpad-hold-tap.lua"
|
||||
grep -qx 'MINIMUM_PAUSE_MS=175' "$config"
|
||||
grep -qx 'MINIMUM_ANCHOR_AGE_MS=125' "$config"
|
||||
grep -qx 'MINIMUM_TAP_MS=15' "$config"
|
||||
grep -qx 'MAXIMUM_TAP_MS=225' "$config"
|
||||
grep -qx 'OUTPUT_EVENT=KEY_F13' "$config"
|
||||
grep -q 'minimum_pause_ms = 175' "$override"
|
||||
grep -q 'minimum_anchor_age_ms = 125' "$override"
|
||||
grep -q 'minimum_tap_ms = 15' "$override"
|
||||
grep -q 'maximum_tap_ms = 225' "$override"
|
||||
grep -q 'output_event = "KEY_F13"' "$override"
|
||||
|
||||
TOUCHPAD_HOLD_TAP_TEST_ROOT=$test_root bash "$configurator" reset
|
||||
[[ ! -e $config ]]
|
||||
[[ ! -e $override ]]
|
||||
|
||||
printf 'touchpad configurator tests passed\n'
|
||||
@@ -5,6 +5,8 @@ local function usage(event_type, code)
|
||||
end
|
||||
|
||||
evdev = {
|
||||
BTN_LEFT = usage(1, 272),
|
||||
BTN_RIGHT = usage(1, 273),
|
||||
BTN_MIDDLE = usage(1, 274),
|
||||
BTN_TOUCH = usage(1, 330),
|
||||
BTN_TOOL_FINGER = usage(1, 325),
|
||||
@@ -20,8 +22,19 @@ evdev = {
|
||||
ABS_MT_POSITION_Y = usage(3, 54),
|
||||
ABS_MT_TOOL_Y = usage(3, 61),
|
||||
ABS_MT_TRACKING_ID = usage(3, 57),
|
||||
KEY_F13 = usage(1, 183),
|
||||
}
|
||||
|
||||
local output_event = arg[2] or "BTN_MIDDLE"
|
||||
local allow_moving_anchor = arg[3] == "allow-moving-anchor"
|
||||
local expected_output = evdev[output_event]
|
||||
if arg[2] or allow_moving_anchor then
|
||||
TOUCHPAD_HOLD_TAP_SETTINGS = {
|
||||
output_event = output_event,
|
||||
minimum_pause_ms = allow_moving_anchor and 0 or nil,
|
||||
}
|
||||
end
|
||||
|
||||
local plugin_callbacks = {}
|
||||
libinput = {
|
||||
register = function(_, versions)
|
||||
@@ -66,6 +79,8 @@ local function new_device(device_type)
|
||||
return { ID_INPUT_TOUCHPAD = true }
|
||||
elseif device_type == "mouse" then
|
||||
return { ID_INPUT_MOUSE = true }
|
||||
elseif device_type == "keyboard" then
|
||||
return { ID_INPUT_KEYBOARD = true }
|
||||
end
|
||||
return {}
|
||||
end
|
||||
@@ -159,12 +174,13 @@ local function up(device, timestamp, slot, count_after)
|
||||
return feed(device, timestamp, frame)
|
||||
end
|
||||
|
||||
local button_device = new_device("mouse")
|
||||
local output_device = new_device(output_event:sub(1, 4) == "KEY_" and
|
||||
"keyboard" or "mouse")
|
||||
|
||||
local function click_count()
|
||||
local function action_count()
|
||||
local count = 0
|
||||
for _, frame in ipairs(button_device.appended) do
|
||||
if contains(frame, evdev.BTN_MIDDLE, 1) then
|
||||
for _, frame in ipairs(output_device.appended) do
|
||||
if contains(frame, expected_output, 1) then
|
||||
count = count + 1
|
||||
end
|
||||
end
|
||||
@@ -181,7 +197,7 @@ function tests.hold_tap_emits_middle_and_hides_tapper()
|
||||
assert(not contains(second_down, evdev.BTN_TOOL_DOUBLETAP, 1))
|
||||
local second_up = up(device, 260000, 1, 1)
|
||||
assert(not contains(second_up, evdev.ABS_MT_TRACKING_ID, -1))
|
||||
assert(click_count() == 1)
|
||||
assert(action_count() == 1)
|
||||
assert(#device.appended == 0)
|
||||
up(device, 440000, 0, 0)
|
||||
end
|
||||
@@ -193,7 +209,7 @@ function tests.hold_doubletap_emits_two_middle_clicks()
|
||||
up(device, 250000, 1, 1)
|
||||
down(device, 360000, 1, 22, 1400, 1000, 2)
|
||||
up(device, 420000, 1, 1)
|
||||
assert(click_count() == 2)
|
||||
assert(action_count() == 2)
|
||||
up(device, 600000, 0, 0)
|
||||
end
|
||||
|
||||
@@ -205,7 +221,7 @@ function tests.ordinary_two_finger_tap_passes_through()
|
||||
assert(contains(second, evdev.BTN_TOOL_DOUBLETAP, 1))
|
||||
up(device, 70000, 1, 1)
|
||||
up(device, 80000, 0, 0)
|
||||
assert(click_count() == 0)
|
||||
assert(action_count() == 0)
|
||||
end
|
||||
|
||||
function tests.long_stationary_tap_is_consumed_without_click()
|
||||
@@ -214,10 +230,19 @@ function tests.long_stationary_tap_is_consumed_without_click()
|
||||
down(device, 200000, 1, 41, 1400, 1000, 2)
|
||||
local second_up = up(device, 360001, 1, 1)
|
||||
assert(not contains(second_up, evdev.ABS_MT_TRACKING_ID, -1))
|
||||
assert(click_count() == 0)
|
||||
assert(action_count() == 0)
|
||||
up(device, 500000, 0, 0)
|
||||
end
|
||||
|
||||
function tests.implausibly_short_contact_is_consumed_without_click()
|
||||
local device = new_device("touchpad")
|
||||
down(device, 0, 0, 45, 1000, 1000, 1)
|
||||
down(device, 200000, 1, 46, 1400, 1000, 2)
|
||||
up(device, 201000, 1, 1)
|
||||
assert(action_count() == 0)
|
||||
up(device, 400000, 0, 0)
|
||||
end
|
||||
|
||||
function tests.tapper_movement_restores_normal_input()
|
||||
local device = new_device("touchpad")
|
||||
down(device, 0, 0, 50, 1000, 1000, 1)
|
||||
@@ -231,7 +256,7 @@ function tests.tapper_movement_restores_normal_input()
|
||||
assert(not contains(moved, evdev.ABS_MT_POSITION_X, 1430))
|
||||
up(device, 260000, 1, 1)
|
||||
up(device, 400000, 0, 0)
|
||||
assert(click_count() == 0)
|
||||
assert(action_count() == 0)
|
||||
end
|
||||
|
||||
function tests.anchor_movement_prevents_recognition()
|
||||
@@ -239,10 +264,11 @@ function tests.anchor_movement_prevents_recognition()
|
||||
down(device, 0, 0, 60, 1000, 1000, 1)
|
||||
feed(device, 150000, { event(evdev.ABS_MT_POSITION_X, 1040) })
|
||||
local second = down(device, 200000, 1, 61, 1400, 1000, 2)
|
||||
assert(contains(second, evdev.ABS_MT_TRACKING_ID, 61))
|
||||
assert(contains(second, evdev.ABS_MT_TRACKING_ID, 61) ~=
|
||||
allow_moving_anchor)
|
||||
up(device, 250000, 1, 1)
|
||||
up(device, 400000, 0, 0)
|
||||
assert(click_count() == 0)
|
||||
assert(action_count() == (allow_moving_anchor and 1 or 0))
|
||||
end
|
||||
|
||||
function tests.anchor_movement_during_tap_restores_normal_input()
|
||||
@@ -253,12 +279,54 @@ function tests.anchor_movement_during_tap_restores_normal_input()
|
||||
event(evdev.ABS_MT_SLOT, 0),
|
||||
event(evdev.ABS_MT_POSITION_X, 1040),
|
||||
})
|
||||
assert(#device.prepended == 1)
|
||||
assert(contains(device.prepended[1], evdev.ABS_MT_TRACKING_ID, 63))
|
||||
assert(#device.prepended == (allow_moving_anchor and 0 or 1))
|
||||
if not allow_moving_anchor then
|
||||
assert(contains(device.prepended[1], evdev.ABS_MT_TRACKING_ID, 63))
|
||||
end
|
||||
assert(contains(moved, evdev.ABS_MT_POSITION_X, 1040))
|
||||
up(device, 260000, 1, 1)
|
||||
up(device, 400000, 0, 0)
|
||||
assert(click_count() == 0)
|
||||
assert(action_count() == (allow_moving_anchor and 1 or 0))
|
||||
end
|
||||
|
||||
function tests.rejected_tap_does_not_poison_anchor()
|
||||
local device = new_device("touchpad")
|
||||
down(device, 0, 0, 64, 1000, 1000, 1)
|
||||
|
||||
-- This near-simultaneous contact is passed through as an ordinary
|
||||
-- two-finger interaction.
|
||||
local early = down(device, 10000, 1, 65, 1400, 1000, 2)
|
||||
assert(contains(early, evdev.ABS_MT_TRACKING_ID, 65))
|
||||
up(device, 60000, 1, 1)
|
||||
|
||||
-- The same anchor can recognize a later tap without being lifted first.
|
||||
local later = down(device, 200000, 1, 66, 1400, 1000, 2)
|
||||
assert(not contains(later, evdev.ABS_MT_TRACKING_ID, 66))
|
||||
up(device, 250000, 1, 1)
|
||||
assert(action_count() == 1)
|
||||
up(device, 300000, 0, 0)
|
||||
end
|
||||
|
||||
function tests.tap_while_moving_does_not_poison_anchor()
|
||||
if allow_moving_anchor then
|
||||
return
|
||||
end
|
||||
|
||||
local device = new_device("touchpad")
|
||||
down(device, 0, 0, 67, 1000, 1000, 1)
|
||||
feed(device, 150000, {
|
||||
event(evdev.ABS_MT_SLOT, 0),
|
||||
event(evdev.ABS_MT_POSITION_X, 1040),
|
||||
})
|
||||
local moving_tap = down(device, 160000, 1, 68, 1400, 1000, 2)
|
||||
assert(contains(moving_tap, evdev.ABS_MT_TRACKING_ID, 68))
|
||||
up(device, 210000, 1, 1)
|
||||
|
||||
local later = down(device, 320000, 1, 69, 1400, 1000, 2)
|
||||
assert(not contains(later, evdev.ABS_MT_TRACKING_ID, 69))
|
||||
up(device, 370000, 1, 1)
|
||||
assert(action_count() == 1)
|
||||
up(device, 400000, 0, 0)
|
||||
end
|
||||
|
||||
function tests.anchor_can_move_pause_and_tap_repeatedly()
|
||||
@@ -272,7 +340,7 @@ function tests.anchor_can_move_pause_and_tap_repeatedly()
|
||||
})
|
||||
down(device, 650000, 1, 66, 1500, 1100, 2)
|
||||
up(device, 700000, 1, 1)
|
||||
assert(click_count() == 1)
|
||||
assert(action_count() == 1)
|
||||
|
||||
feed(device, 800000, {
|
||||
event(evdev.ABS_MT_SLOT, 0),
|
||||
@@ -281,7 +349,7 @@ function tests.anchor_can_move_pause_and_tap_repeatedly()
|
||||
})
|
||||
down(device, 950000, 1, 67, 2000, 1200, 2)
|
||||
up(device, 1000000, 1, 1)
|
||||
assert(click_count() == 2)
|
||||
assert(action_count() == 2)
|
||||
|
||||
up(device, 1100000, 0, 0)
|
||||
end
|
||||
@@ -297,7 +365,7 @@ function tests.third_contact_restores_candidate()
|
||||
up(device, 260000, 2, 2)
|
||||
up(device, 280000, 1, 1)
|
||||
up(device, 400000, 0, 0)
|
||||
assert(click_count() == 0)
|
||||
assert(action_count() == 0)
|
||||
end
|
||||
|
||||
function tests.two_finger_hold_tap_remains_passthrough()
|
||||
@@ -309,7 +377,7 @@ function tests.two_finger_hold_tap_remains_passthrough()
|
||||
up(device, 360000, 2, 2)
|
||||
up(device, 500000, 1, 1)
|
||||
up(device, 510000, 0, 0)
|
||||
assert(click_count() == 0)
|
||||
assert(action_count() == 0)
|
||||
assert(#device.prepended == 0)
|
||||
end
|
||||
|
||||
@@ -320,8 +388,8 @@ function tests.non_touchpad_is_ignored()
|
||||
end
|
||||
|
||||
function tests.pointer_device_is_used_for_button_output()
|
||||
assert(button_device.enabled[evdev.BTN_MIDDLE])
|
||||
assert(button_device.callbacks["evdev-frame"] == nil)
|
||||
assert(output_device.enabled[expected_output])
|
||||
assert(output_device.callbacks["evdev-frame"] == nil)
|
||||
end
|
||||
|
||||
local names = {}
|
||||
@@ -330,7 +398,7 @@ for name in pairs(tests) do
|
||||
end
|
||||
table.sort(names)
|
||||
for _, name in ipairs(names) do
|
||||
button_device.appended = {}
|
||||
output_device.appended = {}
|
||||
tests[name]()
|
||||
print("ok - " .. name)
|
||||
end
|
||||
|
||||
@@ -0,0 +1,192 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
# SPDX-License-Identifier: MIT
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
root=${TOUCHPAD_HOLD_TAP_TEST_ROOT:-}
|
||||
config_file="$root/etc/touchpad-hold-tap.conf"
|
||||
override_file="$root/etc/libinput/plugins/90-touchpad-hold-tap.lua"
|
||||
template_file="$root/usr/lib64/libinput/plugins/90-touchpad-hold-tap.lua"
|
||||
if [[ ! -e $template_file ]]; then
|
||||
template_file="$root/usr/lib/libinput/plugins/90-touchpad-hold-tap.lua"
|
||||
fi
|
||||
|
||||
minimum_anchor_age_ms=100
|
||||
minimum_pause_ms=100
|
||||
minimum_tap_ms=10
|
||||
maximum_tap_ms=150
|
||||
output_event=BTN_MIDDLE
|
||||
|
||||
die()
|
||||
{
|
||||
printf 'touchpad-hold-tap-config: %s\n' "$*" >&2
|
||||
exit 1
|
||||
}
|
||||
|
||||
require_root()
|
||||
{
|
||||
if [[ -z $root && $EUID -ne 0 ]]; then
|
||||
die "this operation must be run as root"
|
||||
fi
|
||||
}
|
||||
|
||||
validate()
|
||||
{
|
||||
[[ $minimum_anchor_age_ms =~ ^[0-9]+$ ]] || die "minimum anchor age must be an integer"
|
||||
[[ $minimum_pause_ms =~ ^[0-9]+$ ]] || die "minimum pause must be an integer"
|
||||
[[ $minimum_tap_ms =~ ^[0-9]+$ ]] || die "minimum tap duration must be an integer"
|
||||
[[ $maximum_tap_ms =~ ^[0-9]+$ ]] || die "maximum tap duration must be an integer"
|
||||
(( minimum_anchor_age_ms <= 2000 )) || die "minimum anchor age must not exceed 2000 ms"
|
||||
(( minimum_pause_ms <= 2000 )) || die "minimum pause must not exceed 2000 ms"
|
||||
(( maximum_tap_ms >= 1 && minimum_tap_ms <= maximum_tap_ms && maximum_tap_ms <= 2000 )) || \
|
||||
die "tap durations must be between 0 and 2000 ms, with a positive maximum and minimum not exceeding maximum"
|
||||
[[ $output_event =~ ^(BTN|KEY)_[A-Z0-9_]+$ ]] || \
|
||||
die "output event must be an evdev BTN_* or KEY_* name"
|
||||
}
|
||||
|
||||
read_config()
|
||||
{
|
||||
if [[ ! -r $config_file ]]; then
|
||||
return 0
|
||||
fi
|
||||
|
||||
while IFS='=' read -r key value; do
|
||||
case $key in
|
||||
MINIMUM_ANCHOR_AGE_MS) minimum_anchor_age_ms=$value ;;
|
||||
MINIMUM_PAUSE_MS) minimum_pause_ms=$value ;;
|
||||
MINIMUM_TAP_MS) minimum_tap_ms=$value ;;
|
||||
MAXIMUM_TAP_MS) maximum_tap_ms=$value ;;
|
||||
OUTPUT_EVENT) output_event=$value ;;
|
||||
''|'#'*) ;;
|
||||
*) die "unknown setting: $key" ;;
|
||||
esac
|
||||
done < "$config_file"
|
||||
validate
|
||||
}
|
||||
|
||||
write_config()
|
||||
{
|
||||
mkdir -p "$(dirname -- "$config_file")"
|
||||
local temporary
|
||||
temporary=$(mktemp "${config_file}.XXXXXX")
|
||||
trap 'rm -f -- "$temporary"' RETURN
|
||||
printf 'MINIMUM_ANCHOR_AGE_MS=%s\nMINIMUM_PAUSE_MS=%s\nMINIMUM_TAP_MS=%s\nMAXIMUM_TAP_MS=%s\nOUTPUT_EVENT=%s\n' \
|
||||
"$minimum_anchor_age_ms" "$minimum_pause_ms" "$minimum_tap_ms" "$maximum_tap_ms" \
|
||||
"$output_event" > "$temporary"
|
||||
chmod 0644 "$temporary"
|
||||
mv -f -- "$temporary" "$config_file"
|
||||
trap - RETURN
|
||||
}
|
||||
|
||||
write_override()
|
||||
{
|
||||
[[ -r $template_file ]] || die "installed plugin was not found"
|
||||
mkdir -p "$(dirname -- "$override_file")"
|
||||
local temporary
|
||||
temporary=$(mktemp "${override_file}.XXXXXX")
|
||||
trap 'rm -f -- "$temporary"' RETURN
|
||||
awk -v age="$minimum_anchor_age_ms" -v pause="$minimum_pause_ms" \
|
||||
-v mintap="$minimum_tap_ms" \
|
||||
-v maxtap="$maximum_tap_ms" \
|
||||
-v output="$output_event" '
|
||||
/^local settings = / {
|
||||
print "local settings = {"
|
||||
print " minimum_anchor_age_ms = " age ","
|
||||
print " minimum_pause_ms = " pause ","
|
||||
print " minimum_tap_ms = " mintap ","
|
||||
print " maximum_tap_ms = " maxtap ","
|
||||
print " output_event = \"" output "\","
|
||||
print "}"
|
||||
replaced = 1
|
||||
next
|
||||
}
|
||||
{ print }
|
||||
END { if (!replaced) exit 1 }
|
||||
' "$template_file" > "$temporary" || die "installed plugin has an unsupported format"
|
||||
chmod 0644 "$temporary"
|
||||
mv -f -- "$temporary" "$override_file"
|
||||
trap - RETURN
|
||||
}
|
||||
|
||||
usage()
|
||||
{
|
||||
cat <<'EOF'
|
||||
Usage:
|
||||
touchpad-hold-tap-config show
|
||||
touchpad-hold-tap-config set [--minimum-anchor-age-ms N] [--minimum-pause-ms N] [--minimum-tap-ms N] [--maximum-tap-ms N] [--output-event NAME]
|
||||
touchpad-hold-tap-config reset
|
||||
touchpad-hold-tap-config refresh
|
||||
EOF
|
||||
}
|
||||
|
||||
command=${1:-}
|
||||
case $command in
|
||||
show)
|
||||
[[ $# -eq 1 ]] || die "show takes no arguments"
|
||||
read_config
|
||||
printf 'MINIMUM_ANCHOR_AGE_MS=%s\nMINIMUM_PAUSE_MS=%s\nMINIMUM_TAP_MS=%s\nMAXIMUM_TAP_MS=%s\nOUTPUT_EVENT=%s\n' \
|
||||
"$minimum_anchor_age_ms" "$minimum_pause_ms" "$minimum_tap_ms" "$maximum_tap_ms" \
|
||||
"$output_event"
|
||||
;;
|
||||
set)
|
||||
shift
|
||||
read_config
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case $1 in
|
||||
--minimum-anchor-age-ms)
|
||||
[[ $# -ge 2 ]] || die "$1 requires a value"
|
||||
minimum_anchor_age_ms=$2
|
||||
shift 2
|
||||
;;
|
||||
--minimum-pause-ms)
|
||||
[[ $# -ge 2 ]] || die "$1 requires a value"
|
||||
minimum_pause_ms=$2
|
||||
shift 2
|
||||
;;
|
||||
--minimum-tap-ms)
|
||||
[[ $# -ge 2 ]] || die "$1 requires a value"
|
||||
minimum_tap_ms=$2
|
||||
shift 2
|
||||
;;
|
||||
--maximum-tap-ms)
|
||||
[[ $# -ge 2 ]] || die "$1 requires a value"
|
||||
maximum_tap_ms=$2
|
||||
shift 2
|
||||
;;
|
||||
--output-event)
|
||||
[[ $# -ge 2 ]] || die "$1 requires a value"
|
||||
output_event=$2
|
||||
shift 2
|
||||
;;
|
||||
*) die "unknown option: $1" ;;
|
||||
esac
|
||||
done
|
||||
require_root
|
||||
validate
|
||||
write_config
|
||||
write_override
|
||||
;;
|
||||
reset)
|
||||
[[ $# -eq 1 ]] || die "reset takes no arguments"
|
||||
require_root
|
||||
rm -f -- "$config_file" "$override_file"
|
||||
;;
|
||||
refresh)
|
||||
[[ $# -eq 1 ]] || die "refresh takes no arguments"
|
||||
require_root
|
||||
if [[ -e $config_file ]]; then
|
||||
read_config
|
||||
write_override
|
||||
else
|
||||
rm -f -- "$override_file"
|
||||
fi
|
||||
;;
|
||||
-h|--help)
|
||||
usage
|
||||
;;
|
||||
*)
|
||||
usage >&2
|
||||
exit 2
|
||||
;;
|
||||
esac
|
||||
@@ -1,6 +1,6 @@
|
||||
Name: touchpad-hold-tap
|
||||
Version: 0.1.0
|
||||
Release: 6%{?dist}
|
||||
Release: 9%{?dist}
|
||||
Summary: Hold-tap middle-click gesture for touchpads
|
||||
|
||||
License: MIT
|
||||
@@ -9,6 +9,8 @@ Source0: 90-touchpad-hold-tap.lua
|
||||
Source1: LICENSE
|
||||
Source2: test-touchpad-hold-tap.lua
|
||||
Source3: README.md
|
||||
Source4: touchpad-hold-tap-config
|
||||
Source5: test-touchpad-hold-tap-config
|
||||
|
||||
BuildRequires: lua
|
||||
Requires: libinput >= 1.30
|
||||
@@ -25,10 +27,15 @@ gestures. Other touchpad input continues through libinput normally.
|
||||
|
||||
%check
|
||||
lua %{SOURCE2} %{SOURCE0}
|
||||
lua %{SOURCE2} %{SOURCE0} KEY_F13
|
||||
lua %{SOURCE2} %{SOURCE0} BTN_MIDDLE allow-moving-anchor
|
||||
bash %{SOURCE5} %{SOURCE0} %{SOURCE4}
|
||||
|
||||
%install
|
||||
install -Dpm 0644 %{SOURCE0} \
|
||||
%{buildroot}%{_libdir}/libinput/plugins/90-touchpad-hold-tap.lua
|
||||
install -Dpm 0755 %{SOURCE4} \
|
||||
%{buildroot}%{_bindir}/touchpad-hold-tap-config
|
||||
install -Dpm 0644 %{SOURCE1} \
|
||||
%{buildroot}%{_licensedir}/%{name}/LICENSE
|
||||
install -Dpm 0644 %{SOURCE3} \
|
||||
@@ -37,9 +44,29 @@ install -Dpm 0644 %{SOURCE3} \
|
||||
%files
|
||||
%license %{_licensedir}/%{name}/LICENSE
|
||||
%doc %{_docdir}/%{name}/README.md
|
||||
%{_bindir}/touchpad-hold-tap-config
|
||||
%{_libdir}/libinput/plugins/90-touchpad-hold-tap.lua
|
||||
|
||||
%posttrans
|
||||
%{_bindir}/touchpad-hold-tap-config refresh || :
|
||||
|
||||
%postun
|
||||
if [ "$1" -eq 0 ]; then
|
||||
rm -f %{_sysconfdir}/touchpad-hold-tap.conf \
|
||||
%{_sysconfdir}/libinput/plugins/90-touchpad-hold-tap.lua
|
||||
fi
|
||||
|
||||
%changelog
|
||||
* Sat Sep 05 2026 fedora-tools contributors - 0.1.0-9
|
||||
- Separate minimum anchor age from the optional stationary period
|
||||
- Allow a held anchor to recover after rejected taps
|
||||
|
||||
* Sat Sep 05 2026 fedora-tools contributors - 0.1.0-8
|
||||
- Add a configurable minimum tap duration
|
||||
|
||||
* Sat Sep 05 2026 fedora-tools contributors - 0.1.0-7
|
||||
- Add persistent timing and output-event configuration
|
||||
|
||||
* Sat Sep 05 2026 fedora-tools contributors - 0.1.0-6
|
||||
- Advertise the tool to Fedora Tools settings
|
||||
|
||||
|
||||
Reference in New Issue
Block a user