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
|
The optional `fedora-tools-settings` package adds a **Fedora Tools** page to
|
||||||
Plasma System Settings. It discovers tools from this RPM repository, shows
|
Plasma System Settings. It discovers tools from this RPM repository, shows
|
||||||
which are installed, and can install available tools after administrator
|
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
|
### 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,
|
Repeating the tap while holding the first finger produces repeated clicks,
|
||||||
including double middle 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
|
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
|
that enables libinput plugins. Log out and back in after installation or an
|
||||||
update so the compositor reloads the plugin.
|
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
|
application starts** determines whether that separate launcher remains while
|
||||||
the application is running. Other groups retain the Task Manager's sort order.
|
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
|
If application grouping is disabled—or an application is excluded from
|
||||||
grouping—each of its windows remains a separate shortcut position. **Only group
|
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
|
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)
|
include(KDECompilerSettings NO_POLICY_SCOPE)
|
||||||
|
|
||||||
find_package(Qt6 6.8 REQUIRED COMPONENTS Core Quick Test)
|
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)
|
add_subdirectory(src)
|
||||||
if(BUILD_TESTING)
|
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
|
package names that independently resolve to that capability in the
|
||||||
`fedora-tools` repository.
|
`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
|
Installing this module does not install or activate any other tool. Tools
|
||||||
remain usable without the module.
|
remain usable without the module.
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
Name: fedora-tools-settings
|
Name: fedora-tools-settings
|
||||||
Version: 0.1.0
|
Version: 0.1.0
|
||||||
Release: 1%{?dist}
|
Release: 5%{?dist}
|
||||||
Summary: Plasma System Settings module for Fedora Tools
|
Summary: Plasma System Settings module for Fedora Tools
|
||||||
|
|
||||||
License: MIT
|
License: MIT
|
||||||
@@ -14,6 +14,7 @@ BuildRequires: extra-cmake-modules
|
|||||||
BuildRequires: gcc-c++
|
BuildRequires: gcc-c++
|
||||||
BuildRequires: kf6-kauth-devel
|
BuildRequires: kf6-kauth-devel
|
||||||
BuildRequires: kf6-kcmutils-devel
|
BuildRequires: kf6-kcmutils-devel
|
||||||
|
BuildRequires: kf6-kconfig-devel
|
||||||
BuildRequires: kf6-ki18n-devel
|
BuildRequires: kf6-ki18n-devel
|
||||||
BuildRequires: kf6-kirigami-devel
|
BuildRequires: kf6-kirigami-devel
|
||||||
BuildRequires: qt6-qtdeclarative-devel
|
BuildRequires: qt6-qtdeclarative-devel
|
||||||
@@ -58,5 +59,20 @@ install -Dpm 0644 %{SOURCE2} \
|
|||||||
%{_datadir}/polkit-1/actions/se.ajpanton.fedoratools.policy
|
%{_datadir}/polkit-1/actions/se.ajpanton.fedoratools.policy
|
||||||
|
|
||||||
%changelog
|
%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
|
* Sat Sep 05 2026 fedora-tools contributors - 0.1.0-1
|
||||||
- Initial package
|
- Initial package
|
||||||
|
|||||||
@@ -13,6 +13,7 @@ target_link_libraries(kcm_fedora_tools PRIVATE
|
|||||||
Qt6::Core
|
Qt6::Core
|
||||||
Qt6::Quick
|
Qt6::Quick
|
||||||
KF6::AuthCore
|
KF6::AuthCore
|
||||||
|
KF6::ConfigCore
|
||||||
KF6::CoreAddons
|
KF6::CoreAddons
|
||||||
KF6::I18n
|
KF6::I18n
|
||||||
KF6::KCMUtilsQuick
|
KF6::KCMUtilsQuick
|
||||||
|
|||||||
@@ -4,10 +4,14 @@
|
|||||||
|
|
||||||
#include <KAuth/Action>
|
#include <KAuth/Action>
|
||||||
#include <KAuth/ExecuteJob>
|
#include <KAuth/ExecuteJob>
|
||||||
|
#include <KConfigGroup>
|
||||||
#include <KJob>
|
#include <KJob>
|
||||||
#include <KLocalizedString>
|
#include <KLocalizedString>
|
||||||
#include <KPluginFactory>
|
#include <KPluginFactory>
|
||||||
|
#include <KSharedConfig>
|
||||||
|
|
||||||
|
#include <QFile>
|
||||||
|
#include <QRegularExpression>
|
||||||
#include <QTimer>
|
#include <QTimer>
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
@@ -33,8 +37,10 @@ FedoraToolsKcm::FedoraToolsKcm(QObject *parent, const KPluginMetaData &data)
|
|||||||
}
|
}
|
||||||
setMessage(i18n("Could not start %1.", m_query.program()), true);
|
setMessage(i18n("Could not start %1.", m_query.program()), true);
|
||||||
m_stage = QueryStage::Idle;
|
m_stage = QueryStage::Idle;
|
||||||
|
setPackageOperation({}, {});
|
||||||
setBusy(false);
|
setBusy(false);
|
||||||
});
|
});
|
||||||
|
loadToolSettings();
|
||||||
QTimer::singleShot(0, this, [this] {
|
QTimer::singleShot(0, this, [this] {
|
||||||
refresh();
|
refresh();
|
||||||
});
|
});
|
||||||
@@ -65,6 +71,56 @@ bool FedoraToolsKcm::fingerprintEnabled() const
|
|||||||
return m_fingerprintEnabled;
|
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)
|
void FedoraToolsKcm::refresh(bool refreshMetadata)
|
||||||
{
|
{
|
||||||
if (m_busy) {
|
if (m_busy) {
|
||||||
@@ -98,12 +154,107 @@ void FedoraToolsKcm::installTool(const QString &packageName)
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setPackageOperation(packageName, QStringLiteral("install"));
|
||||||
runAuthorizedAction(QStringLiteral("installpackage"),
|
runAuthorizedAction(QStringLiteral("installpackage"),
|
||||||
{{QStringLiteral("packageName"), packageName}},
|
{{QStringLiteral("packageName"), packageName}},
|
||||||
i18n("%1 is now installed.", packageName),
|
i18n("%1 is now installed.", packageName),
|
||||||
true);
|
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)
|
void FedoraToolsKcm::enableFingerprintWorkaround(const QUrl &rpmFile, bool force)
|
||||||
{
|
{
|
||||||
if (m_busy) {
|
if (m_busy) {
|
||||||
@@ -155,6 +306,7 @@ void FedoraToolsKcm::queryFinished(int exitCode, QProcess::ExitStatus exitStatus
|
|||||||
if (exitStatus != QProcess::NormalExit) {
|
if (exitStatus != QProcess::NormalExit) {
|
||||||
setMessage(i18n("Package discovery terminated unexpectedly."), true);
|
setMessage(i18n("Package discovery terminated unexpectedly."), true);
|
||||||
m_stage = QueryStage::Idle;
|
m_stage = QueryStage::Idle;
|
||||||
|
setPackageOperation({}, {});
|
||||||
setBusy(false);
|
setBusy(false);
|
||||||
return;
|
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);
|
setMessage(details.isEmpty() ? i18n("The Fedora Tools repository is unavailable.") : details, true);
|
||||||
updateFingerprintStatus();
|
updateFingerprintStatus();
|
||||||
m_stage = QueryStage::Idle;
|
m_stage = QueryStage::Idle;
|
||||||
|
setPackageOperation({}, {});
|
||||||
setBusy(false);
|
setBusy(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -244,6 +397,7 @@ void FedoraToolsKcm::finishRefresh()
|
|||||||
m_tools.setPackages(m_installedPackages, m_availablePackages, m_updates);
|
m_tools.setPackages(m_installedPackages, m_availablePackages, m_updates);
|
||||||
updateFingerprintStatus();
|
updateFingerprintStatus();
|
||||||
m_stage = QueryStage::Idle;
|
m_stage = QueryStage::Idle;
|
||||||
|
setPackageOperation({}, {});
|
||||||
setBusy(false);
|
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 helperMessage = executeJob ? executeJob->data().value(QStringLiteral("message")).toString() : QString();
|
||||||
const QString errorMessage = helperMessage.isEmpty() ? job->errorString() : helperMessage;
|
const QString errorMessage = helperMessage.isEmpty() ? job->errorString() : helperMessage;
|
||||||
setMessage(errorMessage, true);
|
setMessage(errorMessage, true);
|
||||||
|
setPackageOperation({}, {});
|
||||||
setBusy(false);
|
setBusy(false);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -283,6 +438,7 @@ void FedoraToolsKcm::actionFinished(KJob *job, const QString &successMessage, bo
|
|||||||
refresh();
|
refresh();
|
||||||
} else {
|
} else {
|
||||||
updateFingerprintStatus();
|
updateFingerprintStatus();
|
||||||
|
loadToolSettings();
|
||||||
setBusy(false);
|
setBusy(false);
|
||||||
}
|
}
|
||||||
setMessage(successMessage);
|
setMessage(successMessage);
|
||||||
@@ -297,6 +453,16 @@ void FedoraToolsKcm::setBusy(bool busy)
|
|||||||
Q_EMIT busyChanged();
|
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)
|
void FedoraToolsKcm::setMessage(const QString &message, bool error)
|
||||||
{
|
{
|
||||||
if (m_message == message && m_error == 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"
|
#include "fedoratoolskcm.moc"
|
||||||
|
|||||||
@@ -21,6 +21,16 @@ class FedoraToolsKcm : public KQuickConfigModule
|
|||||||
Q_PROPERTY(QString message READ message NOTIFY messageChanged)
|
Q_PROPERTY(QString message READ message NOTIFY messageChanged)
|
||||||
Q_PROPERTY(bool error READ error NOTIFY messageChanged)
|
Q_PROPERTY(bool error READ error NOTIFY messageChanged)
|
||||||
Q_PROPERTY(bool fingerprintEnabled READ fingerprintEnabled NOTIFY fingerprintEnabledChanged)
|
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:
|
public:
|
||||||
FedoraToolsKcm(QObject *parent, const KPluginMetaData &data);
|
FedoraToolsKcm(QObject *parent, const KPluginMetaData &data);
|
||||||
@@ -30,10 +40,25 @@ public:
|
|||||||
QString message() const;
|
QString message() const;
|
||||||
bool error() const;
|
bool error() const;
|
||||||
bool fingerprintEnabled() 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 refresh(bool refreshMetadata = false);
|
||||||
Q_INVOKABLE void clearMessage();
|
Q_INVOKABLE void clearMessage();
|
||||||
Q_INVOKABLE void installTool(const QString &packageName);
|
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 enableFingerprintWorkaround(const QUrl &rpmFile, bool force);
|
||||||
Q_INVOKABLE void disableFingerprintWorkaround();
|
Q_INVOKABLE void disableFingerprintWorkaround();
|
||||||
|
|
||||||
@@ -41,6 +66,8 @@ Q_SIGNALS:
|
|||||||
void busyChanged();
|
void busyChanged();
|
||||||
void messageChanged();
|
void messageChanged();
|
||||||
void fingerprintEnabledChanged();
|
void fingerprintEnabledChanged();
|
||||||
|
void packageOperationChanged();
|
||||||
|
void toolSettingsChanged();
|
||||||
|
|
||||||
private:
|
private:
|
||||||
enum class QueryStage {
|
enum class QueryStage {
|
||||||
@@ -57,8 +84,10 @@ private:
|
|||||||
void runAuthorizedAction(const QString &name, const QVariantMap &arguments, const QString &successMessage, bool refreshTools);
|
void runAuthorizedAction(const QString &name, const QVariantMap &arguments, const QString &successMessage, bool refreshTools);
|
||||||
void actionFinished(KJob *job, const QString &successMessage, bool refreshTools);
|
void actionFinished(KJob *job, const QString &successMessage, bool refreshTools);
|
||||||
void setBusy(bool busy);
|
void setBusy(bool busy);
|
||||||
|
void setPackageOperation(const QString &packageName, const QString &operation);
|
||||||
void setMessage(const QString &message, bool error = false);
|
void setMessage(const QString &message, bool error = false);
|
||||||
void updateFingerprintStatus();
|
void updateFingerprintStatus();
|
||||||
|
void loadToolSettings();
|
||||||
|
|
||||||
ToolModel m_tools;
|
ToolModel m_tools;
|
||||||
QProcess m_query;
|
QProcess m_query;
|
||||||
@@ -70,5 +99,15 @@ private:
|
|||||||
bool m_busy = false;
|
bool m_busy = false;
|
||||||
bool m_error = false;
|
bool m_error = false;
|
||||||
bool m_fingerprintEnabled = false;
|
bool m_fingerprintEnabled = false;
|
||||||
|
QString m_activePackage;
|
||||||
|
QString m_packageOperation;
|
||||||
QString m_message;
|
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 <QFileInfo>
|
||||||
#include <QObject>
|
#include <QObject>
|
||||||
#include <QProcess>
|
#include <QProcess>
|
||||||
|
#include <QRegularExpression>
|
||||||
|
|
||||||
namespace
|
namespace
|
||||||
{
|
{
|
||||||
@@ -71,6 +72,37 @@ bool isPublishedTool(const QString &packageName)
|
|||||||
}
|
}
|
||||||
return false;
|
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
|
class FedoraToolsHelper : public QObject
|
||||||
@@ -94,6 +126,76 @@ public Q_SLOTS:
|
|||||||
packageName});
|
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)
|
KAuth::ActionReply fingerprintworkaround(const QVariantMap &arguments)
|
||||||
{
|
{
|
||||||
const QString operation = arguments.value(QStringLiteral("operation")).toString();
|
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
|
Description=Install a package from the Fedora Tools repository
|
||||||
Policy=auth_admin
|
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]
|
[se.ajpanton.fedoratools.fingerprintworkaround]
|
||||||
Name=Change the Plasma fingerprint workaround
|
Name=Change the Plasma fingerprint workaround
|
||||||
Description=Install or remove the experimental patched KScreenLocker package
|
Description=Install or remove the experimental patched KScreenLocker package
|
||||||
|
|||||||
@@ -42,7 +42,10 @@ QVariant ToolModel::data(const QModelIndex &index, int role) const
|
|||||||
case UpdateAvailableRole:
|
case UpdateAvailableRole:
|
||||||
return tool.updateAvailable;
|
return tool.updateAvailable;
|
||||||
case ConfigurableRole:
|
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:
|
default:
|
||||||
return {};
|
return {};
|
||||||
}
|
}
|
||||||
@@ -83,16 +86,15 @@ void ToolModel::setPackages(const QList<PackageRecord> &installed,
|
|||||||
tool.packageName = package.name;
|
tool.packageName = package.name;
|
||||||
tool.availableVersion = package.version;
|
tool.availableVersion = package.version;
|
||||||
tool.architecture = package.architecture;
|
tool.architecture = package.architecture;
|
||||||
tool.summary = package.summary;
|
if (!tool.installed) {
|
||||||
|
tool.summary = package.summary;
|
||||||
|
}
|
||||||
tool.available = true;
|
tool.available = true;
|
||||||
tool.updateAvailable = tool.installed && updates.contains(package.name);
|
tool.updateAvailable = tool.installed && updates.contains(package.name);
|
||||||
}
|
}
|
||||||
|
|
||||||
QList<Tool> merged = tools.values();
|
QList<Tool> merged = tools.values();
|
||||||
std::ranges::sort(merged, [](const Tool &left, const Tool &right) {
|
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;
|
return left.packageName < right.packageName;
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -107,3 +109,10 @@ bool ToolModel::mayInstall(const QString &packageName) const
|
|||||||
return tool.packageName == packageName && tool.available;
|
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 QList<PackageRecord> &available,
|
||||||
const QSet<QString> &updates = {});
|
const QSet<QString> &updates = {});
|
||||||
bool mayInstall(const QString &packageName) const;
|
bool mayInstall(const QString &packageName) const;
|
||||||
|
bool mayRemove(const QString &packageName) const;
|
||||||
|
|
||||||
private:
|
private:
|
||||||
QList<Tool> m_tools;
|
QList<Tool> m_tools;
|
||||||
|
|||||||
@@ -12,14 +12,25 @@ import org.kde.kirigami as Kirigami
|
|||||||
KCM.SimpleKCM {
|
KCM.SimpleKCM {
|
||||||
id: root
|
id: root
|
||||||
|
|
||||||
property bool showingFingerprintSettings: false
|
property string settingsPage: ""
|
||||||
|
|
||||||
implicitWidth: Kirigami.Units.gridUnit * 36
|
implicitWidth: Kirigami.Units.gridUnit * 36
|
||||||
implicitHeight: Kirigami.Units.gridUnit * 30
|
implicitHeight: Kirigami.Units.gridUnit * 30
|
||||||
|
|
||||||
StackLayout {
|
StackLayout {
|
||||||
anchors.fill: parent
|
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 {
|
ColumnLayout {
|
||||||
spacing: Kirigami.Units.largeSpacing
|
spacing: Kirigami.Units.largeSpacing
|
||||||
@@ -50,7 +61,7 @@ KCM.SimpleKCM {
|
|||||||
|
|
||||||
Controls.BusyIndicator {
|
Controls.BusyIndicator {
|
||||||
Layout.alignment: Qt.AlignHCenter
|
Layout.alignment: Qt.AlignHCenter
|
||||||
visible: kcm.busy
|
visible: kcm.busy && kcm.activePackage.length === 0
|
||||||
running: visible
|
running: visible
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -83,6 +94,7 @@ KCM.SimpleKCM {
|
|||||||
|
|
||||||
Kirigami.Icon {
|
Kirigami.Icon {
|
||||||
source: "applications-system"
|
source: "applications-system"
|
||||||
|
opacity: toolCard.installed ? 1 : 0
|
||||||
implicitWidth: Kirigami.Units.iconSizes.medium
|
implicitWidth: Kirigami.Units.iconSizes.medium
|
||||||
implicitHeight: width
|
implicitHeight: width
|
||||||
}
|
}
|
||||||
@@ -124,14 +136,96 @@ KCM.SimpleKCM {
|
|||||||
visible: toolCard.configurable
|
visible: toolCard.configurable
|
||||||
text: i18n("Configure…")
|
text: i18n("Configure…")
|
||||||
enabled: !kcm.busy
|
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)
|
visible: toolCard.available && (!toolCard.installed || toolCard.updateAvailable)
|
||||||
text: toolCard.installed ? i18n("Update") : i18n("Install")
|
Layout.minimumWidth: Kirigami.Units.gridUnit * 5
|
||||||
enabled: !kcm.busy
|
Layout.preferredWidth: Layout.minimumWidth
|
||||||
onClicked: kcm.installTool(toolCard.packageName)
|
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
|
enabled: !kcm.busy
|
||||||
onClicked: {
|
onClicked: {
|
||||||
kcm.clearMessage()
|
kcm.clearMessage()
|
||||||
root.showingFingerprintSettings = false
|
root.settingsPage = ""
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -256,6 +350,323 @@ KCM.SimpleKCM {
|
|||||||
Layout.fillHeight: true
|
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 {
|
Dialogs.FileDialog {
|
||||||
|
|||||||
@@ -23,6 +23,8 @@ private Q_SLOTS:
|
|||||||
QCOMPARE(model.data(index, ToolModel::AvailableRole).toBool(), true);
|
QCOMPARE(model.data(index, ToolModel::AvailableRole).toBool(), true);
|
||||||
QCOMPARE(model.data(index, ToolModel::UpdateAvailableRole).toBool(), true);
|
QCOMPARE(model.data(index, ToolModel::UpdateAvailableRole).toBool(), true);
|
||||||
QCOMPARE(model.mayInstall(QStringLiteral("touchpad-hold-tap")), 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()
|
void doesNotGuessUpdatesFromDifferentVersionStrings()
|
||||||
@@ -44,6 +46,34 @@ private Q_SLOTS:
|
|||||||
|
|
||||||
QCOMPARE(model.data(model.index(0), ToolModel::ConfigurableRole).toBool(), true);
|
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)
|
QTEST_MAIN(ToolModelTest)
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
Name: plasma-always-show-unlock
|
Name: plasma-always-show-unlock
|
||||||
Version: 0.1.0
|
Version: 0.1.0
|
||||||
Release: 9%{?dist}
|
Release: 10%{?dist}
|
||||||
Summary: Always show the Plasma lock-screen unlock prompt
|
Summary: Immediately show the Plasma unlock prompt without requiring input
|
||||||
|
|
||||||
License: MIT AND GPL-2.0-or-later
|
License: MIT AND GPL-2.0-or-later
|
||||||
URL: https://git.ajpanton.se/ajp_anton/fedora-tools
|
URL: https://git.ajpanton.se/ajp_anton/fedora-tools
|
||||||
@@ -57,6 +57,9 @@ fi
|
|||||||
%{_datadir}/plasma-always-show-unlock/LockScreenUi.patch
|
%{_datadir}/plasma-always-show-unlock/LockScreenUi.patch
|
||||||
|
|
||||||
%changelog
|
%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
|
* Sat Sep 05 2026 fedora-tools contributors - 0.1.0-9
|
||||||
- Advertise the tool to Fedora Tools settings
|
- Advertise the tool to Fedora Tools settings
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
Name: plasma-fingerprint-workaround
|
Name: plasma-fingerprint-workaround
|
||||||
Version: 0.1.0
|
Version: 0.1.0
|
||||||
Release: 2%{?dist}
|
Release: 3%{?dist}
|
||||||
Summary: Opt-in installer for a patched Fedora KScreenLocker
|
Summary: Opt-in patched KScreenLocker for fingerprint recovery after suspend
|
||||||
|
|
||||||
License: MIT
|
License: MIT
|
||||||
URL: https://git.ajpanton.se/ajp_anton/fedora-tools
|
URL: https://git.ajpanton.se/ajp_anton/fedora-tools
|
||||||
@@ -51,6 +51,9 @@ install -D -m 0644 %{SOURCE3} \
|
|||||||
%{_datadir}/plasma-fingerprint-workaround/payload.conf
|
%{_datadir}/plasma-fingerprint-workaround/payload.conf
|
||||||
|
|
||||||
%changelog
|
%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
|
* Sat Sep 05 2026 fedora-tools contributors - 0.1.0-2
|
||||||
- Advertise the tool to Fedora Tools settings
|
- 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 componentId = "se.ajpanton.plasma-task-group-shortcuts";
|
||||||
constexpr auto stateFile = "plasma-task-group-shortcutsrc";
|
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)
|
KConfigGroup displacedShortcutGroup(int number)
|
||||||
{
|
{
|
||||||
const auto config = KSharedConfig::openConfig(QString::fromLatin1(stateFile));
|
const auto config = KSharedConfig::openConfig(QString::fromLatin1(stateFile));
|
||||||
@@ -282,15 +299,20 @@ private:
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const ShortcutSettings settings = shortcutSettings();
|
||||||
const bool continuing = row == m_lastShortcut;
|
const bool continuing = row == m_lastShortcut;
|
||||||
if (!continuing) {
|
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 {
|
} else {
|
||||||
m_cyclePosition += shifted ? -1 : 1;
|
m_cyclePosition += shifted && settings.shiftCyclesBackward ? -1 : 1;
|
||||||
}
|
}
|
||||||
m_lastShortcut = row;
|
m_lastShortcut = row;
|
||||||
|
|
||||||
if (shifted && !continuing) {
|
if (shifted && !continuing && settings.initialShiftOpensNewInstance) {
|
||||||
m_model.requestNewInstance(m_model.index(tasks.at(row), 0));
|
m_model.requestNewInstance(m_model.index(tasks.at(row), 0));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
Name: plasma-task-group-shortcuts
|
Name: plasma-task-group-shortcuts
|
||||||
Version: 0.1.0
|
Version: 0.1.0
|
||||||
Release: 13%{?dist}
|
Release: 15%{?dist}
|
||||||
Summary: Application-group shortcuts for the Plasma Task Manager
|
Summary: Windows-like application-group shortcuts for the Plasma Task Manager
|
||||||
|
|
||||||
License: MIT
|
License: MIT
|
||||||
URL: https://git.ajpanton.se/ajp_anton/fedora-tools
|
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
|
%{_sysconfdir}/xdg/autostart/se.ajpanton.plasma-task-group-shortcuts-autostart.desktop
|
||||||
|
|
||||||
%changelog
|
%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
|
* Sat Sep 05 2026 fedora-tools contributors - 0.1.0-13
|
||||||
- Advertise the tool to Fedora Tools settings
|
- Advertise the tool to Fedora Tools settings
|
||||||
|
|
||||||
|
|||||||
@@ -72,3 +72,22 @@ QVector<int> logicalTaskRows(const QAbstractItemModel &model, bool separateLaunc
|
|||||||
}
|
}
|
||||||
return rows;
|
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);
|
QModelIndex taskToActivate(const QAbstractItemModel &model, int row, int cyclePosition);
|
||||||
QVector<int> logicalTaskRows(const QAbstractItemModel &model, bool separateLaunchers);
|
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 returnsTopLevelTask();
|
||||||
void selectsChildByCyclePosition();
|
void selectsChildByCyclePosition();
|
||||||
void startsWithFirstChild();
|
void startsWithFirstChild();
|
||||||
|
void startsWithLastChildWhenReversing();
|
||||||
|
void startsAfterActiveChild();
|
||||||
|
void startsBeforeActiveChildWhenReversing();
|
||||||
void keepsSeparateLaunchersAsSlots();
|
void keepsSeparateLaunchersAsSlots();
|
||||||
void mergesLauncherWithRunningApplication();
|
void mergesLauncherWithRunningApplication();
|
||||||
};
|
};
|
||||||
@@ -59,6 +62,45 @@ void TaskSelectionTest::startsWithFirstChild()
|
|||||||
QCOMPARE(taskToActivate(model, 0, 0), model.index(0, 0, model.index(0, 0)));
|
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()
|
void TaskSelectionTest::keepsSeparateLaunchersAsSlots()
|
||||||
{
|
{
|
||||||
QStandardItemModel model;
|
QStandardItemModel model;
|
||||||
|
|||||||
@@ -14,6 +14,12 @@ install -m 0644 \
|
|||||||
"$repo_root/touchpad-hold-tap/tests/test-touchpad-hold-tap.lua" \
|
"$repo_root/touchpad-hold-tap/tests/test-touchpad-hold-tap.lua" \
|
||||||
"$topdir/SOURCES/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 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 \
|
rpmbuild \
|
||||||
--define "_topdir $topdir" \
|
--define "_topdir $topdir" \
|
||||||
|
|||||||
@@ -1,18 +1,23 @@
|
|||||||
-- SPDX-License-Identifier: MIT
|
-- SPDX-License-Identifier: MIT
|
||||||
--
|
--
|
||||||
-- Recognize one touchpad contact that has paused before a quick second-finger
|
-- Recognize one established touchpad contact followed by a quick second-finger
|
||||||
-- tap. Hide the tapping contact from libinput and emit a middle click instead.
|
-- tap. Hide the tapping contact from libinput and emit a configured action.
|
||||||
|
|
||||||
libinput:register({1})
|
libinput:register({1})
|
||||||
|
|
||||||
local MINIMUM_STATIONARY_US = 100 * 1000
|
local settings = TOUCHPAD_HOLD_TAP_SETTINGS or {}
|
||||||
local MAXIMUM_TAP_US = 150 * 1000
|
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_ANCHOR_MOVEMENT_MM = 1.5
|
||||||
local MAXIMUM_TAPPER_MOVEMENT_MM = 1.0
|
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 states = {}
|
||||||
local button_devices = {}
|
local output_devices = {}
|
||||||
local button_device = nil
|
local output_device = nil
|
||||||
|
|
||||||
local tool_usages = {
|
local tool_usages = {
|
||||||
{ usage = evdev.BTN_TOOL_FINGER, count = 1 },
|
{ usage = evdev.BTN_TOOL_FINGER, count = 1 },
|
||||||
@@ -131,18 +136,19 @@ local function restore_candidate(device, state, reason)
|
|||||||
"touchpad-hold-tap: restored candidate (" .. reason .. ")")
|
"touchpad-hold-tap: restored candidate (" .. reason .. ")")
|
||||||
end
|
end
|
||||||
|
|
||||||
local function emit_middle_click()
|
local function emit_action()
|
||||||
if not button_device then
|
if not output_device then
|
||||||
libinput:log_error(
|
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
|
return false
|
||||||
end
|
end
|
||||||
|
|
||||||
button_device:append_frame({
|
output_device:append_frame({
|
||||||
{ usage = evdev.BTN_MIDDLE, value = 1 },
|
{ usage = OUTPUT_USAGE, value = 1 },
|
||||||
})
|
})
|
||||||
button_device:append_frame({
|
output_device:append_frame({
|
||||||
{ usage = evdev.BTN_MIDDLE, value = 0 },
|
{ usage = OUTPUT_USAGE, value = 0 },
|
||||||
})
|
})
|
||||||
return true
|
return true
|
||||||
end
|
end
|
||||||
@@ -185,8 +191,14 @@ local function handle_tracking_begin(device, state, tracking_id, timestamp)
|
|||||||
state.anchor_slot = contact.slot
|
state.anchor_slot = contact.slot
|
||||||
elseif state.mode == "anchor" and previous_count == 1 then
|
elseif state.mode == "anchor" and previous_count == 1 then
|
||||||
local anchor = state.contacts[state.anchor_slot]
|
local anchor = state.contacts[state.anchor_slot]
|
||||||
if anchor and anchor.rest_x and anchor.rest_y and
|
local old_enough = anchor and
|
||||||
timestamp - anchor.stationary_since >= MINIMUM_STATIONARY_US then
|
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
|
contact.hidden = true
|
||||||
state.mode = "candidate"
|
state.mode = "candidate"
|
||||||
state.candidate_slot = contact.slot
|
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
|
if contact.hidden and state.source_slot == state.candidate_slot then
|
||||||
local anchor = state.contacts[state.anchor_slot]
|
local anchor = state.contacts[state.anchor_slot]
|
||||||
local duration = timestamp - contact.down_time
|
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.contacts[state.source_slot] = nil
|
||||||
state.candidate_slot = nil
|
state.candidate_slot = nil
|
||||||
@@ -224,11 +238,12 @@ local function handle_tracking_end(device, state, timestamp)
|
|||||||
end
|
end
|
||||||
|
|
||||||
if accepted then
|
if accepted then
|
||||||
if emit_middle_click() then
|
if emit_action() then
|
||||||
libinput:log_debug("touchpad-hold-tap: middle click")
|
libinput:log_debug(
|
||||||
|
"touchpad-hold-tap: emitted " .. OUTPUT_EVENT_NAME)
|
||||||
end
|
end
|
||||||
else
|
else
|
||||||
libinput:log_debug("touchpad-hold-tap: ignored long tap")
|
libinput:log_debug("touchpad-hold-tap: ignored tap duration")
|
||||||
end
|
end
|
||||||
return false, true
|
return false, true
|
||||||
end
|
end
|
||||||
@@ -243,6 +258,12 @@ local function handle_tracking_end(device, state, timestamp)
|
|||||||
if contact_count(state, false) > 0 then
|
if contact_count(state, false) > 0 then
|
||||||
state.mode = "passthrough"
|
state.mode = "passthrough"
|
||||||
end
|
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
|
end
|
||||||
reset_if_clear(state)
|
reset_if_clear(state)
|
||||||
return true, false
|
return true, false
|
||||||
@@ -272,7 +293,7 @@ local function handle_mt_axis(device, state, event, timestamp)
|
|||||||
remember_axis(contact, event.usage, event.value)
|
remember_axis(contact, event.usage, event.value)
|
||||||
|
|
||||||
if contact.slot == state.anchor_slot then
|
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(
|
movement_exceeded_from(
|
||||||
state, contact, state.candidate_anchor_x,
|
state, contact, state.candidate_anchor_x,
|
||||||
state.candidate_anchor_y, MAXIMUM_ANCHOR_MOVEMENT_MM) then
|
state.candidate_anchor_y, MAXIMUM_ANCHOR_MOVEMENT_MM) then
|
||||||
@@ -373,31 +394,45 @@ end
|
|||||||
|
|
||||||
local function device_removed(device)
|
local function device_removed(device)
|
||||||
states[device] = nil
|
states[device] = nil
|
||||||
button_devices[device] = nil
|
output_devices[device] = nil
|
||||||
if button_device == device then
|
if output_device == device then
|
||||||
button_device = nil
|
output_device = nil
|
||||||
for candidate in pairs(button_devices) do
|
for candidate in pairs(output_devices) do
|
||||||
button_device = candidate
|
output_device = candidate
|
||||||
break
|
break
|
||||||
end
|
end
|
||||||
end
|
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)
|
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 properties = device:udev_properties()
|
||||||
local usages = device:usages()
|
local usages = device:usages()
|
||||||
|
|
||||||
-- Clickpads intentionally ignore BTN_MIDDLE from their own event node.
|
-- Clickpads ignore synthesized buttons on their own event node. Keyboard
|
||||||
-- Route synthesized buttons through an existing pointer device instead.
|
-- events likewise belong on a keyboard, so use a suitable existing device.
|
||||||
if (properties.ID_INPUT_MOUSE or properties.ID_INPUT_POINTINGSTICK) and
|
if can_emit_output(properties, usages) then
|
||||||
not properties.ID_INPUT_TOUCHPAD and
|
if not usages[OUTPUT_USAGE] then
|
||||||
usages[evdev.REL_X] and usages[evdev.REL_Y] then
|
device:enable_evdev_usage(OUTPUT_USAGE)
|
||||||
if not usages[evdev.BTN_MIDDLE] then
|
|
||||||
device:enable_evdev_usage(evdev.BTN_MIDDLE)
|
|
||||||
end
|
end
|
||||||
button_devices[device] = true
|
output_devices[device] = true
|
||||||
if not button_device then
|
if not output_device then
|
||||||
button_device = device
|
output_device = device
|
||||||
end
|
end
|
||||||
device:connect("device-removed", device_removed)
|
device:connect("device-removed", device_removed)
|
||||||
end
|
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
|
end
|
||||||
|
|
||||||
evdev = {
|
evdev = {
|
||||||
|
BTN_LEFT = usage(1, 272),
|
||||||
|
BTN_RIGHT = usage(1, 273),
|
||||||
BTN_MIDDLE = usage(1, 274),
|
BTN_MIDDLE = usage(1, 274),
|
||||||
BTN_TOUCH = usage(1, 330),
|
BTN_TOUCH = usage(1, 330),
|
||||||
BTN_TOOL_FINGER = usage(1, 325),
|
BTN_TOOL_FINGER = usage(1, 325),
|
||||||
@@ -20,8 +22,19 @@ evdev = {
|
|||||||
ABS_MT_POSITION_Y = usage(3, 54),
|
ABS_MT_POSITION_Y = usage(3, 54),
|
||||||
ABS_MT_TOOL_Y = usage(3, 61),
|
ABS_MT_TOOL_Y = usage(3, 61),
|
||||||
ABS_MT_TRACKING_ID = usage(3, 57),
|
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 = {}
|
local plugin_callbacks = {}
|
||||||
libinput = {
|
libinput = {
|
||||||
register = function(_, versions)
|
register = function(_, versions)
|
||||||
@@ -66,6 +79,8 @@ local function new_device(device_type)
|
|||||||
return { ID_INPUT_TOUCHPAD = true }
|
return { ID_INPUT_TOUCHPAD = true }
|
||||||
elseif device_type == "mouse" then
|
elseif device_type == "mouse" then
|
||||||
return { ID_INPUT_MOUSE = true }
|
return { ID_INPUT_MOUSE = true }
|
||||||
|
elseif device_type == "keyboard" then
|
||||||
|
return { ID_INPUT_KEYBOARD = true }
|
||||||
end
|
end
|
||||||
return {}
|
return {}
|
||||||
end
|
end
|
||||||
@@ -159,12 +174,13 @@ local function up(device, timestamp, slot, count_after)
|
|||||||
return feed(device, timestamp, frame)
|
return feed(device, timestamp, frame)
|
||||||
end
|
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
|
local count = 0
|
||||||
for _, frame in ipairs(button_device.appended) do
|
for _, frame in ipairs(output_device.appended) do
|
||||||
if contains(frame, evdev.BTN_MIDDLE, 1) then
|
if contains(frame, expected_output, 1) then
|
||||||
count = count + 1
|
count = count + 1
|
||||||
end
|
end
|
||||||
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))
|
assert(not contains(second_down, evdev.BTN_TOOL_DOUBLETAP, 1))
|
||||||
local second_up = up(device, 260000, 1, 1)
|
local second_up = up(device, 260000, 1, 1)
|
||||||
assert(not contains(second_up, evdev.ABS_MT_TRACKING_ID, -1))
|
assert(not contains(second_up, evdev.ABS_MT_TRACKING_ID, -1))
|
||||||
assert(click_count() == 1)
|
assert(action_count() == 1)
|
||||||
assert(#device.appended == 0)
|
assert(#device.appended == 0)
|
||||||
up(device, 440000, 0, 0)
|
up(device, 440000, 0, 0)
|
||||||
end
|
end
|
||||||
@@ -193,7 +209,7 @@ function tests.hold_doubletap_emits_two_middle_clicks()
|
|||||||
up(device, 250000, 1, 1)
|
up(device, 250000, 1, 1)
|
||||||
down(device, 360000, 1, 22, 1400, 1000, 2)
|
down(device, 360000, 1, 22, 1400, 1000, 2)
|
||||||
up(device, 420000, 1, 1)
|
up(device, 420000, 1, 1)
|
||||||
assert(click_count() == 2)
|
assert(action_count() == 2)
|
||||||
up(device, 600000, 0, 0)
|
up(device, 600000, 0, 0)
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -205,7 +221,7 @@ function tests.ordinary_two_finger_tap_passes_through()
|
|||||||
assert(contains(second, evdev.BTN_TOOL_DOUBLETAP, 1))
|
assert(contains(second, evdev.BTN_TOOL_DOUBLETAP, 1))
|
||||||
up(device, 70000, 1, 1)
|
up(device, 70000, 1, 1)
|
||||||
up(device, 80000, 0, 0)
|
up(device, 80000, 0, 0)
|
||||||
assert(click_count() == 0)
|
assert(action_count() == 0)
|
||||||
end
|
end
|
||||||
|
|
||||||
function tests.long_stationary_tap_is_consumed_without_click()
|
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)
|
down(device, 200000, 1, 41, 1400, 1000, 2)
|
||||||
local second_up = up(device, 360001, 1, 1)
|
local second_up = up(device, 360001, 1, 1)
|
||||||
assert(not contains(second_up, evdev.ABS_MT_TRACKING_ID, -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)
|
up(device, 500000, 0, 0)
|
||||||
end
|
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()
|
function tests.tapper_movement_restores_normal_input()
|
||||||
local device = new_device("touchpad")
|
local device = new_device("touchpad")
|
||||||
down(device, 0, 0, 50, 1000, 1000, 1)
|
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))
|
assert(not contains(moved, evdev.ABS_MT_POSITION_X, 1430))
|
||||||
up(device, 260000, 1, 1)
|
up(device, 260000, 1, 1)
|
||||||
up(device, 400000, 0, 0)
|
up(device, 400000, 0, 0)
|
||||||
assert(click_count() == 0)
|
assert(action_count() == 0)
|
||||||
end
|
end
|
||||||
|
|
||||||
function tests.anchor_movement_prevents_recognition()
|
function tests.anchor_movement_prevents_recognition()
|
||||||
@@ -239,10 +264,11 @@ function tests.anchor_movement_prevents_recognition()
|
|||||||
down(device, 0, 0, 60, 1000, 1000, 1)
|
down(device, 0, 0, 60, 1000, 1000, 1)
|
||||||
feed(device, 150000, { event(evdev.ABS_MT_POSITION_X, 1040) })
|
feed(device, 150000, { event(evdev.ABS_MT_POSITION_X, 1040) })
|
||||||
local second = down(device, 200000, 1, 61, 1400, 1000, 2)
|
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, 250000, 1, 1)
|
||||||
up(device, 400000, 0, 0)
|
up(device, 400000, 0, 0)
|
||||||
assert(click_count() == 0)
|
assert(action_count() == (allow_moving_anchor and 1 or 0))
|
||||||
end
|
end
|
||||||
|
|
||||||
function tests.anchor_movement_during_tap_restores_normal_input()
|
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_SLOT, 0),
|
||||||
event(evdev.ABS_MT_POSITION_X, 1040),
|
event(evdev.ABS_MT_POSITION_X, 1040),
|
||||||
})
|
})
|
||||||
assert(#device.prepended == 1)
|
assert(#device.prepended == (allow_moving_anchor and 0 or 1))
|
||||||
assert(contains(device.prepended[1], evdev.ABS_MT_TRACKING_ID, 63))
|
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))
|
assert(contains(moved, evdev.ABS_MT_POSITION_X, 1040))
|
||||||
up(device, 260000, 1, 1)
|
up(device, 260000, 1, 1)
|
||||||
up(device, 400000, 0, 0)
|
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
|
end
|
||||||
|
|
||||||
function tests.anchor_can_move_pause_and_tap_repeatedly()
|
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)
|
down(device, 650000, 1, 66, 1500, 1100, 2)
|
||||||
up(device, 700000, 1, 1)
|
up(device, 700000, 1, 1)
|
||||||
assert(click_count() == 1)
|
assert(action_count() == 1)
|
||||||
|
|
||||||
feed(device, 800000, {
|
feed(device, 800000, {
|
||||||
event(evdev.ABS_MT_SLOT, 0),
|
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)
|
down(device, 950000, 1, 67, 2000, 1200, 2)
|
||||||
up(device, 1000000, 1, 1)
|
up(device, 1000000, 1, 1)
|
||||||
assert(click_count() == 2)
|
assert(action_count() == 2)
|
||||||
|
|
||||||
up(device, 1100000, 0, 0)
|
up(device, 1100000, 0, 0)
|
||||||
end
|
end
|
||||||
@@ -297,7 +365,7 @@ function tests.third_contact_restores_candidate()
|
|||||||
up(device, 260000, 2, 2)
|
up(device, 260000, 2, 2)
|
||||||
up(device, 280000, 1, 1)
|
up(device, 280000, 1, 1)
|
||||||
up(device, 400000, 0, 0)
|
up(device, 400000, 0, 0)
|
||||||
assert(click_count() == 0)
|
assert(action_count() == 0)
|
||||||
end
|
end
|
||||||
|
|
||||||
function tests.two_finger_hold_tap_remains_passthrough()
|
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, 360000, 2, 2)
|
||||||
up(device, 500000, 1, 1)
|
up(device, 500000, 1, 1)
|
||||||
up(device, 510000, 0, 0)
|
up(device, 510000, 0, 0)
|
||||||
assert(click_count() == 0)
|
assert(action_count() == 0)
|
||||||
assert(#device.prepended == 0)
|
assert(#device.prepended == 0)
|
||||||
end
|
end
|
||||||
|
|
||||||
@@ -320,8 +388,8 @@ function tests.non_touchpad_is_ignored()
|
|||||||
end
|
end
|
||||||
|
|
||||||
function tests.pointer_device_is_used_for_button_output()
|
function tests.pointer_device_is_used_for_button_output()
|
||||||
assert(button_device.enabled[evdev.BTN_MIDDLE])
|
assert(output_device.enabled[expected_output])
|
||||||
assert(button_device.callbacks["evdev-frame"] == nil)
|
assert(output_device.callbacks["evdev-frame"] == nil)
|
||||||
end
|
end
|
||||||
|
|
||||||
local names = {}
|
local names = {}
|
||||||
@@ -330,7 +398,7 @@ for name in pairs(tests) do
|
|||||||
end
|
end
|
||||||
table.sort(names)
|
table.sort(names)
|
||||||
for _, name in ipairs(names) do
|
for _, name in ipairs(names) do
|
||||||
button_device.appended = {}
|
output_device.appended = {}
|
||||||
tests[name]()
|
tests[name]()
|
||||||
print("ok - " .. name)
|
print("ok - " .. name)
|
||||||
end
|
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
|
Name: touchpad-hold-tap
|
||||||
Version: 0.1.0
|
Version: 0.1.0
|
||||||
Release: 6%{?dist}
|
Release: 9%{?dist}
|
||||||
Summary: Hold-tap middle-click gesture for touchpads
|
Summary: Hold-tap middle-click gesture for touchpads
|
||||||
|
|
||||||
License: MIT
|
License: MIT
|
||||||
@@ -9,6 +9,8 @@ Source0: 90-touchpad-hold-tap.lua
|
|||||||
Source1: LICENSE
|
Source1: LICENSE
|
||||||
Source2: test-touchpad-hold-tap.lua
|
Source2: test-touchpad-hold-tap.lua
|
||||||
Source3: README.md
|
Source3: README.md
|
||||||
|
Source4: touchpad-hold-tap-config
|
||||||
|
Source5: test-touchpad-hold-tap-config
|
||||||
|
|
||||||
BuildRequires: lua
|
BuildRequires: lua
|
||||||
Requires: libinput >= 1.30
|
Requires: libinput >= 1.30
|
||||||
@@ -25,10 +27,15 @@ gestures. Other touchpad input continues through libinput normally.
|
|||||||
|
|
||||||
%check
|
%check
|
||||||
lua %{SOURCE2} %{SOURCE0}
|
lua %{SOURCE2} %{SOURCE0}
|
||||||
|
lua %{SOURCE2} %{SOURCE0} KEY_F13
|
||||||
|
lua %{SOURCE2} %{SOURCE0} BTN_MIDDLE allow-moving-anchor
|
||||||
|
bash %{SOURCE5} %{SOURCE0} %{SOURCE4}
|
||||||
|
|
||||||
%install
|
%install
|
||||||
install -Dpm 0644 %{SOURCE0} \
|
install -Dpm 0644 %{SOURCE0} \
|
||||||
%{buildroot}%{_libdir}/libinput/plugins/90-touchpad-hold-tap.lua
|
%{buildroot}%{_libdir}/libinput/plugins/90-touchpad-hold-tap.lua
|
||||||
|
install -Dpm 0755 %{SOURCE4} \
|
||||||
|
%{buildroot}%{_bindir}/touchpad-hold-tap-config
|
||||||
install -Dpm 0644 %{SOURCE1} \
|
install -Dpm 0644 %{SOURCE1} \
|
||||||
%{buildroot}%{_licensedir}/%{name}/LICENSE
|
%{buildroot}%{_licensedir}/%{name}/LICENSE
|
||||||
install -Dpm 0644 %{SOURCE3} \
|
install -Dpm 0644 %{SOURCE3} \
|
||||||
@@ -37,9 +44,29 @@ install -Dpm 0644 %{SOURCE3} \
|
|||||||
%files
|
%files
|
||||||
%license %{_licensedir}/%{name}/LICENSE
|
%license %{_licensedir}/%{name}/LICENSE
|
||||||
%doc %{_docdir}/%{name}/README.md
|
%doc %{_docdir}/%{name}/README.md
|
||||||
|
%{_bindir}/touchpad-hold-tap-config
|
||||||
%{_libdir}/libinput/plugins/90-touchpad-hold-tap.lua
|
%{_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
|
%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
|
* Sat Sep 05 2026 fedora-tools contributors - 0.1.0-6
|
||||||
- Advertise the tool to Fedora Tools settings
|
- Advertise the tool to Fedora Tools settings
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user