2 changed files with 254 additions and 0 deletions
@ -0,0 +1,213 @@
|
||||
#include "user_activity_monitor.h" |
||||
#include "debug_ui.h" |
||||
|
||||
#include <QApplication> |
||||
#include <QEvent> |
||||
#include <QGuiApplication> |
||||
#include <cmath> |
||||
#include <limits> |
||||
|
||||
#ifdef UTUN_ACTIVITY_DBUS |
||||
#include <QDBusConnection> |
||||
#include <QDBusMessage> |
||||
#include <QDBusPendingCallWatcher> |
||||
#include <QDBusPendingReply> |
||||
#endif |
||||
#ifdef UTUN_ACTIVITY_KIDLETIME |
||||
#include <KIdleTime> |
||||
#endif |
||||
#ifdef Q_OS_WIN |
||||
#include <qt_windows.h> |
||||
#elif defined(Q_OS_MACOS) |
||||
#include <ApplicationServices/ApplicationServices.h> |
||||
#elif defined(UTUN_ACTIVITY_X11) |
||||
#include <X11/Xlib.h> |
||||
#include <X11/extensions/scrnsaver.h> |
||||
#undef KeyPress |
||||
#undef KeyRelease |
||||
#endif |
||||
|
||||
UserActivityMonitor::UserActivityMonitor(QObject* parent) : QObject(parent) { |
||||
m_pollTimer.setInterval(250); |
||||
connect(&m_pollTimer, &QTimer::timeout, this, &UserActivityMonitor::poll); |
||||
} |
||||
|
||||
UserActivityMonitor::~UserActivityMonitor() { |
||||
stop(); |
||||
#ifdef UTUN_ACTIVITY_X11 |
||||
if (m_display) XCloseDisplay(static_cast<Display*>(m_display)); |
||||
#endif |
||||
GUI_DEBUG("activity: monitor destroyed"); |
||||
} |
||||
|
||||
void UserActivityMonitor::start() { |
||||
stop(); |
||||
m_watching = true; |
||||
m_elapsed.start(); |
||||
qApp->installEventFilter(this); |
||||
const QString platform = QGuiApplication::platformName(); |
||||
const bool wayland = platform.startsWith("wayland") || |
||||
(platform == "xcb" && qEnvironmentVariable("XDG_SESSION_TYPE") == "wayland"); |
||||
#ifdef UTUN_ACTIVITY_DBUS |
||||
if (wayland && !m_gnomeUnavailable) { |
||||
m_useGnome = true; |
||||
m_pollTimer.start(); |
||||
poll(); |
||||
return; |
||||
} |
||||
#endif |
||||
if (wayland) { startWaylandFallback(); return; } |
||||
if (systemIdleTime() >= 0) { |
||||
GUI_DEBUG("activity: watching system input platform=%s poll_ms=%d", qPrintable(platform), m_pollTimer.interval()); |
||||
m_pollTimer.start(); |
||||
} else useLocalActivity(); |
||||
} |
||||
|
||||
void UserActivityMonitor::stop() { |
||||
++m_generation; |
||||
m_pollTimer.stop(); |
||||
if (m_watching && qApp) qApp->removeEventFilter(this); |
||||
#ifdef UTUN_ACTIVITY_KIDLETIME |
||||
if (m_useKIdleTime) KIdleTime::instance()->stopCatchingResumeEvent(); |
||||
#endif |
||||
m_watching = false; |
||||
m_dbusPending = false; |
||||
m_useGnome = false; |
||||
m_useKIdleTime = false; |
||||
} |
||||
|
||||
bool UserActivityMonitor::eventFilter(QObject*, QEvent* event) { |
||||
if (!m_watching || !event->spontaneous()) return false; |
||||
switch (event->type()) { |
||||
case QEvent::MouseMove: |
||||
case QEvent::MouseButtonPress: |
||||
case QEvent::Wheel: |
||||
case QEvent::KeyPress: |
||||
case QEvent::TouchBegin: |
||||
case QEvent::TouchUpdate: |
||||
case QEvent::TabletMove: |
||||
case QEvent::TabletPress: |
||||
GUI_DEBUG("activity: application input event=%d elapsed_ms=%lld", int(event->type()), (long long)m_elapsed.elapsed()); |
||||
stop(); |
||||
emit activity(); |
||||
break; |
||||
default: |
||||
break; |
||||
} |
||||
return false; |
||||
} |
||||
|
||||
void UserActivityMonitor::checkIdleTime(qint64 idleMs, qint64 elapsedMs) { |
||||
if (!m_watching || idleMs < 0 || idleMs >= elapsedMs) return; |
||||
GUI_DEBUG("activity: system input idle_ms=%lld elapsed_ms=%lld", (long long)idleMs, (long long)elapsedMs); |
||||
stop(); |
||||
emit activity(); |
||||
} |
||||
|
||||
void UserActivityMonitor::poll() { |
||||
#ifdef UTUN_ACTIVITY_DBUS |
||||
if (m_useGnome) { |
||||
if (m_dbusPending) return; |
||||
auto message = QDBusMessage::createMethodCall("org.gnome.Mutter.IdleMonitor", "/org/gnome/Mutter/IdleMonitor/Core", |
||||
"org.gnome.Mutter.IdleMonitor", "GetIdletime"); |
||||
message.setAutoStartService(false); |
||||
auto* watcher = new QDBusPendingCallWatcher(QDBusConnection::sessionBus().asyncCall(message, 1000), this); |
||||
const quint64 generation = m_generation; |
||||
const qint64 elapsedMs = m_elapsed.elapsed(); |
||||
m_dbusPending = true; |
||||
connect(watcher, &QDBusPendingCallWatcher::finished, this, [this, watcher, generation, elapsedMs] { |
||||
QDBusPendingReply<qulonglong> reply = *watcher; |
||||
watcher->deleteLater(); |
||||
if (!m_watching || generation != m_generation) return; |
||||
m_dbusPending = false; |
||||
if (reply.isError()) { |
||||
GUI_WARN("activity: GNOME idle monitor unavailable: %s", qPrintable(reply.error().message())); |
||||
m_gnomeUnavailable = true; |
||||
m_useGnome = false; |
||||
m_pollTimer.stop(); |
||||
startWaylandFallback(); |
||||
return; |
||||
} |
||||
if (elapsedMs == 0) GUI_DEBUG("activity: GNOME system idle monitor ready"); |
||||
if (reply.value() <= qulonglong(std::numeric_limits<qint64>::max())) checkIdleTime(qint64(reply.value()), elapsedMs); |
||||
else GUI_WARN("activity: GNOME idle time out of range: %llu", (unsigned long long)reply.value()); |
||||
}); |
||||
return; |
||||
} |
||||
#endif |
||||
const qint64 elapsedMs = m_elapsed.elapsed(); |
||||
const qint64 idleMs = systemIdleTime(); |
||||
if (idleMs < 0) { m_pollTimer.stop(); useLocalActivity(); return; } |
||||
checkIdleTime(idleMs, elapsedMs); |
||||
} |
||||
|
||||
void UserActivityMonitor::startWaylandFallback() { |
||||
#ifdef UTUN_ACTIVITY_KIDLETIME |
||||
if (QGuiApplication::platformName().startsWith("wayland")) { |
||||
auto* idle = KIdleTime::instance(); |
||||
const int timeout = idle->addIdleTimeout(0); |
||||
if (timeout) { |
||||
idle->removeIdleTimeout(timeout); |
||||
connect(idle, &KIdleTime::resumingFromIdle, this, [this] { |
||||
if (!m_watching || !m_useKIdleTime) return; |
||||
GUI_DEBUG("activity: Wayland compositor reported user activity"); |
||||
stop(); |
||||
emit activity(); |
||||
}); |
||||
m_useKIdleTime = true; |
||||
idle->catchNextResumeEvent(); |
||||
GUI_DEBUG("activity: watching Wayland resume through KIdleTime (subject to compositor idle inhibitors)"); |
||||
return; |
||||
} |
||||
GUI_WARN("activity: KIdleTime has no available compositor backend"); |
||||
} |
||||
#endif |
||||
useLocalActivity(); |
||||
} |
||||
|
||||
void UserActivityMonitor::useLocalActivity() { |
||||
if (!m_localWarningLogged) { |
||||
GUI_WARN("activity: global input unavailable platform=%s session=%s; using ChatGUI input and manual dismissal", |
||||
qPrintable(QGuiApplication::platformName()), qPrintable(qEnvironmentVariable("XDG_SESSION_TYPE"))); |
||||
m_localWarningLogged = true; |
||||
} |
||||
} |
||||
|
||||
qint64 UserActivityMonitor::systemIdleTime() { |
||||
#ifdef Q_OS_WIN |
||||
LASTINPUTINFO info = {sizeof(info), 0}; |
||||
if (!GetLastInputInfo(&info)) { GUI_WARN("activity: GetLastInputInfo failed error=%lu", GetLastError()); return -1; } |
||||
return DWORD(GetTickCount() - info.dwTime); |
||||
#elif defined(Q_OS_MACOS) |
||||
const double seconds = CGEventSourceSecondsSinceLastEventType(kCGEventSourceStateCombinedSessionState, kCGAnyInputEventType); |
||||
if (!std::isfinite(seconds) || seconds < 0 || seconds > double(std::numeric_limits<qint64>::max() / 1000)) { |
||||
GUI_WARN("activity: invalid macOS idle time seconds=%g", seconds); |
||||
return -1; |
||||
} |
||||
return qint64(seconds * 1000); |
||||
#elif defined(UTUN_ACTIVITY_X11) |
||||
if (QGuiApplication::platformName() != "xcb") return -1; |
||||
if (!m_x11Checked) { |
||||
m_x11Checked = true; |
||||
auto* display = XOpenDisplay(nullptr); |
||||
int eventBase = 0, errorBase = 0; |
||||
if (!display) { GUI_WARN("activity: cannot open X11 display"); return -1; } |
||||
if (!XScreenSaverQueryExtension(display, &eventBase, &errorBase)) { |
||||
GUI_WARN("activity: X11 screen saver extension unavailable"); |
||||
XCloseDisplay(display); |
||||
return -1; |
||||
} |
||||
m_display = display; |
||||
} |
||||
if (!m_display) return -1; |
||||
auto* display = static_cast<Display*>(m_display); |
||||
XScreenSaverInfo info{}; |
||||
if (!XScreenSaverQueryInfo(display, DefaultRootWindow(display), &info)) { |
||||
GUI_WARN("activity: XScreenSaverQueryInfo failed"); |
||||
return -1; |
||||
} |
||||
return qint64(info.idle); |
||||
#else |
||||
return -1; |
||||
#endif |
||||
} |
||||
@ -0,0 +1,41 @@
|
||||
/* Первая активность после start(): системный idle time или события приложения при ограничениях ОС.
|
||||
* stop() отменяет наблюдение и запоздалые D-Bus ответы. Все вызовы и сигнал — в GUI-потоке. */ |
||||
#pragma once |
||||
|
||||
#include <QObject> |
||||
#include <QElapsedTimer> |
||||
#include <QTimer> |
||||
|
||||
class UserActivityMonitor : public QObject { |
||||
Q_OBJECT |
||||
public: |
||||
explicit UserActivityMonitor(QObject* parent = nullptr); |
||||
~UserActivityMonitor() override; |
||||
void start(); |
||||
void stop(); |
||||
|
||||
signals: |
||||
void activity(); |
||||
|
||||
protected: |
||||
bool eventFilter(QObject* object, QEvent* event) override; |
||||
virtual qint64 systemIdleTime(); |
||||
|
||||
private: |
||||
void poll(); |
||||
void checkIdleTime(qint64 idleMs, qint64 elapsedMs); |
||||
void useLocalActivity(); |
||||
void startWaylandFallback(); |
||||
|
||||
QTimer m_pollTimer; |
||||
QElapsedTimer m_elapsed; |
||||
bool m_watching = false; |
||||
bool m_localWarningLogged = false; |
||||
bool m_dbusPending = false; |
||||
bool m_useGnome = false; |
||||
bool m_gnomeUnavailable = false; |
||||
bool m_useKIdleTime = false; |
||||
quint64 m_generation = 0; |
||||
void* m_display = nullptr; |
||||
bool m_x11Checked = false; |
||||
}; |
||||
Loading…
Reference in new issue