From a0c4a8e766a2160213838daf6f71c7ae6c3705df Mon Sep 17 00:00:00 2001 From: Vlad Zahorodnii Date: Tue, 16 Jun 2020 11:03:25 +0300 Subject: [PATCH] [x11] Force FocusIn events for already focused windows Depending on the current focus stealing prevention level, it's possible for kwin to call XSetInputFocus() on a window that already has the input focus. In which case, we won't receive the corresponding FocusIn event and the client will remain inactive from kwin's perspective even though it isn't. In order to work around this issue, we can move the input focus to the null window. By doing so, it's guaranteed that we're going to receive the matching FocusIn event for the client. This commit indirectly fixes a bug where fullscreen games are displayed below panels. --- autotests/integration/x11_client_test.cpp | 67 +++++++++++++++++++++++ x11client.cpp | 5 ++ 2 files changed, 72 insertions(+) diff --git a/autotests/integration/x11_client_test.cpp b/autotests/integration/x11_client_test.cpp index 3d4792ec86..cabf2e9c6a 100644 --- a/autotests/integration/x11_client_test.cpp +++ b/autotests/integration/x11_client_test.cpp @@ -57,6 +57,7 @@ private Q_SLOTS: void testCaptionWmName(); void testCaptionMultipleWindows(); void testFullscreenWindowGroups(); + void testActivateFocusedWindow(); }; void X11ClientTest::initTestCase() @@ -626,5 +627,71 @@ void X11ClientTest::testFullscreenWindowGroups() QTRY_COMPARE(client->layer(), ActiveLayer); } +void X11ClientTest::testActivateFocusedWindow() +{ + // The window manager may call XSetInputFocus() on a window that already has focus, in which + // case no FocusIn event will be generated and the window won't be marked as active. This test + // verifies that we handle that subtle case properly. + + QScopedPointer connection(xcb_connect(nullptr, nullptr)); + QVERIFY(!xcb_connection_has_error(connection.data())); + + QSignalSpy windowCreatedSpy(workspace(), &Workspace::clientAdded); + QVERIFY(windowCreatedSpy.isValid()); + + const QRect windowGeometry(0, 0, 100, 200); + xcb_size_hints_t hints; + memset(&hints, 0, sizeof(hints)); + xcb_icccm_size_hints_set_position(&hints, 1, windowGeometry.x(), windowGeometry.y()); + xcb_icccm_size_hints_set_size(&hints, 1, windowGeometry.width(), windowGeometry.height()); + + // Create the first test window. + const xcb_window_t window1 = xcb_generate_id(connection.data()); + xcb_create_window(connection.data(), XCB_COPY_FROM_PARENT, window1, rootWindow(), + windowGeometry.x(), windowGeometry.y(), + windowGeometry.width(), windowGeometry.height(), + 0, XCB_WINDOW_CLASS_INPUT_OUTPUT, XCB_COPY_FROM_PARENT, 0, nullptr); + xcb_icccm_set_wm_normal_hints(connection.data(), window1, &hints); + xcb_change_property(connection.data(), XCB_PROP_MODE_REPLACE, window1, + atoms->wm_client_leader, XCB_ATOM_WINDOW, 32, 1, &window1); + xcb_map_window(connection.data(), window1); + xcb_flush(connection.data()); + QVERIFY(windowCreatedSpy.wait()); + X11Client *client1 = windowCreatedSpy.first().first().value(); + QVERIFY(client1); + QCOMPARE(client1->windowId(), window1); + QCOMPARE(client1->isActive(), true); + + // Create the second test window. + const xcb_window_t window2 = xcb_generate_id(connection.data()); + xcb_create_window(connection.data(), XCB_COPY_FROM_PARENT, window2, rootWindow(), + windowGeometry.x(), windowGeometry.y(), + windowGeometry.width(), windowGeometry.height(), + 0, XCB_WINDOW_CLASS_INPUT_OUTPUT, XCB_COPY_FROM_PARENT, 0, nullptr); + xcb_icccm_set_wm_normal_hints(connection.data(), window2, &hints); + xcb_change_property(connection.data(), XCB_PROP_MODE_REPLACE, window2, + atoms->wm_client_leader, XCB_ATOM_WINDOW, 32, 1, &window2); + xcb_map_window(connection.data(), window2); + xcb_flush(connection.data()); + QVERIFY(windowCreatedSpy.wait()); + X11Client *client2 = windowCreatedSpy.last().first().value(); + QVERIFY(client2); + QCOMPARE(client2->windowId(), window2); + QCOMPARE(client2->isActive(), true); + + // When the second test window is destroyed, the window manager will attempt to activate the + // next client in the focus chain, which is the first window. + xcb_set_input_focus(connection.data(), XCB_INPUT_FOCUS_POINTER_ROOT, window1, XCB_CURRENT_TIME); + xcb_destroy_window(connection.data(), window2); + xcb_flush(connection.data()); + QVERIFY(Test::waitForWindowDestroyed(client2)); + QVERIFY(client1->isActive()); + + // Destroy the first test window. + xcb_destroy_window(connection.data(), window1); + xcb_flush(connection.data()); + QVERIFY(Test::waitForWindowDestroyed(client1)); +} + WAYLANDTEST_MAIN(X11ClientTest) #include "x11_client_test.moc" diff --git a/x11client.cpp b/x11client.cpp index ba7f103de0..501779bd1e 100644 --- a/x11client.cpp +++ b/x11client.cpp @@ -2059,6 +2059,11 @@ void X11Client::setOnAllActivities(bool on) */ void X11Client::takeFocus() { + // Force a FocusIn event if the window is already focused but inactive. + Xcb::CurrentInput currentInput; + if (!currentInput.isNull() && currentInput.window() == window()) + workspace()->focusToNull(); + if (rules()->checkAcceptFocus(info->input())) m_client.focus(); else