diff --git a/autotests/client/test_idle.cpp b/autotests/client/test_idle.cpp --- a/autotests/client/test_idle.cpp +++ b/autotests/client/test_idle.cpp @@ -42,6 +42,8 @@ void testTimeout(); void testSimulateUserActivity(); + void testIdleInhibit(); + void testIdleInhibitBlocksTimeout(); private: Display *m_display = nullptr; @@ -186,5 +188,73 @@ m_display->dispatchEvents(); } +void IdleTest::testIdleInhibit() +{ + QCOMPARE(m_idleInterface->isInhibited(), false); + QSignalSpy idleInhibitedSpy(m_idleInterface, &IdleInterface::inhibitedChanged); + QVERIFY(idleInhibitedSpy.isValid()); + m_idleInterface->inhibit(); + QCOMPARE(m_idleInterface->isInhibited(), true); + QCOMPARE(idleInhibitedSpy.count(), 1); + m_idleInterface->inhibit(); + QCOMPARE(m_idleInterface->isInhibited(), true); + QCOMPARE(idleInhibitedSpy.count(), 1); + m_idleInterface->uninhibit(); + QCOMPARE(m_idleInterface->isInhibited(), true); + QCOMPARE(idleInhibitedSpy.count(), 1); + m_idleInterface->uninhibit(); + QCOMPARE(m_idleInterface->isInhibited(), false); + QCOMPARE(idleInhibitedSpy.count(), 2); +} + +void IdleTest::testIdleInhibitBlocksTimeout() +{ + // this test verifies that a timeout does not fire when the system is inhibited + + // so first inhibit + QCOMPARE(m_idleInterface->isInhibited(), false); + m_idleInterface->inhibit(); + + QScopedPointer timeout(m_idle->getTimeout(1, m_seat)); + QVERIFY(timeout->isValid()); + QSignalSpy idleSpy(timeout.data(), &IdleTimeout::idle); + QVERIFY(idleSpy.isValid()); + QSignalSpy resumedFormIdleSpy(timeout.data(), &IdleTimeout::resumeFromIdle); + QVERIFY(resumedFormIdleSpy.isValid()); + + // we requested a timeout of 1 msec, but the minimum the server sets is 5 sec + QVERIFY(!idleSpy.wait(500)); + // the default of 5 sec won't pass + QVERIFY(!idleSpy.wait()); + + // simulate some activity + QVERIFY(resumedFormIdleSpy.isEmpty()); + m_seatInterface->setTimestamp(1); + // resume from idle should not fire + QVERIFY(!resumedFormIdleSpy.wait()); + + // let's uninhibit + m_idleInterface->uninhibit(); + QCOMPARE(m_idleInterface->isInhibited(), false); + // we requested a timeout of 1 msec, but the minimum the server sets is 5 sec + QVERIFY(!idleSpy.wait(500)); + // the default of 5 sec will now pass + QVERIFY(idleSpy.wait()); + + // if we inhibit now it will trigger a resume from idle + QVERIFY(resumedFormIdleSpy.isEmpty()); + m_idleInterface->inhibit(); + QVERIFY(resumedFormIdleSpy.wait()); + + // let's wait again just to verify that also inhibit for already existing IdleTimeout works + QVERIFY(!idleSpy.wait(500)); + QVERIFY(!idleSpy.wait()); + QCOMPARE(idleSpy.count(), 1); + + timeout.reset(); + m_connection->flush(); + m_display->dispatchEvents(); +} + QTEST_GUILESS_MAIN(IdleTest) #include "test_idle.moc" diff --git a/src/server/idle_interface.h b/src/server/idle_interface.h --- a/src/server/idle_interface.h +++ b/src/server/idle_interface.h @@ -58,10 +58,57 @@ public: virtual ~IdleInterface(); + /** + * Inhibits the IdleInterface. While inhibited no IdleTimeoutInterface interface gets + * notified about an idle timeout. + * + * This can be used to inhibit power management, screen locking, etc. directly from + * Compositor side. + * + * To resume idle timeouts invoke @link{uninhibit}. It is possible to invoke inhibit several + * times, in that case uninhibit needs to called the same amount as inhibit has been called. + * @see uninhibit + * @see isInhibited + * @see inhibitedChanged + * @since 5.41 + **/ + void inhibit(); + + /** + * Unhibits the IdleInterface. The idle timeouts are only restarted if uninhibit has been + * called the same amount as inhibit. + * + * @see inhibit + * @see isInhibited + * @see inhibitedChanged + * @since 5.41 + **/ + void uninhibit(); + + /** + * @returns Whether idle timeouts are currently inhibited + * @see inhibit + * @see uninhibit + * @see inhibitedChanged + * @since 5.41 + **/ + bool isInhibited() const; + +Q_SIGNALS: + /** + * Emitted when the system gets inhibited or uninhibited. + * @see inhibit + * @see uninhibit + * @see isInhibited + * @since 5.41 + **/ + void inhibitedChanged(); + private: explicit IdleInterface(Display *display, QObject *parent = nullptr); friend class Display; class Private; + Private *d_func() const; }; // TODO: KF6 make private class diff --git a/src/server/idle_interface.cpp b/src/server/idle_interface.cpp --- a/src/server/idle_interface.cpp +++ b/src/server/idle_interface.cpp @@ -38,6 +38,8 @@ public: Private(IdleInterface *q, Display *d); + int inhibitCount = 0; + private: void bind(wl_client *client, uint32_t version, uint32_t id) override; static void getIdleTimeoutCallback(wl_client *client, wl_resource *resource, uint32_t id, wl_resource *seat, uint32_t timeout); @@ -123,6 +125,35 @@ IdleInterface::~IdleInterface() = default; +void IdleInterface::inhibit() +{ + Q_D(); + d->inhibitCount++; + if (d->inhibitCount == 1) { + emit inhibitedChanged(); + } +} + +void IdleInterface::uninhibit() +{ + Q_D(); + d->inhibitCount--; + if (d->inhibitCount == 0) { + emit inhibitedChanged(); + } +} + +bool IdleInterface::isInhibited() const +{ + Q_D(); + return d->inhibitCount > 0; +} + +IdleInterface::Private *IdleInterface::d_func() const +{ + return reinterpret_cast(d.data()); +} + #ifndef DOXYGEN_SHOULD_SKIP_THIS const struct org_kde_kwin_idle_timeout_interface IdleTimeoutInterface::Private::s_interface = { resourceDestroyedCallback, @@ -146,6 +177,10 @@ // not yet configured return; } + if (qobject_cast(p->global)->isInhibited()) { + // ignored while inhibited + return; + } if (!p->timer->isActive() && p->resource) { org_kde_kwin_idle_timeout_send_resumed(p->resource); } @@ -168,6 +203,10 @@ } } ); + if (qobject_cast(global)->isInhibited()) { + // don't start if inhibited + return; + } timer->start(); } @@ -181,12 +220,33 @@ // not yet configured return; } + if (qobject_cast(d->global)->isInhibited()) { + // ignored while inhibited + return; + } if (!d->timer->isActive() && d->resource) { org_kde_kwin_idle_timeout_send_resumed(d->resource); } d->timer->start(); } ); + connect(parent, &IdleInterface::inhibitedChanged, this, + [this] { + Q_D(); + if (!d->timer) { + // not yet configured + return; + } + if (qobject_cast(d->global)->isInhibited()) { + if (!d->timer->isActive() && d->resource) { + org_kde_kwin_idle_timeout_send_resumed(d->resource); + } + d->timer->stop(); + } else { + d->timer->start(); + } + } + ); } IdleTimeoutInterface::~IdleTimeoutInterface() = default;