diff --git a/tools/chatgui/src/user_activity_monitor.cpp b/tools/chatgui/src/user_activity_monitor.cpp new file mode 100644 index 00000000..72b96033 --- /dev/null +++ b/tools/chatgui/src/user_activity_monitor.cpp @@ -0,0 +1,213 @@ +#include "user_activity_monitor.h" +#include "debug_ui.h" + +#include +#include +#include +#include +#include + +#ifdef UTUN_ACTIVITY_DBUS +#include +#include +#include +#include +#endif +#ifdef UTUN_ACTIVITY_KIDLETIME +#include +#endif +#ifdef Q_OS_WIN +#include +#elif defined(Q_OS_MACOS) +#include +#elif defined(UTUN_ACTIVITY_X11) +#include +#include +#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(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 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::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::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(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 +} diff --git a/tools/chatgui/src/user_activity_monitor.h b/tools/chatgui/src/user_activity_monitor.h new file mode 100644 index 00000000..dc8b6186 --- /dev/null +++ b/tools/chatgui/src/user_activity_monitor.h @@ -0,0 +1,41 @@ +/* Первая активность после start(): системный idle time или события приложения при ограничениях ОС. + * stop() отменяет наблюдение и запоздалые D-Bus ответы. Все вызовы и сигнал — в GUI-потоке. */ +#pragma once + +#include +#include +#include + +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; +};