diff --git a/CMakeLists.txt b/CMakeLists.txt --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -493,10 +493,10 @@ sm.cpp subsurfacemonitor.cpp syncalarmx11filter.cpp + tablet_input.cpp thumbnailitem.cpp toplevel.cpp touch_hide_cursor_spy.cpp - tablet_input.cpp touch_input.cpp udev.cpp unmanaged.cpp @@ -509,6 +509,9 @@ was_user_interaction_x11_filter.cpp wayland_cursor_theme.cpp wayland_server.cpp + waylandclient.cpp + waylandshellintegration.cpp + waylandxdgshellintegration.cpp window_property_notify_x11_filter.cpp workspace.cpp x11client.cpp diff --git a/abstract_client.h b/abstract_client.h --- a/abstract_client.h +++ b/abstract_client.h @@ -1129,6 +1129,7 @@ * Base implementation returns @c true. */ virtual bool doStartMoveResize(); + virtual void doFinishMoveResize(); void finishMoveResize(bool cancel); /** * Leaves the move resize mode. diff --git a/abstract_client.cpp b/abstract_client.cpp --- a/abstract_client.cpp +++ b/abstract_client.cpp @@ -941,6 +941,8 @@ const bool wasResize = isResize(); // store across leaveMoveResize leaveMoveResize(); + doFinishMoveResize(); + if (cancel) setFrameGeometry(initialMoveResizeGeometry()); else { @@ -2066,6 +2068,10 @@ return true; } +void AbstractClient::doFinishMoveResize() +{ +} + void AbstractClient::positionGeometryTip() { } diff --git a/autotests/integration/debug_console_test.cpp b/autotests/integration/debug_console_test.cpp --- a/autotests/integration/debug_console_test.cpp +++ b/autotests/integration/debug_console_test.cpp @@ -388,16 +388,8 @@ surface->attachBuffer(Buffer::Ptr()); surface->commit(Surface::CommitFlag::None); - shellSurface.reset(); - Test::flushWaylandConnection(); - qDebug() << rowsRemovedSpy.count(); - QEXPECT_FAIL("wlShell", "Deleting a ShellSurface does not result in the server removing the XdgShellClient", Continue); - QVERIFY(rowsRemovedSpy.wait(500)); - surface.reset(); - - if (rowsRemovedSpy.isEmpty()) { - QVERIFY(rowsRemovedSpy.wait()); - } + QVERIFY(rowsRemovedSpy.wait()); + QCOMPARE(rowsRemovedSpy.count(), 1); QCOMPARE(rowsRemovedSpy.first().first().value(), waylandTopLevelIndex); QCOMPARE(rowsRemovedSpy.first().at(1).value(), 0); diff --git a/autotests/integration/effects/CMakeLists.txt b/autotests/integration/effects/CMakeLists.txt --- a/autotests/integration/effects/CMakeLists.txt +++ b/autotests/integration/effects/CMakeLists.txt @@ -3,7 +3,6 @@ integrationTest(NAME testSlidingPopups SRCS slidingpopups_test.cpp LIBS XCB::ICCCM) integrationTest(NAME testShadeWobblyWindows SRCS wobbly_shade_test.cpp LIBS XCB::ICCCM) endif() -integrationTest(NAME testFade SRCS fade_test.cpp) integrationTest(WAYLAND_ONLY NAME testEffectWindowGeometry SRCS windowgeometry_test.cpp) integrationTest(NAME testScriptedEffects SRCS scripted_effects_test.cpp) integrationTest(WAYLAND_ONLY NAME testToplevelOpenCloseAnimation SRCS toplevel_open_close_animation_test.cpp) diff --git a/autotests/integration/effects/fade_test.cpp b/autotests/integration/effects/fade_test.cpp deleted file mode 100644 --- a/autotests/integration/effects/fade_test.cpp +++ /dev/null @@ -1,179 +0,0 @@ -/******************************************************************** -KWin - the KDE window manager -This file is part of the KDE project. - -Copyright (C) 2016 Martin Gräßlin - -This program is free software; you can redistribute it and/or modify -it under the terms of the GNU General Public License as published by -the Free Software Foundation; either version 2 of the License, or -(at your option) any later version. - -This program is distributed in the hope that it will be useful, -but WITHOUT ANY WARRANTY; without even the implied warranty of -MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -GNU General Public License for more details. - -You should have received a copy of the GNU General Public License -along with this program. If not, see . -*********************************************************************/ -#include "kwin_wayland_test.h" -#include "abstract_client.h" -#include "composite.h" -#include "effects.h" -#include "effectloader.h" -#include "cursor.h" -#include "platform.h" -#include "wayland_server.h" -#include "workspace.h" -#include "effect_builtins.h" - -#include - -#include -#include - -using namespace KWin; -using namespace KWayland::Client; -static const QString s_socketName = QStringLiteral("wayland_test_effects_translucency-0"); - -class FadeTest : public QObject -{ -Q_OBJECT -private Q_SLOTS: - void initTestCase(); - void init(); - void cleanup(); - - void testWindowCloseAfterWindowHidden_data(); - void testWindowCloseAfterWindowHidden(); - -private: - Effect *m_fadeEffect = nullptr; -}; - -void FadeTest::initTestCase() -{ - qputenv("XDG_DATA_DIRS", QCoreApplication::applicationDirPath().toUtf8()); - qRegisterMetaType(); - qRegisterMetaType(); - QSignalSpy workspaceCreatedSpy(kwinApp(), &Application::workspaceCreated); - QVERIFY(workspaceCreatedSpy.isValid()); - kwinApp()->platform()->setInitialWindowSize(QSize(1280, 1024)); - QVERIFY(waylandServer()->init(s_socketName.toLocal8Bit())); - - // disable all effects - we don't want to have it interact with the rendering - auto config = KSharedConfig::openConfig(QString(), KConfig::SimpleConfig); - KConfigGroup plugins(config, QStringLiteral("Plugins")); - ScriptedEffectLoader loader; - const auto builtinNames = BuiltInEffects::availableEffectNames() << loader.listOfKnownEffects(); - for (QString name : builtinNames) { - plugins.writeEntry(name + QStringLiteral("Enabled"), false); - } - - config->sync(); - kwinApp()->setConfig(config); - - qputenv("KWIN_EFFECTS_FORCE_ANIMATIONS", "1"); - kwinApp()->start(); - QVERIFY(workspaceCreatedSpy.wait()); - QVERIFY(KWin::Compositor::self()); -} - -void FadeTest::init() -{ - QVERIFY(Test::setupWaylandConnection()); - - // load the translucency effect - EffectsHandlerImpl *e = static_cast(effects); - // find the effectsloader - auto effectloader = e->findChild(); - QVERIFY(effectloader); - QSignalSpy effectLoadedSpy(effectloader, &AbstractEffectLoader::effectLoaded); - QVERIFY(effectLoadedSpy.isValid()); - - QVERIFY(!e->isEffectLoaded(QStringLiteral("kwin4_effect_fade"))); - QVERIFY(e->loadEffect(QStringLiteral("kwin4_effect_fade"))); - QVERIFY(e->isEffectLoaded(QStringLiteral("kwin4_effect_fade"))); - - QCOMPARE(effectLoadedSpy.count(), 1); - m_fadeEffect = effectLoadedSpy.first().first().value(); - QVERIFY(m_fadeEffect); -} - -void FadeTest::cleanup() -{ - Test::destroyWaylandConnection(); - EffectsHandlerImpl *e = static_cast(effects); - if (e->isEffectLoaded(QStringLiteral("kwin4_effect_fade"))) { - e->unloadEffect(QStringLiteral("kwin4_effect_fade")); - } - QVERIFY(!e->isEffectLoaded(QStringLiteral("kwin4_effect_fade"))); - m_fadeEffect = nullptr; -} - -void FadeTest::testWindowCloseAfterWindowHidden_data() -{ - QTest::addColumn("type"); - - QTest::newRow("xdgWmBase") << Test::XdgShellSurfaceType::XdgShellStable; -} - -void FadeTest::testWindowCloseAfterWindowHidden() -{ - // this test simulates the showing/hiding/closing of a Wayland window - // especially the situation that a window got unmapped and destroyed way later - QVERIFY(!m_fadeEffect->isActive()); - - QSignalSpy windowAddedSpy(effects, &EffectsHandler::windowAdded); - QVERIFY(windowAddedSpy.isValid()); - QSignalSpy windowHiddenSpy(effects, &EffectsHandler::windowHidden); - QVERIFY(windowHiddenSpy.isValid()); - QSignalSpy windowShownSpy(effects, &EffectsHandler::windowShown); - QVERIFY(windowShownSpy.isValid()); - QSignalSpy windowClosedSpy(effects, &EffectsHandler::windowClosed); - QVERIFY(windowClosedSpy.isValid()); - - QScopedPointer surface(Test::createSurface()); - QFETCH(Test::XdgShellSurfaceType, type); - QScopedPointer shellSurface(Test::createXdgShellSurface(type, surface.data())); - auto c = Test::renderAndWaitForShown(surface.data(), QSize(100, 50), Qt::blue); - QVERIFY(c); - QTRY_COMPARE(windowAddedSpy.count(), 1); - QTRY_COMPARE(m_fadeEffect->isActive(), true); - - QTest::qWait(500); - QTRY_COMPARE(m_fadeEffect->isActive(), false); - - // now unmap the surface - surface->attachBuffer(Buffer::Ptr()); - surface->commit(Surface::CommitFlag::None); - QVERIFY(windowHiddenSpy.wait()); - QCOMPARE(m_fadeEffect->isActive(), true); - QTest::qWait(500); - QTRY_COMPARE(m_fadeEffect->isActive(), false); - - // and map again - Test::render(surface.data(), QSize(100, 50), Qt::red); - QVERIFY(windowShownSpy.wait()); - QTRY_COMPARE(m_fadeEffect->isActive(), true); - QTest::qWait(500); - QTRY_COMPARE(m_fadeEffect->isActive(), false); - - // and unmap once more - surface->attachBuffer(Buffer::Ptr()); - surface->commit(Surface::CommitFlag::None); - QVERIFY(windowHiddenSpy.wait()); - QCOMPARE(m_fadeEffect->isActive(), true); - QTest::qWait(500); - QTRY_COMPARE(m_fadeEffect->isActive(), false); - - // and now destroy - shellSurface.reset(); - surface.reset(); - QVERIFY(windowClosedSpy.wait()); - QCOMPARE(m_fadeEffect->isActive(), false); -} - -WAYLANDTEST_MAIN(FadeTest) -#include "fade_test.moc" diff --git a/autotests/integration/idle_inhibition_test.cpp b/autotests/integration/idle_inhibition_test.cpp --- a/autotests/integration/idle_inhibition_test.cpp +++ b/autotests/integration/idle_inhibition_test.cpp @@ -240,7 +240,7 @@ // This test verifies that the idle inhibitor object is not honored by KWin // when the associated client is unmapped. - // Get reference to the idle interface. + // Get reference to the idle interface. auto idle = waylandServer()->display()->findChild(); QVERIFY(idle); QVERIFY(!idle->isInhibited()); @@ -252,45 +252,65 @@ QVERIFY(!surface.isNull()); QScopedPointer shellSurface(Test::createXdgShellStableSurface(surface.data())); QVERIFY(!shellSurface.isNull()); + QSignalSpy configureRequestedSpy(shellSurface.data(), &XdgShellSurface::configureRequested); + QVERIFY(configureRequestedSpy.isValid()); // Create the inhibitor object. QScopedPointer inhibitor(Test::waylandIdleInhibitManager()->createInhibitor(surface.data())); QVERIFY(inhibitor->isValid()); - // Render the client. - auto c = Test::renderAndWaitForShown(surface.data(), QSize(100, 50), Qt::blue); - QVERIFY(c); + // Map the client. + QSignalSpy clientAddedSpy(waylandServer(), &WaylandServer::shellClientAdded); + QVERIFY(clientAddedSpy.isValid()); + Test::render(surface.data(), QSize(100, 50), Qt::blue); + QVERIFY(clientAddedSpy.isEmpty()); + QVERIFY(clientAddedSpy.wait()); + QCOMPARE(clientAddedSpy.count(), 1); + AbstractClient *client = clientAddedSpy.last().first().value(); + QVERIFY(client); + QCOMPARE(client->readyForPainting(), true); + + // The compositor will respond with a configure event when the surface becomes active. + QVERIFY(configureRequestedSpy.wait()); + QCOMPARE(configureRequestedSpy.count(), 1); // This should inhibit our server object. QVERIFY(idle->isInhibited()); QCOMPARE(inhibitedSpy.count(), 1); // Unmap the client. - QSignalSpy hiddenSpy(c, &AbstractClient::windowHidden); - QVERIFY(hiddenSpy.isValid()); surface->attachBuffer(Buffer::Ptr()); surface->commit(Surface::CommitFlag::None); - QVERIFY(hiddenSpy.wait()); + QVERIFY(Test::waitForWindowDestroyed(client)); - // The surface is no longer visible, so the compositor don't have to honor the + // The surface is no longer visible, so the compositor doesn't have to honor the // idle inhibitor object. QVERIFY(!idle->isInhibited()); QCOMPARE(inhibitedSpy.count(), 2); + // Tell the compositor that we want to map the surface. + surface->commit(Surface::CommitFlag::None); + + // The compositor will respond with a configure event. + QVERIFY(configureRequestedSpy.wait()); + QCOMPARE(configureRequestedSpy.count(), 2); + // Map the client. - QSignalSpy windowShownSpy(c, &AbstractClient::windowShown); - QVERIFY(windowShownSpy.isValid()); Test::render(surface.data(), QSize(100, 50), Qt::blue); - QVERIFY(windowShownSpy.wait()); + QVERIFY(clientAddedSpy.wait()); + QCOMPARE(clientAddedSpy.count(), 2); + client = clientAddedSpy.last().first().value(); + QVERIFY(client); + QCOMPARE(client->readyForPainting(), true); // The test client became visible again, so the compositor has to honor the idle // inhibitor object back again. QVERIFY(idle->isInhibited()); QCOMPARE(inhibitedSpy.count(), 3); // Destroy the test client. shellSurface.reset(); - QVERIFY(Test::waitForWindowDestroyed(c)); + QVERIFY(Test::waitForWindowDestroyed(client)); QTRY_VERIFY(!idle->isInhibited()); QCOMPARE(inhibitedSpy.count(), 4); } diff --git a/autotests/integration/maximize_test.cpp b/autotests/integration/maximize_test.cpp --- a/autotests/integration/maximize_test.cpp +++ b/autotests/integration/maximize_test.cpp @@ -309,6 +309,8 @@ // test case verifies that borderless maximized windows doesn't cause // clients to render client-side decorations instead (BUG 405385) + XdgShellSurface::States states; + // adjust config auto group = kwinApp()->config()->group("Windows"); group.writeEntry("BorderlessMaximizedWindows", true); @@ -324,29 +326,32 @@ QVERIFY(decorationConfiguredSpy.isValid()); auto client = Test::renderAndWaitForShown(surface.data(), QSize(100, 50), Qt::blue); + QVERIFY(client->isDecorated()); + QVERIFY(!client->noBorder()); QSignalSpy frameGeometryChangedSpy(client, &AbstractClient::frameGeometryChanged); QVERIFY(frameGeometryChangedSpy.isValid()); - QSignalSpy sizeChangeRequestedSpy(xdgShellSurface.data(), &XdgShellSurface::sizeChanged); - QVERIFY(sizeChangeRequestedSpy.isValid()); QSignalSpy configureRequestedSpy(xdgShellSurface.data(), &XdgShellSurface::configureRequested); QVERIFY(configureRequestedSpy.isValid()); - QVERIFY(client->isDecorated()); - QVERIFY(!client->noBorder()); - configureRequestedSpy.wait(); + // Wait for the compositor to send a configure event with the Activated state. + QVERIFY(configureRequestedSpy.wait()); + QCOMPARE(configureRequestedSpy.count(), 1); + states = configureRequestedSpy.last().at(1).value(); + QVERIFY(states & XdgShellSurface::State::Activated); + QCOMPARE(decorationConfiguredSpy.count(), 1); QCOMPARE(deco->mode(), XdgDecoration::Mode::ServerSide); // go to maximized xdgShellSurface->setMaximized(true); - QVERIFY(sizeChangeRequestedSpy.wait()); - QCOMPARE(sizeChangeRequestedSpy.count(), 1); + QVERIFY(configureRequestedSpy.wait()); + QCOMPARE(configureRequestedSpy.count(), 2); + states = configureRequestedSpy.last().at(1).value(); + QVERIFY(states & XdgShellSurface::State::Maximized); - for (const auto &it: configureRequestedSpy) { - xdgShellSurface->ackConfigure(it[2].toInt()); - } - Test::render(surface.data(), sizeChangeRequestedSpy.last().first().toSize(), Qt::red); + xdgShellSurface->ackConfigure(configureRequestedSpy.last().at(2).value()); + Test::render(surface.data(), configureRequestedSpy.last().first().toSize(), Qt::red); QVERIFY(frameGeometryChangedSpy.wait()); // no deco @@ -357,13 +362,13 @@ // go back to normal xdgShellSurface->setMaximized(false); - QVERIFY(sizeChangeRequestedSpy.wait()); - QCOMPARE(sizeChangeRequestedSpy.count(), 2); + QVERIFY(configureRequestedSpy.wait()); + QCOMPARE(configureRequestedSpy.count(), 3); + states = configureRequestedSpy.last().at(1).value(); + QVERIFY(!(states & XdgShellSurface::State::Maximized)); - for (const auto &it: configureRequestedSpy) { - xdgShellSurface->ackConfigure(it[2].toInt()); - } - Test::render(surface.data(), sizeChangeRequestedSpy.last().first().toSize(), Qt::red); + xdgShellSurface->ackConfigure(configureRequestedSpy.last().at(2).value()); + Test::render(surface.data(), configureRequestedSpy.last().first().toSize(), Qt::red); QVERIFY(frameGeometryChangedSpy.wait()); QVERIFY(client->isDecorated()); diff --git a/autotests/integration/move_resize_window_test.cpp b/autotests/integration/move_resize_window_test.cpp --- a/autotests/integration/move_resize_window_test.cpp +++ b/autotests/integration/move_resize_window_test.cpp @@ -80,8 +80,6 @@ void testResizeForVirtualKeyboardWithFullScreen(); void testDestroyMoveClient(); void testDestroyResizeClient(); - void testUnmapMoveClient(); - void testUnmapResizeClient(); private: KWayland::Client::ConnectionThread *m_connection = nullptr; @@ -316,15 +314,14 @@ QCOMPARE(moveResizedChangedSpy.count(), 2); QCOMPARE(c->isResize(), false); QCOMPARE(workspace()->moveResizeClient(), nullptr); - QEXPECT_FAIL("", "XdgShellClient currently doesn't send final configure event", Abort); QVERIFY(configureRequestedSpy.wait()); QCOMPARE(configureRequestedSpy.count(), 6); states = configureRequestedSpy.last().at(1).value(); QVERIFY(states.testFlag(XdgShellSurface::State::Activated)); QVERIFY(!states.testFlag(XdgShellSurface::State::Resizing)); // Destroy the client. - surface.reset(); + shellSurface.reset(); QVERIFY(Test::waitForWindowDestroyed(c)); } @@ -1112,98 +1109,6 @@ QCOMPARE(workspace()->moveResizeClient(), nullptr); } -void MoveResizeWindowTest::testUnmapMoveClient() -{ - // This test verifies that active move operation gets cancelled when - // the associated client is unmapped. - - // Create the test client. - using namespace KWayland::Client; - QScopedPointer surface(Test::createSurface()); - QVERIFY(!surface.isNull()); - QScopedPointer shellSurface(Test::createXdgShellStableSurface(surface.data())); - QVERIFY(!shellSurface.isNull()); - AbstractClient *client = Test::renderAndWaitForShown(surface.data(), QSize(100, 50), Qt::blue); - QVERIFY(client); - - // Start resizing the client. - QSignalSpy clientStartMoveResizedSpy(client, &AbstractClient::clientStartUserMovedResized); - QVERIFY(clientStartMoveResizedSpy.isValid()); - QSignalSpy clientFinishUserMovedResizedSpy(client, &AbstractClient::clientFinishUserMovedResized); - QVERIFY(clientFinishUserMovedResizedSpy.isValid()); - - QCOMPARE(workspace()->moveResizeClient(), nullptr); - QCOMPARE(client->isMove(), false); - QCOMPARE(client->isResize(), false); - workspace()->slotWindowMove(); - QCOMPARE(clientStartMoveResizedSpy.count(), 1); - QCOMPARE(workspace()->moveResizeClient(), client); - QCOMPARE(client->isMove(), true); - QCOMPARE(client->isResize(), false); - - // Unmap the client while we're moving it. - QSignalSpy hiddenSpy(client, &AbstractClient::windowHidden); - QVERIFY(hiddenSpy.isValid()); - surface->attachBuffer(Buffer::Ptr()); - surface->commit(Surface::CommitFlag::None); - QVERIFY(hiddenSpy.wait()); - QCOMPARE(clientFinishUserMovedResizedSpy.count(), 0); - QCOMPARE(workspace()->moveResizeClient(), nullptr); - QCOMPARE(client->isMove(), false); - QCOMPARE(client->isResize(), false); - - // Destroy the client. - shellSurface.reset(); - QVERIFY(Test::waitForWindowDestroyed(client)); - QCOMPARE(clientFinishUserMovedResizedSpy.count(), 0); -} - -void MoveResizeWindowTest::testUnmapResizeClient() -{ - // This test verifies that active resize operation gets cancelled when - // the associated client is unmapped. - - // Create the test client. - using namespace KWayland::Client; - QScopedPointer surface(Test::createSurface()); - QVERIFY(!surface.isNull()); - QScopedPointer shellSurface(Test::createXdgShellStableSurface(surface.data())); - QVERIFY(!shellSurface.isNull()); - AbstractClient *client = Test::renderAndWaitForShown(surface.data(), QSize(100, 50), Qt::blue); - QVERIFY(client); - - // Start resizing the client. - QSignalSpy clientStartMoveResizedSpy(client, &AbstractClient::clientStartUserMovedResized); - QVERIFY(clientStartMoveResizedSpy.isValid()); - QSignalSpy clientFinishUserMovedResizedSpy(client, &AbstractClient::clientFinishUserMovedResized); - QVERIFY(clientFinishUserMovedResizedSpy.isValid()); - - QCOMPARE(workspace()->moveResizeClient(), nullptr); - QCOMPARE(client->isMove(), false); - QCOMPARE(client->isResize(), false); - workspace()->slotWindowResize(); - QCOMPARE(clientStartMoveResizedSpy.count(), 1); - QCOMPARE(workspace()->moveResizeClient(), client); - QCOMPARE(client->isMove(), false); - QCOMPARE(client->isResize(), true); - - // Unmap the client while we're resizing it. - QSignalSpy hiddenSpy(client, &AbstractClient::windowHidden); - QVERIFY(hiddenSpy.isValid()); - surface->attachBuffer(Buffer::Ptr()); - surface->commit(Surface::CommitFlag::None); - QVERIFY(hiddenSpy.wait()); - QCOMPARE(clientFinishUserMovedResizedSpy.count(), 0); - QCOMPARE(workspace()->moveResizeClient(), nullptr); - QCOMPARE(client->isMove(), false); - QCOMPARE(client->isResize(), false); - - // Destroy the client. - shellSurface.reset(); - QVERIFY(Test::waitForWindowDestroyed(client)); - QCOMPARE(clientFinishUserMovedResizedSpy.count(), 0); -} - } WAYLANDTEST_MAIN(KWin::MoveResizeWindowTest) diff --git a/autotests/integration/quick_tiling_test.cpp b/autotests/integration/quick_tiling_test.cpp --- a/autotests/integration/quick_tiling_test.cpp +++ b/autotests/integration/quick_tiling_test.cpp @@ -192,7 +192,6 @@ // but we got requested a new geometry QVERIFY(configureRequestedSpy.wait()); - QEXPECT_FAIL("maximize", "Two configure events are sent for maximized", Continue); QCOMPARE(configureRequestedSpy.count(), 2); QCOMPARE(configureRequestedSpy.last().at(0).toSize(), expectedGeometry.size()); @@ -272,7 +271,6 @@ // but we got requested a new geometry QVERIFY(configureRequestedSpy.wait()); - QEXPECT_FAIL("", "Two configure events are sent for maximized", Continue); QCOMPARE(configureRequestedSpy.count(), 2); QCOMPARE(configureRequestedSpy.last().at(0).toSize(), QSize(1280, 1024)); @@ -305,7 +303,6 @@ QCOMPARE(c->geometryRestore(), QRect(0, 0, 100, 50)); // we got requested a new geometry QVERIFY(configureRequestedSpy.wait()); - QEXPECT_FAIL("", "Two configure events are sent for maximized", Continue); QCOMPARE(configureRequestedSpy.count(), 3); QCOMPARE(configureRequestedSpy.last().at(0).toSize(), QSize(100, 50)); diff --git a/autotests/integration/xdgshellclient_rules_test.cpp b/autotests/integration/xdgshellclient_rules_test.cpp --- a/autotests/integration/xdgshellclient_rules_test.cpp +++ b/autotests/integration/xdgshellclient_rules_test.cpp @@ -840,9 +840,7 @@ QVERIFY(!client->isMove()); QVERIFY(!client->isResize()); - QEXPECT_FAIL("", "Interactive resize is not spec-compliant", Continue); QVERIFY(configureRequestedSpy->wait(10)); - QEXPECT_FAIL("", "Interactive resize is not spec-compliant", Continue); QCOMPARE(configureRequestedSpy->count(), 5); // The rule should be applied again if the client appears after it's been closed. @@ -978,9 +976,7 @@ QVERIFY(!client->isMove()); QVERIFY(!client->isResize()); - QEXPECT_FAIL("", "Interactive resize is not spec-compliant", Continue); QVERIFY(configureRequestedSpy->wait(10)); - QEXPECT_FAIL("", "Interactive resize is not spec-compliant", Continue); QCOMPARE(configureRequestedSpy->count(), 5); // If the client appears again, it should have the last known size. @@ -1380,6 +1376,7 @@ workspace()->slotWindowMaximize(); QVERIFY(configureRequestedSpy->wait()); QCOMPARE(configureRequestedSpy->count(), 3); + QEXPECT_FAIL("", "Geometry restore is set to the first valid geometry", Continue); QCOMPARE(configureRequestedSpy->last().at(0).toSize(), QSize(0, 0)); states = configureRequestedSpy->last().at(1).value(); QVERIFY(states.testFlag(XdgShellSurface::State::Activated)); @@ -1492,6 +1489,7 @@ workspace()->slotWindowMaximize(); QVERIFY(configureRequestedSpy->wait()); QCOMPARE(configureRequestedSpy->count(), 3); + QEXPECT_FAIL("", "Geometry restore is set to the first valid geometry", Continue); QCOMPARE(configureRequestedSpy->last().at(0).toSize(), QSize(0, 0)); states = configureRequestedSpy->last().at(1).value(); QVERIFY(states.testFlag(XdgShellSurface::State::Activated)); diff --git a/autotests/integration/xdgshellclient_test.cpp b/autotests/integration/xdgshellclient_test.cpp --- a/autotests/integration/xdgshellclient_test.cpp +++ b/autotests/integration/xdgshellclient_test.cpp @@ -70,10 +70,8 @@ void init(); void cleanup(); - void testMapUnmapMap_data(); - void testMapUnmapMap(); + void testMapUnmap(); void testDesktopPresenceChanged(); - void testTransientPositionAfterRemap(); void testWindowOutputs_data(); void testWindowOutputs(); void testMinimizeActiveWindow_data(); @@ -158,109 +156,68 @@ Test::destroyWaylandConnection(); } -void TestXdgShellClient::testMapUnmapMap_data() +void TestXdgShellClient::testMapUnmap() { - QTest::addColumn("type"); + // This test verifies that the compositor destroys XdgToplevelClient when the + // associated xdg_toplevel surface is unmapped. - QTest::newRow("xdgWmBase") << Test::XdgShellSurfaceType::XdgShellStable; -} + // Create a wl_surface and an xdg_toplevel, but don't commit them yet! + QScopedPointer surface(Test::createSurface()); + QScopedPointer shellSurface( + Test::createXdgShellStableSurface(surface.data(), nullptr, Test::CreationSetup::CreateOnly)); -void TestXdgShellClient::testMapUnmapMap() -{ - // this test verifies that mapping a previously mapped window works correctly QSignalSpy clientAddedSpy(waylandServer(), &WaylandServer::shellClientAdded); QVERIFY(clientAddedSpy.isValid()); - QSignalSpy effectsWindowShownSpy(effects, &EffectsHandler::windowShown); - QVERIFY(effectsWindowShownSpy.isValid()); - QSignalSpy effectsWindowHiddenSpy(effects, &EffectsHandler::windowHidden); - QVERIFY(effectsWindowHiddenSpy.isValid()); - QScopedPointer surface(Test::createSurface()); - QFETCH(Test::XdgShellSurfaceType, type); - QScopedPointer shellSurface(Test::createXdgShellSurface(type, surface.data())); + QSignalSpy configureRequestedSpy(shellSurface.data(), &XdgShellSurface::configureRequested); + QVERIFY(configureRequestedSpy.isValid()); - // now let's render - Test::render(surface.data(), QSize(100, 50), Qt::blue); + // Tell the compositor that we want to map the surface. + surface->commit(Surface::CommitFlag::None); - QVERIFY(clientAddedSpy.isEmpty()); + // The compositor will respond with a configure event. + QVERIFY(configureRequestedSpy.wait()); + QCOMPARE(configureRequestedSpy.count(), 1); + + // Now we can attach a buffer with actual data to the surface. + Test::render(surface.data(), QSize(100, 50), Qt::blue); QVERIFY(clientAddedSpy.wait()); - auto client = clientAddedSpy.first().first().value(); + QCOMPARE(clientAddedSpy.count(), 1); + AbstractClient *client = clientAddedSpy.last().first().value(); QVERIFY(client); - QVERIFY(client->isShown(true)); - QCOMPARE(client->isHiddenInternal(), false); QCOMPARE(client->readyForPainting(), true); - QCOMPARE(client->depth(), 32); - QVERIFY(client->hasAlpha()); - QCOMPARE(client->icon().name(), QStringLiteral("wayland")); - QCOMPARE(workspace()->activeClient(), client); - QVERIFY(effectsWindowShownSpy.isEmpty()); - QVERIFY(client->isMaximizable()); - QVERIFY(client->isMovable()); - QVERIFY(client->isMovableAcrossScreens()); - QVERIFY(client->isResizable()); - QVERIFY(client->property("maximizable").toBool()); - QVERIFY(client->property("moveable").toBool()); - QVERIFY(client->property("moveableAcrossScreens").toBool()); - QVERIFY(client->property("resizeable").toBool()); - QCOMPARE(client->isInternal(), false); - QVERIFY(client->effectWindow()); - QVERIFY(!client->effectWindow()->internalWindow()); - QCOMPARE(client->internalId().isNull(), false); - const auto uuid = client->internalId(); - QUuid deletedUuid; - QCOMPARE(deletedUuid.isNull(), true); - - connect(client, &AbstractClient::windowClosed, this, [&deletedUuid] (Toplevel *, Deleted *d) { deletedUuid = d->internalId(); }); - - // now unmap - QSignalSpy hiddenSpy(client, &AbstractClient::windowHidden); - QVERIFY(hiddenSpy.isValid()); - QSignalSpy windowClosedSpy(client, &AbstractClient::windowClosed); - QVERIFY(windowClosedSpy.isValid()); + + // When the client becomes active, the compositor will send another configure event. + QVERIFY(configureRequestedSpy.wait()); + QCOMPARE(configureRequestedSpy.count(), 2); + + // Unmap the xdg_toplevel surface by committing a null buffer. surface->attachBuffer(Buffer::Ptr()); surface->commit(Surface::CommitFlag::None); - QVERIFY(hiddenSpy.wait()); - QCOMPARE(client->readyForPainting(), true); - QCOMPARE(client->isHiddenInternal(), true); - QVERIFY(windowClosedSpy.isEmpty()); - QVERIFY(!workspace()->activeClient()); - QCOMPARE(effectsWindowHiddenSpy.count(), 1); - QCOMPARE(effectsWindowHiddenSpy.first().first().value(), client->effectWindow()); + QVERIFY(Test::waitForWindowDestroyed(client)); - QSignalSpy windowShownSpy(client, &AbstractClient::windowShown); - QVERIFY(windowShownSpy.isValid()); - Test::render(surface.data(), QSize(100, 50), Qt::blue, QImage::Format_RGB32); - QCOMPARE(clientAddedSpy.count(), 1); - QVERIFY(windowShownSpy.wait()); - QCOMPARE(windowShownSpy.count(), 1); - QCOMPARE(clientAddedSpy.count(), 1); - QCOMPARE(client->readyForPainting(), true); - QCOMPARE(client->isHiddenInternal(), false); - QCOMPARE(client->depth(), 24); - QVERIFY(!client->hasAlpha()); - QCOMPARE(workspace()->activeClient(), client); - QCOMPARE(effectsWindowShownSpy.count(), 1); - QCOMPARE(effectsWindowShownSpy.first().first().value(), client->effectWindow()); - - // let's unmap again - surface->attachBuffer(Buffer::Ptr()); + // Tell the compositor that we want to re-map the xdg_toplevel surface. surface->commit(Surface::CommitFlag::None); - QVERIFY(hiddenSpy.wait()); - QCOMPARE(hiddenSpy.count(), 2); + + // The compositor will respond with a configure event. + QVERIFY(configureRequestedSpy.wait()); + QCOMPARE(configureRequestedSpy.count(), 3); + + // Now we can attach a buffer with actual data to the surface. + Test::render(surface.data(), QSize(100, 50), Qt::blue); + QVERIFY(clientAddedSpy.wait()); + QCOMPARE(clientAddedSpy.count(), 2); + client = clientAddedSpy.last().first().value(); + QVERIFY(client); QCOMPARE(client->readyForPainting(), true); - QCOMPARE(client->isHiddenInternal(), true); - QCOMPARE(client->internalId(), uuid); - QVERIFY(windowClosedSpy.isEmpty()); - QCOMPARE(effectsWindowHiddenSpy.count(), 2); - QCOMPARE(effectsWindowHiddenSpy.last().first().value(), client->effectWindow()); + // The compositor will respond with a configure event. + QVERIFY(configureRequestedSpy.wait()); + QCOMPARE(configureRequestedSpy.count(), 4); + + // Destroy the test client. shellSurface.reset(); - surface.reset(); - QVERIFY(windowClosedSpy.wait()); - QCOMPARE(windowClosedSpy.count(), 1); - QCOMPARE(effectsWindowHiddenSpy.count(), 2); - QCOMPARE(deletedUuid.isNull(), false); - QCOMPARE(deletedUuid, uuid); + QVERIFY(Test::waitForWindowDestroyed(client)); } void TestXdgShellClient::testDesktopPresenceChanged() @@ -296,43 +253,6 @@ QCOMPARE(desktopPresenceChangedEffectsSpy.first().at(2).toInt(), 2); } -void TestXdgShellClient::testTransientPositionAfterRemap() -{ - // this test simulates the situation that a transient window gets reused and the parent window - // moved between the two usages - QScopedPointer surface(Test::createSurface()); - QScopedPointer shellSurface(Test::createXdgShellStableSurface(surface.data())); - auto c = Test::renderAndWaitForShown(surface.data(), QSize(100, 50), Qt::blue); - QVERIFY(c); - - // create the Transient window - XdgPositioner positioner(QSize(50, 40), QRect(0, 0, 5, 10)); - positioner.setAnchorEdge(Qt::BottomEdge | Qt::RightEdge); - positioner.setGravity(Qt::BottomEdge | Qt::RightEdge); - QScopedPointer transientSurface(Test::createSurface()); - QScopedPointer transientShellSurface(Test::createXdgShellStablePopup(transientSurface.data(), shellSurface.data(), positioner)); - auto transient = Test::renderAndWaitForShown(transientSurface.data(), positioner.initialSize(), Qt::blue); - QVERIFY(transient); - QCOMPARE(transient->frameGeometry(), QRect(c->frameGeometry().topLeft() + QPoint(5, 10), QSize(50, 40))); - - // unmap the transient - QSignalSpy windowHiddenSpy(transient, &AbstractClient::windowHidden); - QVERIFY(windowHiddenSpy.isValid()); - transientSurface->attachBuffer(Buffer::Ptr()); - transientSurface->commit(Surface::CommitFlag::None); - QVERIFY(windowHiddenSpy.wait()); - - // now move the parent surface - c->setFrameGeometry(c->frameGeometry().translated(5, 10)); - - // now map the transient again - QSignalSpy windowShownSpy(transient, &AbstractClient::windowShown); - QVERIFY(windowShownSpy.isValid()); - Test::render(transientSurface.data(), QSize(50, 40), Qt::blue); - QVERIFY(windowShownSpy.wait()); - QCOMPARE(transient->frameGeometry(), QRect(c->frameGeometry().topLeft() + QPoint(5, 10), QSize(50, 40))); -} - void TestXdgShellClient::testWindowOutputs_data() { QTest::addColumn("type"); @@ -430,6 +350,9 @@ void TestXdgShellClient::testFullscreen() { // this test verifies that a window can be properly fullscreened + + XdgShellSurface::States states; + QScopedPointer surface(Test::createSurface()); QFETCH(Test::XdgShellSurfaceType, type); QScopedPointer shellSurface(Test::createXdgShellSurface(type, surface.data())); @@ -445,52 +368,77 @@ QVERIFY(decoSpy.wait()); QCOMPARE(deco->mode(), decoMode); - auto c = Test::renderAndWaitForShown(surface.data(), QSize(100, 50), Qt::blue); - QVERIFY(c); - QVERIFY(c->isActive()); - QCOMPARE(c->layer(), NormalLayer); - QVERIFY(!c->isFullScreen()); - QCOMPARE(c->clientSize(), QSize(100, 50)); - QCOMPARE(c->isDecorated(), decoMode == ServerSideDecoration::Mode::Server); - QCOMPARE(c->clientSizeToFrameSize(c->clientSize()), c->frameGeometry().size()); - QSignalSpy fullscreenChangedSpy(c, &AbstractClient::fullScreenChanged); - QVERIFY(fullscreenChangedSpy.isValid()); - QSignalSpy frameGeometryChangedSpy(c, &AbstractClient::frameGeometryChanged); + auto client = Test::renderAndWaitForShown(surface.data(), QSize(100, 50), Qt::blue); + QVERIFY(client); + QVERIFY(client->isActive()); + QCOMPARE(client->layer(), NormalLayer); + QVERIFY(!client->isFullScreen()); + QCOMPARE(client->clientSize(), QSize(100, 50)); + QCOMPARE(client->isDecorated(), decoMode == ServerSideDecoration::Mode::Server); + QCOMPARE(client->clientSizeToFrameSize(client->clientSize()), client->size()); + + QSignalSpy fullScreenChangedSpy(client, &AbstractClient::fullScreenChanged); + QVERIFY(fullScreenChangedSpy.isValid()); + QSignalSpy frameGeometryChangedSpy(client, &AbstractClient::frameGeometryChanged); QVERIFY(frameGeometryChangedSpy.isValid()); - QSignalSpy sizeChangeRequestedSpy(shellSurface.data(), &XdgShellSurface::sizeChanged); - QVERIFY(sizeChangeRequestedSpy.isValid()); QSignalSpy configureRequestedSpy(shellSurface.data(), &XdgShellSurface::configureRequested); QVERIFY(configureRequestedSpy.isValid()); + // Wait for the compositor to send a configure event with the Activated state. + QVERIFY(configureRequestedSpy.wait()); + QCOMPARE(configureRequestedSpy.count(), 1); + states = configureRequestedSpy.last().at(1).value(); + QVERIFY(states & XdgShellSurface::State::Activated); + + // Ask the compositor to show the window in full screen mode. shellSurface->setFullscreen(true); - QVERIFY(fullscreenChangedSpy.wait()); - QVERIFY(sizeChangeRequestedSpy.wait()); - QCOMPARE(sizeChangeRequestedSpy.count(), 1); - QCOMPARE(sizeChangeRequestedSpy.first().first().toSize(), QSize(screens()->size(0))); - // TODO: should switch to fullscreen once it's updated - QVERIFY(c->isFullScreen()); - QCOMPARE(c->clientSize(), QSize(100, 50)); - QVERIFY(frameGeometryChangedSpy.isEmpty()); + QVERIFY(configureRequestedSpy.wait()); + QCOMPARE(configureRequestedSpy.count(), 2); + states = configureRequestedSpy.last().at(1).value(); + QVERIFY(states & XdgShellSurface::State::Fullscreen); + QCOMPARE(configureRequestedSpy.last().at(0).value(), screens()->size(0)); shellSurface->ackConfigure(configureRequestedSpy.last().at(2).value()); - Test::render(surface.data(), sizeChangeRequestedSpy.first().first().toSize(), Qt::red); + Test::render(surface.data(), configureRequestedSpy.last().at(0).value(), Qt::red); + +#if 0 // TODO: Uncomment when full screen state updates are truly asynchronous. + QVERIFY(fullScreenChangedSpy.wait()); + QCOMPARE(fullScreenChangedSpy.count(), 1); +#else QVERIFY(frameGeometryChangedSpy.wait()); - QCOMPARE(frameGeometryChangedSpy.count(), 1); - QVERIFY(c->isFullScreen()); - QVERIFY(!c->isDecorated()); - QCOMPARE(c->frameGeometry(), QRect(QPoint(0, 0), sizeChangeRequestedSpy.first().first().toSize())); - QCOMPARE(c->layer(), ActiveLayer); + QCOMPARE(fullScreenChangedSpy.count(), 1); +#endif + QVERIFY(client->isFullScreen()); + QVERIFY(!client->isDecorated()); + QCOMPARE(client->layer(), ActiveLayer); + QCOMPARE(client->frameGeometry(), QRect(QPoint(0, 0), screens()->size(0))); - // swap back to normal + // Ask the compositor to show the window in normal mode. shellSurface->setFullscreen(false); - QVERIFY(fullscreenChangedSpy.wait()); - QVERIFY(sizeChangeRequestedSpy.wait()); - QCOMPARE(sizeChangeRequestedSpy.count(), 2); - QCOMPARE(sizeChangeRequestedSpy.last().first().toSize(), QSize(100, 50)); - // TODO: should switch to fullscreen once it's updated - QVERIFY(!c->isFullScreen()); - QCOMPARE(c->layer(), NormalLayer); - QCOMPARE(c->isDecorated(), decoMode == ServerSideDecoration::Mode::Server); + QVERIFY(configureRequestedSpy.wait()); + QCOMPARE(configureRequestedSpy.count(), 3); + states = configureRequestedSpy.last().at(1).value(); + QVERIFY(!(states & XdgShellSurface::State::Fullscreen)); + QCOMPARE(configureRequestedSpy.last().at(0).value(), QSize(100, 50)); + + shellSurface->ackConfigure(configureRequestedSpy.last().at(2).value()); + Test::render(surface.data(), configureRequestedSpy.last().at(0).value(), Qt::blue); + +#if 0 // TODO: Uncomment when full screen state updates are truly asynchronous. + QVERIFY(fullScreenChangedSpy.wait()); + QCOMPARE(fullScreenChangedSpy.count(), 2); +#else + QVERIFY(frameGeometryChangedSpy.wait()); + QCOMPARE(fullScreenChangedSpy.count(), 2); +#endif + QCOMPARE(client->clientSize(), QSize(100, 50)); + QVERIFY(!client->isFullScreen()); + QCOMPARE(client->isDecorated(), decoMode == ServerSideDecoration::Mode::Server); + QCOMPARE(client->layer(), NormalLayer); + + // Destroy the client. + shellSurface.reset(); + QVERIFY(Test::waitForWindowDestroyed(client)); } void TestXdgShellClient::testFullscreenRestore_data() @@ -646,6 +594,9 @@ void TestXdgShellClient::testMaximizedToFullscreen() { // this test verifies that a window can be properly fullscreened after maximizing + + XdgShellSurface::States states; + QScopedPointer surface(Test::createSurface()); QFETCH(Test::XdgShellSurfaceType, type); QScopedPointer shellSurface(Test::createXdgShellSurface(type, surface.data())); @@ -661,67 +612,81 @@ QVERIFY(decoSpy.wait()); QCOMPARE(deco->mode(), decoMode); - auto c = Test::renderAndWaitForShown(surface.data(), QSize(100, 50), Qt::blue); - QVERIFY(c); - QVERIFY(c->isActive()); - QVERIFY(!c->isFullScreen()); - QCOMPARE(c->clientSize(), QSize(100, 50)); - QCOMPARE(c->isDecorated(), decoMode == ServerSideDecoration::Mode::Server); - QSignalSpy fullscreenChangedSpy(c, &AbstractClient::fullScreenChanged); + auto client = Test::renderAndWaitForShown(surface.data(), QSize(100, 50), Qt::blue); + QVERIFY(client); + QVERIFY(client->isActive()); + QVERIFY(!client->isFullScreen()); + QCOMPARE(client->clientSize(), QSize(100, 50)); + QCOMPARE(client->isDecorated(), decoMode == ServerSideDecoration::Mode::Server); + + QSignalSpy fullscreenChangedSpy(client, &AbstractClient::fullScreenChanged); QVERIFY(fullscreenChangedSpy.isValid()); - QSignalSpy frameGeometryChangedSpy(c, &AbstractClient::frameGeometryChanged); + QSignalSpy frameGeometryChangedSpy(client, &AbstractClient::frameGeometryChanged); QVERIFY(frameGeometryChangedSpy.isValid()); - QSignalSpy sizeChangeRequestedSpy(shellSurface.data(), &XdgShellSurface::sizeChanged); - QVERIFY(sizeChangeRequestedSpy.isValid()); QSignalSpy configureRequestedSpy(shellSurface.data(), &XdgShellSurface::configureRequested); QVERIFY(configureRequestedSpy.isValid()); + + // Wait for the compositor to send a configure event with the Activated state. + QVERIFY(configureRequestedSpy.wait()); + QCOMPARE(configureRequestedSpy.count(), 1); + states = configureRequestedSpy.last().at(1).value(); + QVERIFY(states & XdgShellSurface::State::Activated); + + // Ask the compositor to maximize the window. shellSurface->setMaximized(true); - QVERIFY(sizeChangeRequestedSpy.wait()); - QCOMPARE(sizeChangeRequestedSpy.count(), 1); + QVERIFY(configureRequestedSpy.wait()); + QCOMPARE(configureRequestedSpy.count(), 2); + states = configureRequestedSpy.last().at(1).value(); + QVERIFY(states & XdgShellSurface::State::Maximized); shellSurface->ackConfigure(configureRequestedSpy.last().at(2).value()); - Test::render(surface.data(), sizeChangeRequestedSpy.last().first().toSize(), Qt::red); + Test::render(surface.data(), configureRequestedSpy.last().at(0).value(), Qt::red); QVERIFY(frameGeometryChangedSpy.wait()); + QCOMPARE(client->maximizeMode(), MaximizeFull); - QCOMPARE(c->maximizeMode(), MaximizeFull); - QCOMPARE(frameGeometryChangedSpy.isEmpty(), false); - frameGeometryChangedSpy.clear(); - - // fullscreen the window + // Ask the compositor to show the window in full screen mode. shellSurface->setFullscreen(true); - QVERIFY(fullscreenChangedSpy.wait()); - if (decoMode == ServerSideDecoration::Mode::Server) { - QVERIFY(sizeChangeRequestedSpy.wait()); - QCOMPARE(sizeChangeRequestedSpy.count(), 2); - } - QCOMPARE(sizeChangeRequestedSpy.last().first().toSize(), QSize(screens()->size(0))); - // TODO: should switch to fullscreen once it's updated - QVERIFY(c->isFullScreen()); + QVERIFY(configureRequestedSpy.wait()); + QCOMPARE(configureRequestedSpy.count(), 3); + QCOMPARE(configureRequestedSpy.last().at(0).value(), screens()->size(0)); + states = configureRequestedSpy.last().at(1).value(); + QVERIFY(states & XdgShellSurface::State::Maximized); + QVERIFY(states & XdgShellSurface::State::Fullscreen); - // render at the new size shellSurface->ackConfigure(configureRequestedSpy.last().at(2).value()); - Test::render(surface.data(), sizeChangeRequestedSpy.last().first().toSize(), Qt::red); + Test::render(surface.data(), configureRequestedSpy.last().at(0).value(), Qt::red); - QVERIFY(c->isFullScreen()); - QVERIFY(!c->isDecorated()); - QCOMPARE(c->frameGeometry(), QRect(QPoint(0, 0), sizeChangeRequestedSpy.last().first().toSize())); - sizeChangeRequestedSpy.clear(); +#if 0 // TODO: Uncomment when full screen changes are truly asynchronous. + QVERIFY(fullScreenChangedSpy.wait()); + QCOMPARE(fullScreenChangedSpy.count(), 1); +#else + QTRY_COMPARE(fullscreenChangedSpy.count(), 1); +#endif + QCOMPARE(client->maximizeMode(), MaximizeFull); + QVERIFY(client->isFullScreen()); + QVERIFY(!client->isDecorated()); - // swap back to normal + // Switch back to normal mode. shellSurface->setFullscreen(false); shellSurface->setMaximized(false); - QVERIFY(fullscreenChangedSpy.wait()); - if (decoMode == ServerSideDecoration::Mode::Server) { - QVERIFY(sizeChangeRequestedSpy.wait()); - // XDG will legitimately get two updates. They might be batched - if (shellSurface && sizeChangeRequestedSpy.count() == 1) { - QVERIFY(sizeChangeRequestedSpy.wait()); - } - QCOMPARE(sizeChangeRequestedSpy.last().first().toSize(), QSize(100, 50)); - } - // TODO: should switch to fullscreen once it's updated - QVERIFY(!c->isFullScreen()); - QCOMPARE(c->isDecorated(), decoMode == ServerSideDecoration::Mode::Server); + QVERIFY(configureRequestedSpy.wait()); + QCOMPARE(configureRequestedSpy.count(), 4); + QCOMPARE(configureRequestedSpy.last().at(0).value(), QSize(100, 50)); + states = configureRequestedSpy.last().at(1).value(); + QVERIFY(!(states & XdgShellSurface::State::Maximized)); + QVERIFY(!(states & XdgShellSurface::State::Fullscreen)); + + shellSurface->ackConfigure(configureRequestedSpy.last().at(2).value()); + Test::render(surface.data(), configureRequestedSpy.last().at(0).value(), Qt::red); + + QVERIFY(frameGeometryChangedSpy.wait()); + QVERIFY(!client->isFullScreen()); + QCOMPARE(client->isDecorated(), decoMode == ServerSideDecoration::Mode::Server); + QCOMPARE(client->maximizeMode(), MaximizeRestore); + + // Destroy the client. + shellSurface.reset(); + QVERIFY(Test::waitForWindowDestroyed(client)); } void TestXdgShellClient::testWindowOpensLargerThanScreen_data() @@ -753,10 +718,9 @@ auto c = Test::renderAndWaitForShown(surface.data(), screens()->size(0), Qt::blue); QVERIFY(c); QVERIFY(c->isActive()); - QCOMPARE(c->clientSize(), screens()->size(0)); QVERIFY(c->isDecorated()); QEXPECT_FAIL("", "BUG 366632", Continue); - QVERIFY(sizeChangeRequestedSpy.wait(10)); + QCOMPARE(c->frameGeometry(), QRect(QPoint(0, 0), screens()->size(0))); } void TestXdgShellClient::testHidden_data() @@ -1352,17 +1316,17 @@ QVERIFY(frameGeometryChangedSpy.wait()); QCOMPARE(frameGeometryChangedSpy.count(), 2); QCOMPARE(client->frameGeometry().topLeft(), oldPosition); - QCOMPARE(client->frameGeometry().size(), QSize(100, 50)); + QCOMPARE(client->frameGeometry().size(), QSize(90, 40)); QCOMPARE(client->bufferGeometry().topLeft(), oldPosition - QPoint(10, 10)); QCOMPARE(client->bufferGeometry().size(), QSize(100, 50)); - shellSurface->setWindowGeometry(QRect(5, 5, 90, 40)); + shellSurface->setWindowGeometry(QRect(0, 0, 100, 50)); surface->commit(Surface::CommitFlag::None); QVERIFY(frameGeometryChangedSpy.wait()); QCOMPARE(frameGeometryChangedSpy.count(), 3); QCOMPARE(client->frameGeometry().topLeft(), oldPosition); - QCOMPARE(client->frameGeometry().size(), QSize(90, 40)); - QCOMPARE(client->bufferGeometry().topLeft(), oldPosition - QPoint(5, 5)); + QCOMPARE(client->frameGeometry().size(), QSize(100, 50)); + QCOMPARE(client->bufferGeometry().topLeft(), oldPosition); QCOMPARE(client->bufferGeometry().size(), QSize(100, 50)); shellSurface.reset(); @@ -1497,13 +1461,10 @@ client->keyPressEvent(Qt::Key_Enter); QCOMPARE(clientFinishUserMovedResizedSpy.count(), 1); QCOMPARE(workspace()->moveResizeClient(), nullptr); -#if 0 - QEXPECT_FAIL("", "XdgShellClient currently doesn't send final configure event", Abort); QVERIFY(configureRequestedSpy.wait()); QCOMPARE(configureRequestedSpy.count(), 5); states = configureRequestedSpy.last().at(1).value(); QVERIFY(!states.testFlag(XdgShellSurface::State::Resizing)); -#endif shellSurface.reset(); QVERIFY(Test::waitForWindowDestroyed(client)); diff --git a/deleted.cpp b/deleted.cpp --- a/deleted.cpp +++ b/deleted.cpp @@ -25,7 +25,7 @@ #include "group.h" #include "netinfo.h" #include "shadow.h" -#include "xdgshellclient.h" +#include "waylandclient.h" #include "decorations/decoratedclient.h" #include "decorations/decorationrenderer.h" @@ -149,7 +149,7 @@ }); } - m_wasWaylandClient = qobject_cast(c) != nullptr; + m_wasWaylandClient = qobject_cast(c) != nullptr; m_wasX11Client = qobject_cast(c) != nullptr; m_wasPopupWindow = c->isPopupWindow(); m_wasOutline = c->isOutline(); diff --git a/effects.h b/effects.h --- a/effects.h +++ b/effects.h @@ -292,7 +292,6 @@ protected Q_SLOTS: void slotClientShown(KWin::Toplevel*); - void slotWaylandClientShown(KWin::Toplevel*); void slotUnmanagedShown(KWin::Toplevel*); void slotWindowClosed(KWin::Toplevel *c, KWin::Deleted *d); void slotClientMaximized(KWin::AbstractClient *c, MaximizeMode maxMode); diff --git a/effects.cpp b/effects.cpp --- a/effects.cpp +++ b/effects.cpp @@ -55,7 +55,7 @@ #include "composite.h" #include "xcbutils.h" #include "platform.h" -#include "xdgshellclient.h" +#include "waylandclient.h" #include "wayland_server.h" #include "decorations/decorationbridge.h" @@ -260,16 +260,16 @@ if (auto w = waylandServer()) { connect(w, &WaylandServer::shellClientAdded, this, [this](AbstractClient *c) { if (c->readyForPainting()) - slotWaylandClientShown(c); + slotClientShown(c); else - connect(c, &Toplevel::windowShown, this, &EffectsHandlerImpl::slotWaylandClientShown); + connect(c, &Toplevel::windowShown, this, &EffectsHandlerImpl::slotClientShown); }); const auto clients = waylandServer()->clients(); for (AbstractClient *c : clients) { if (c->readyForPainting()) { setupClientConnections(c); } else { - connect(c, &Toplevel::windowShown, this, &EffectsHandlerImpl::slotWaylandClientShown); + connect(c, &Toplevel::windowShown, this, &EffectsHandlerImpl::slotClientShown); } } } @@ -571,13 +571,6 @@ emit windowAdded(c->effectWindow()); } -void EffectsHandlerImpl::slotWaylandClientShown(Toplevel *toplevel) -{ - AbstractClient *client = static_cast(toplevel); - setupClientConnections(client); - emit windowAdded(toplevel->effectWindow()); -} - void EffectsHandlerImpl::slotUnmanagedShown(KWin::Toplevel *t) { // regardless, unmanaged windows are -yet?- not synced anyway Q_ASSERT(qobject_cast(t)); @@ -1738,7 +1731,7 @@ // can still figure out whether it is/was a managed window. managed = toplevel->isClient(); - waylandClient = qobject_cast(toplevel) != nullptr; + waylandClient = qobject_cast(toplevel) != nullptr; x11Client = qobject_cast(toplevel) != nullptr || qobject_cast(toplevel) != nullptr; } diff --git a/effects/fade/package/contents/code/main.js b/effects/fade/package/contents/code/main.js --- a/effects/fade/package/contents/code/main.js +++ b/effects/fade/package/contents/code/main.js @@ -97,9 +97,7 @@ } } effects.windowAdded.connect(fadeInHandler); -effects.windowShown.connect(fadeInHandler); effects.windowClosed.connect(fadeOutHandler); -effects.windowHidden.connect(fadeOutHandler); effects.windowDataChanged.connect(function (window, role) { if (role == Effect.WindowAddedGrabRole) { if (effect.isGrabbed(window, Effect.WindowAddedGrabRole)) { diff --git a/effects/logout/package/contents/code/main.js b/effects/logout/package/contents/code/main.js --- a/effects/logout/package/contents/code/main.js +++ b/effects/logout/package/contents/code/main.js @@ -75,9 +75,7 @@ init: function () { logoutEffect.loadConfig(); effects.windowAdded.connect(logoutEffect.opened); - effects.windowShown.connect(logoutEffect.opened); effects.windowClosed.connect(logoutEffect.closed); - effects.windowHidden.connect(logoutEffect.closed); } }; logoutEffect.init(); diff --git a/effects/slidingpopups/slidingpopups.cpp b/effects/slidingpopups/slidingpopups.cpp --- a/effects/slidingpopups/slidingpopups.cpp +++ b/effects/slidingpopups/slidingpopups.cpp @@ -52,8 +52,6 @@ connect(effects, &EffectsHandler::windowClosed, this, &SlidingPopupsEffect::slideOut); connect(effects, &EffectsHandler::windowDeleted, this, &SlidingPopupsEffect::slotWindowDeleted); connect(effects, &EffectsHandler::propertyNotify, this, &SlidingPopupsEffect::slotPropertyNotify); - connect(effects, &EffectsHandler::windowShown, this, &SlidingPopupsEffect::slideIn); - connect(effects, &EffectsHandler::windowHidden, this, &SlidingPopupsEffect::slideOut); connect(effects, &EffectsHandler::xcbConnectionChanged, this, [this] { m_atom = effects->announceSupportProperty(QByteArrayLiteral("_KDE_SLIDE"), this); diff --git a/placement.cpp b/placement.cpp --- a/placement.cpp +++ b/placement.cpp @@ -210,7 +210,7 @@ * with ideas from xfce. */ - if (!c->size().isValid()) { + if (!c->frameGeometry().isValid()) { return; } @@ -385,7 +385,7 @@ { Q_ASSERT(area.isValid()); - if (!c->size().isValid()) { + if (!c->frameGeometry().isValid()) { return; } diff --git a/wayland_server.h b/wayland_server.h --- a/wayland_server.h +++ b/wayland_server.h @@ -62,23 +62,23 @@ class PlasmaWindowManagementInterface; class OutputManagementInterface; class OutputConfigurationInterface; -class XdgDecorationManagerInterface; -class XdgShellInterface; class XdgForeignInterface; class XdgOutputManagerInterface; class KeyStateInterface; class LinuxDmabufUnstableV1Interface; class LinuxDmabufUnstableV1Buffer; class TabletManagerInterface; +class XdgDecorationManagerV1Interface; } namespace KWin { -class XdgShellClient; class AbstractClient; class Toplevel; +class XdgPopupClient; +class XdgToplevelClient; class KWIN_EXPORT WaylandServer : public QObject { @@ -133,7 +133,7 @@ void removeClient(AbstractClient *c); AbstractClient *findClient(quint32 id) const; AbstractClient *findClient(KWaylandServer::SurfaceInterface *surface) const; - XdgShellClient *findXdgShellClient(KWaylandServer::SurfaceInterface *surface) const; + XdgToplevelClient *findXdgToplevelClient(KWaylandServer::SurfaceInterface *surface) const; /** * @returns a transient parent of a surface imported with the foreign protocol, if any @@ -246,15 +246,16 @@ void shellClientShown(Toplevel *t); quint16 createClientId(KWaylandServer::ClientConnection *c); void destroyInternalConnection(); - template - void createSurface(T *surface); void initScreenLocker(); + void registerXdgGenericClient(AbstractClient *client); + void registerXdgToplevelClient(XdgToplevelClient *client); + void registerXdgPopupClient(XdgPopupClient *client); + void registerShellClient(AbstractClient *client); KWaylandServer::Display *m_display = nullptr; KWaylandServer::CompositorInterface *m_compositor = nullptr; KWaylandServer::SeatInterface *m_seat = nullptr; KWaylandServer::TabletManagerInterface *m_tabletManager = nullptr; KWaylandServer::DataDeviceManagerInterface *m_dataDeviceManager = nullptr; - KWaylandServer::XdgShellInterface *m_xdgShell = nullptr; KWaylandServer::PlasmaShellInterface *m_plasmaShell = nullptr; KWaylandServer::PlasmaWindowManagementInterface *m_windowManagement = nullptr; KWaylandServer::PlasmaVirtualDesktopManagementInterface *m_virtualDesktopManagement = nullptr; @@ -264,7 +265,7 @@ KWaylandServer::ServerSideDecorationPaletteManagerInterface *m_paletteManager = nullptr; KWaylandServer::IdleInterface *m_idle = nullptr; KWaylandServer::XdgOutputManagerInterface *m_xdgOutputManager = nullptr; - KWaylandServer::XdgDecorationManagerInterface *m_xdgDecorationManager = nullptr; + KWaylandServer::XdgDecorationManagerV1Interface *m_xdgDecorationManagerV1 = nullptr; KWaylandServer::LinuxDmabufUnstableV1Interface *m_linuxDmabuf = nullptr; QSet m_linuxDmabufBuffers; struct { diff --git a/wayland_server.cpp b/wayland_server.cpp --- a/wayland_server.cpp +++ b/wayland_server.cpp @@ -23,8 +23,9 @@ #include "composite.h" #include "idle_inhibition.h" #include "screens.h" -#include "xdgshellclient.h" +#include "waylandxdgshellintegration.h" #include "workspace.h" +#include "xdgshellclient.h" // Client #include @@ -59,7 +60,7 @@ #include #include #include -#include +#include #include #include #include @@ -143,52 +144,72 @@ } } -template -void WaylandServer::createSurface(T *surface) +void WaylandServer::registerShellClient(AbstractClient *client) { - if (!Workspace::self()) { - // it's possible that a Surface gets created before Workspace is created - return; + if (client->readyForPainting()) { + emit shellClientAdded(client); + } else { + connect(client, &AbstractClient::windowShown, this, &WaylandServer::shellClientShown); } + m_clients << client; +} + +void WaylandServer::registerXdgToplevelClient(XdgToplevelClient *client) +{ + // TODO: Find a better way and more generic to install extensions. + + SurfaceInterface *surface = client->surface(); if (surface->client() == m_xwayland.client) { - // skip Xwayland clients, those are created using standard X11 way return; } if (surface->client() == m_screenLockerClientConnection) { ScreenLocker::KSldApp::self()->lockScreenShown(); } - XdgShellClient *client = new XdgShellClient(surface); - if (ServerSideDecorationInterface *deco = ServerSideDecorationInterface::get(surface->surface())) { - client->installServerSideDecoration(deco); - } + + registerShellClient(client); + auto it = std::find_if(m_plasmaShellSurfaces.begin(), m_plasmaShellSurfaces.end(), - [client] (PlasmaShellSurfaceInterface *surface) { - return client->surface() == surface->surface(); + [surface] (PlasmaShellSurfaceInterface *plasmaSurface) { + return plasmaSurface->surface() == surface; } ); if (it != m_plasmaShellSurfaces.end()) { client->installPlasmaShellSurface(*it); m_plasmaShellSurfaces.erase(it); } - if (auto menu = m_appMenuManager->appMenuForSurface(surface->surface())) { + if (auto decoration = ServerSideDecorationInterface::get(surface)) { + client->installServerDecoration(decoration); + } + if (auto decoration = XdgToplevelDecorationV1Interface::get(client->shellSurface())) { + client->installXdgDecoration(decoration); + } + if (auto menu = m_appMenuManager->appMenuForSurface(surface)) { client->installAppMenu(menu); } - if (auto palette = m_paletteManager->paletteForSurface(surface->surface())) { + if (auto palette = m_paletteManager->paletteForSurface(surface)) { client->installPalette(palette); } - m_clients << client; - if (client->readyForPainting()) { - emit shellClientAdded(client); - } else { - connect(client, &XdgShellClient::windowShown, this, &WaylandServer::shellClientShown); - } - //not directly connected as the connection is tied to client instead of this - connect(m_XdgForeign, &KWaylandServer::XdgForeignInterface::transientChanged, client, [this](KWaylandServer::SurfaceInterface *child) { + connect(m_XdgForeign, &XdgForeignInterface::transientChanged, client, [this](SurfaceInterface *child) { emit foreignTransientChanged(child); }); } +void WaylandServer::registerXdgGenericClient(AbstractClient *client) +{ + XdgToplevelClient *toplevelClient = qobject_cast(client); + if (toplevelClient) { + registerXdgToplevelClient(toplevelClient); + return; + } + XdgPopupClient *popupClient = qobject_cast(client); + if (popupClient) { + registerShellClient(popupClient); + return; + } + qCDebug(KWIN_CORE) << "Received invalid xdg client:" << client->surface(); +} + class KWinDisplay : public KWaylandServer::FilteredDisplay { public: @@ -316,18 +337,18 @@ m_tabletManager = m_display->createTabletManagerInterface(m_display); - m_xdgShell = m_display->createXdgShell(XdgShellInterfaceVersion::Stable, m_display); - m_xdgShell->create(); - connect(m_xdgShell, &XdgShellInterface::surfaceCreated, this, &WaylandServer::createSurface); - connect(m_xdgShell, &XdgShellInterface::xdgPopupCreated, this, &WaylandServer::createSurface); + auto shellIntegration = new WaylandXdgShellIntegration(this); + connect(shellIntegration, &WaylandXdgShellIntegration::clientCreated, + this, &WaylandServer::registerXdgGenericClient); - m_xdgDecorationManager = m_display->createXdgDecorationManager(m_xdgShell, m_display); - m_xdgDecorationManager->create(); - connect(m_xdgDecorationManager, &XdgDecorationManagerInterface::xdgDecorationInterfaceCreated, this, [this] (XdgDecorationInterface *deco) { - if (XdgShellClient *client = findXdgShellClient(deco->surface()->surface())) { - client->installXdgDecoration(deco); + m_xdgDecorationManagerV1 = m_display->createXdgDecorationManagerV1(m_display); + connect(m_xdgDecorationManagerV1, &XdgDecorationManagerV1Interface::decorationCreated, this, + [this](XdgToplevelDecorationV1Interface *decoration) { + if (XdgToplevelClient *toplevel = findXdgToplevelClient(decoration->toplevel()->surface())) { + toplevel->installXdgDecoration(decoration); + } } - }); + ); m_display->createShm(); m_seat = m_display->createSeat(m_display); @@ -345,32 +366,30 @@ m_plasmaShell->create(); connect(m_plasmaShell, &PlasmaShellInterface::surfaceCreated, [this] (PlasmaShellSurfaceInterface *surface) { - if (XdgShellClient *client = findXdgShellClient(surface->surface())) { + if (XdgToplevelClient *client = findXdgToplevelClient(surface->surface())) { client->installPlasmaShellSurface(surface); - } else { - m_plasmaShellSurfaces << surface; - connect(surface, &QObject::destroyed, this, - [this, surface] { - m_plasmaShellSurfaces.removeOne(surface); - } - ); + return; } + m_plasmaShellSurfaces.append(surface); + connect(surface, &QObject::destroyed, this, [this, surface] { + m_plasmaShellSurfaces.removeOne(surface); + }); } ); m_appMenuManager = m_display->createAppMenuManagerInterface(m_display); m_appMenuManager->create(); connect(m_appMenuManager, &AppMenuManagerInterface::appMenuCreated, [this] (AppMenuInterface *appMenu) { - if (XdgShellClient *client = findXdgShellClient(appMenu->surface())) { + if (XdgToplevelClient *client = findXdgToplevelClient(appMenu->surface())) { client->installAppMenu(appMenu); } } ); m_paletteManager = m_display->createServerSideDecorationPaletteManager(m_display); m_paletteManager->create(); connect(m_paletteManager, &ServerSideDecorationPaletteManagerInterface::paletteCreated, [this] (ServerSideDecorationPaletteInterface *palette) { - if (XdgShellClient *client = findXdgShellClient(palette->surface())) { + if (XdgToplevelClient *client = findXdgToplevelClient(palette->surface())) { client->installPalette(palette); } } @@ -414,14 +433,14 @@ m_decorationManager = m_display->createServerSideDecorationManager(m_display); connect(m_decorationManager, &ServerSideDecorationManagerInterface::decorationCreated, this, - [this] (ServerSideDecorationInterface *deco) { - if (XdgShellClient *c = findXdgShellClient(deco->surface())) { - c->installServerSideDecoration(deco); + [this] (ServerSideDecorationInterface *decoration) { + if (XdgToplevelClient *client = findXdgToplevelClient(decoration->surface())) { + client->installServerDecoration(decoration); } - connect(deco, &ServerSideDecorationInterface::modeRequested, this, - [deco] (ServerSideDecorationManagerInterface::Mode mode) { + connect(decoration, &ServerSideDecorationInterface::modeRequested, this, + [decoration] (ServerSideDecorationManagerInterface::Mode mode) { // always acknowledge the requested mode - deco->setMode(mode); + decoration->setMode(mode); } ); } @@ -463,15 +482,15 @@ return m_XdgForeign->transientFor(surface); } -void WaylandServer::shellClientShown(Toplevel *t) +void WaylandServer::shellClientShown(Toplevel *toplevel) { - XdgShellClient *c = dynamic_cast(t); - if (!c) { - qCWarning(KWIN_CORE) << "Failed to cast a Toplevel which is supposed to be a XdgShellClient to XdgShellClient"; + AbstractClient *client = qobject_cast(toplevel); + if (!client) { + qCWarning(KWIN_CORE) << "Failed to cast a Toplevel which is supposed to be an AbstractClient to AbstractClient"; return; } - disconnect(c, &XdgShellClient::windowShown, this, &WaylandServer::shellClientShown); - emit shellClientAdded(c); + disconnect(client, &AbstractClient::windowShown, this, &WaylandServer::shellClientShown); + emit shellClientAdded(client); } void WaylandServer::initWorkspace() @@ -749,9 +768,9 @@ return nullptr; } -XdgShellClient *WaylandServer::findXdgShellClient(SurfaceInterface *surface) const +XdgToplevelClient *WaylandServer::findXdgToplevelClient(SurfaceInterface *surface) const { - return qobject_cast(findClient(surface)); + return qobject_cast(findClient(surface)); } quint32 WaylandServer::createWindowId(SurfaceInterface *surface) diff --git a/waylandclient.h b/waylandclient.h new file mode 100644 --- /dev/null +++ b/waylandclient.h @@ -0,0 +1,71 @@ +/* + * Copyright (C) 2020 Vlad Zahorodnii + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#pragma once + +#include "abstract_client.h" + +namespace KWin +{ + +class WaylandClient : public AbstractClient +{ + Q_OBJECT + +public: + WaylandClient(KWaylandServer::SurfaceInterface *surface); + + QString captionNormal() const override; + QString captionSuffix() const override; + QStringList activities() const override; + void setOnAllActivities(bool set) override; + void blockActivityUpdates(bool b = true) override; + QPoint clientContentPos() const override; + QRect transparentRect() const override; + quint32 windowId() const override; + pid_t pid() const override; + bool isLockScreen() const override; + bool isInputMethod() const override; + bool isLocalhost() const override; + double opacity() const override; + void setOpacity(double opacity) override; + AbstractClient *findModal(bool allow_itself = false) override; + void resizeWithChecks(const QSize &size, ForceGeometry_t force = NormalGeometrySet) override; + void killWindow() override; + QByteArray windowRole() const override; + + void setCaption(const QString &caption); + +protected: + bool belongsToSameApplication(const AbstractClient *other, SameApplicationChecks checks) const override; + bool belongsToDesktop() const override; + void doSetActive() override; + void updateCaption() override; + +private: + void updateClientArea(); + void updateClientOutputs(); + void updateIcon(); + void updateResourceName(); + + QString m_captionNormal; + QString m_captionSuffix; + double m_opacity = 1.0; + quint32 m_windowId; +}; + +} // namespace KWin diff --git a/waylandclient.cpp b/waylandclient.cpp new file mode 100644 --- /dev/null +++ b/waylandclient.cpp @@ -0,0 +1,286 @@ +/* + * Copyright (C) 2015 Martin Flöser + * Copyright (C) 2018 David Edmundson + * Copyright (C) 2020 Vlad Zahorodnii + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "waylandclient.h" +#include "screens.h" +#include "wayland_server.h" +#include "workspace.h" + +#include +#include +#include + +#include + +#include + +#include +#include + +using namespace KWaylandServer; + +namespace KWin +{ + +WaylandClient::WaylandClient(SurfaceInterface *surface) +{ + // Note that we cannot setup compositing here because we may need to call visibleRect(), + // which in its turn will call bufferGeometry(), which is a pure virtual method. + + setSurface(surface); + + m_windowId = waylandServer()->createWindowId(surface); + + connect(this, &WaylandClient::frameGeometryChanged, + this, &WaylandClient::updateClientOutputs); + connect(this, &WaylandClient::frameGeometryChanged, + this, &WaylandClient::updateClientArea); + connect(this, &WaylandClient::desktopFileNameChanged, + this, &WaylandClient::updateIcon); + connect(screens(), &Screens::changed, this, + &WaylandClient::updateClientOutputs); + + updateResourceName(); + updateIcon(); +} + +QString WaylandClient::captionNormal() const +{ + return m_captionNormal; +} + +QString WaylandClient::captionSuffix() const +{ + return m_captionSuffix; +} + +QStringList WaylandClient::activities() const +{ + return QStringList(); +} + +void WaylandClient::setOnAllActivities(bool set) +{ + Q_UNUSED(set) +} + +void WaylandClient::blockActivityUpdates(bool b) +{ + Q_UNUSED(b) +} + +QPoint WaylandClient::clientContentPos() const +{ + return -clientPos(); +} + +QRect WaylandClient::transparentRect() const +{ + return QRect(); +} + +quint32 WaylandClient::windowId() const +{ + return m_windowId; +} + +pid_t WaylandClient::pid() const +{ + return surface()->client()->processId(); +} + +bool WaylandClient::isLockScreen() const +{ + return surface()->client() == waylandServer()->screenLockerClientConnection(); +} + +bool WaylandClient::isInputMethod() const +{ + return surface()->client() == waylandServer()->inputMethodConnection(); +} + +bool WaylandClient::isLocalhost() const +{ + return true; +} + +double WaylandClient::opacity() const +{ + return m_opacity; +} + +void WaylandClient::setOpacity(double opacity) +{ + const qreal newOpacity = qBound(0.0, opacity, 1.0); + if (newOpacity == m_opacity) { + return; + } + const qreal oldOpacity = m_opacity; + m_opacity = newOpacity; + addRepaintFull(); + emit opacityChanged(this, oldOpacity); +} + +AbstractClient *WaylandClient::findModal(bool allow_itself) +{ + Q_UNUSED(allow_itself) + return nullptr; +} + +void WaylandClient::resizeWithChecks(const QSize &size, ForceGeometry_t force) +{ + const QRect area = workspace()->clientArea(WorkArea, this); + + int width = size.width(); + int height = size.height(); + + // don't allow growing larger than workarea + if (width > area.width()) { + width = area.width(); + } + if (height > area.height()) { + height = area.height(); + } + setFrameGeometry(QRect(x(), y(), width, height), force); +} + +void WaylandClient::killWindow() +{ + if (!surface()) { + return; + } + auto c = surface()->client(); + if (c->processId() == getpid() || c->processId() == 0) { + c->destroy(); + return; + } + ::kill(c->processId(), SIGTERM); + // give it time to terminate and only if terminate fails, try destroy Wayland connection + QTimer::singleShot(5000, c, &ClientConnection::destroy); +} + +QByteArray WaylandClient::windowRole() const +{ + return QByteArray(); +} + +bool WaylandClient::belongsToSameApplication(const AbstractClient *other, SameApplicationChecks checks) const +{ + if (checks.testFlag(SameApplicationCheck::AllowCrossProcesses)) { + if (other->desktopFileName() == desktopFileName()) { + return true; + } + } + if (auto s = other->surface()) { + return s->client() == surface()->client(); + } + return false; +} + +bool WaylandClient::belongsToDesktop() const +{ + const auto clients = waylandServer()->clients(); + + return std::any_of(clients.constBegin(), clients.constEnd(), + [this](const AbstractClient *client) { + if (belongsToSameApplication(client, SameApplicationChecks())) { + return client->isDesktop(); + } + return false; + } + ); +} + +void WaylandClient::updateClientArea() +{ + if (hasStrut()) { + workspace()->updateClientArea(); + } +} + +void WaylandClient::updateClientOutputs() +{ + const auto outputs = waylandServer()->display()->outputs(); + QVector clientOutputs; + clientOutputs.reserve(outputs.count()); + for (OutputInterface *output : outputs) { + const QRect outputGeometry(output->globalPosition(), output->pixelSize() / output->scale()); + if (frameGeometry().intersects(outputGeometry)) { + clientOutputs << output; + } + } + surface()->setOutputs(clientOutputs); +} + +void WaylandClient::updateIcon() +{ + const QString waylandIconName = QStringLiteral("wayland"); + const QString dfIconName = iconFromDesktopFile(); + const QString iconName = dfIconName.isEmpty() ? waylandIconName : dfIconName; + if (iconName == icon().name()) { + return; + } + setIcon(QIcon::fromTheme(iconName)); +} + +void WaylandClient::updateResourceName() +{ + const QFileInfo fileInfo(surface()->client()->executablePath()); + if (fileInfo.exists()) { + setResourceClass(fileInfo.fileName().toUtf8()); + } +} + +void WaylandClient::updateCaption() +{ + const QString oldSuffix = m_captionSuffix; + const auto shortcut = shortcutCaptionSuffix(); + m_captionSuffix = shortcut; + if ((!isSpecialWindow() || isToolbar()) && findClientWithSameCaption()) { + int i = 2; + do { + m_captionSuffix = shortcut + QLatin1String(" <") + QString::number(i) + QLatin1Char('>'); + i++; + } while (findClientWithSameCaption()); + } + if (m_captionSuffix != oldSuffix) { + emit captionChanged(); + } +} + +void WaylandClient::setCaption(const QString &caption) +{ + const QString oldSuffix = m_captionSuffix; + m_captionNormal = caption.simplified(); + updateCaption(); + if (m_captionSuffix == oldSuffix) { + // Don't emit caption change twice it already got emitted by the changing suffix. + emit captionChanged(); + } +} + +void WaylandClient::doSetActive() +{ + if (isActive()) { // TODO: Xwayland clients must be unfocused somewhere else. + StackingUpdatesBlocker blocker(workspace()); + workspace()->focusToNull(); + } +} + +} // namespace KWin diff --git a/waylandshellintegration.h b/waylandshellintegration.h new file mode 100644 --- /dev/null +++ b/waylandshellintegration.h @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2020 Vlad Zahorodnii + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#pragma once + +#include "abstract_client.h" + +namespace KWin +{ + +class WaylandShellIntegration : public QObject +{ + Q_OBJECT + +public: + explicit WaylandShellIntegration(QObject *parent = nullptr); + +Q_SIGNALS: + void clientCreated(AbstractClient *client); +}; + +} // namespace KWin diff --git a/waylandshellintegration.cpp b/waylandshellintegration.cpp new file mode 100644 --- /dev/null +++ b/waylandshellintegration.cpp @@ -0,0 +1,28 @@ +/* + * Copyright (C) 2020 Vlad Zahorodnii + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "waylandshellintegration.h" + +namespace KWin +{ + +WaylandShellIntegration::WaylandShellIntegration(QObject *parent) + : QObject(parent) +{ +} + +} // namespace KWin diff --git a/waylandxdgshellintegration.h b/waylandxdgshellintegration.h new file mode 100644 --- /dev/null +++ b/waylandxdgshellintegration.h @@ -0,0 +1,44 @@ +/* + * Copyright (C) 2020 Vlad Zahorodnii + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#pragma once + +#include "waylandshellintegration.h" + +namespace KWaylandServer +{ +class XdgToplevelInterface; +class XdgPopupInterface; +} + +namespace KWin +{ + +class WaylandXdgShellIntegration : public WaylandShellIntegration +{ + Q_OBJECT + +public: + explicit WaylandXdgShellIntegration(QObject *parent = nullptr); + +private: + void registerXdgToplevel(KWaylandServer::XdgToplevelInterface *toplevel); + void registerXdgPopup(KWaylandServer::XdgPopupInterface *popup); + void createXdgToplevelClient(KWaylandServer::XdgToplevelInterface *surface); +}; + +} // namespace KWin diff --git a/waylandxdgshellintegration.cpp b/waylandxdgshellintegration.cpp new file mode 100644 --- /dev/null +++ b/waylandxdgshellintegration.cpp @@ -0,0 +1,82 @@ +/* + * Copyright (C) 2020 Vlad Zahorodnii + * + * This program is free software; you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation; either version 2 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ + +#include "waylandxdgshellintegration.h" +#include "wayland_server.h" +#include "workspace.h" +#include "xdgshellclient.h" + +#include +#include + +using namespace KWaylandServer; + +namespace KWin +{ + +/** + * The WaylandXdgShellIntegration class is a factory class for xdg-shell clients. + * + * The xdg-shell protocol defines two surface roles - xdg_toplevel and xdg_popup. On the + * compositor side, those roles are represented by XdgToplevelClient and XdgPopupClient, + * respectively. + * + * WaylandXdgShellIntegration monitors for new xdg_toplevel and xdg_popup objects. If it + * detects one, it will create an XdgToplevelClient or XdgPopupClient based on the current + * surface role of the underlying xdg_surface object. + */ + +WaylandXdgShellIntegration::WaylandXdgShellIntegration(QObject *parent) + : WaylandShellIntegration(parent) +{ + XdgShellInterface *shell = waylandServer()->display()->createXdgShell(this); + + connect(shell, &XdgShellInterface::toplevelCreated, + this, &WaylandXdgShellIntegration::registerXdgToplevel); + connect(shell, &XdgShellInterface::popupCreated, + this, &WaylandXdgShellIntegration::registerXdgPopup); +} + +void WaylandXdgShellIntegration::registerXdgToplevel(XdgToplevelInterface *toplevel) +{ + // Note that the client is going to be destroyed and immediately re-created when the + // underlying surface is unmapped. XdgToplevelClient is re-created right away since + // we don't want too loose any client requests that are allowed to be sent prior to + // the first initial commit, e.g. set_maximized or set_fullscreen. + connect(toplevel, &XdgToplevelInterface::resetOccurred, + this, [this, toplevel] { createXdgToplevelClient(toplevel); }); + + createXdgToplevelClient(toplevel); +} + +void WaylandXdgShellIntegration::createXdgToplevelClient(XdgToplevelInterface *toplevel) +{ + if (!workspace()) + return; // TODO: Shouldn't we create the client when workspace is initialized? + + emit clientCreated(new XdgToplevelClient(toplevel)); +} + +void WaylandXdgShellIntegration::registerXdgPopup(XdgPopupInterface *popup) +{ + if (!workspace()) + return; // TODO: Shouldn't we create the client when workspace is initialized? + + emit clientCreated(new XdgPopupClient(popup)); +} + +} // namespace KWin diff --git a/workspace.cpp b/workspace.cpp --- a/workspace.cpp +++ b/workspace.cpp @@ -740,11 +740,6 @@ updateTabbox(); connect(client, &AbstractClient::windowShown, this, [this, client] { updateClientLayer(client); - // TODO: when else should we send the client through placement? - if (client->hasTransientPlacementHint()) { - const QRect area = clientArea(PlacementArea, Screens::self()->current(), client->desktop()); - client->placeIn(area); - } markXStackingOrderAsDirty(); updateStackingOrder(true); updateClientArea(); diff --git a/xdgshellclient.h b/xdgshellclient.h --- a/xdgshellclient.h +++ b/xdgshellclient.h @@ -22,243 +22,269 @@ #pragma once -#include "abstract_client.h" +#include "waylandclient.h" #include +#include +#include + namespace KWaylandServer { -class ServerSideDecorationInterface; -class ServerSideDecorationPaletteInterface; class AppMenuInterface; class PlasmaShellSurfaceInterface; -class XdgDecorationInterface; +class ServerSideDecorationInterface; +class ServerSideDecorationPaletteInterface; +class XdgToplevelDecorationV1Interface; } namespace KWin { -/** - * @brief The reason for which the server pinged a client surface - */ -enum class PingReason { - CloseWindow = 0, - FocusWindow +class XdgSurfaceConfigure +{ +public: + enum ConfigureField { + PositionField = 0x1, + SizeField = 0x2, + }; + Q_DECLARE_FLAGS(ConfigureFields, ConfigureField) + + ConfigureFields presentFields; + QPoint position; + QSize size; + qreal serial; }; -class KWIN_EXPORT XdgShellClient : public AbstractClient +class XdgSurfaceClient : public WaylandClient { Q_OBJECT public: - XdgShellClient(KWaylandServer::XdgShellSurfaceInterface *surface); - XdgShellClient(KWaylandServer::XdgShellPopupInterface *surface); - ~XdgShellClient() override; + explicit XdgSurfaceClient(KWaylandServer::XdgSurfaceInterface *shellSurface); + ~XdgSurfaceClient() override; QRect inputGeometry() const override; QRect bufferGeometry() const override; - QStringList activities() const override; - QPoint clientContentPos() const override; QSize clientSize() const override; + QMatrix4x4 inputTransformation() const override; + void setFrameGeometry(const QRect &rect, ForceGeometry_t force = NormalGeometrySet) override; + using AbstractClient::move; + void move(int x, int y, ForceGeometry_t force = NormalGeometrySet) override; + bool isShown(bool shaded_is_shown) const override; + bool isHiddenInternal() const override; + void hideClient(bool hide) override; + void destroyClient() override; + + QRect frameRectToBufferRect(const QRect &rect) const; + QRect requestedFrameGeometry() const; + QPoint requestedPos() const; + QSize requestedSize() const; + QRect requestedClientGeometry() const; + QSize requestedClientSize() const; + QRect clientGeometry() const; + bool isClosing() const; + bool isHidden() const; + +protected: + void addDamage(const QRegion &damage) override; + + virtual XdgSurfaceConfigure *sendRoleConfigure() const = 0; + virtual void handleRoleCommit(); + virtual bool stateCompare() const; + + XdgSurfaceConfigure *lastAcknowledgedConfigure() const; + void scheduleConfigure(); + void sendConfigure(); + void requestGeometry(const QRect &rect); + void updateGeometry(const QRect &rect); + +private: + void handleConfigureAcknowledged(quint32 serial); + void handleCommit(); + void handleNextWindowGeometry(); + bool haveNextWindowGeometry() const; + void setHaveNextWindowGeometry(); + void resetHaveNextWindowGeometry(); + QRect adjustMoveResizeGeometry(const QRect &rect) const; + void updateGeometryRestoreHack(); + void updateDepth(); + void internalShow(); + void internalHide(); + void cleanGrouping(); + void cleanTabBox(); + + KWaylandServer::XdgSurfaceInterface *m_shellSurface; + QTimer *m_configureTimer; + QQueue m_configureEvents; + QScopedPointer m_lastAcknowledgedConfigure; + QRect m_windowGeometry; + QRect m_requestedFrameGeometry; + QRect m_bufferGeometry; + QRect m_requestedClientGeometry; + QRect m_clientGeometry; + bool m_isClosing = false; + bool m_isHidden = false; + bool m_haveNextWindowGeometry = false; +}; + +class XdgToplevelConfigure final : public XdgSurfaceConfigure +{ +public: + KWaylandServer::XdgToplevelInterface::States states; +}; + +class XdgToplevelClient final : public XdgSurfaceClient +{ + Q_OBJECT + + enum class PingReason { CloseWindow, FocusWindow }; + +public: + explicit XdgToplevelClient(KWaylandServer::XdgToplevelInterface *shellSurface); + ~XdgToplevelClient() override; + + KWaylandServer::XdgToplevelInterface *shellSurface() const; + + void debug(QDebug &stream) const override; + NET::WindowType windowType(bool direct = false, int supported_types = 0) const override; + MaximizeMode maximizeMode() const override; + MaximizeMode requestedMaximizeMode() const override; QSize minSize() const override; QSize maxSize() const override; - QRect transparentRect() const override; - NET::WindowType windowType(bool direct = false, int supported_types = 0) const override; - void debug(QDebug &stream) const override; - double opacity() const override; - void setOpacity(double opacity) override; - QByteArray windowRole() const override; - void blockActivityUpdates(bool b = true) override; - QString captionNormal() const override; - QString captionSuffix() const override; - void closeWindow() override; - AbstractClient *findModal(bool allow_itself = false) override; + bool isFullScreen() const override; + bool isMovableAcrossScreens() const override; + bool isMovable() const override; + bool isResizable() const override; bool isCloseable() const override; bool isFullScreenable() const override; - bool isFullScreen() const override; bool isMaximizable() const override; bool isMinimizable() const override; - bool isMovable() const override; - bool isMovableAcrossScreens() const override; - bool isResizable() const override; - bool isShown(bool shaded_is_shown) const override; - bool isHiddenInternal() const override; - void hideClient(bool hide) override; - MaximizeMode maximizeMode() const override; - MaximizeMode requestedMaximizeMode() const override; + bool isTransient() const override; + bool userCanSetFullScreen() const override; + bool userCanSetNoBorder() const override; bool noBorder() const override; - void setFullScreen(bool set, bool user = true) override; void setNoBorder(bool set) override; void updateDecoration(bool check_workspace_pos, bool force = false) override; - void setOnAllActivities(bool set) override; + void updateColorScheme() override; + bool supportsWindowRules() const override; void takeFocus() override; - bool userCanSetFullScreen() const override; - bool userCanSetNoBorder() const override; bool wantsInput() const override; bool dockWantsInput() const override; - void resizeWithChecks(const QSize &size, ForceGeometry_t force = NormalGeometrySet) override; - void setFrameGeometry(const QRect &rect, ForceGeometry_t force = NormalGeometrySet) override; bool hasStrut() const override; - quint32 windowId() const override; - pid_t pid() const override; - bool isLockScreen() const override; - bool isInputMethod() const override; - bool isInitialPositionSet() const override; - bool isTransient() const override; - bool hasTransientPlacementHint() const override; - QRect transientPlacement(const QRect &bounds) const override; - QMatrix4x4 inputTransformation() const override; void showOnScreenEdge() override; - bool hasPopupGrab() const override; - void popupDone() override; - void updateColorScheme() override; - bool isPopupWindow() const override; - void killWindow() override; - bool isLocalhost() const override; - bool supportsWindowRules() const override; - void destroyClient() override; + bool isInitialPositionSet() const override; + void setFullScreen(bool set, bool user) override; + void closeWindow() override; - void installPlasmaShellSurface(KWaylandServer::PlasmaShellSurfaceInterface *surface); - void installServerSideDecoration(KWaylandServer::ServerSideDecorationInterface *decoration); - void installAppMenu(KWaylandServer::AppMenuInterface *appmenu); + void installAppMenu(KWaylandServer::AppMenuInterface *appMenu); + void installServerDecoration(KWaylandServer::ServerSideDecorationInterface *decoration); void installPalette(KWaylandServer::ServerSideDecorationPaletteInterface *palette); - void installXdgDecoration(KWaylandServer::XdgDecorationInterface *decoration); + void installPlasmaShellSurface(KWaylandServer::PlasmaShellSurfaceInterface *shellSurface); + void installXdgDecoration(KWaylandServer::XdgToplevelDecorationV1Interface *decoration); protected: - void addDamage(const QRegion &damage) override; - bool belongsToSameApplication(const AbstractClient *other, SameApplicationChecks checks) const override; - void doSetActive() override; - bool belongsToDesktop() const override; - Layer layerForDock() const override; - void changeMaximize(bool horizontal, bool vertical, bool adjust) override; + XdgSurfaceConfigure *sendRoleConfigure() const override; + void handleRoleCommit() override; + void doMinimize() override; void doResizeSync() override; + void doSetActive() override; + void doSetFullScreen(); + void doSetMaximized(); + bool doStartMoveResize() override; + void doFinishMoveResize() override; bool acceptsFocus() const override; - void doMinimize() override; - void updateCaption() override; - void doMove(int x, int y) override; + void changeMaximize(bool horizontal, bool vertical, bool adjust) override; + Layer layerForDock() const override; + bool stateCompare() const override; -private Q_SLOTS: - void handleConfigureAcknowledged(quint32 serial); - void handleTransientForChanged(); - void handleWindowClassChanged(const QByteArray &windowClass); - void handleWindowGeometryChanged(const QRect &windowGeometry); - void handleWindowTitleChanged(const QString &title); +private: + void handleWindowTitleChanged(); + void handleWindowClassChanged(); + void handleWindowMenuRequested(KWaylandServer::SeatInterface *seat, + const QPoint &surfacePos, quint32 serial); void handleMoveRequested(KWaylandServer::SeatInterface *seat, quint32 serial); - void handleResizeRequested(KWaylandServer::SeatInterface *seat, quint32 serial, Qt::Edges edges); + void handleResizeRequested(KWaylandServer::SeatInterface *seat, Qt::Edges, quint32 serial); + void handleStatesAcknowledged(const KWaylandServer::XdgToplevelInterface::States &states); + void handleMaximizeRequested(); + void handleUnmaximizeRequested(); + void handleFullscreenRequested(KWaylandServer::OutputInterface *output); + void handleUnfullscreenRequested(); void handleMinimizeRequested(); - void handleMaximizeRequested(bool maximized); - void handleFullScreenRequested(bool fullScreen, KWaylandServer::OutputInterface *output); - void handleWindowMenuRequested(KWaylandServer::SeatInterface *seat, quint32 serial, const QPoint &surfacePos); - void handleGrabRequested(KWaylandServer::SeatInterface *seat, quint32 serial); - void handlePingDelayed(quint32 serial); + void handleTransientForChanged(); + void handleForeignTransientForChanged(KWaylandServer::SurfaceInterface *child); void handlePingTimeout(quint32 serial); + void handlePingDelayed(quint32 serial); void handlePongReceived(quint32 serial); - void handleCommitted(); - -private: - /** - * Called when the shell is created. - */ - void init(); - /** - * Called for the XDG case when the shell surface is committed to the surface. - * At this point all initial properties should have been set by the client. - */ - void finishInit(); - void createWindowId(); - void updateIcon(); - bool shouldExposeToWindowManagement(); - void updateClientOutputs(); - KWaylandServer::XdgShellSurfaceInterface::States xdgSurfaceStates() const; - void updateShowOnScreenEdge(); + void initialize(); void updateMaximizeMode(MaximizeMode maximizeMode); - // called on surface commit and processes all m_pendingConfigureRequests up to m_lastAckedConfigureReqest - void updatePendingGeometry(); - QPoint popupOffset(const QRect &anchorRect, const Qt::Edges anchorEdge, const Qt::Edges gravity, const QSize popupSize) const; - void requestGeometry(const QRect &rect); - void doSetGeometry(const QRect &rect); - void unmap(); - void markAsMapped(); - QRect determineBufferGeometry() const; - void ping(PingReason reason); - static void deleteClient(XdgShellClient *c); - - QRect adjustMoveGeometry(const QRect &rect) const; - QRect adjustResizeGeometry(const QRect &rect) const; - - KWaylandServer::XdgShellSurfaceInterface *m_xdgShellToplevel; - KWaylandServer::XdgShellPopupInterface *m_xdgShellPopup; - - QRect m_bufferGeometry; - QRect m_windowGeometry; - bool m_hasWindowGeometry = false; - - // last size we requested or empty if we haven't sent an explicit request to the client - // if empty the client should choose their own default size - QSize m_requestedClientSize = QSize(0, 0); - - struct PendingConfigureRequest { - //note for wl_shell we have no serial, so serialId and m_lastAckedConfigureRequest will always be 0 - //meaning we treat a surface commit as having processed all requests - quint32 serialId = 0; - // position to apply after a resize operation has been completed - QPoint positionAfterResize; - MaximizeMode maximizeMode; - }; - QVector m_pendingConfigureRequests; - quint32 m_lastAckedConfigureRequest = 0; - - //mode in use by the current buffer - MaximizeMode m_maximizeMode = MaximizeRestore; - //mode we currently want to be, could be pending on client updating, could be not sent yet - MaximizeMode m_requestedMaximizeMode = MaximizeRestore; + void updateFullScreenMode(bool set); + void updateShowOnScreenEdge(); + void setupWindowManagementIntegration(); + void setupPlasmaShellIntegration(); + void sendPing(PingReason reason); - QRect m_geomFsRestore; //size and position of the window before it was set to fullscreen - bool m_closing = false; - quint32 m_windowId = 0; - bool m_unmapped = true; - NET::WindowType m_windowType = NET::Normal; QPointer m_plasmaShellSurface; QPointer m_appMenuInterface; QPointer m_paletteInterface; - KWaylandServer::ServerSideDecorationInterface *m_serverDecoration = nullptr; - KWaylandServer::XdgDecorationInterface *m_xdgDecoration = nullptr; + QPointer m_serverDecoration; + QPointer m_xdgDecoration; + KWaylandServer::XdgToplevelInterface *m_shellSurface; + KWaylandServer::XdgToplevelInterface::States m_requestedStates; + KWaylandServer::XdgToplevelInterface::States m_acknowledgedStates; + QMap m_pings; + QRect m_fullScreenGeometryRestore; + NET::WindowType m_windowType = NET::Normal; + MaximizeMode m_maximizeMode = MaximizeRestore; + MaximizeMode m_requestedMaximizeMode = MaximizeRestore; + bool m_isFullScreen = false; bool m_userNoBorder = false; - bool m_fullScreen = false; - bool m_transient = false; - bool m_hidden = false; - bool m_hasPopupGrab = false; - qreal m_opacity = 1.0; - - class RequestGeometryBlocker { //TODO rename ConfigureBlocker when this class is Xdg only - public: - RequestGeometryBlocker(XdgShellClient *client) - : m_client(client) - { - m_client->m_requestGeometryBlockCounter++; - } - ~RequestGeometryBlocker() - { - m_client->m_requestGeometryBlockCounter--; - if (m_client->m_requestGeometryBlockCounter == 0) { - m_client->requestGeometry(m_client->m_blockedRequestGeometry); - } - } - private: - XdgShellClient *m_client; - }; - friend class RequestGeometryBlocker; - int m_requestGeometryBlockCounter = 0; - QRect m_blockedRequestGeometry; - QString m_caption; - QString m_captionSuffix; - QHash m_pingSerials; + bool m_isTransient = false; +}; + +class XdgPopupClient final : public XdgSurfaceClient +{ + Q_OBJECT + +public: + explicit XdgPopupClient(KWaylandServer::XdgPopupInterface *shellSurface); + ~XdgPopupClient() override; + + void debug(QDebug &stream) const override; + NET::WindowType windowType(bool direct = false, int supported_types = 0) const override; + bool hasPopupGrab() const override; + void popupDone() override; + bool isPopupWindow() const override; + bool isTransient() const override; + bool isResizable() const override; + bool isMovable() const override; + bool isMovableAcrossScreens() const override; + bool hasTransientPlacementHint() const override; + QRect transientPlacement(const QRect &bounds) const override; + bool isCloseable() const override; + void closeWindow() override; + void updateColorScheme() override; + bool noBorder() const override; + bool userCanSetNoBorder() const override; + void setNoBorder(bool set) override; + void updateDecoration(bool check_workspace_pos, bool force = false) override; + void showOnScreenEdge() override; + bool wantsInput() const override; + void takeFocus() override; + bool supportsWindowRules() const override; - bool m_isInitialized = false; +protected: + bool acceptsFocus() const override; + XdgSurfaceConfigure *sendRoleConfigure() const override; - friend class Workspace; +private: + void handleGrabRequested(KWaylandServer::SeatInterface *seat, quint32 serial); + void initialize(); + + KWaylandServer::XdgPopupInterface *m_shellSurface; + bool m_haveExplicitGrab = false; }; -} +} // namespace KWin -Q_DECLARE_METATYPE(KWin::XdgShellClient *) +Q_DECLARE_OPERATORS_FOR_FLAGS(KWin::XdgSurfaceConfigure::ConfigureFields) diff --git a/xdgshellclient.cpp b/xdgshellclient.cpp --- a/xdgshellclient.cpp +++ b/xdgshellclient.cpp @@ -20,913 +20,801 @@ along with this program. If not, see . *********************************************************************/ #include "xdgshellclient.h" -#include "cursor.h" -#include "decorations/decoratedclient.h" -#include "decorations/decorationbridge.h" #include "deleted.h" -#include "placement.h" #include "screenedge.h" #include "screens.h" +#include "subsurfacemonitor.h" +#include "wayland_server.h" +#include "workspace.h" + #ifdef KWIN_BUILD_TABBOX #include "tabbox.h" #endif -#include "virtualdesktops.h" -#include "wayland_server.h" -#include "workspace.h" #include #include #include #include -#include -#include +#include #include -#include #include #include #include -#include #include -#include - -#include - -#include -#include - -#include - -Q_DECLARE_METATYPE(NET::WindowType) +#include +#include using namespace KWaylandServer; namespace KWin { -XdgShellClient::XdgShellClient(XdgShellSurfaceInterface *surface) - : AbstractClient() - , m_xdgShellToplevel(surface) - , m_xdgShellPopup(nullptr) -{ - setSurface(surface->surface()); - init(); -} - -XdgShellClient::XdgShellClient(XdgShellPopupInterface *surface) - : AbstractClient() - , m_xdgShellToplevel(nullptr) - , m_xdgShellPopup(surface) -{ - setSurface(surface->surface()); - init(); -} - -XdgShellClient::~XdgShellClient() = default; - -void XdgShellClient::init() +XdgSurfaceClient::XdgSurfaceClient(XdgSurfaceInterface *shellSurface) + : WaylandClient(shellSurface->surface()) + , m_shellSurface(shellSurface) + , m_configureTimer(new QTimer(this)) { - m_requestGeometryBlockCounter++; - - connect(this, &XdgShellClient::desktopFileNameChanged, this, &XdgShellClient::updateIcon); - createWindowId(); setupCompositing(); - updateIcon(); - - // TODO: Initialize with null rect. - m_frameGeometry = QRect(0, 0, -1, -1); - m_windowGeometry = QRect(0, 0, -1, -1); - - if (waylandServer()->inputMethodConnection() == surface()->client()) { - m_windowType = NET::OnScreenDisplay; - } - - connect(surface(), &SurfaceInterface::unmapped, this, &XdgShellClient::unmap); - connect(surface(), &SurfaceInterface::unbound, this, &XdgShellClient::destroyClient); - connect(surface(), &SurfaceInterface::destroyed, this, &XdgShellClient::destroyClient); - - if (m_xdgShellToplevel) { - connect(m_xdgShellToplevel, &XdgShellSurfaceInterface::destroyed, this, &XdgShellClient::destroyClient); - connect(m_xdgShellToplevel, &XdgShellSurfaceInterface::configureAcknowledged, this, &XdgShellClient::handleConfigureAcknowledged); - - m_caption = m_xdgShellToplevel->title().simplified(); - connect(m_xdgShellToplevel, &XdgShellSurfaceInterface::titleChanged, this, &XdgShellClient::handleWindowTitleChanged); - QTimer::singleShot(0, this, &XdgShellClient::updateCaption); - - connect(m_xdgShellToplevel, &XdgShellSurfaceInterface::moveRequested, this, &XdgShellClient::handleMoveRequested); - connect(m_xdgShellToplevel, &XdgShellSurfaceInterface::resizeRequested, this, &XdgShellClient::handleResizeRequested); - - // Determine the resource name, this is inspired from ICCCM 4.1.2.5 - // the binary name of the invoked client. - QFileInfo info{m_xdgShellToplevel->client()->executablePath()}; - QByteArray resourceName; - if (info.exists()) { - resourceName = info.fileName().toUtf8(); - } - setResourceClass(resourceName, m_xdgShellToplevel->windowClass()); - setDesktopFileName(m_xdgShellToplevel->windowClass()); - connect(m_xdgShellToplevel, &XdgShellSurfaceInterface::windowClassChanged, this, &XdgShellClient::handleWindowClassChanged); - - connect(m_xdgShellToplevel, &XdgShellSurfaceInterface::minimizeRequested, this, &XdgShellClient::handleMinimizeRequested); - connect(m_xdgShellToplevel, &XdgShellSurfaceInterface::maximizedChanged, this, &XdgShellClient::handleMaximizeRequested); - connect(m_xdgShellToplevel, &XdgShellSurfaceInterface::fullscreenChanged, this, &XdgShellClient::handleFullScreenRequested); - connect(m_xdgShellToplevel, &XdgShellSurfaceInterface::windowMenuRequested, this, &XdgShellClient::handleWindowMenuRequested); - connect(m_xdgShellToplevel, &XdgShellSurfaceInterface::transientForChanged, this, &XdgShellClient::handleTransientForChanged); - connect(m_xdgShellToplevel, &XdgShellSurfaceInterface::windowGeometryChanged, this, &XdgShellClient::handleWindowGeometryChanged); - - auto global = static_cast(m_xdgShellToplevel->global()); - connect(global, &XdgShellInterface::pingDelayed, this, &XdgShellClient::handlePingDelayed); - connect(global, &XdgShellInterface::pingTimeout, this, &XdgShellClient::handlePingTimeout); - connect(global, &XdgShellInterface::pongReceived, this, &XdgShellClient::handlePongReceived); - - auto configure = [this] { - if (m_closing) { - return; - } - if (m_requestGeometryBlockCounter != 0 || areGeometryUpdatesBlocked()) { - return; - } - m_xdgShellToplevel->configure(xdgSurfaceStates(), m_requestedClientSize); - }; - connect(this, &AbstractClient::activeChanged, this, configure); - connect(this, &AbstractClient::clientStartUserMovedResized, this, configure); - connect(this, &AbstractClient::clientFinishUserMovedResized, this, configure); - - connect(this, &XdgShellClient::frameGeometryChanged, this, &XdgShellClient::updateClientOutputs); - connect(screens(), &Screens::changed, this, &XdgShellClient::updateClientOutputs); - } else if (m_xdgShellPopup) { - connect(m_xdgShellPopup, &XdgShellPopupInterface::configureAcknowledged, this, &XdgShellClient::handleConfigureAcknowledged); - connect(m_xdgShellPopup, &XdgShellPopupInterface::grabRequested, this, &XdgShellClient::handleGrabRequested); - connect(m_xdgShellPopup, &XdgShellPopupInterface::destroyed, this, &XdgShellClient::destroyClient); - connect(m_xdgShellPopup, &XdgShellPopupInterface::windowGeometryChanged, this, &XdgShellClient::handleWindowGeometryChanged); - } - - // set initial desktop - setDesktop(VirtualDesktopManager::self()->current()); - - // setup shadow integration - updateShadow(); - connect(surface(), &SurfaceInterface::shadowChanged, this, &Toplevel::updateShadow); - - connect(waylandServer(), &WaylandServer::foreignTransientChanged, this, [this](KWaylandServer::SurfaceInterface *child) { - if (child == surface()) { - handleTransientForChanged(); - } - }); - handleTransientForChanged(); - - AbstractClient::updateColorScheme(QString()); - - connect(surface(), &SurfaceInterface::committed, this, &XdgShellClient::finishInit); -} - -void XdgShellClient::finishInit() -{ - disconnect(surface(), &SurfaceInterface::committed, this, &XdgShellClient::finishInit); - - connect(surface(), &SurfaceInterface::committed, this, &XdgShellClient::handleCommitted); - - bool needsPlacement = !isInitialPositionSet(); - - if (supportsWindowRules()) { - setupWindowRules(false); - - const QRect originalGeometry = frameGeometry(); - const QRect ruledGeometry = rules()->checkGeometry(originalGeometry, true); - if (originalGeometry != ruledGeometry) { - setFrameGeometry(ruledGeometry); - } - - maximize(rules()->checkMaximize(maximizeMode(), true)); - - setDesktop(rules()->checkDesktop(desktop(), true)); - setDesktopFileName(rules()->checkDesktopFile(desktopFileName(), true).toUtf8()); - if (rules()->checkMinimize(isMinimized(), true)) { - minimize(true); // No animation. - } - setSkipTaskbar(rules()->checkSkipTaskbar(skipTaskbar(), true)); - setSkipPager(rules()->checkSkipPager(skipPager(), true)); - setSkipSwitcher(rules()->checkSkipSwitcher(skipSwitcher(), true)); - setKeepAbove(rules()->checkKeepAbove(keepAbove(), true)); - setKeepBelow(rules()->checkKeepBelow(keepBelow(), true)); - setShortcut(rules()->checkShortcut(shortcut().toString(), true)); - updateColorScheme(); - - // Don't place the client if its position is set by a rule. - if (rules()->checkPosition(invalidPoint, true) != invalidPoint) { - needsPlacement = false; - } - - // Don't place the client if the maximize state is set by a rule. - if (requestedMaximizeMode() != MaximizeRestore) { - needsPlacement = false; - } - - discardTemporaryRules(); - RuleBook::self()->discardUsed(this, false); // Remove Apply Now rules. - updateWindowRules(Rules::All); - } - - if (isFullScreen()) { - needsPlacement = false; - } - - if (needsPlacement) { - const QRect area = workspace()->clientArea(PlacementArea, Screens::self()->current(), desktop()); - placeIn(area); - } - - m_requestGeometryBlockCounter--; - if (m_requestGeometryBlockCounter == 0) { - requestGeometry(m_blockedRequestGeometry); - } - m_isInitialized = true; -} - -void XdgShellClient::destroyClient() -{ - m_closing = true; -#ifdef KWIN_BUILD_TABBOX - TabBox::TabBox *tabBox = TabBox::TabBox::self(); - if (tabBox->isDisplayed() && tabBox->currentClient() == this) { - tabBox->nextPrev(true); - } + connect(shellSurface, &XdgSurfaceInterface::configureAcknowledged, + this, &XdgSurfaceClient::handleConfigureAcknowledged); + connect(shellSurface, &XdgSurfaceInterface::resetOccurred, + this, &XdgSurfaceClient::destroyClient); + connect(shellSurface->surface(), &SurfaceInterface::committed, + this, &XdgSurfaceClient::handleCommit); + connect(shellSurface->surface(), &SurfaceInterface::shadowChanged, + this, &XdgSurfaceClient::updateShadow); +#if 0 // TODO: Refactor kwin core in order to uncomment this code. + connect(shellSurface->surface(), &SurfaceInterface::mapped, + this, &XdgSurfaceClient::setReadyForPainting); #endif - if (isMoveResize()) { - leaveMoveResize(); - } - - // Replace ShellClient with an instance of Deleted in the stacking order. - Deleted *deleted = Deleted::create(this); - emit windowClosed(this, deleted); + connect(shellSurface->surface(), &SurfaceInterface::unbound, + this, &XdgSurfaceClient::destroyClient); + connect(shellSurface->surface(), &SurfaceInterface::destroyed, + this, &XdgSurfaceClient::destroyClient); - // Remove Force Temporarily rules. - RuleBook::self()->discardUsed(this, true); + // The effective window geometry is determined by two things: (a) the rectangle that bounds + // the main surface and all of its sub-surfaces, (b) the client-specified window geometry, if + // any. If the client hasn't provided the window geometry, we fallback to the bounding sub- + // surface rectangle. If the client has provided the window geometry, we intersect it with + // the bounding rectangle and that will be the effective window geometry. It's worth to point + // out that geometry updates do not occur that frequently, so we don't need to recompute the + // bounding geometry every time the client commits the surface. - destroyWindowManagementInterface(); - destroyDecoration(); + SubSurfaceMonitor *treeMonitor = new SubSurfaceMonitor(surface(), this); - StackingUpdatesBlocker blocker(workspace()); - if (transientFor()) { - transientFor()->removeTransient(this); - } - for (auto it = transients().constBegin(); it != transients().constEnd();) { - if ((*it)->transientFor() == this) { - removeTransient(*it); - it = transients().constBegin(); // restart, just in case something more has changed with the list - } else { - ++it; - } - } + connect(treeMonitor, &SubSurfaceMonitor::subSurfaceAdded, + this, &XdgSurfaceClient::setHaveNextWindowGeometry); + connect(treeMonitor, &SubSurfaceMonitor::subSurfaceRemoved, + this, &XdgSurfaceClient::setHaveNextWindowGeometry); + connect(treeMonitor, &SubSurfaceMonitor::subSurfaceMoved, + this, &XdgSurfaceClient::setHaveNextWindowGeometry); + connect(treeMonitor, &SubSurfaceMonitor::subSurfaceResized, + this, &XdgSurfaceClient::setHaveNextWindowGeometry); + connect(shellSurface, &XdgSurfaceInterface::windowGeometryChanged, + this, &XdgSurfaceClient::setHaveNextWindowGeometry); + connect(surface(), &SurfaceInterface::sizeChanged, + this, &XdgSurfaceClient::setHaveNextWindowGeometry); - waylandServer()->removeClient(this); + // Configure events are not sent immediately, but rather scheduled to be sent when the event + // loop is about to be idle. By doing this, we can avoid sending configure events that do + // nothing, and implementation-wise, it's simpler. - deleted->unrefWindow(); + m_configureTimer->setSingleShot(true); + connect(m_configureTimer, &QTimer::timeout, this, &XdgSurfaceClient::sendConfigure); - m_xdgShellToplevel = nullptr; - m_xdgShellPopup = nullptr; - deleteClient(this); -} + // Unfortunately, AbstractClient::checkWorkspacePosition() operates on the geometry restore + // so we need to initialize it with some reasonable value; otherwise bad things will happen + // when we want to decorate the client or move the client to another screen. This is a hack. -void XdgShellClient::deleteClient(XdgShellClient *c) -{ - delete c; + connect(this, &XdgSurfaceClient::frameGeometryChanged, + this, &XdgSurfaceClient::updateGeometryRestoreHack); } -QRect XdgShellClient::inputGeometry() const +XdgSurfaceClient::~XdgSurfaceClient() { - if (isDecorated()) { - return AbstractClient::inputGeometry(); - } - // TODO: What about sub-surfaces sticking outside the main surface? - return m_bufferGeometry; + qDeleteAll(m_configureEvents); } -QRect XdgShellClient::bufferGeometry() const +QRect XdgSurfaceClient::requestedFrameGeometry() const { - return m_bufferGeometry; + return m_requestedFrameGeometry; } -QStringList XdgShellClient::activities() const +QPoint XdgSurfaceClient::requestedPos() const { - // TODO: implement - return QStringList(); + return m_requestedFrameGeometry.topLeft(); } -QPoint XdgShellClient::clientContentPos() const +QSize XdgSurfaceClient::requestedSize() const { - return -1 * clientPos(); + return m_requestedFrameGeometry.size(); } -QSize XdgShellClient::clientSize() const +QRect XdgSurfaceClient::requestedClientGeometry() const { - const QRect boundingRect = surface()->boundingRect(); - return m_windowGeometry.size().boundedTo(boundingRect.size()); + return m_requestedClientGeometry; } -QSize XdgShellClient::minSize() const +QRect XdgSurfaceClient::inputGeometry() const { - if (m_xdgShellToplevel) { - return rules()->checkMinSize(m_xdgShellToplevel->minimumSize()); - } - return QSize(0, 0); + return isDecorated() ? AbstractClient::inputGeometry() : bufferGeometry(); } -QSize XdgShellClient::maxSize() const +QRect XdgSurfaceClient::bufferGeometry() const { - if (m_xdgShellToplevel) { - return rules()->checkMaxSize(m_xdgShellToplevel->maximumSize()); - } - return QSize(INT_MAX, INT_MAX); + return m_bufferGeometry; } -void XdgShellClient::debug(QDebug &stream) const +QSize XdgSurfaceClient::requestedClientSize() const { - stream.nospace(); - stream << "\'XdgShellClient:" << surface() << ";WMCLASS:" << resourceClass() << ":" - << resourceName() << ";Caption:" << caption() << "\'"; + return requestedClientGeometry().size(); } -bool XdgShellClient::belongsToDesktop() const +QRect XdgSurfaceClient::clientGeometry() const { - const auto clients = waylandServer()->clients(); - - return std::any_of(clients.constBegin(), clients.constEnd(), - [this](const AbstractClient *client) { - if (belongsToSameApplication(client, SameApplicationChecks())) { - return client->isDesktop(); - } - return false; - } - ); + return m_clientGeometry; } -Layer XdgShellClient::layerForDock() const +QSize XdgSurfaceClient::clientSize() const { - if (m_plasmaShellSurface) { - switch (m_plasmaShellSurface->panelBehavior()) { - case PlasmaShellSurfaceInterface::PanelBehavior::WindowsCanCover: - return NormalLayer; - case PlasmaShellSurfaceInterface::PanelBehavior::AutoHide: - return AboveLayer; - case PlasmaShellSurfaceInterface::PanelBehavior::WindowsGoBelow: - case PlasmaShellSurfaceInterface::PanelBehavior::AlwaysVisible: - return DockLayer; - default: - Q_UNREACHABLE(); - break; - } - } - return AbstractClient::layerForDock(); + return m_clientGeometry.size(); } -QRect XdgShellClient::transparentRect() const +QMatrix4x4 XdgSurfaceClient::inputTransformation() const { - // TODO: implement - return QRect(); + QMatrix4x4 transformation; + transformation.translate(-m_bufferGeometry.x(), -m_bufferGeometry.y()); + return transformation; } -NET::WindowType XdgShellClient::windowType(bool direct, int supported_types) const +XdgSurfaceConfigure *XdgSurfaceClient::lastAcknowledgedConfigure() const { - // TODO: implement - Q_UNUSED(direct) - Q_UNUSED(supported_types) - return m_windowType; + return m_lastAcknowledgedConfigure.data(); } -double XdgShellClient::opacity() const +bool XdgSurfaceClient::stateCompare() const { - return m_opacity; + if (m_requestedFrameGeometry != m_frameGeometry) + return true; + if (m_requestedClientGeometry != m_clientGeometry) + return true; + if (m_requestedClientGeometry.isEmpty()) + return true; + return false; } -void XdgShellClient::setOpacity(double opacity) +void XdgSurfaceClient::scheduleConfigure() { - const qreal newOpacity = qBound(0.0, opacity, 1.0); - if (newOpacity == m_opacity) { + if (isClosing()) return; - } - const qreal oldOpacity = m_opacity; - m_opacity = newOpacity; - addRepaintFull(); - emit opacityChanged(this, oldOpacity); -} - -void XdgShellClient::addDamage(const QRegion &damage) -{ - const int offsetX = m_bufferGeometry.x() - frameGeometry().x(); - const int offsetY = m_bufferGeometry.y() - frameGeometry().y(); - repaints_region += damage.translated(offsetX, offsetY); - Toplevel::addDamage(damage); + if (stateCompare()) + m_configureTimer->start(); + else + m_configureTimer->stop(); } -void XdgShellClient::markAsMapped() +void XdgSurfaceClient::sendConfigure() { - if (!m_unmapped) { - return; - } + XdgSurfaceConfigure *configureEvent = sendRoleConfigure(); - m_unmapped = false; - if (!ready_for_painting) { - setReadyForPainting(); - } else { - addRepaintFull(); - emit windowShown(this); - } - if (shouldExposeToWindowManagement()) { - setupWindowManagementInterface(); - } - updateShowOnScreenEdge(); + if (configureEvent->position != pos()) + configureEvent->presentFields |= XdgSurfaceConfigure::PositionField; + if (configureEvent->size != size()) + configureEvent->presentFields |= XdgSurfaceConfigure::SizeField; + + m_configureEvents.append(configureEvent); } -void XdgShellClient::updateDecoration(bool check_workspace_pos, bool force) +void XdgSurfaceClient::handleConfigureAcknowledged(quint32 serial) { - if (!force && - ((!isDecorated() && noBorder()) || (isDecorated() && !noBorder()))) - return; - QRect oldgeom = frameGeometry(); - QRect oldClientGeom = oldgeom.adjusted(borderLeft(), borderTop(), -borderRight(), -borderBottom()); - blockGeometryUpdates(true); - if (force) - destroyDecoration(); - if (!noBorder()) { - createDecoration(oldgeom); - } else - destroyDecoration(); - if (m_serverDecoration && isDecorated()) { - m_serverDecoration->setMode(KWaylandServer::ServerSideDecorationManagerInterface::Mode::Server); - } - if (m_xdgDecoration) { - auto mode = isDecorated() || m_userNoBorder ? XdgDecorationInterface::Mode::ServerSide: XdgDecorationInterface::Mode::ClientSide; - m_xdgDecoration->configure(mode); - if (m_requestGeometryBlockCounter == 0) { - m_xdgShellToplevel->configure(xdgSurfaceStates(), m_requestedClientSize); + while (!m_configureEvents.isEmpty()) { + if (serial < m_configureEvents.first()->serial) { + break; } + m_lastAcknowledgedConfigure.reset(m_configureEvents.takeFirst()); } - updateShadow(); - if (check_workspace_pos) - checkWorkspacePosition(oldgeom, -2, oldClientGeom); - blockGeometryUpdates(false); } -void XdgShellClient::setFrameGeometry(const QRect &rect, ForceGeometry_t force) +void XdgSurfaceClient::handleCommit() { - const QRect newGeometry = rules()->checkGeometry(rect); - - if (areGeometryUpdatesBlocked()) { - // when the GeometryUpdateBlocker exits the current geom is passed to setGeometry - // thus we need to set it here. - m_frameGeometry = newGeometry; - if (pendingGeometryUpdate() == PendingGeometryForced) { - // maximum, nothing needed - } else if (force == ForceGeometrySet) { - setPendingGeometryUpdate(PendingGeometryForced); - } else { - setPendingGeometryUpdate(PendingGeometryNormal); - } + if (!surface()->buffer()) { return; } - if (pendingGeometryUpdate() != PendingGeometryNone) { - // reset geometry to the one before blocking, so that we can compare properly - m_frameGeometry = frameGeometryBeforeUpdateBlocking(); + if (haveNextWindowGeometry()) { + handleNextWindowGeometry(); + resetHaveNextWindowGeometry(); } - const QSize requestedClientSize = newGeometry.size() - QSize(borderLeft() + borderRight(), borderTop() + borderBottom()); + handleRoleCommit(); + m_lastAcknowledgedConfigure.reset(); - if (requestedClientSize == m_windowGeometry.size() && - (m_requestedClientSize.isEmpty() || requestedClientSize == m_requestedClientSize)) { - // size didn't change, and we don't need to explicitly request a new size - doSetGeometry(newGeometry); - updateMaximizeMode(m_requestedMaximizeMode); - } else { - // size did change, Client needs to provide a new buffer - requestGeometry(newGeometry); - } + setReadyForPainting(); + updateDepth(); } -QRect XdgShellClient::determineBufferGeometry() const +void XdgSurfaceClient::handleRoleCommit() { - // Offset of the main surface relative to the frame rect. - const int offsetX = borderLeft() - m_windowGeometry.left(); - const int offsetY = borderTop() - m_windowGeometry.top(); - - QRect bufferGeometry; - bufferGeometry.setX(x() + offsetX); - bufferGeometry.setY(y() + offsetY); - bufferGeometry.setSize(surface()->size()); - - return bufferGeometry; } -void XdgShellClient::doSetGeometry(const QRect &rect) +void XdgSurfaceClient::handleNextWindowGeometry() { - bool frameGeometryIsChanged = false; - bool bufferGeometryIsChanged = false; + const QRect boundingGeometry = surface()->boundingRect(); - if (m_frameGeometry != rect) { - m_frameGeometry = rect; - frameGeometryIsChanged = true; - } + // The effective window geometry is defined as the intersection of the window geometry + // and the rectangle that bounds the main surface and all of its sub-surfaces. If the + // client hasn't specified the window geometry, we must fallback to the bounding geometry. + // Note that the xdg-shell spec is not clear about when exactly we have to clamp the + // window geometry. - const QRect bufferGeometry = determineBufferGeometry(); - if (m_bufferGeometry != bufferGeometry) { - m_bufferGeometry = bufferGeometry; - bufferGeometryIsChanged = true; + m_windowGeometry = m_shellSurface->windowGeometry(); + if (m_windowGeometry.isValid()) { + m_windowGeometry &= boundingGeometry; + } else { + m_windowGeometry = boundingGeometry; } - if (!frameGeometryIsChanged && !bufferGeometryIsChanged) { - return; + if (m_windowGeometry.isEmpty()) { + qCWarning(KWIN_CORE) << "Committed empty window geometry, dealing with a buggy client!"; } - if (m_unmapped && geometryRestore().isEmpty() && !m_frameGeometry.isEmpty()) { - // use first valid geometry as restore geometry - setGeometryRestore(m_frameGeometry); - } + QRect frameGeometry(pos(), clientSizeToFrameSize(m_windowGeometry.size())); - if (frameGeometryIsChanged) { - if (hasStrut()) { - workspace()->updateClientArea(); - } - updateWindowRules(Rules::Position | Rules::Size); - emit frameGeometryChanged(this, frameGeometryBeforeUpdateBlocking()); - } + // We're not done yet. The xdg-shell spec allows clients to attach buffers smaller than + // we asked. Normally, this is not a big deal, but when the client is being interactively + // resized, it may cause the window contents to bounce. In order to counter this, we have + // to "gravitate" the new geometry according to the current move-resize pointer mode so + // the opposite window corner stays still. - emit geometryShapeChanged(this, frameGeometryBeforeUpdateBlocking()); + if (isMoveResize()) { + frameGeometry = adjustMoveResizeGeometry(frameGeometry); + } else if (lastAcknowledgedConfigure()) { + XdgSurfaceConfigure *configureEvent = lastAcknowledgedConfigure(); - addRepaintDuringGeometryUpdates(); - updateGeometryBeforeUpdateBlocking(); + if (configureEvent->presentFields & XdgSurfaceConfigure::PositionField) + frameGeometry.moveTopLeft(configureEvent->position); + } + + updateGeometry(frameGeometry); if (isResize()) { performMoveResize(); } } -void XdgShellClient::doMove(int x, int y) +bool XdgSurfaceClient::haveNextWindowGeometry() const { - Q_UNUSED(x) - Q_UNUSED(y) - m_bufferGeometry = determineBufferGeometry(); + return m_haveNextWindowGeometry || m_lastAcknowledgedConfigure; } -QByteArray XdgShellClient::windowRole() const +void XdgSurfaceClient::setHaveNextWindowGeometry() { - return QByteArray(); + m_haveNextWindowGeometry = true; } -bool XdgShellClient::belongsToSameApplication(const AbstractClient *other, SameApplicationChecks checks) const +void XdgSurfaceClient::resetHaveNextWindowGeometry() { - if (checks.testFlag(SameApplicationCheck::AllowCrossProcesses)) { - if (other->desktopFileName() == desktopFileName()) { - return true; - } - } - if (auto s = other->surface()) { - return s->client() == surface()->client(); - } - return false; + m_haveNextWindowGeometry = false; } -void XdgShellClient::blockActivityUpdates(bool b) +QRect XdgSurfaceClient::adjustMoveResizeGeometry(const QRect &rect) const { - Q_UNUSED(b) -} + QRect geometry = rect; -QString XdgShellClient::captionNormal() const -{ - return m_caption; -} + switch (moveResizePointerMode()) { + case PositionTopLeft: + geometry.moveRight(moveResizeGeometry().right()); + geometry.moveBottom(moveResizeGeometry().bottom()); + break; + case PositionTop: + case PositionTopRight: + geometry.moveLeft(moveResizeGeometry().left()); + geometry.moveBottom(moveResizeGeometry().bottom()); + break; + case PositionRight: + case PositionBottomRight: + case PositionBottom: + case PositionCenter: + geometry.moveLeft(moveResizeGeometry().left()); + geometry.moveTop(moveResizeGeometry().top()); + break; + case PositionBottomLeft: + case PositionLeft: + geometry.moveRight(moveResizeGeometry().right()); + geometry.moveTop(moveResizeGeometry().top()); + break; + } -QString XdgShellClient::captionSuffix() const -{ - return m_captionSuffix; + return geometry; } -void XdgShellClient::updateCaption() -{ - const QString oldSuffix = m_captionSuffix; - const auto shortcut = shortcutCaptionSuffix(); - m_captionSuffix = shortcut; - if ((!isSpecialWindow() || isToolbar()) && findClientWithSameCaption()) { - int i = 2; - do { - m_captionSuffix = shortcut + QLatin1String(" <") + QString::number(i) + QLatin1Char('>'); - i++; - } while (findClientWithSameCaption()); - } - if (m_captionSuffix != oldSuffix) { - emit captionChanged(); +/** + * Sets the frame geometry of the XdgSurfaceClient to \a rect. + * + * Because geometry updates are asynchronous on Wayland, there are no any guarantees that + * the frame geometry will be changed immediately. We may need to send a configure event to + * the client if the current window geometry size and the requested window geometry size + * don't match. frameGeometryChanged() will be emitted when the requested frame geometry + * has been applied. + * + * Notice that the client may attach a buffer smaller than the one in the configure event. + */ +void XdgSurfaceClient::setFrameGeometry(const QRect &rect, ForceGeometry_t force) +{ + m_requestedFrameGeometry = rect; + + // XdgToplevelClient currently doesn't support shaded clients, but let's stick with + // what X11Client does. Hopefully, one day we will be able to unify setFrameGeometry() + // for all AbstractClient subclasses. It's going to be great! + + if (isShade()) { + if (m_requestedFrameGeometry.height() == borderTop() + borderBottom()) { + qCDebug(KWIN_CORE) << "Passed shaded frame geometry to setFrameGeometry()"; + } else { + m_requestedClientGeometry = frameRectToClientRect(m_requestedFrameGeometry); + m_requestedFrameGeometry.setHeight(borderTop() + borderBottom()); + } + } else { + m_requestedClientGeometry = frameRectToClientRect(m_requestedFrameGeometry); } -} -void XdgShellClient::closeWindow() -{ - if (m_xdgShellToplevel && isCloseable()) { - m_xdgShellToplevel->close(); - ping(PingReason::CloseWindow); + if (areGeometryUpdatesBlocked()) { + m_frameGeometry = m_requestedFrameGeometry; + if (pendingGeometryUpdate() == PendingGeometryForced) { + return; + } + if (force == ForceGeometrySet) { + setPendingGeometryUpdate(PendingGeometryForced); + } else { + setPendingGeometryUpdate(PendingGeometryNormal); + } + return; } -} -AbstractClient *XdgShellClient::findModal(bool allow_itself) -{ - Q_UNUSED(allow_itself) - return nullptr; + m_frameGeometry = frameGeometryBeforeUpdateBlocking(); + + // Notice that the window geometry size of (0, 0) has special meaning to xdg shell clients. + // It basically says "pick whatever size you think is the best, dawg." + + if (requestedClientSize() != clientSize()) { + requestGeometry(requestedFrameGeometry()); + } else { + updateGeometry(requestedFrameGeometry()); + } } -bool XdgShellClient::isCloseable() const +void XdgSurfaceClient::move(int x, int y, ForceGeometry_t force) { - if (m_windowType == NET::Desktop || m_windowType == NET::Dock) { - return false; + Q_ASSERT(pendingGeometryUpdate() == PendingGeometryNone || areGeometryUpdatesBlocked()); + QPoint p(x, y); + if (!areGeometryUpdatesBlocked() && p != rules()->checkPosition(p)) { + qCDebug(KWIN_CORE) << "forced position fail:" << p << ":" << rules()->checkPosition(p); } - if (m_xdgShellToplevel) { - return true; + m_requestedFrameGeometry.moveTopLeft(p); + m_requestedClientGeometry.moveTopLeft(framePosToClientPos(p)); + if (force == NormalGeometrySet && m_frameGeometry.topLeft() == p) { + return; } - return false; + m_frameGeometry.moveTopLeft(m_requestedFrameGeometry.topLeft()); + if (areGeometryUpdatesBlocked()) { + if (pendingGeometryUpdate() == PendingGeometryForced) { + return; + } + if (force == ForceGeometrySet) { + setPendingGeometryUpdate(PendingGeometryForced); + } else { + setPendingGeometryUpdate(PendingGeometryNormal); + } + return; + } + m_clientGeometry.moveTopLeft(m_requestedClientGeometry.topLeft()); + m_bufferGeometry = frameRectToBufferRect(m_frameGeometry); + updateWindowRules(Rules::Position); + screens()->setCurrent(this); + workspace()->updateStackingOrder(); + emit frameGeometryChanged(this, frameGeometryBeforeUpdateBlocking()); + addRepaintDuringGeometryUpdates(); + updateGeometryBeforeUpdateBlocking(); } -bool XdgShellClient::isFullScreen() const +void XdgSurfaceClient::requestGeometry(const QRect &rect) { - return m_fullScreen; + m_requestedFrameGeometry = rect; + m_requestedClientGeometry = frameRectToClientRect(rect); + + scheduleConfigure(); // Send the configure event later. } -bool XdgShellClient::isMaximizable() const +void XdgSurfaceClient::updateGeometry(const QRect &rect) { - if (!isResizable()) { - return false; - } - if (rules()->checkMaximize(MaximizeRestore) != MaximizeRestore || rules()->checkMaximize(MaximizeFull) != MaximizeFull) { - return false; - } - return true; + const QRect oldFrameGeometry = m_frameGeometry; + + m_frameGeometry = rect; + m_bufferGeometry = frameRectToBufferRect(rect); + m_clientGeometry = frameRectToClientRect(rect); + + if (oldFrameGeometry == m_frameGeometry) + return; + + updateWindowRules(Rules::Position | Rules::Size); + updateGeometryBeforeUpdateBlocking(); + + emit frameGeometryChanged(this, oldFrameGeometry); + emit geometryShapeChanged(this, oldFrameGeometry); + + addRepaintDuringGeometryUpdates(); } -bool XdgShellClient::isMinimizable() const +/** + * \internal + * \todo We have to check the current frame geometry in checkWorskpacePosition(). + * + * Sets the geometry restore to the first valid frame geometry. This is a HACK! + * + * Unfortunately, AbstractClient::checkWorkspacePosition() operates on the geometry restore + * rather than the current frame geometry, so we have to ensure that it's initialized with + * some reasonable value even if the client is not maximized or quick tiled. + */ +void XdgSurfaceClient::updateGeometryRestoreHack() { - if (!rules()->checkMinimize(true)) { - return false; + if (geometryRestore().isEmpty() && !frameGeometry().isEmpty()) { + setGeometryRestore(frameGeometry()); } - return (!m_plasmaShellSurface || m_plasmaShellSurface->role() == PlasmaShellSurfaceInterface::Role::Normal); } -bool XdgShellClient::isMovable() const +void XdgSurfaceClient::updateDepth() { - if (isFullScreen()) { - return false; - } - if (rules()->checkPosition(invalidPoint) != invalidPoint) { - return false; - } - if (m_plasmaShellSurface) { - return m_plasmaShellSurface->role() == PlasmaShellSurfaceInterface::Role::Normal; - } - if (m_xdgShellPopup) { - return false; + if (surface()->buffer()->hasAlphaChannel() && !isDesktop()) { + setDepth(32); + } else { + setDepth(24); } - return true; } -bool XdgShellClient::isMovableAcrossScreens() const +QRect XdgSurfaceClient::frameRectToBufferRect(const QRect &rect) const { - if (rules()->checkPosition(invalidPoint) != invalidPoint) { - return false; - } - if (m_plasmaShellSurface) { - return m_plasmaShellSurface->role() == PlasmaShellSurfaceInterface::Role::Normal; - } - if (m_xdgShellPopup) { - return false; - } - return true; + const int left = rect.left() + borderLeft() - m_windowGeometry.left(); + const int top = rect.top() + borderTop() - m_windowGeometry.top(); + return QRect(QPoint(left, top), surface()->size()); } -bool XdgShellClient::isResizable() const +void XdgSurfaceClient::addDamage(const QRegion &damage) { - if (isFullScreen()) { - return false; - } - if (rules()->checkSize(QSize()).isValid()) { - return false; - } - if (m_plasmaShellSurface) { - return m_plasmaShellSurface->role() == PlasmaShellSurfaceInterface::Role::Normal; - } - if (m_xdgShellPopup) { - return false; - } - return true; + const int offsetX = m_bufferGeometry.x() - m_frameGeometry.x(); + const int offsetY = m_bufferGeometry.y() - m_frameGeometry.y(); + repaints_region += damage.translated(offsetX, offsetY); + Toplevel::addDamage(damage); } -bool XdgShellClient::isShown(bool shaded_is_shown) const +bool XdgSurfaceClient::isShown(bool shaded_is_shown) const { Q_UNUSED(shaded_is_shown) - return !m_closing && !m_unmapped && !isMinimized() && !m_hidden; + return !isClosing() && !isHidden() && !isMinimized(); } -bool XdgShellClient::isHiddenInternal() const +bool XdgSurfaceClient::isHiddenInternal() const { - return m_unmapped || m_hidden; + return isHidden(); } -void XdgShellClient::hideClient(bool hide) +void XdgSurfaceClient::hideClient(bool hide) { - if (m_hidden == hide) { - return; - } - m_hidden = hide; if (hide) { - addWorkspaceRepaint(visibleRect()); - workspace()->clientHidden(this); - emit windowHidden(this); + internalHide(); } else { - emit windowShown(this); + internalShow(); } } -static bool changeMaximizeRecursion = false; -void XdgShellClient::changeMaximize(bool horizontal, bool vertical, bool adjust) +bool XdgSurfaceClient::isHidden() const { - if (changeMaximizeRecursion) { + return m_isHidden; +} + +void XdgSurfaceClient::internalShow() +{ + if (!isHidden()) { return; } + m_isHidden = false; + addRepaintFull(); + emit windowShown(this); +} - if (!isResizable()) { +void XdgSurfaceClient::internalHide() +{ + if (isHidden()) { return; } - - const QRect clientArea = isElectricBorderMaximizing() ? - workspace()->clientArea(MaximizeArea, Cursors::self()->mouse()->pos(), desktop()) : - workspace()->clientArea(MaximizeArea, this); - - const MaximizeMode oldMode = m_requestedMaximizeMode; - const QRect oldGeometry = frameGeometry(); - - // 'adjust == true' means to update the size only, e.g. after changing workspace size - if (!adjust) { - if (vertical) - m_requestedMaximizeMode = MaximizeMode(m_requestedMaximizeMode ^ MaximizeVertical); - if (horizontal) - m_requestedMaximizeMode = MaximizeMode(m_requestedMaximizeMode ^ MaximizeHorizontal); + if (isMoveResize()) { + leaveMoveResize(); } + m_isHidden = true; + addWorkspaceRepaint(visibleRect()); + workspace()->clientHidden(this); + emit windowHidden(this); +} - m_requestedMaximizeMode = rules()->checkMaximize(m_requestedMaximizeMode); - if (!adjust && m_requestedMaximizeMode == oldMode) { - return; - } +bool XdgSurfaceClient::isClosing() const +{ + return m_isClosing; +} +void XdgSurfaceClient::destroyClient() +{ + m_isClosing = true; + m_configureTimer->stop(); + if (isMoveResize()) { + leaveMoveResize(); + } + cleanTabBox(); + Deleted *deleted = Deleted::create(this); + emit windowClosed(this, deleted); StackingUpdatesBlocker blocker(workspace()); - RequestGeometryBlocker geometryBlocker(this); + RuleBook::self()->discardUsed(this, true); + destroyWindowManagementInterface(); + destroyDecoration(); + cleanGrouping(); + waylandServer()->removeClient(this); + deleted->unrefWindow(); + delete this; +} - // call into decoration update borders - if (isDecorated() && decoration()->client() && !(options->borderlessMaximizedWindows() && m_requestedMaximizeMode == KWin::MaximizeFull)) { - changeMaximizeRecursion = true; - const auto c = decoration()->client().toStrongRef(); - if ((m_requestedMaximizeMode & MaximizeVertical) != (oldMode & MaximizeVertical)) { - emit c->maximizedVerticallyChanged(m_requestedMaximizeMode & MaximizeVertical); - } - if ((m_requestedMaximizeMode & MaximizeHorizontal) != (oldMode & MaximizeHorizontal)) { - emit c->maximizedHorizontallyChanged(m_requestedMaximizeMode & MaximizeHorizontal); - } - if ((m_requestedMaximizeMode == MaximizeFull) != (oldMode == MaximizeFull)) { - emit c->maximizedChanged(m_requestedMaximizeMode & MaximizeFull); +void XdgSurfaceClient::cleanGrouping() +{ + if (transientFor()) { + transientFor()->removeTransient(this); + } + for (auto it = transients().constBegin(); it != transients().constEnd();) { + if ((*it)->transientFor() == this) { + removeTransient(*it); + it = transients().constBegin(); // restart, just in case something more has changed with the list + } else { + ++it; } - changeMaximizeRecursion = false; } +} - if (options->borderlessMaximizedWindows()) { - // triggers a maximize change. - // The next setNoBorder interation will exit since there's no change but the first recursion pullutes the restore geometry - changeMaximizeRecursion = true; - setNoBorder(rules()->checkNoBorder(m_requestedMaximizeMode == MaximizeFull)); - changeMaximizeRecursion = false; +void XdgSurfaceClient::cleanTabBox() +{ +#ifdef KWIN_BUILD_TABBOX + TabBox::TabBox *tabBox = TabBox::TabBox::self(); + if (tabBox->isDisplayed() && tabBox->currentClient() == this) { + tabBox->nextPrev(true); } +#endif +} - // Conditional quick tiling exit points - const auto oldQuickTileMode = quickTileMode(); - if (quickTileMode() != QuickTileMode(QuickTileFlag::None)) { - if (oldMode == MaximizeFull && - !clientArea.contains(geometryRestore().center())) { - // Not restoring on the same screen - // TODO: The following doesn't work for some reason - //quick_tile_mode = QuickTileNone; // And exit quick tile mode manually - } else if ((oldMode == MaximizeVertical && m_requestedMaximizeMode == MaximizeRestore) || - (oldMode == MaximizeFull && m_requestedMaximizeMode == MaximizeHorizontal)) { - // Modifying geometry of a tiled window - updateQuickTileMode(QuickTileFlag::None); // Exit quick tile mode without restoring geometry - } - } +XdgToplevelClient::XdgToplevelClient(XdgToplevelInterface *shellSurface) + : XdgSurfaceClient(shellSurface->xdgSurface()) + , m_shellSurface(shellSurface) +{ + setupWindowManagementIntegration(); + setupPlasmaShellIntegration(); + setDesktop(VirtualDesktopManager::self()->current()); - if (m_requestedMaximizeMode == MaximizeFull) { - setGeometryRestore(oldGeometry); - // TODO: Client has more checks - if (options->electricBorderMaximize()) { - updateQuickTileMode(QuickTileFlag::Maximize); - } else { - updateQuickTileMode(QuickTileFlag::None); - } - if (quickTileMode() != oldQuickTileMode) { - emit quickTileModeChanged(); - } - setFrameGeometry(workspace()->clientArea(MaximizeArea, this)); - workspace()->raiseClient(this); - } else { - if (m_requestedMaximizeMode == MaximizeRestore) { - updateQuickTileMode(QuickTileFlag::None); - } - if (quickTileMode() != oldQuickTileMode) { - emit quickTileModeChanged(); - } + if (waylandServer()->inputMethodConnection() == surface()->client()) + m_windowType = NET::OnScreenDisplay; - if (geometryRestore().isValid()) { - setFrameGeometry(geometryRestore()); - } else { - setFrameGeometry(workspace()->clientArea(PlacementArea, this)); - } - } + connect(shellSurface, &XdgToplevelInterface::windowTitleChanged, + this, &XdgToplevelClient::handleWindowTitleChanged); + connect(shellSurface, &XdgToplevelInterface::windowClassChanged, + this, &XdgToplevelClient::handleWindowClassChanged); + connect(shellSurface, &XdgToplevelInterface::windowMenuRequested, + this, &XdgToplevelClient::handleWindowMenuRequested); + connect(shellSurface, &XdgToplevelInterface::moveRequested, + this, &XdgToplevelClient::handleMoveRequested); + connect(shellSurface, &XdgToplevelInterface::resizeRequested, + this, &XdgToplevelClient::handleResizeRequested); + connect(shellSurface, &XdgToplevelInterface::maximizeRequested, + this, &XdgToplevelClient::handleMaximizeRequested); + connect(shellSurface, &XdgToplevelInterface::unmaximizeRequested, + this, &XdgToplevelClient::handleUnmaximizeRequested); + connect(shellSurface, &XdgToplevelInterface::fullscreenRequested, + this, &XdgToplevelClient::handleFullscreenRequested); + connect(shellSurface, &XdgToplevelInterface::unfullscreenRequested, + this, &XdgToplevelClient::handleUnfullscreenRequested); + connect(shellSurface, &XdgToplevelInterface::minimizeRequested, + this, &XdgToplevelClient::handleMinimizeRequested); + connect(shellSurface, &XdgToplevelInterface::parentXdgToplevelChanged, + this, &XdgToplevelClient::handleTransientForChanged); + connect(shellSurface, &XdgToplevelInterface::initializeRequested, + this, &XdgToplevelClient::initialize); + connect(shellSurface, &XdgToplevelInterface::destroyed, + this, &XdgToplevelClient::destroyClient); + connect(shellSurface->shell(), &XdgShellInterface::pingTimeout, + this, &XdgToplevelClient::handlePingTimeout); + connect(shellSurface->shell(), &XdgShellInterface::pingDelayed, + this, &XdgToplevelClient::handlePingDelayed); + connect(shellSurface->shell(), &XdgShellInterface::pongReceived, + this, &XdgToplevelClient::handlePongReceived); + + connect(waylandServer(), &WaylandServer::foreignTransientChanged, + this, &XdgToplevelClient::handleForeignTransientForChanged); +} + +XdgToplevelClient::~XdgToplevelClient() +{ +} + +XdgToplevelInterface *XdgToplevelClient::shellSurface() const +{ + return m_shellSurface; +} + +void XdgToplevelClient::debug(QDebug &stream) const +{ + stream << this; +} + +NET::WindowType XdgToplevelClient::windowType(bool direct, int supported_types) const +{ + Q_UNUSED(direct) + Q_UNUSED(supported_types) + return m_windowType; } -MaximizeMode XdgShellClient::maximizeMode() const +MaximizeMode XdgToplevelClient::maximizeMode() const { return m_maximizeMode; } -MaximizeMode XdgShellClient::requestedMaximizeMode() const +MaximizeMode XdgToplevelClient::requestedMaximizeMode() const { return m_requestedMaximizeMode; } -bool XdgShellClient::noBorder() const +QSize XdgToplevelClient::minSize() const { - if (m_serverDecoration) { - if (m_serverDecoration->mode() == ServerSideDecorationManagerInterface::Mode::Server) { - return m_userNoBorder || isFullScreen(); - } + return rules()->checkMinSize(m_shellSurface->minimumSize()); +} + +QSize XdgToplevelClient::maxSize() const +{ + return rules()->checkMaxSize(m_shellSurface->maximumSize()); +} + +bool XdgToplevelClient::isFullScreen() const +{ + return m_isFullScreen; +} + +bool XdgToplevelClient::isMovable() const +{ + if (isFullScreen()) { + return false; + } + if (isSpecialWindow() && !isSplash() && !isToolbar()) { + return false; + } + if (rules()->checkPosition(invalidPoint) != invalidPoint) { + return false; + } + return true; +} + +bool XdgToplevelClient::isMovableAcrossScreens() const +{ + if (isSpecialWindow() && !isSplash() && !isToolbar()) { + return false; } - if (m_xdgDecoration && m_xdgDecoration->requestedMode() != XdgDecorationInterface::Mode::ClientSide) { - return m_userNoBorder || isFullScreen(); + if (rules()->checkPosition(invalidPoint) != invalidPoint) { + return false; } return true; } -bool XdgShellClient::isFullScreenable() const +bool XdgToplevelClient::isResizable() const { - if (!rules()->checkFullScreen(true)) { + if (isFullScreen()) { return false; } - return !isSpecialWindow(); + if (isSpecialWindow() || isSplash() || isToolbar()) { + return false; + } + if (rules()->checkSize(QSize()).isValid()) { + return false; + } + const QSize min = minSize(); + const QSize max = maxSize(); + return min.width() < max.width() || min.height() < max.height(); } -void XdgShellClient::setFullScreen(bool set, bool user) +bool XdgToplevelClient::isCloseable() const { - set = rules()->checkFullScreen(set); + return !isDesktop() && !isDock(); +} - const bool wasFullscreen = isFullScreen(); - if (wasFullscreen == set) { - return; +bool XdgToplevelClient::isFullScreenable() const +{ + if (!rules()->checkFullScreen(true)) { + return false; } - if (isSpecialWindow()) { - return; + return !isSpecialWindow(); +} + +bool XdgToplevelClient::isMaximizable() const +{ + if (!isResizable()) { + return false; } - if (user && !userCanSetFullScreen()) { - return; + if (rules()->checkMaximize(MaximizeRestore) != MaximizeRestore || + rules()->checkMaximize(MaximizeFull) != MaximizeFull) { + return false; } + return true; +} - if (wasFullscreen) { - workspace()->updateFocusMousePosition(Cursors::self()->mouse()->pos()); // may cause leave event - } else { - m_geomFsRestore = frameGeometry(); +bool XdgToplevelClient::isMinimizable() const +{ + if (isSpecialWindow() && !isTransient()) { + return false; } - m_fullScreen = set; - - if (set) { - workspace()->raiseClient(this); + if (!rules()->checkMinimize(true)) { + return false; } - RequestGeometryBlocker requestBlocker(this); - StackingUpdatesBlocker blocker1(workspace()); - GeometryUpdatesBlocker blocker2(this); + return true; +} - workspace()->updateClientLayer(this); // active fullscreens get different layer - updateDecoration(false, false); +bool XdgToplevelClient::isTransient() const +{ + return m_isTransient; +} - if (set) { - setFrameGeometry(workspace()->clientArea(FullScreenArea, this)); - } else { - if (m_geomFsRestore.isValid()) { - int currentScreen = screen(); - setFrameGeometry(QRect(m_geomFsRestore.topLeft(), constrainFrameSize(m_geomFsRestore.size()))); - if( currentScreen != screen()) - workspace()->sendClientToScreen( this, currentScreen ); - } else { - // this can happen when the window was first shown already fullscreen, - // so let the client set the size by itself - setFrameGeometry(QRect(workspace()->clientArea(PlacementArea, this).topLeft(), QSize(0, 0))); +bool XdgToplevelClient::userCanSetFullScreen() const +{ + return true; +} + +bool XdgToplevelClient::userCanSetNoBorder() const +{ + if (m_serverDecoration) { + switch (m_serverDecoration->mode()) { + case ServerSideDecorationManagerInterface::Mode::Server: + return !isFullScreen() && !isShade(); + case ServerSideDecorationManagerInterface::Mode::Client: + case ServerSideDecorationManagerInterface::Mode::None: + return false; + } + } + if (m_xdgDecoration) { + switch (m_xdgDecoration->preferredMode()) { + case XdgToplevelDecorationV1Interface::Mode::Server: + case XdgToplevelDecorationV1Interface::Mode::Undefined: + return !isFullScreen() && !isShade(); + case XdgToplevelDecorationV1Interface::Mode::Client: + return false; + } + } + return false; +} + +bool XdgToplevelClient::noBorder() const +{ + if (m_serverDecoration) { + switch (m_serverDecoration->mode()) { + case ServerSideDecorationManagerInterface::Mode::Server: + return m_userNoBorder || isFullScreen(); + case ServerSideDecorationManagerInterface::Mode::Client: + case ServerSideDecorationManagerInterface::Mode::None: + return true; } } - - updateWindowRules(Rules::Fullscreen|Rules::Position|Rules::Size); - emit fullScreenChanged(); + if (m_xdgDecoration) { + switch (m_xdgDecoration->preferredMode()) { + case XdgToplevelDecorationV1Interface::Mode::Server: + case XdgToplevelDecorationV1Interface::Mode::Undefined: + return m_userNoBorder || isFullScreen(); + case XdgToplevelDecorationV1Interface::Mode::Client: + return true; + } + } + return true; } -void XdgShellClient::setNoBorder(bool set) +void XdgToplevelClient::setNoBorder(bool set) { if (!userCanSetNoBorder()) { return; @@ -940,252 +828,273 @@ updateWindowRules(Rules::NoBorder); } -void XdgShellClient::setOnAllActivities(bool set) -{ - Q_UNUSED(set) -} - -void XdgShellClient::takeFocus() +void XdgToplevelClient::updateDecoration(bool check_workspace_pos, bool force) { - if (rules()->checkAcceptFocus(wantsInput())) { - if (m_xdgShellToplevel) { - ping(PingReason::FocusWindow); + if (!force && ((!isDecorated() && noBorder()) || (isDecorated() && !noBorder()))) { + return; + } + const QRect oldFrameGeometry = frameGeometry(); + const QRect oldClientGeometry = clientGeometry(); + blockGeometryUpdates(true); + if (force) { + destroyDecoration(); + } + if (!noBorder()) { + createDecoration(oldFrameGeometry); + } else { + destroyDecoration(); + } + if (m_serverDecoration && isDecorated()) { + m_serverDecoration->setMode(ServerSideDecorationManagerInterface::Mode::Server); + } + if (m_xdgDecoration) { + if (isDecorated() || m_userNoBorder) { + m_xdgDecoration->sendConfigure(XdgToplevelDecorationV1Interface::Mode::Server); + } else { + m_xdgDecoration->sendConfigure(XdgToplevelDecorationV1Interface::Mode::Client); } - setActive(true); + scheduleConfigure(); } - - if (!keepAbove() && !isOnScreenDisplay() && !belongsToDesktop()) { - workspace()->setShowingDesktop(false); + updateShadow(); + if (check_workspace_pos) { + checkWorkspacePosition(oldFrameGeometry, -2, oldClientGeometry); } + blockGeometryUpdates(false); } -void XdgShellClient::doSetActive() +bool XdgToplevelClient::supportsWindowRules() const { - if (!isActive()) { - return; - } - StackingUpdatesBlocker blocker(workspace()); - workspace()->focusToNull(); + return !m_plasmaShellSurface; } -bool XdgShellClient::userCanSetFullScreen() const +bool XdgToplevelClient::hasStrut() const { - if (m_xdgShellToplevel) { - return true; + if (!isShown(true)) { + return false; } - return false; + if (!m_plasmaShellSurface) { + return false; + } + if (m_plasmaShellSurface->role() != PlasmaShellSurfaceInterface::Role::Panel) { + return false; + } + return m_plasmaShellSurface->panelBehavior() == PlasmaShellSurfaceInterface::PanelBehavior::AlwaysVisible; } -bool XdgShellClient::userCanSetNoBorder() const +void XdgToplevelClient::showOnScreenEdge() { - if (m_serverDecoration && m_serverDecoration->mode() == ServerSideDecorationManagerInterface::Mode::Server) { - return !isFullScreen() && !isShade(); + if (!m_plasmaShellSurface) { + return; } - if (m_xdgDecoration && m_xdgDecoration->requestedMode() != XdgDecorationInterface::Mode::ClientSide) { - return !isFullScreen() && !isShade(); + hideClient(false); + workspace()->raiseClient(this); + if (m_plasmaShellSurface->panelBehavior() == PlasmaShellSurfaceInterface::PanelBehavior::AutoHide) { + m_plasmaShellSurface->showAutoHidingPanel(); } - return false; } -bool XdgShellClient::wantsInput() const +bool XdgToplevelClient::isInitialPositionSet() const { - return rules()->checkAcceptFocus(acceptsFocus()); + return m_plasmaShellSurface ? m_plasmaShellSurface->isPositionSet() : false; } -bool XdgShellClient::acceptsFocus() const +void XdgToplevelClient::closeWindow() { - if (waylandServer()->inputMethodConnection() == surface()->client()) { - return false; + if (isCloseable()) { + sendPing(PingReason::CloseWindow); + m_shellSurface->sendClose(); } - if (m_plasmaShellSurface) { - if (m_plasmaShellSurface->role() == PlasmaShellSurfaceInterface::Role::OnScreenDisplay || - m_plasmaShellSurface->role() == PlasmaShellSurfaceInterface::Role::ToolTip) { - return false; - } +} - if (m_plasmaShellSurface->role() == PlasmaShellSurfaceInterface::Role::Notification || - m_plasmaShellSurface->role() == PlasmaShellSurfaceInterface::Role::CriticalNotification) { - return m_plasmaShellSurface->panelTakesFocus(); - } - } - if (m_closing) { - // a closing window does not accept focus - return false; - } - if (m_unmapped) { - // an unmapped window does not accept focus - return false; - } - if (m_xdgShellToplevel) { - // TODO: proper +XdgSurfaceConfigure *XdgToplevelClient::sendRoleConfigure() const +{ + const quint32 serial = m_shellSurface->sendConfigure(requestedClientSize(), m_requestedStates); + + XdgToplevelConfigure *configureEvent = new XdgToplevelConfigure(); + configureEvent->position = requestedPos(); + configureEvent->size = requestedSize(); + configureEvent->states = m_requestedStates; + configureEvent->serial = serial; + + return configureEvent; +} + +bool XdgToplevelClient::stateCompare() const +{ + if (m_requestedStates != m_acknowledgedStates) return true; - } - return false; + return XdgSurfaceClient::stateCompare(); } -void XdgShellClient::createWindowId() +void XdgToplevelClient::handleRoleCommit() { - m_windowId = waylandServer()->createWindowId(surface()); + auto configureEvent = static_cast(lastAcknowledgedConfigure()); + if (configureEvent) + handleStatesAcknowledged(configureEvent->states); } -pid_t XdgShellClient::pid() const +void XdgToplevelClient::doMinimize() { - return surface()->client()->processId(); + if (isMinimized()) { + workspace()->clientHidden(this); + } else { + emit windowShown(this); + } + workspace()->updateMinimizedOfTransients(this); } -bool XdgShellClient::isLockScreen() const +void XdgToplevelClient::doResizeSync() { - return surface()->client() == waylandServer()->screenLockerClientConnection(); + requestGeometry(moveResizeGeometry()); } -bool XdgShellClient::isInputMethod() const +void XdgToplevelClient::doSetActive() { - return surface()->client() == waylandServer()->inputMethodConnection(); + WaylandClient::doSetActive(); + + if (isActive()) + m_requestedStates |= XdgToplevelInterface::State::Activated; + else + m_requestedStates &= ~XdgToplevelInterface::State::Activated; + + scheduleConfigure(); } -void XdgShellClient::requestGeometry(const QRect &rect) +void XdgToplevelClient::doSetFullScreen() { - if (m_requestGeometryBlockCounter != 0) { - m_blockedRequestGeometry = rect; - return; - } + if (isFullScreen()) + m_requestedStates |= XdgToplevelInterface::State::FullScreen; + else + m_requestedStates &= ~XdgToplevelInterface::State::FullScreen; - QSize size; - if (rect.isValid()) { - size = rect.size() - QSize(borderLeft() + borderRight(), borderTop() + borderBottom()); - } else { - size = QSize(0, 0); - } - m_requestedClientSize = size; + scheduleConfigure(); +} - quint64 serialId = 0; +void XdgToplevelClient::doSetMaximized() +{ + if (requestedMaximizeMode() & MaximizeHorizontal) + m_requestedStates |= XdgToplevelInterface::State::MaximizedHorizontal; + else + m_requestedStates &= ~XdgToplevelInterface::State::MaximizedHorizontal; - if (m_xdgShellToplevel) { - serialId = m_xdgShellToplevel->configure(xdgSurfaceStates(), size); - } - if (m_xdgShellPopup) { - auto parent = transientFor(); - if (parent) { - const QPoint globalClientContentPos = parent->frameGeometry().topLeft() + parent->clientPos(); - const QPoint relativeOffset = rect.topLeft() - globalClientContentPos; - serialId = m_xdgShellPopup->configure(QRect(relativeOffset, size)); - } - } + if (requestedMaximizeMode() & MaximizeVertical) + m_requestedStates |= XdgToplevelInterface::State::MaximizedVertical; + else + m_requestedStates &= ~XdgToplevelInterface::State::MaximizedVertical; - if (rect.isValid()) { //if there's no requested size, then there's implicity no positional information worth using - PendingConfigureRequest configureRequest; - configureRequest.serialId = serialId; - configureRequest.positionAfterResize = rect.topLeft(); - configureRequest.maximizeMode = m_requestedMaximizeMode; - m_pendingConfigureRequests.append(configureRequest); - } + scheduleConfigure(); +} - m_blockedRequestGeometry = QRect(); +bool XdgToplevelClient::doStartMoveResize() +{ + if (moveResizePointerMode() != PositionCenter) + m_requestedStates |= XdgToplevelInterface::State::Resizing; + + scheduleConfigure(); + return true; } -void XdgShellClient::updatePendingGeometry() +void XdgToplevelClient::doFinishMoveResize() { - QPoint position = pos(); - MaximizeMode maximizeMode = m_maximizeMode; - for (auto it = m_pendingConfigureRequests.begin(); it != m_pendingConfigureRequests.end(); it++) { - if (it->serialId > m_lastAckedConfigureRequest) { - //this serial is not acked yet, therefore we know all future serials are not - break; - } - if (it->serialId == m_lastAckedConfigureRequest) { - if (position != it->positionAfterResize) { - addLayerRepaint(frameGeometry()); - } - position = it->positionAfterResize; - maximizeMode = it->maximizeMode; + m_requestedStates &= ~XdgToplevelInterface::State::Resizing; + scheduleConfigure(); +} - m_pendingConfigureRequests.erase(m_pendingConfigureRequests.begin(), ++it); - break; - } - //else serialId < m_lastAckedConfigureRequest and the state is now irrelevant and can be ignored - } - QRect geometry = QRect(position, adjustedSize()); - if (isMove()) { - geometry = adjustMoveGeometry(geometry); +void XdgToplevelClient::takeFocus() +{ + if (wantsInput()) { + sendPing(PingReason::FocusWindow); + setActive(true); } - if (isResize()) { - geometry = adjustResizeGeometry(geometry); + if (!keepAbove() && !isOnScreenDisplay() && !belongsToDesktop()) { + workspace()->setShowingDesktop(false); } - doSetGeometry(geometry); - updateMaximizeMode(maximizeMode); } -void XdgShellClient::handleConfigureAcknowledged(quint32 serial) +bool XdgToplevelClient::wantsInput() const { - m_lastAckedConfigureRequest = serial; + return rules()->checkAcceptFocus(acceptsFocus()); } -void XdgShellClient::handleTransientForChanged() +bool XdgToplevelClient::dockWantsInput() const { - SurfaceInterface *transientSurface = nullptr; - if (m_xdgShellToplevel) { - if (auto transient = m_xdgShellToplevel->transientFor().data()) { - transientSurface = transient->surface(); + if (m_plasmaShellSurface) { + if (m_plasmaShellSurface->role() == PlasmaShellSurfaceInterface::Role::Panel) { + return m_plasmaShellSurface->panelTakesFocus(); } } - if (m_xdgShellPopup) { - transientSurface = m_xdgShellPopup->transientFor().data(); - } - if (!transientSurface) { - transientSurface = waylandServer()->findForeignTransientForSurface(surface()); + return false; +} + +bool XdgToplevelClient::acceptsFocus() const +{ + if (isInputMethod()) { + return false; } - AbstractClient *transientClient = waylandServer()->findClient(transientSurface); - if (transientClient != transientFor()) { - // Remove from main client. - if (transientFor()) { - transientFor()->removeTransient(this); + if (m_plasmaShellSurface) { + if (m_plasmaShellSurface->role() == PlasmaShellSurfaceInterface::Role::OnScreenDisplay || + m_plasmaShellSurface->role() == PlasmaShellSurfaceInterface::Role::ToolTip) { + return false; } - setTransientFor(transientClient); - if (transientClient) { - transientClient->addTransient(this); + if (m_plasmaShellSurface->role() == PlasmaShellSurfaceInterface::Role::Notification || + m_plasmaShellSurface->role() == PlasmaShellSurfaceInterface::Role::CriticalNotification) { + return m_plasmaShellSurface->panelTakesFocus(); } } - m_transient = (transientSurface != nullptr); + return !isClosing() && readyForPainting(); } -void XdgShellClient::handleWindowClassChanged(const QByteArray &windowClass) +Layer XdgToplevelClient::layerForDock() const { - setResourceClass(resourceName(), windowClass); - if (m_isInitialized && supportsWindowRules()) { - setupWindowRules(true); - applyWindowRules(); + if (m_plasmaShellSurface) { + switch (m_plasmaShellSurface->panelBehavior()) { + case PlasmaShellSurfaceInterface::PanelBehavior::WindowsCanCover: + return NormalLayer; + case PlasmaShellSurfaceInterface::PanelBehavior::AutoHide: + return AboveLayer; + case PlasmaShellSurfaceInterface::PanelBehavior::WindowsGoBelow: + case PlasmaShellSurfaceInterface::PanelBehavior::AlwaysVisible: + return DockLayer; + default: + Q_UNREACHABLE(); + break; + } } - setDesktopFileName(windowClass); + return AbstractClient::layerForDock(); } -void XdgShellClient::handleWindowGeometryChanged(const QRect &windowGeometry) +void XdgToplevelClient::handleWindowTitleChanged() { - m_windowGeometry = windowGeometry; - m_hasWindowGeometry = true; + setCaption(m_shellSurface->windowTitle()); } -void XdgShellClient::handleWindowTitleChanged(const QString &title) +void XdgToplevelClient::handleWindowClassChanged() { - const QString oldSuffix = m_captionSuffix; - m_caption = title.simplified(); - updateCaption(); - if (m_captionSuffix == oldSuffix) { - // Don't emit caption change twice it already got emitted by the changing suffix. - emit captionChanged(); + const QByteArray applicationId = m_shellSurface->windowClass().toUtf8(); + setResourceClass(resourceName(), applicationId); + if (shellSurface()->isConfigured() && supportsWindowRules()) { + evaluateWindowRules(); } + setDesktopFileName(applicationId); } -void XdgShellClient::handleMoveRequested(SeatInterface *seat, quint32 serial) +void XdgToplevelClient::handleWindowMenuRequested(SeatInterface *seat, const QPoint &surfacePos, + quint32 serial) +{ + Q_UNUSED(seat) + Q_UNUSED(serial) + performMouseCommand(Options::MouseOperationsMenu, pos() + surfacePos); +} + +void XdgToplevelClient::handleMoveRequested(SeatInterface *seat, quint32 serial) { - // FIXME: Check the seat and serial. Q_UNUSED(seat) Q_UNUSED(serial) performMouseCommand(Options::MouseMove, Cursors::self()->mouse()->pos()); } -void XdgShellClient::handleResizeRequested(SeatInterface *seat, quint32 serial, Qt::Edges edges) +void XdgToplevelClient::handleResizeRequested(SeatInterface *seat, Qt::Edges edges, quint32 serial) { - // FIXME: Check the seat and serial. Q_UNUSED(seat) Q_UNUSED(serial) if (!isResizable() || isShade()) { @@ -1219,129 +1128,295 @@ updateCursor(); } -void XdgShellClient::handleMinimizeRequested() +void XdgToplevelClient::handleStatesAcknowledged(const XdgToplevelInterface::States &states) +{ + const XdgToplevelInterface::States delta = m_acknowledgedStates ^ states; + + if (delta & XdgToplevelInterface::State::Maximized) { + MaximizeMode maximizeMode = MaximizeRestore; + if (states & XdgToplevelInterface::State::MaximizedHorizontal) { + maximizeMode = MaximizeMode(maximizeMode | MaximizeHorizontal); + } + if (states & XdgToplevelInterface::State::MaximizedVertical) { + maximizeMode = MaximizeMode(maximizeMode | MaximizeVertical); + } + updateMaximizeMode(maximizeMode); + } + if (delta & XdgToplevelInterface::State::FullScreen) { + updateFullScreenMode(states & XdgToplevelInterface::State::FullScreen); + } + + m_acknowledgedStates = states; +} + +void XdgToplevelClient::handleMaximizeRequested() +{ + maximize(MaximizeFull); + scheduleConfigure(); +} + +void XdgToplevelClient::handleUnmaximizeRequested() +{ + maximize(MaximizeRestore); + scheduleConfigure(); +} + +void XdgToplevelClient::handleFullscreenRequested(OutputInterface *output) +{ + Q_UNUSED(output) + setFullScreen(/* set */ true, /* user */ false); + scheduleConfigure(); +} + +void XdgToplevelClient::handleUnfullscreenRequested() +{ + setFullScreen(/* set */ false, /* user */ false); + scheduleConfigure(); +} + +void XdgToplevelClient::handleMinimizeRequested() { performMouseCommand(Options::MouseMinimize, Cursors::self()->mouse()->pos()); } -void XdgShellClient::handleMaximizeRequested(bool maximized) +void XdgToplevelClient::handleTransientForChanged() +{ + SurfaceInterface *transientForSurface = nullptr; + if (XdgToplevelInterface *parentToplevel = m_shellSurface->parentXdgToplevel()) { + transientForSurface = parentToplevel->surface(); + } + if (!transientForSurface) { + transientForSurface = waylandServer()->findForeignTransientForSurface(surface()); + } + AbstractClient *transientForClient = waylandServer()->findClient(transientForSurface); + if (transientForClient != transientFor()) { + if (transientFor()) { + transientFor()->removeTransient(this); + } + if (transientForClient) { + transientForClient->addTransient(this); + } + setTransientFor(transientForClient); + } + m_isTransient = transientForClient; +} + +void XdgToplevelClient::handleForeignTransientForChanged(SurfaceInterface *child) +{ + if (surface() == child) { + handleTransientForChanged(); + } +} + +void XdgToplevelClient::handlePingTimeout(quint32 serial) { - // If the maximized state of the client hasn't been changed due to a window - // rule or because the requested state is the same as the current, then the - // compositor still has to send a configure event. - RequestGeometryBlocker blocker(this); + auto pingIt = m_pings.find(serial); + if (pingIt == m_pings.end()) { + return; + } + if (pingIt.value() == PingReason::CloseWindow) { + qCDebug(KWIN_CORE) << "Final ping timeout on a close attempt, asking to kill:" << caption(); + + // For internal windows, killing the window will delete this. + QPointer guard(this); + killWindow(); + if (!guard) { + return; + } + } + m_pings.erase(pingIt); +} - maximize(maximized ? MaximizeFull : MaximizeRestore); +void XdgToplevelClient::handlePingDelayed(quint32 serial) +{ + auto it = m_pings.find(serial); + if (it != m_pings.end()) { + qCDebug(KWIN_CORE) << "First ping timeout:" << caption(); + setUnresponsive(true); + } } -void XdgShellClient::handleFullScreenRequested(bool fullScreen, OutputInterface *output) +void XdgToplevelClient::handlePongReceived(quint32 serial) { - // FIXME: Consider output as well. - Q_UNUSED(output); - setFullScreen(fullScreen, false); + auto it = m_pings.find(serial); + if (it != m_pings.end()) { + setUnresponsive(false); + m_pings.erase(it); + } } -void XdgShellClient::handleWindowMenuRequested(SeatInterface *seat, quint32 serial, const QPoint &surfacePos) +void XdgToplevelClient::sendPing(PingReason reason) { - // FIXME: Check the seat and serial. - Q_UNUSED(seat) - Q_UNUSED(serial) - performMouseCommand(Options::MouseOperationsMenu, pos() + surfacePos); + XdgShellInterface *shell = m_shellSurface->shell(); + XdgSurfaceInterface *surface = m_shellSurface->xdgSurface(); + + const quint32 serial = shell->ping(surface); + m_pings.insert(serial, reason); } -void XdgShellClient::handleGrabRequested(SeatInterface *seat, quint32 serial) -{ - // FIXME: Check the seat and serial as well whether the parent had focus. - Q_UNUSED(seat) - Q_UNUSED(serial) - m_hasPopupGrab = true; +void XdgToplevelClient::initialize() +{ + blockGeometryUpdates(true); + + bool needsPlacement = !isInitialPositionSet(); + + if (supportsWindowRules()) { + setupWindowRules(false); + + const QRect originalGeometry = frameGeometry(); + const QRect ruledGeometry = rules()->checkGeometry(originalGeometry, true); + if (originalGeometry != ruledGeometry) { + setFrameGeometry(ruledGeometry); + } + maximize(rules()->checkMaximize(maximizeMode(), true)); + setDesktop(rules()->checkDesktop(desktop(), true)); + setDesktopFileName(rules()->checkDesktopFile(desktopFileName(), true).toUtf8()); + if (rules()->checkMinimize(isMinimized(), true)) { + minimize(true); // No animation. + } + setSkipTaskbar(rules()->checkSkipTaskbar(skipTaskbar(), true)); + setSkipPager(rules()->checkSkipPager(skipPager(), true)); + setSkipSwitcher(rules()->checkSkipSwitcher(skipSwitcher(), true)); + setKeepAbove(rules()->checkKeepAbove(keepAbove(), true)); + setKeepBelow(rules()->checkKeepBelow(keepBelow(), true)); + setShortcut(rules()->checkShortcut(shortcut().toString(), true)); + + // Don't place the client if its position is set by a rule. + if (rules()->checkPosition(invalidPoint, true) != invalidPoint) { + needsPlacement = false; + } + + // Don't place the client if the maximize state is set by a rule. + if (requestedMaximizeMode() != MaximizeRestore) { + needsPlacement = false; + } + + discardTemporaryRules(); + RuleBook::self()->discardUsed(this, false); // Remove Apply Now rules. + updateWindowRules(Rules::All); + } + if (isFullScreen()) { + needsPlacement = false; + } + if (needsPlacement) { + const QRect area = workspace()->clientArea(PlacementArea, Screens::self()->current(), desktop()); + placeIn(area); + } + + blockGeometryUpdates(false); + scheduleConfigure(); + updateColorScheme(); } -void XdgShellClient::handlePingDelayed(quint32 serial) +void XdgToplevelClient::updateMaximizeMode(MaximizeMode maximizeMode) { - auto it = m_pingSerials.find(serial); - if (it != m_pingSerials.end()) { - qCDebug(KWIN_CORE) << "First ping timeout:" << caption(); - setUnresponsive(true); + if (m_maximizeMode == maximizeMode) { + return; } + m_maximizeMode = maximizeMode; + updateWindowRules(Rules::MaximizeVert | Rules::MaximizeHoriz); + emit clientMaximizedStateChanged(this, maximizeMode); + emit clientMaximizedStateChanged(this, maximizeMode & MaximizeHorizontal, maximizeMode & MaximizeVertical); } -void XdgShellClient::handlePingTimeout(quint32 serial) +void XdgToplevelClient::updateFullScreenMode(bool set) { - auto it = m_pingSerials.find(serial); - if (it != m_pingSerials.end()) { - if (it.value() == PingReason::CloseWindow) { - qCDebug(KWIN_CORE) << "Final ping timeout on a close attempt, asking to kill:" << caption(); - - //for internal windows, killing the window will delete this - QPointer guard(this); - killWindow(); - if (!guard) { - return; - } - } - m_pingSerials.erase(it); + if (m_isFullScreen == set) { + return; } + m_isFullScreen = set; + updateWindowRules(Rules::Fullscreen); + emit fullScreenChanged(); } -void XdgShellClient::handlePongReceived(quint32 serial) +void XdgToplevelClient::updateColorScheme() { - auto it = m_pingSerials.find(serial); - if (it != m_pingSerials.end()) { - setUnresponsive(false); - m_pingSerials.erase(it); + if (m_paletteInterface) { + AbstractClient::updateColorScheme(rules()->checkDecoColor(m_paletteInterface->palette())); + } else { + AbstractClient::updateColorScheme(rules()->checkDecoColor(QString())); } } -void XdgShellClient::handleCommitted() +void XdgToplevelClient::installAppMenu(AppMenuInterface *appMenu) { - if (!surface()->buffer()) { - return; - } + m_appMenuInterface = appMenu; - if (!m_hasWindowGeometry) { - m_windowGeometry = surface()->boundingRect(); - } + auto updateMenu = [this](const AppMenuInterface::InterfaceAddress &address) { + updateApplicationMenuServiceName(address.serviceName); + updateApplicationMenuObjectPath(address.objectPath); + }; + connect(m_appMenuInterface, &AppMenuInterface::addressChanged, this, updateMenu); + updateMenu(appMenu->address()); +} - updatePendingGeometry(); +void XdgToplevelClient::installServerDecoration(ServerSideDecorationInterface *decoration) +{ + m_serverDecoration = decoration; - setDepth((surface()->buffer()->hasAlphaChannel() && !isDesktop()) ? 32 : 24); - markAsMapped(); + connect(m_serverDecoration, &ServerSideDecorationInterface::destroyed, this, [this] { + if (!isClosing() && readyForPainting()) { + updateDecoration(/* check_workspace_pos */ true); + } + }); + connect(m_serverDecoration, &ServerSideDecorationInterface::modeRequested, this, + [this] (ServerSideDecorationManagerInterface::Mode mode) { + const bool changed = mode != m_serverDecoration->mode(); + if (changed && readyForPainting()) { + updateDecoration(/* check_workspace_pos */ false); + } + } + ); + if (readyForPainting()) { + updateDecoration(/* check_workspace_pos */ true); + } } -void XdgShellClient::resizeWithChecks(const QSize &size, ForceGeometry_t force) +void XdgToplevelClient::installXdgDecoration(XdgToplevelDecorationV1Interface *decoration) { - // don't allow growing larger than workarea - const QRect area = workspace()->clientArea(WorkArea, this); - setFrameGeometry(QRect{pos(), size.boundedTo(area.size())}, force); + m_xdgDecoration = decoration; + + connect(m_xdgDecoration, &XdgToplevelDecorationV1Interface::destroyed, this, [this] { + if (!isClosing()) { + updateDecoration(/* check_workspace_pos */ true); + } + }); + connect(m_xdgDecoration, &XdgToplevelDecorationV1Interface::preferredModeChanged, this, [this] { + // force is true as we must send a new configure response. + updateDecoration(/* check_workspace_pos */ false, /* force */ true); + }); } -void XdgShellClient::unmap() +void XdgToplevelClient::installPalette(ServerSideDecorationPaletteInterface *palette) { - m_unmapped = true; - if (isMoveResize()) { - leaveMoveResize(); - } - m_requestedClientSize = QSize(0, 0); - destroyWindowManagementInterface(); - if (Workspace::self()) { - addWorkspaceRepaint(visibleRect()); - workspace()->clientHidden(this); - } - emit windowHidden(this); + m_paletteInterface = palette; + + auto updatePalette = [this](const QString &palette) { + AbstractClient::updateColorScheme(rules()->checkDecoColor(palette)); + }; + connect(m_paletteInterface, &ServerSideDecorationPaletteInterface::paletteChanged, this, [=](const QString &palette) { + updatePalette(palette); + }); + connect(m_paletteInterface, &QObject::destroyed, this, [=]() { + updatePalette(QString()); + }); + updatePalette(palette->palette()); } -void XdgShellClient::installPlasmaShellSurface(PlasmaShellSurfaceInterface *surface) +/** + * \todo This whole plasma shell surface thing doesn't seem right. It turns xdg-toplevel into + * something completely different! Perhaps plasmashell surfaces need to be implemented via a + * proprietary protocol that doesn't piggyback on existing shell surface protocols. It'll lead + * to cleaner code and will be technically correct, but I'm not sure whether this is do-able. + */ +void XdgToplevelClient::installPlasmaShellSurface(PlasmaShellSurfaceInterface *shellSurface) { - m_plasmaShellSurface = surface; - auto updatePosition = [this, surface] { - // That's a mis-use of doSetGeometry method. One should instead use move method. - QRect rect = QRect(surface->position(), size()); - doSetGeometry(rect); - }; - auto updateRole = [this, surface] { + m_plasmaShellSurface = shellSurface; + + auto updatePosition = [this, shellSurface] { move(shellSurface->position()); }; + auto updateRole = [this, shellSurface] { NET::WindowType type = NET::Unknown; - switch (surface->role()) { + switch (shellSurface->role()) { case PlasmaShellSurfaceInterface::Role::Desktop: type = NET::Desktop; break; @@ -1365,70 +1440,79 @@ type = NET::Normal; break; } - if (type != m_windowType) { - m_windowType = type; - if (m_windowType == NET::Desktop || type == NET::Dock || type == NET::OnScreenDisplay || type == NET::Notification || type == NET::Tooltip || type == NET::CriticalNotification) { - setOnAllDesktops(true); - } - workspace()->updateClientArea(); + if (m_windowType == type) { + return; + } + m_windowType = type; + switch (m_windowType) { + case NET::Desktop: + case NET::Dock: + case NET::OnScreenDisplay: + case NET::Notification: + case NET::CriticalNotification: + case NET::Tooltip: + setOnAllDesktops(true); + break; + default: + break; } + workspace()->updateClientArea(); }; - connect(surface, &PlasmaShellSurfaceInterface::panelTakesFocusChanged , this, [this, surface]() { - if (surface->panelTakesFocus()) { + connect(shellSurface, &PlasmaShellSurfaceInterface::positionChanged, this, updatePosition); + connect(shellSurface, &PlasmaShellSurfaceInterface::roleChanged, this, updateRole); + connect(shellSurface, &PlasmaShellSurfaceInterface::panelBehaviorChanged, this, [this] { + updateShowOnScreenEdge(); + workspace()->updateClientArea(); + }); + connect(shellSurface, &PlasmaShellSurfaceInterface::panelAutoHideHideRequested, this, [this] { + hideClient(true); + m_plasmaShellSurface->hideAutoHidingPanel(); + updateShowOnScreenEdge(); + }); + connect(shellSurface, &PlasmaShellSurfaceInterface::panelAutoHideShowRequested, this, [this] { + hideClient(false); + ScreenEdges::self()->reserve(this, ElectricNone); + m_plasmaShellSurface->showAutoHidingPanel(); + }); + connect(shellSurface, &PlasmaShellSurfaceInterface::panelTakesFocusChanged, this, [this] { + if (m_plasmaShellSurface->panelTakesFocus()) { workspace()->activateClient(this); } }); - connect(surface, &PlasmaShellSurfaceInterface::positionChanged, this, updatePosition); - connect(surface, &PlasmaShellSurfaceInterface::roleChanged, this, updateRole); - connect(surface, &PlasmaShellSurfaceInterface::panelBehaviorChanged, this, - [this] { - updateShowOnScreenEdge(); - workspace()->updateClientArea(); - } - ); - connect(surface, &PlasmaShellSurfaceInterface::panelAutoHideHideRequested, this, - [this] { - hideClient(true); - m_plasmaShellSurface->hideAutoHidingPanel(); - updateShowOnScreenEdge(); - } - ); - connect(surface, &PlasmaShellSurfaceInterface::panelAutoHideShowRequested, this, - [this] { - hideClient(false); - ScreenEdges::self()->reserve(this, ElectricNone); - m_plasmaShellSurface->showAutoHidingPanel(); - } - ); - if (surface->isPositionSet()) + if (shellSurface->isPositionSet()) updatePosition(); updateRole(); updateShowOnScreenEdge(); - connect(this, &XdgShellClient::frameGeometryChanged, this, &XdgShellClient::updateShowOnScreenEdge); + connect(this, &XdgToplevelClient::frameGeometryChanged, + this, &XdgToplevelClient::updateShowOnScreenEdge); + connect(this, &XdgToplevelClient::windowShown, + this, &XdgToplevelClient::updateShowOnScreenEdge); - setSkipTaskbar(surface->skipTaskbar()); - connect(surface, &PlasmaShellSurfaceInterface::skipTaskbarChanged, this, [this] { + setSkipTaskbar(shellSurface->skipTaskbar()); + connect(shellSurface, &PlasmaShellSurfaceInterface::skipTaskbarChanged, this, [this] { setSkipTaskbar(m_plasmaShellSurface->skipTaskbar()); }); - setSkipSwitcher(surface->skipSwitcher()); - connect(surface, &PlasmaShellSurfaceInterface::skipSwitcherChanged, this, [this] { + setSkipSwitcher(shellSurface->skipSwitcher()); + connect(shellSurface, &PlasmaShellSurfaceInterface::skipSwitcherChanged, this, [this] { setSkipSwitcher(m_plasmaShellSurface->skipSwitcher()); }); } -void XdgShellClient::updateShowOnScreenEdge() +void XdgToplevelClient::updateShowOnScreenEdge() { if (!ScreenEdges::self()) { return; } - if (m_unmapped || !m_plasmaShellSurface || m_plasmaShellSurface->role() != PlasmaShellSurfaceInterface::Role::Panel) { + if (!readyForPainting() || !m_plasmaShellSurface || + m_plasmaShellSurface->role() != PlasmaShellSurfaceInterface::Role::Panel) { ScreenEdges::self()->reserve(this, ElectricNone); return; } - if ((m_plasmaShellSurface->panelBehavior() == PlasmaShellSurfaceInterface::PanelBehavior::AutoHide && m_hidden) || - m_plasmaShellSurface->panelBehavior() == PlasmaShellSurfaceInterface::PanelBehavior::WindowsCanCover) { - // screen edge API requires an edge, thus we need to figure out which edge the window borders + const PlasmaShellSurfaceInterface::PanelBehavior panelBehavior = m_plasmaShellSurface->panelBehavior(); + if ((panelBehavior == PlasmaShellSurfaceInterface::PanelBehavior::AutoHide && isHidden()) || + panelBehavior == PlasmaShellSurfaceInterface::PanelBehavior::WindowsCanCover) { + // Screen edge API requires an edge, thus we need to figure out which edge the window borders. const QRect clientGeometry = frameGeometry(); Qt::Edges edges; for (int i = 0; i < screens()->count(); i++) { @@ -1446,23 +1530,24 @@ edges |= Qt::BottomEdge; } } - // a panel might border multiple screen edges. E.g. a horizontal panel at the bottom will - // also border the left and right edge - // let's remove such cases - if (edges.testFlag(Qt::LeftEdge) && edges.testFlag(Qt::RightEdge)) { + + // A panel might border multiple screen edges. E.g. a horizontal panel at the bottom will + // also border the left and right edge. Let's remove such cases. + if (edges & Qt::LeftEdge && edges & Qt::RightEdge) { edges = edges & (~(Qt::LeftEdge | Qt::RightEdge)); } - if (edges.testFlag(Qt::TopEdge) && edges.testFlag(Qt::BottomEdge)) { + if (edges & Qt::TopEdge && edges & Qt::BottomEdge) { edges = edges & (~(Qt::TopEdge | Qt::BottomEdge)); } - // it's still possible that a panel borders two edges, e.g. bottom and left - // in that case the one which is sharing more with the edge wins - auto check = [clientGeometry](Qt::Edges edges, Qt::Edge horiz, Qt::Edge vert) { - if (edges.testFlag(horiz) && edges.testFlag(vert)) { + + // It's still possible that a panel borders two edges, e.g. bottom and left + // in that case the one which is sharing more with the edge wins. + auto check = [clientGeometry](Qt::Edges edges, Qt::Edge horizontal, Qt::Edge vertical) { + if (edges & horizontal && edges & vertical) { if (clientGeometry.width() >= clientGeometry.height()) { - return edges & ~horiz; + return edges & ~horizontal; } else { - return edges & ~vert; + return edges & ~vertical; } } return edges; @@ -1473,136 +1558,339 @@ edges = check(edges, Qt::RightEdge, Qt::BottomEdge); ElectricBorder border = ElectricNone; - if (edges.testFlag(Qt::LeftEdge)) { + if (edges & Qt::LeftEdge) { border = ElectricLeft; } - if (edges.testFlag(Qt::RightEdge)) { + if (edges & Qt::RightEdge) { border = ElectricRight; } - if (edges.testFlag(Qt::TopEdge)) { + if (edges & Qt::TopEdge) { border = ElectricTop; } - if (edges.testFlag(Qt::BottomEdge)) { + if (edges & Qt::BottomEdge) { border = ElectricBottom; } ScreenEdges::self()->reserve(this, border); } else { ScreenEdges::self()->reserve(this, ElectricNone); } } -bool XdgShellClient::isInitialPositionSet() const +void XdgToplevelClient::setupWindowManagementIntegration() { - if (m_plasmaShellSurface) { - return m_plasmaShellSurface->isPositionSet(); + if (isLockScreen()) { + return; } - return false; + connect(surface(), &SurfaceInterface::mapped, + this, &XdgToplevelClient::setupWindowManagementInterface); } -void XdgShellClient::installAppMenu(AppMenuInterface *menu) +void XdgToplevelClient::setupPlasmaShellIntegration() { - m_appMenuInterface = menu; + connect(surface(), &SurfaceInterface::mapped, + this, &XdgToplevelClient::updateShowOnScreenEdge); +} - auto updateMenu = [this](AppMenuInterface::InterfaceAddress address) { - updateApplicationMenuServiceName(address.serviceName); - updateApplicationMenuObjectPath(address.objectPath); - }; - connect(m_appMenuInterface, &AppMenuInterface::addressChanged, this, [=](AppMenuInterface::InterfaceAddress address) { - updateMenu(address); - }); - updateMenu(menu->address()); +void XdgToplevelClient::setFullScreen(bool set, bool user) +{ + set = rules()->checkFullScreen(set); + + const bool wasFullscreen = isFullScreen(); + if (wasFullscreen == set) { + return; + } + if (isSpecialWindow()) { + return; + } + if (user && !userCanSetFullScreen()) { + return; + } + + if (wasFullscreen) { + workspace()->updateFocusMousePosition(Cursors::self()->mouse()->pos()); // may cause leave event + } else { + m_fullScreenGeometryRestore = frameGeometry(); + } + m_isFullScreen = set; + + if (set) { + workspace()->raiseClient(this); + } + StackingUpdatesBlocker blocker1(workspace()); + GeometryUpdatesBlocker blocker2(this); + + workspace()->updateClientLayer(this); // active fullscreens get different layer + updateDecoration(false, false); + + if (set) { + setFrameGeometry(workspace()->clientArea(FullScreenArea, this)); + } else { + if (m_fullScreenGeometryRestore.isValid()) { + int currentScreen = screen(); + setFrameGeometry(QRect(m_fullScreenGeometryRestore.topLeft(), + constrainFrameSize(m_fullScreenGeometryRestore.size()))); + if( currentScreen != screen()) + workspace()->sendClientToScreen( this, currentScreen ); + } else { + // this can happen when the window was first shown already fullscreen, + // so let the client set the size by itself + setFrameGeometry(QRect(workspace()->clientArea(PlacementArea, this).topLeft(), QSize(0, 0))); + } + } + + doSetFullScreen(); + + updateWindowRules(Rules::Fullscreen|Rules::Position|Rules::Size); + emit fullScreenChanged(); +} + +/** + * \todo Move to AbstractClient. + */ +static bool changeMaximizeRecursion = false; +void XdgToplevelClient::changeMaximize(bool horizontal, bool vertical, bool adjust) +{ + if (changeMaximizeRecursion) { + return; + } + + if (!isResizable()) { + return; + } + + const QRect clientArea = isElectricBorderMaximizing() ? + workspace()->clientArea(MaximizeArea, Cursors::self()->mouse()->pos(), desktop()) : + workspace()->clientArea(MaximizeArea, this); + + const MaximizeMode oldMode = m_requestedMaximizeMode; + const QRect oldGeometry = frameGeometry(); + + // 'adjust == true' means to update the size only, e.g. after changing workspace size + if (!adjust) { + if (vertical) + m_requestedMaximizeMode = MaximizeMode(m_requestedMaximizeMode ^ MaximizeVertical); + if (horizontal) + m_requestedMaximizeMode = MaximizeMode(m_requestedMaximizeMode ^ MaximizeHorizontal); + } + + m_requestedMaximizeMode = rules()->checkMaximize(m_requestedMaximizeMode); + if (!adjust && m_requestedMaximizeMode == oldMode) { + return; + } + + StackingUpdatesBlocker blocker(workspace()); + + // call into decoration update borders + if (isDecorated() && decoration()->client() && !(options->borderlessMaximizedWindows() && m_requestedMaximizeMode == KWin::MaximizeFull)) { + changeMaximizeRecursion = true; + const auto c = decoration()->client().toStrongRef(); + if ((m_requestedMaximizeMode & MaximizeVertical) != (oldMode & MaximizeVertical)) { + emit c->maximizedVerticallyChanged(m_requestedMaximizeMode & MaximizeVertical); + } + if ((m_requestedMaximizeMode & MaximizeHorizontal) != (oldMode & MaximizeHorizontal)) { + emit c->maximizedHorizontallyChanged(m_requestedMaximizeMode & MaximizeHorizontal); + } + if ((m_requestedMaximizeMode == MaximizeFull) != (oldMode == MaximizeFull)) { + emit c->maximizedChanged(m_requestedMaximizeMode & MaximizeFull); + } + changeMaximizeRecursion = false; + } + + if (options->borderlessMaximizedWindows()) { + // triggers a maximize change. + // The next setNoBorder interation will exit since there's no change but the first recursion pullutes the restore geometry + changeMaximizeRecursion = true; + setNoBorder(rules()->checkNoBorder(m_requestedMaximizeMode == MaximizeFull)); + changeMaximizeRecursion = false; + } + + // Conditional quick tiling exit points + const auto oldQuickTileMode = quickTileMode(); + if (quickTileMode() != QuickTileMode(QuickTileFlag::None)) { + if (oldMode == MaximizeFull && + !clientArea.contains(geometryRestore().center())) { + // Not restoring on the same screen + // TODO: The following doesn't work for some reason + //quick_tile_mode = QuickTileNone; // And exit quick tile mode manually + } else if ((oldMode == MaximizeVertical && m_requestedMaximizeMode == MaximizeRestore) || + (oldMode == MaximizeFull && m_requestedMaximizeMode == MaximizeHorizontal)) { + // Modifying geometry of a tiled window + updateQuickTileMode(QuickTileFlag::None); // Exit quick tile mode without restoring geometry + } + } + + if (m_requestedMaximizeMode == MaximizeFull) { + setGeometryRestore(oldGeometry); + // TODO: Client has more checks + if (options->electricBorderMaximize()) { + updateQuickTileMode(QuickTileFlag::Maximize); + } else { + updateQuickTileMode(QuickTileFlag::None); + } + if (quickTileMode() != oldQuickTileMode) { + emit quickTileModeChanged(); + } + setFrameGeometry(workspace()->clientArea(MaximizeArea, this)); + workspace()->raiseClient(this); + } else { + if (m_requestedMaximizeMode == MaximizeRestore) { + updateQuickTileMode(QuickTileFlag::None); + } + if (quickTileMode() != oldQuickTileMode) { + emit quickTileModeChanged(); + } + + if (geometryRestore().isValid()) { + setFrameGeometry(geometryRestore()); + } else { + setFrameGeometry(workspace()->clientArea(PlacementArea, this)); + } + } + + doSetMaximized(); +} + +XdgPopupClient::XdgPopupClient(XdgPopupInterface *shellSurface) + : XdgSurfaceClient(shellSurface->xdgSurface()) + , m_shellSurface(shellSurface) +{ + setDesktop(VirtualDesktopManager::self()->current()); + + connect(shellSurface, &XdgPopupInterface::grabRequested, + this, &XdgPopupClient::handleGrabRequested); + connect(shellSurface, &XdgPopupInterface::initializeRequested, + this, &XdgPopupClient::initialize); + connect(shellSurface, &XdgPopupInterface::destroyed, + this, &XdgPopupClient::destroyClient); + + // The xdg-shell spec states that the parent xdg-surface may be null if it is specified + // via "some other protocol," but we don't support any such protocol yet. Notice that the + // xdg-foreign protocol is only for toplevel surfaces. + + XdgSurfaceInterface *parentShellSurface = shellSurface->parentXdgSurface(); + AbstractClient *parentClient = waylandServer()->findClient(parentShellSurface->surface()); + parentClient->addTransient(this); + setTransientFor(parentClient); +} + +XdgPopupClient::~XdgPopupClient() +{ +} + +void XdgPopupClient::debug(QDebug &stream) const +{ + stream << this; +} + +NET::WindowType XdgPopupClient::windowType(bool direct, int supported_types) const +{ + Q_UNUSED(direct) + Q_UNUSED(supported_types) + return NET::Normal; +} + +bool XdgPopupClient::hasPopupGrab() const +{ + return m_haveExplicitGrab; } -void XdgShellClient::installPalette(ServerSideDecorationPaletteInterface *palette) +void XdgPopupClient::popupDone() { - m_paletteInterface = palette; - - auto updatePalette = [this](const QString &palette) { - AbstractClient::updateColorScheme(rules()->checkDecoColor(palette)); - }; - connect(m_paletteInterface, &ServerSideDecorationPaletteInterface::paletteChanged, this, [=](const QString &palette) { - updatePalette(palette); - }); - connect(m_paletteInterface, &QObject::destroyed, this, [=]() { - updatePalette(QString()); - }); - updatePalette(palette->palette()); + m_shellSurface->sendPopupDone(); } -void XdgShellClient::updateColorScheme() +bool XdgPopupClient::isPopupWindow() const { - if (m_paletteInterface) { - AbstractClient::updateColorScheme(rules()->checkDecoColor(m_paletteInterface->palette())); - } else { - AbstractClient::updateColorScheme(rules()->checkDecoColor(QString())); - } + return true; } -void XdgShellClient::updateMaximizeMode(MaximizeMode maximizeMode) +bool XdgPopupClient::isTransient() const { - if (maximizeMode == m_maximizeMode) { - return; - } - - m_maximizeMode = maximizeMode; - updateWindowRules(Rules::MaximizeHoriz | Rules::MaximizeVert | Rules::Position | Rules::Size); - - emit clientMaximizedStateChanged(this, m_maximizeMode); - emit clientMaximizedStateChanged(this, m_maximizeMode & MaximizeHorizontal, m_maximizeMode & MaximizeVertical); + return true; } -bool XdgShellClient::hasStrut() const +bool XdgPopupClient::isResizable() const { - if (!isShown(true)) { - return false; - } - if (!m_plasmaShellSurface) { - return false; - } - if (m_plasmaShellSurface->role() != PlasmaShellSurfaceInterface::Role::Panel) { - return false; - } - return m_plasmaShellSurface->panelBehavior() == PlasmaShellSurfaceInterface::PanelBehavior::AlwaysVisible; + return false; } -quint32 XdgShellClient::windowId() const +bool XdgPopupClient::isMovable() const { - return m_windowId; + return false; } -void XdgShellClient::updateIcon() +bool XdgPopupClient::isMovableAcrossScreens() const { - const QString waylandIconName = QStringLiteral("wayland"); - const QString dfIconName = iconFromDesktopFile(); - const QString iconName = dfIconName.isEmpty() ? waylandIconName : dfIconName; - if (iconName == icon().name()) { - return; - } - setIcon(QIcon::fromTheme(iconName)); + return false; } -bool XdgShellClient::isTransient() const +bool XdgPopupClient::hasTransientPlacementHint() const { - return m_transient; + return true; } -bool XdgShellClient::hasTransientPlacementHint() const +static QPoint popupOffset(const QRect &anchorRect, const Qt::Edges anchorEdge, + const Qt::Edges gravity, const QSize popupSize) { - return isTransient() && transientFor() && m_xdgShellPopup; + QPoint anchorPoint; + switch (anchorEdge & (Qt::LeftEdge | Qt::RightEdge)) { + case Qt::LeftEdge: + anchorPoint.setX(anchorRect.x()); + break; + case Qt::RightEdge: + anchorPoint.setX(anchorRect.x() + anchorRect.width()); + break; + default: + anchorPoint.setX(qRound(anchorRect.x() + anchorRect.width() / 2.0)); + } + switch (anchorEdge & (Qt::TopEdge | Qt::BottomEdge)) { + case Qt::TopEdge: + anchorPoint.setY(anchorRect.y()); + break; + case Qt::BottomEdge: + anchorPoint.setY(anchorRect.y() + anchorRect.height()); + break; + default: + anchorPoint.setY(qRound(anchorRect.y() + anchorRect.height() / 2.0)); + } + + // calculate where the top left point of the popup will end up with the applied gravity + // gravity indicates direction. i.e if gravitating towards the top the popup's bottom edge + // will next to the anchor point + QPoint popupPosAdjust; + switch (gravity & (Qt::LeftEdge | Qt::RightEdge)) { + case Qt::LeftEdge: + popupPosAdjust.setX(-popupSize.width()); + break; + case Qt::RightEdge: + popupPosAdjust.setX(0); + break; + default: + popupPosAdjust.setX(qRound(-popupSize.width() / 2.0)); + } + switch (gravity & (Qt::TopEdge | Qt::BottomEdge)) { + case Qt::TopEdge: + popupPosAdjust.setY(-popupSize.height()); + break; + case Qt::BottomEdge: + popupPosAdjust.setY(0); + break; + default: + popupPosAdjust.setY(qRound(-popupSize.height() / 2.0)); + } + + return anchorPoint + popupPosAdjust; } -QRect XdgShellClient::transientPlacement(const QRect &bounds) const +QRect XdgPopupClient::transientPlacement(const QRect &bounds) const { - Q_ASSERT(m_xdgShellPopup); + const XdgPositioner positioner = m_shellSurface->positioner(); - QRect anchorRect; - Qt::Edges anchorEdge; - Qt::Edges gravity; - QPoint offset; - PositionerConstraints constraintAdjustments; - QSize size = frameGeometry().size(); + QSize desiredSize = size(); + if (desiredSize.isEmpty()) + desiredSize = positioner.size(); - const QPoint parentClientPos = transientFor()->pos() + transientFor()->clientPos(); + const QPoint parentPosition = transientFor()->framePosToClientPos(transientFor()->pos()); // returns if a target is within the supplied bounds, optional edges argument states which side to check auto inBounds = [bounds](const QRect &target, Qt::Edges edges = Qt::LeftEdge | Qt::RightEdge | Qt::TopEdge | Qt::BottomEdge) -> bool { @@ -1622,51 +1910,42 @@ return true; }; - anchorRect = m_xdgShellPopup->anchorRect(); - anchorEdge = m_xdgShellPopup->anchorEdge(); - gravity = m_xdgShellPopup->gravity(); - offset = m_xdgShellPopup->anchorOffset(); - constraintAdjustments = m_xdgShellPopup->constraintAdjustments(); - if (!size.isValid()) { - size = m_xdgShellPopup->initialSize(); - } - - QRect popupRect(popupOffset(anchorRect, anchorEdge, gravity, size) + offset + parentClientPos, size); + QRect popupRect(popupOffset(positioner.anchorRect(), positioner.anchorEdges(), positioner.gravityEdges(), desiredSize) + positioner.offset() + parentPosition, desiredSize); //if that fits, we don't need to do anything if (inBounds(popupRect)) { return popupRect; } //otherwise apply constraint adjustment per axis in order XDG Shell Popup states - if (constraintAdjustments & PositionerConstraint::FlipX) { + if (positioner.flipConstraintAdjustments() & Qt::Horizontal) { if (!inBounds(popupRect, Qt::LeftEdge | Qt::RightEdge)) { //flip both edges (if either bit is set, XOR both) - auto flippedAnchorEdge = anchorEdge; + auto flippedAnchorEdge = positioner.anchorEdges(); if (flippedAnchorEdge & (Qt::LeftEdge | Qt::RightEdge)) { flippedAnchorEdge ^= (Qt::LeftEdge | Qt::RightEdge); } - auto flippedGravity = gravity; + auto flippedGravity = positioner.gravityEdges(); if (flippedGravity & (Qt::LeftEdge | Qt::RightEdge)) { flippedGravity ^= (Qt::LeftEdge | Qt::RightEdge); } - auto flippedPopupRect = QRect(popupOffset(anchorRect, flippedAnchorEdge, flippedGravity, size) + offset + parentClientPos, size); + auto flippedPopupRect = QRect(popupOffset(positioner.anchorRect(), flippedAnchorEdge, flippedGravity, desiredSize) + positioner.offset() + parentPosition, desiredSize); //if it still doesn't fit we should continue with the unflipped version if (inBounds(flippedPopupRect, Qt::LeftEdge | Qt::RightEdge)) { popupRect.moveLeft(flippedPopupRect.left()); } } } - if (constraintAdjustments & PositionerConstraint::SlideX) { + if (positioner.slideConstraintAdjustments() & Qt::Horizontal) { if (!inBounds(popupRect, Qt::LeftEdge)) { popupRect.moveLeft(bounds.left()); } if (!inBounds(popupRect, Qt::RightEdge)) { popupRect.moveRight(bounds.right()); } } - if (constraintAdjustments & PositionerConstraint::ResizeX) { + if (positioner.resizeConstraintAdjustments() & Qt::Horizontal) { QRect unconstrainedRect = popupRect; if (!inBounds(unconstrainedRect, Qt::LeftEdge)) { @@ -1681,34 +1960,34 @@ } } - if (constraintAdjustments & PositionerConstraint::FlipY) { + if (positioner.flipConstraintAdjustments() & Qt::Vertical) { if (!inBounds(popupRect, Qt::TopEdge | Qt::BottomEdge)) { //flip both edges (if either bit is set, XOR both) - auto flippedAnchorEdge = anchorEdge; + auto flippedAnchorEdge = positioner.anchorEdges(); if (flippedAnchorEdge & (Qt::TopEdge | Qt::BottomEdge)) { flippedAnchorEdge ^= (Qt::TopEdge | Qt::BottomEdge); } - auto flippedGravity = gravity; + auto flippedGravity = positioner.gravityEdges(); if (flippedGravity & (Qt::TopEdge | Qt::BottomEdge)) { flippedGravity ^= (Qt::TopEdge | Qt::BottomEdge); } - auto flippedPopupRect = QRect(popupOffset(anchorRect, flippedAnchorEdge, flippedGravity, size) + offset + parentClientPos, size); + auto flippedPopupRect = QRect(popupOffset(positioner.anchorRect(), flippedAnchorEdge, flippedGravity, desiredSize) + positioner.offset() + parentPosition, desiredSize); //if it still doesn't fit we should continue with the unflipped version if (inBounds(flippedPopupRect, Qt::TopEdge | Qt::BottomEdge)) { popupRect.moveTop(flippedPopupRect.top()); } } } - if (constraintAdjustments & PositionerConstraint::SlideY) { + if (positioner.slideConstraintAdjustments() & Qt::Vertical) { if (!inBounds(popupRect, Qt::TopEdge)) { popupRect.moveTop(bounds.top()); } if (!inBounds(popupRect, Qt::BottomEdge)) { popupRect.moveBottom(bounds.bottom()); } } - if (constraintAdjustments & PositionerConstraint::ResizeY) { + if (positioner.resizeConstraintAdjustments() & Qt::Vertical) { QRect unconstrainedRect = popupRect; if (!inBounds(unconstrainedRect, Qt::TopEdge)) { @@ -1726,298 +2005,92 @@ return popupRect; } -QPoint XdgShellClient::popupOffset(const QRect &anchorRect, const Qt::Edges anchorEdge, const Qt::Edges gravity, const QSize popupSize) const -{ - QPoint anchorPoint; - switch (anchorEdge & (Qt::LeftEdge | Qt::RightEdge)) { - case Qt::LeftEdge: - anchorPoint.setX(anchorRect.x()); - break; - case Qt::RightEdge: - anchorPoint.setX(anchorRect.x() + anchorRect.width()); - break; - default: - anchorPoint.setX(qRound(anchorRect.x() + anchorRect.width() / 2.0)); - } - switch (anchorEdge & (Qt::TopEdge | Qt::BottomEdge)) { - case Qt::TopEdge: - anchorPoint.setY(anchorRect.y()); - break; - case Qt::BottomEdge: - anchorPoint.setY(anchorRect.y() + anchorRect.height()); - break; - default: - anchorPoint.setY(qRound(anchorRect.y() + anchorRect.height() / 2.0)); - } - - // calculate where the top left point of the popup will end up with the applied gravity - // gravity indicates direction. i.e if gravitating towards the top the popup's bottom edge - // will next to the anchor point - QPoint popupPosAdjust; - switch (gravity & (Qt::LeftEdge | Qt::RightEdge)) { - case Qt::LeftEdge: - popupPosAdjust.setX(-popupSize.width()); - break; - case Qt::RightEdge: - popupPosAdjust.setX(0); - break; - default: - popupPosAdjust.setX(qRound(-popupSize.width() / 2.0)); - } - switch (gravity & (Qt::TopEdge | Qt::BottomEdge)) { - case Qt::TopEdge: - popupPosAdjust.setY(-popupSize.height()); - break; - case Qt::BottomEdge: - popupPosAdjust.setY(0); - break; - default: - popupPosAdjust.setY(qRound(-popupSize.height() / 2.0)); - } - - return anchorPoint + popupPosAdjust; -} - -void XdgShellClient::doResizeSync() -{ - requestGeometry(moveResizeGeometry()); -} - -QMatrix4x4 XdgShellClient::inputTransformation() const +bool XdgPopupClient::isCloseable() const { - QMatrix4x4 matrix; - matrix.translate(-m_bufferGeometry.x(), -m_bufferGeometry.y()); - return matrix; + return false; } -void XdgShellClient::installServerSideDecoration(KWaylandServer::ServerSideDecorationInterface *deco) +void XdgPopupClient::closeWindow() { - if (m_serverDecoration == deco) { - return; - } - m_serverDecoration = deco; - connect(m_serverDecoration, &ServerSideDecorationInterface::destroyed, this, - [this] { - m_serverDecoration = nullptr; - if (m_closing || !Workspace::self()) { - return; - } - if (!m_unmapped) { - // maybe delay to next event cycle in case the XdgShellClient is getting destroyed, too - updateDecoration(true); - } - } - ); - if (!m_unmapped) { - updateDecoration(true); - } - connect(m_serverDecoration, &ServerSideDecorationInterface::modeRequested, this, - [this] (ServerSideDecorationManagerInterface::Mode mode) { - const bool changed = mode != m_serverDecoration->mode(); - if (changed && !m_unmapped) { - updateDecoration(false); - } - } - ); } -void XdgShellClient::installXdgDecoration(XdgDecorationInterface *deco) +void XdgPopupClient::updateColorScheme() { - Q_ASSERT(m_xdgShellToplevel); - - m_xdgDecoration = deco; - - connect(m_xdgDecoration, &QObject::destroyed, this, - [this] { - m_xdgDecoration = nullptr; - if (m_closing || !Workspace::self()) { - return; - } - updateDecoration(true); - } - ); - - connect(m_xdgDecoration, &XdgDecorationInterface::modeRequested, this, - [this] () { - //force is true as we must send a new configure response - updateDecoration(false, true); - }); + AbstractClient::updateColorScheme(QString()); } -bool XdgShellClient::shouldExposeToWindowManagement() +bool XdgPopupClient::noBorder() const { - if (isLockScreen()) { - return false; - } - if (m_xdgShellPopup) { - return false; - } return true; } -KWaylandServer::XdgShellSurfaceInterface::States XdgShellClient::xdgSurfaceStates() const +bool XdgPopupClient::userCanSetNoBorder() const { - XdgShellSurfaceInterface::States states; - if (isActive()) { - states |= XdgShellSurfaceInterface::State::Activated; - } - if (isFullScreen()) { - states |= XdgShellSurfaceInterface::State::Fullscreen; - } - if (m_requestedMaximizeMode == MaximizeMode::MaximizeFull) { - states |= XdgShellSurfaceInterface::State::Maximized; - } - if (isResize()) { - states |= XdgShellSurfaceInterface::State::Resizing; - } - return states; + return false; } -void XdgShellClient::doMinimize() +void XdgPopupClient::setNoBorder(bool set) { - if (isMinimized()) { - workspace()->clientHidden(this); - } else { - emit windowShown(this); - } - workspace()->updateMinimizedOfTransients(this); + Q_UNUSED(set) } -void XdgShellClient::showOnScreenEdge() +void XdgPopupClient::updateDecoration(bool check_workspace_pos, bool force) { - if (!m_plasmaShellSurface || m_unmapped) { - return; - } - hideClient(false); - workspace()->raiseClient(this); - if (m_plasmaShellSurface->panelBehavior() == PlasmaShellSurfaceInterface::PanelBehavior::AutoHide) { - m_plasmaShellSurface->showAutoHidingPanel(); - } + Q_UNUSED(check_workspace_pos) + Q_UNUSED(force) } -bool XdgShellClient::dockWantsInput() const +void XdgPopupClient::showOnScreenEdge() { - if (m_plasmaShellSurface) { - if (m_plasmaShellSurface->role() == PlasmaShellSurfaceInterface::Role::Panel) { - return m_plasmaShellSurface->panelTakesFocus(); - } - } - return false; } -void XdgShellClient::killWindow() +bool XdgPopupClient::supportsWindowRules() const { - if (!surface()) { - return; - } - auto c = surface()->client(); - if (c->processId() == getpid() || c->processId() == 0) { - c->destroy(); - return; - } - ::kill(c->processId(), SIGTERM); - // give it time to terminate and only if terminate fails, try destroy Wayland connection - QTimer::singleShot(5000, c, &ClientConnection::destroy); + return false; } -bool XdgShellClient::isLocalhost() const +bool XdgPopupClient::wantsInput() const { - return true; + return false; } -bool XdgShellClient::hasPopupGrab() const +void XdgPopupClient::takeFocus() { - return m_hasPopupGrab; } -void XdgShellClient::popupDone() +bool XdgPopupClient::acceptsFocus() const { - if (m_xdgShellPopup) { - m_xdgShellPopup->popupDone(); - } + return false; } -void XdgShellClient::updateClientOutputs() +XdgSurfaceConfigure *XdgPopupClient::sendRoleConfigure() const { - QVector clientOutputs; - const auto outputs = waylandServer()->display()->outputs(); - for (OutputInterface *output : outputs) { - const QRect outputGeometry(output->globalPosition(), output->pixelSize() / output->scale()); - if (frameGeometry().intersects(outputGeometry)) { - clientOutputs << output; - } - } - surface()->setOutputs(clientOutputs); -} + const QPoint parentPosition = transientFor()->framePosToClientPos(transientFor()->pos()); + const QPoint popupPosition = requestedPos() - parentPosition; -bool XdgShellClient::isPopupWindow() const -{ - if (Toplevel::isPopupWindow()) { - return true; - } - if (m_xdgShellPopup != nullptr) { - return true; - } - return false; -} + const quint32 serial = m_shellSurface->sendConfigure(QRect(popupPosition, requestedClientSize())); -bool XdgShellClient::supportsWindowRules() const -{ - if (m_plasmaShellSurface) { - return false; - } - return m_xdgShellToplevel; -} + XdgSurfaceConfigure *configureEvent = new XdgSurfaceConfigure(); + configureEvent->position = requestedPos(); + configureEvent->size = requestedSize(); + configureEvent->serial = serial; -QRect XdgShellClient::adjustMoveGeometry(const QRect &rect) const -{ - QRect geometry = rect; - geometry.moveTopLeft(moveResizeGeometry().topLeft()); - return geometry; + return configureEvent; } -QRect XdgShellClient::adjustResizeGeometry(const QRect &rect) const +void XdgPopupClient::handleGrabRequested(SeatInterface *seat, quint32 serial) { - QRect geometry = rect; - - // We need to adjust frame geometry because configure events carry the maximum window geometry - // size. A client that has aspect ratio can attach a buffer with smaller size than the one in - // a configure event. - switch (moveResizePointerMode()) { - case PositionTopLeft: - geometry.moveRight(moveResizeGeometry().right()); - geometry.moveBottom(moveResizeGeometry().bottom()); - break; - case PositionTop: - case PositionTopRight: - geometry.moveLeft(moveResizeGeometry().left()); - geometry.moveBottom(moveResizeGeometry().bottom()); - break; - case PositionRight: - case PositionBottomRight: - case PositionBottom: - geometry.moveLeft(moveResizeGeometry().left()); - geometry.moveTop(moveResizeGeometry().top()); - break; - case PositionBottomLeft: - case PositionLeft: - geometry.moveRight(moveResizeGeometry().right()); - geometry.moveTop(moveResizeGeometry().top()); - break; - case PositionCenter: - Q_UNREACHABLE(); - } - - return geometry; + Q_UNUSED(seat) + Q_UNUSED(serial) + m_haveExplicitGrab = true; } -void XdgShellClient::ping(PingReason reason) +void XdgPopupClient::initialize() { - Q_ASSERT(m_xdgShellToplevel); + const QRect area = workspace()->clientArea(PlacementArea, Screens::self()->current(), desktop()); + placeIn(area); - XdgShellInterface *shell = static_cast(m_xdgShellToplevel->global()); - const quint32 serial = shell->ping(m_xdgShellToplevel); - m_pingSerials.insert(serial, reason); + scheduleConfigure(); } -} +} // namespace KWin