fix: notice input on the lock screen despite idle inhibitors
This commit is contained in:
1 parent
7c87adb733
commit
77ae883b98
12 files changed
+280
-38
No files matched your search
@@ -17,7 +17,7 @@ jobs:
|
||||
- name: Install the build tools
|
||||
run: |
|
||||
pacman -Syu --noconfirm --needed git base-devel cmake pkgconf \
|
||||
qt6-base qt6-declarative layer-shell-qt kidletime ki18n \
|
||||
qt6-base qt6-declarative layer-shell-qt ki18n \
|
||||
opencv pam systemd-libs gettext scdoc shellcheck desktop-file-utils
|
||||
|
||||
- uses: actions/checkout@v7
|
||||
|
||||
@@ -61,7 +61,7 @@ jobs:
|
||||
git make cmake gcc-c++ gettext scdoc tar curl rpm-build rpm-sign systemd-rpm-macros \
|
||||
'pkgconfig(systemd)' 'pkgconfig(libsystemd)' pam-devel opencv-devel \
|
||||
qt6-qtbase-devel qt6-qtbase-private-devel qt6-qtdeclarative-devel qt6-qtwayland-devel \
|
||||
layer-shell-qt-devel kf6-kidletime-devel kf6-ki18n-devel
|
||||
layer-shell-qt-devel kf6-ki18n-devel
|
||||
|
||||
- uses: actions/checkout@v7
|
||||
|
||||
|
||||
+4
-3
@@ -136,7 +136,6 @@ if(PFU_BUILD_AGENT)
|
||||
set(QT_NO_PRIVATE_MODULE_WARNING ON)
|
||||
find_package(Qt6 REQUIRED COMPONENTS GuiPrivate WaylandClientPrivate)
|
||||
find_package(LayerShellQt REQUIRED)
|
||||
find_package(KF6IdleTime REQUIRED)
|
||||
find_package(KF6I18n REQUIRED)
|
||||
|
||||
qt_add_executable(plasma-face-unlock-agent
|
||||
@@ -147,6 +146,7 @@ if(PFU_BUILD_AGENT)
|
||||
src/agent/bubblewindow.cpp
|
||||
src/agent/bubblecontroller.cpp
|
||||
src/agent/lockcontroller.cpp
|
||||
src/agent/inputwatcher.cpp
|
||||
src/agent/enrollcontroller.cpp
|
||||
)
|
||||
target_include_directories(plasma-face-unlock-agent PRIVATE src/agent)
|
||||
@@ -155,7 +155,8 @@ if(PFU_BUILD_AGENT)
|
||||
endif()
|
||||
|
||||
qt6_generate_wayland_protocol_client_sources(plasma-face-unlock-agent
|
||||
FILES ${CMAKE_CURRENT_SOURCE_DIR}/protocols/kde-lockscreen-overlay-v1.xml)
|
||||
FILES ${CMAKE_CURRENT_SOURCE_DIR}/protocols/kde-lockscreen-overlay-v1.xml
|
||||
${CMAKE_CURRENT_SOURCE_DIR}/protocols/ext-idle-notify-v1.xml)
|
||||
|
||||
# The QML files sit next to the module's qmldir in the resources, so they
|
||||
# see each other (and the Theme singleton) without importing anything.
|
||||
@@ -189,7 +190,7 @@ if(PFU_BUILD_AGENT)
|
||||
Qt6::Gui Qt6::GuiPrivate Qt6::Quick Qt6::DBus Qt6::Network
|
||||
Qt6::WaylandClient Qt6::WaylandClientPrivate
|
||||
LayerShellQt::Interface
|
||||
KF6::IdleTime KF6::I18n KF6::I18nQml
|
||||
KF6::I18n KF6::I18nQml
|
||||
pfu_common
|
||||
)
|
||||
install(TARGETS plasma-face-unlock-agent DESTINATION ${PFU_LIBEXECDIR})
|
||||
|
||||
@@ -207,7 +207,7 @@ sudo make install
|
||||
```
|
||||
|
||||
Needs CMake, a C++20 compiler, Qt 6 (Core, DBus, Network, Gui, Quick, WaylandClient), LayerShellQt,
|
||||
KIdleTime, KI18n, OpenCV 4.5.4 or newer with the DNN module, Linux-PAM and libsystemd. Optionally
|
||||
KI18n, OpenCV 4.5.4 or newer with the DNN module, Linux-PAM and libsystemd. Optionally
|
||||
`msgfmt` (gettext) for translations and `scdoc` for the man page. Supports `PREFIX` and `DESTDIR`.
|
||||
`make check` runs syntax checks and shellcheck.
|
||||
|
||||
|
||||
@@ -171,7 +171,9 @@ The agent watches for the screen to lock (org.freedesktop.ScreenSaver) and
|
||||
scans when somebody comes back:
|
||||
|
||||
- on any key or mouse movement after the screen locked, once 1.5 seconds have
|
||||
passed (the key that locked it does not count);
|
||||
passed (the key that locked it does not count). Enter on the empty password
|
||||
field is a key like any other. This works while a video or an app keeps the
|
||||
screen on, too (ext_idle_notifier_v1, input notification);
|
||||
- when the machine wakes from sleep;
|
||||
- right after locking, if *Look right after the screen locks* is on. Off by
|
||||
default: whoever locks their screen on purpose is usually still in front of
|
||||
@@ -256,7 +258,7 @@ _$XDG_RUNTIME_DIR/plasma-face-unlock/agent.socket_
|
||||
# REQUIREMENTS
|
||||
|
||||
KDE Plasma 6 on Wayland, a camera, OpenCV 4.5.4 or newer with its DNN module,
|
||||
Qt 6, LayerShellQt, KIdleTime, KI18n, systemd, polkit and Linux-PAM.
|
||||
Qt 6, LayerShellQt, KI18n, systemd, polkit and Linux-PAM.
|
||||
|
||||
# SEE ALSO
|
||||
|
||||
|
||||
@@ -11,7 +11,7 @@ arch=('x86_64' 'aarch64')
|
||||
url="https://github.com/LoonixTools/plasma-face-unlock"
|
||||
license=('GPL-3.0-or-later' 'MIT' 'Apache-2.0')
|
||||
depends=('bash' 'coreutils' 'gawk' 'grep' 'sed' 'gettext' 'systemd' 'systemd-libs' 'pam' 'polkit'
|
||||
'opencv' 'qt6-base' 'qt6-declarative' 'layer-shell-qt' 'kidletime' 'ki18n' 'kscreenlocker')
|
||||
'opencv' 'qt6-base' 'qt6-declarative' 'layer-shell-qt' 'ki18n' 'kscreenlocker')
|
||||
makedepends=('cmake' 'scdoc')
|
||||
install="${pkgname}.install"
|
||||
source=("${pkgname}-${pkgver}.tar.gz::${url}/archive/refs/tags/v${pkgver}.tar.gz"
|
||||
|
||||
@@ -34,7 +34,6 @@ BuildRequires: cmake(Qt6WaylandClient)
|
||||
BuildRequires: qt6-qtbase-private-devel
|
||||
BuildRequires: qt6-qtwayland-devel
|
||||
BuildRequires: cmake(LayerShellQt)
|
||||
BuildRequires: cmake(KF6IdleTime)
|
||||
BuildRequires: cmake(KF6I18n)
|
||||
|
||||
Requires: bash >= 4.2
|
||||
|
||||
@@ -0,0 +1,131 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<protocol name="ext_idle_notify_v1">
|
||||
<copyright>
|
||||
Copyright © 2015 Martin Gräßlin
|
||||
Copyright © 2022 Simon Ser
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a
|
||||
copy of this software and associated documentation files (the "Software"),
|
||||
to deal in the Software without restriction, including without limitation
|
||||
the rights to use, copy, modify, merge, publish, distribute, sublicense,
|
||||
and/or sell copies of the Software, and to permit persons to whom the
|
||||
Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice (including the next
|
||||
paragraph) shall be included in all copies or substantial portions of the
|
||||
Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
|
||||
THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
|
||||
DEALINGS IN THE SOFTWARE.
|
||||
</copyright>
|
||||
|
||||
<interface name="ext_idle_notifier_v1" version="2">
|
||||
<description summary="idle notification manager">
|
||||
This interface allows clients to monitor user idle status.
|
||||
|
||||
After binding to this global, clients can create ext_idle_notification_v1
|
||||
objects to get notified when the user is idle for a given amount of time.
|
||||
</description>
|
||||
|
||||
<request name="destroy" type="destructor">
|
||||
<description summary="destroy the manager">
|
||||
Destroy the manager object. All objects created via this interface
|
||||
remain valid.
|
||||
</description>
|
||||
</request>
|
||||
|
||||
<request name="get_idle_notification">
|
||||
<description summary="create a notification object">
|
||||
Create a new idle notification object.
|
||||
|
||||
The notification object has a minimum timeout duration and is tied to a
|
||||
seat. The client will be notified if the seat is inactive for at least
|
||||
the provided timeout. See ext_idle_notification_v1 for more details.
|
||||
|
||||
A zero timeout is valid and means the client wants to be notified as
|
||||
soon as possible when the seat is inactive.
|
||||
</description>
|
||||
<arg name="id" type="new_id" interface="ext_idle_notification_v1"/>
|
||||
<arg name="timeout" type="uint" summary="minimum idle timeout in msec"/>
|
||||
<arg name="seat" type="object" interface="wl_seat"/>
|
||||
</request>
|
||||
|
||||
<!-- Version 2 additions -->
|
||||
|
||||
<request name="get_input_idle_notification" since="2">
|
||||
<description summary="create a notification object">
|
||||
Create a new idle notification object to track input from the
|
||||
user, such as keyboard and mouse movement. Because this object is
|
||||
meant to track user input alone, it ignores idle inhibitors.
|
||||
|
||||
The notification object has a minimum timeout duration and is tied to a
|
||||
seat. The client will be notified if the seat is inactive for at least
|
||||
the provided timeout. See ext_idle_notification_v1 for more details.
|
||||
|
||||
A zero timeout is valid and means the client wants to be notified as
|
||||
soon as possible when the seat is inactive.
|
||||
</description>
|
||||
<arg name="id" type="new_id" interface="ext_idle_notification_v1"/>
|
||||
<arg name="timeout" type="uint" summary="minimum idle timeout in msec"/>
|
||||
<arg name="seat" type="object" interface="wl_seat"/>
|
||||
</request>
|
||||
|
||||
</interface>
|
||||
|
||||
<interface name="ext_idle_notification_v1" version="2">
|
||||
<description summary="idle notification">
|
||||
This interface is used by the compositor to send idle notification events
|
||||
to clients.
|
||||
|
||||
Initially the notification object is not idle. The notification object
|
||||
becomes idle when no user activity has happened for at least the timeout
|
||||
duration, starting from the creation of the notification object. User
|
||||
activity may include input events or a presence sensor, but is
|
||||
compositor-specific.
|
||||
|
||||
How this notification responds to idle inhibitors depends on how
|
||||
it was constructed. If constructed from the
|
||||
get_idle_notification request, then if an idle inhibitor is
|
||||
active (e.g. another client has created a zwp_idle_inhibitor_v1
|
||||
on a visible surface), the compositor must not make the
|
||||
notification object idle. However, if constructed from the
|
||||
get_input_idle_notification request, then idle inhibitors are
|
||||
ignored, and only input from the user, e.g. from a keyboard or
|
||||
mouse, counts as activity.
|
||||
|
||||
When the notification object becomes idle, an idled event is sent. When
|
||||
user activity starts again, the notification object stops being idle,
|
||||
a resumed event is sent and the timeout is restarted.
|
||||
</description>
|
||||
|
||||
<request name="destroy" type="destructor">
|
||||
<description summary="destroy the notification object">
|
||||
Destroy the notification object.
|
||||
</description>
|
||||
</request>
|
||||
|
||||
<event name="idled">
|
||||
<description summary="notification object is idle">
|
||||
This event is sent when the notification object becomes idle.
|
||||
|
||||
It's a compositor protocol error to send this event twice without a
|
||||
resumed event in-between.
|
||||
</description>
|
||||
</event>
|
||||
|
||||
<event name="resumed">
|
||||
<description summary="notification object is no longer idle">
|
||||
This event is sent when the notification object stops being idle.
|
||||
|
||||
It's a compositor protocol error to send this event twice without an
|
||||
idled event in-between. It's a compositor protocol error to send this
|
||||
event prior to any idled event.
|
||||
</description>
|
||||
</event>
|
||||
</interface>
|
||||
</protocol>
|
||||
@@ -0,0 +1,87 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
|
||||
#include "inputwatcher.h"
|
||||
|
||||
#include <QGuiApplication>
|
||||
#include <QWaylandClientExtensionTemplate>
|
||||
#include <QtGui/qguiapplication_platform.h>
|
||||
|
||||
#include <functional>
|
||||
|
||||
#include "qwayland-ext-idle-notify-v1.h"
|
||||
|
||||
class IdleNotifier : public QWaylandClientExtensionTemplate<IdleNotifier>, public QtWayland::ext_idle_notifier_v1
|
||||
{
|
||||
public:
|
||||
IdleNotifier()
|
||||
: QWaylandClientExtensionTemplate<IdleNotifier>(2)
|
||||
{
|
||||
initialize();
|
||||
}
|
||||
~IdleNotifier() override
|
||||
{
|
||||
if (isActive()) {
|
||||
destroy();
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
class IdleNotification : public QtWayland::ext_idle_notification_v1
|
||||
{
|
||||
public:
|
||||
IdleNotification(::ext_idle_notification_v1 *object, std::function<void()> resumed)
|
||||
: QtWayland::ext_idle_notification_v1(object)
|
||||
, m_resumed(std::move(resumed))
|
||||
{
|
||||
}
|
||||
~IdleNotification() override
|
||||
{
|
||||
destroy();
|
||||
}
|
||||
|
||||
protected:
|
||||
// Only ever sent after idled: calm first, then this.
|
||||
void ext_idle_notification_v1_resumed() override
|
||||
{
|
||||
if (!m_fired) {
|
||||
m_fired = true;
|
||||
m_resumed();
|
||||
}
|
||||
}
|
||||
|
||||
private:
|
||||
std::function<void()> m_resumed;
|
||||
bool m_fired = false;
|
||||
};
|
||||
|
||||
InputWatcher::InputWatcher(QObject *parent)
|
||||
: QObject(parent)
|
||||
, m_notifier(std::make_unique<IdleNotifier>())
|
||||
{
|
||||
}
|
||||
|
||||
InputWatcher::~InputWatcher() = default;
|
||||
|
||||
void InputWatcher::watch(int calmMs)
|
||||
{
|
||||
m_notification.reset();
|
||||
auto *wayland = qGuiApp->nativeInterface<QNativeInterface::QWaylandApplication>();
|
||||
if (!m_notifier->isActive() || !wayland || !wayland->seat()) {
|
||||
qWarning("no ext_idle_notifier_v1, so no telling when somebody comes back");
|
||||
return;
|
||||
}
|
||||
// Version 1 has only the notification that inhibitors hold back.
|
||||
::ext_idle_notification_v1 *object = m_notifier->QWaylandClientExtension::version() >= 2
|
||||
? m_notifier->get_input_idle_notification(uint32_t(calmMs), wayland->seat())
|
||||
: m_notifier->get_idle_notification(uint32_t(calmMs), wayland->seat());
|
||||
// Queued: not from inside the Wayland event, which the receiver might
|
||||
// answer by watching anew and so destroying the notification.
|
||||
m_notification = std::make_unique<IdleNotification>(object, [this] {
|
||||
QMetaObject::invokeMethod(this, &InputWatcher::input, Qt::QueuedConnection);
|
||||
});
|
||||
}
|
||||
|
||||
void InputWatcher::stop()
|
||||
{
|
||||
m_notification.reset();
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
//
|
||||
// Somebody touching the machine: a key, a click, the mouse, the touchpad.
|
||||
//
|
||||
// From ext_idle_notifier_v1, like KIdleTime, but with the input notification
|
||||
// of version 2. KIdleTime's own honours idle inhibitors: with a video playing
|
||||
// or an app keeping the screen on, no key reached it and the lock screen never
|
||||
// scanned.
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <QObject>
|
||||
|
||||
#include <memory>
|
||||
|
||||
class IdleNotifier;
|
||||
class IdleNotification;
|
||||
|
||||
class InputWatcher : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
public:
|
||||
explicit InputWatcher(QObject *parent = nullptr);
|
||||
~InputWatcher() override;
|
||||
|
||||
// Emit input() once, for the first input after nothing was touched for
|
||||
// calmMs. 0: the next input. Replaces what was watched before.
|
||||
void watch(int calmMs);
|
||||
void stop();
|
||||
|
||||
Q_SIGNALS:
|
||||
void input();
|
||||
|
||||
private:
|
||||
std::unique_ptr<IdleNotifier> m_notifier;
|
||||
std::unique_ptr<IdleNotification> m_notification;
|
||||
};
|
||||
@@ -6,8 +6,6 @@
|
||||
#include "daemonclient.h"
|
||||
#include "userconfig.h"
|
||||
|
||||
#include <KIdleTime>
|
||||
|
||||
#include <QDBusConnection>
|
||||
#include <QDBusMessage>
|
||||
#include <QDBusPendingCallWatcher>
|
||||
@@ -38,7 +36,10 @@ LockController::LockController(BubbleController *bubble, UserConfig *config, QOb
|
||||
, m_config(config)
|
||||
{
|
||||
m_armTimer.setSingleShot(true);
|
||||
connect(&m_armTimer, &QTimer::timeout, this, &LockController::arm);
|
||||
connect(&m_armTimer, &QTimer::timeout, this, [this] {
|
||||
arm(0);
|
||||
});
|
||||
connect(&m_input, &InputWatcher::input, this, &LockController::onResume);
|
||||
|
||||
QDBusConnection session = QDBusConnection::sessionBus();
|
||||
session.connect(QStringLiteral("org.freedesktop.ScreenSaver"),
|
||||
@@ -55,17 +56,6 @@ LockController::LockController(BubbleController *bubble, UserConfig *config, QOb
|
||||
this,
|
||||
SLOT(onPrepareForSleep(bool)));
|
||||
|
||||
KIdleTime *idle = KIdleTime::instance();
|
||||
connect(idle, &KIdleTime::resumingFromIdle, this, &LockController::onResume);
|
||||
connect(idle, qOverload<int, int>(&KIdleTime::timeoutReached), this, [this, idle](int id, int) {
|
||||
if (id != m_idleId) {
|
||||
return;
|
||||
}
|
||||
idle->removeIdleTimeout(id);
|
||||
m_idleId = -1;
|
||||
arm();
|
||||
});
|
||||
|
||||
// Started while the screen is already locked (the agent restarted).
|
||||
const QDBusMessage get = QDBusMessage::createMethodCall(QStringLiteral("org.freedesktop.ScreenSaver"),
|
||||
QStringLiteral("/ScreenSaver"),
|
||||
@@ -86,16 +76,10 @@ void LockController::onActiveChanged(bool active)
|
||||
if (active == m_locked) {
|
||||
return;
|
||||
}
|
||||
KIdleTime *idle = KIdleTime::instance();
|
||||
|
||||
if (!active) {
|
||||
m_locked = false;
|
||||
m_armTimer.stop();
|
||||
if (m_idleId >= 0) {
|
||||
idle->removeIdleTimeout(m_idleId);
|
||||
m_idleId = -1;
|
||||
}
|
||||
idle->stopCatchingResumeEvent();
|
||||
m_input.stop();
|
||||
if (m_scan) {
|
||||
m_scan->abort();
|
||||
m_scan->deleteLater();
|
||||
@@ -148,12 +132,12 @@ void LockController::onPrepareForSleep(bool sleeping)
|
||||
}
|
||||
}
|
||||
|
||||
void LockController::arm()
|
||||
void LockController::arm(int calmMs)
|
||||
{
|
||||
if (!m_locked || m_stopped || !m_config->scanOnWake()) {
|
||||
return;
|
||||
}
|
||||
KIdleTime::instance()->catchNextResumeEvent();
|
||||
m_input.watch(calmMs);
|
||||
}
|
||||
|
||||
void LockController::onResume()
|
||||
@@ -221,9 +205,7 @@ void LockController::onScanFinished(const QJsonObject &result)
|
||||
m_armTimer.start(GraceAfterLockMs);
|
||||
return;
|
||||
}
|
||||
if (m_idleId < 0) {
|
||||
m_idleId = KIdleTime::instance()->addIdleTimeout(CalmBeforeRetryMs);
|
||||
}
|
||||
arm(CalmBeforeRetryMs);
|
||||
}
|
||||
|
||||
void LockController::unlock()
|
||||
|
||||
@@ -19,6 +19,8 @@
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "inputwatcher.h"
|
||||
|
||||
#include <QElapsedTimer>
|
||||
#include <QObject>
|
||||
#include <QPointer>
|
||||
@@ -44,7 +46,8 @@ private Q_SLOTS:
|
||||
void onPrepareForSleep(bool sleeping);
|
||||
|
||||
private:
|
||||
void arm();
|
||||
// Scan at the next input after calmMs without any.
|
||||
void arm(int calmMs);
|
||||
void onResume();
|
||||
void startScan(const QString &why);
|
||||
void onScanFinished(const QJsonObject &result);
|
||||
@@ -58,5 +61,5 @@ private:
|
||||
QElapsedTimer m_lockedFor;
|
||||
QPointer<DaemonRequest> m_scan;
|
||||
QTimer m_armTimer;
|
||||
int m_idleId = -1;
|
||||
InputWatcher m_input;
|
||||
};
|
||||
Reference in new issue
Block a user